215 lines
5.4 KiB
Elm
215 lines
5.4 KiB
Elm
module Page.Login exposing (ExternalMsg(..), Model, Msg, initialModel, update, view)
|
|
|
|
{-| The login page.
|
|
-}
|
|
|
|
import Data.Session exposing (Session)
|
|
import Data.User exposing (User)
|
|
import Html exposing (..)
|
|
import Html.Attributes exposing (..)
|
|
import Html.Events exposing (..)
|
|
import Http
|
|
import Json.Decode as Decode exposing (Decoder, decodeString, field, string)
|
|
import Json.Decode.Pipeline exposing (decode, optional)
|
|
import Request.User exposing (storeSession)
|
|
import Route exposing (Route)
|
|
import Validate exposing (Validator, ifBlank, validate)
|
|
import Views.Form as Form
|
|
|
|
|
|
-- MODEL --
|
|
|
|
|
|
type alias Model =
|
|
{ errors : List Error
|
|
, email : String
|
|
, password : String
|
|
}
|
|
|
|
|
|
initialModel : Model
|
|
initialModel =
|
|
{ errors = []
|
|
, email = ""
|
|
, password = ""
|
|
}
|
|
|
|
|
|
|
|
-- VIEW --
|
|
|
|
|
|
view : Session -> Model -> Html Msg
|
|
view session model =
|
|
div [ class "auth-page" ]
|
|
[ div [ class "container page" ]
|
|
[ div [ class "row" ]
|
|
[ div [ class "col-md-6 offset-md-3 col-xs-12" ]
|
|
[ h1 [ class "text-xs-center" ] [ text "Sign in" ]
|
|
, p [ class "text-xs-center" ]
|
|
[ a [ Route.href Route.Register ]
|
|
[ text "Need an account?" ]
|
|
]
|
|
, Form.viewErrors model.errors
|
|
, viewForm
|
|
]
|
|
]
|
|
]
|
|
]
|
|
|
|
|
|
viewForm : Html Msg
|
|
viewForm =
|
|
Html.form [ onSubmit SubmitForm ]
|
|
[ Form.input
|
|
[ class "form-control-lg"
|
|
, placeholder "Email"
|
|
, onInput SetEmail
|
|
]
|
|
[]
|
|
, Form.password
|
|
[ class "form-control-lg"
|
|
, placeholder "Password"
|
|
, onInput SetPassword
|
|
]
|
|
[]
|
|
, button [ class "btn btn-lg btn-primary pull-xs-right" ]
|
|
[ text "Sign in" ]
|
|
]
|
|
|
|
|
|
|
|
-- UPDATE --
|
|
|
|
|
|
type Msg
|
|
= SubmitForm
|
|
| SetEmail String
|
|
| SetPassword String
|
|
| LoginCompleted (Result Http.Error User)
|
|
|
|
|
|
type ExternalMsg
|
|
= NoOp
|
|
| SetUser User
|
|
|
|
|
|
update : Msg -> Model -> ( ( Model, Cmd Msg ), ExternalMsg )
|
|
update msg model =
|
|
case msg of
|
|
SubmitForm ->
|
|
case validate modelValidator model of
|
|
[] ->
|
|
( ( { model | errors = [] }
|
|
, Http.send LoginCompleted (Request.User.login model)
|
|
)
|
|
, NoOp
|
|
)
|
|
|
|
errors ->
|
|
( ( { model | errors = errors }
|
|
, Cmd.none
|
|
)
|
|
, NoOp
|
|
)
|
|
|
|
SetEmail email ->
|
|
( ( { model | email = email }
|
|
, Cmd.none
|
|
)
|
|
, NoOp
|
|
)
|
|
|
|
SetPassword password ->
|
|
( ( { model | password = password }
|
|
, Cmd.none
|
|
)
|
|
, NoOp
|
|
)
|
|
|
|
LoginCompleted (Err error) ->
|
|
let
|
|
errorMessages =
|
|
case error of
|
|
Http.BadStatus response ->
|
|
response.body
|
|
|> decodeString (field "errors" errorsDecoder)
|
|
|> Result.withDefault []
|
|
|
|
_ ->
|
|
[ "unable to perform login" ]
|
|
in
|
|
( ( { model | errors = List.map (\errorMessage -> ( Form, errorMessage )) errorMessages }
|
|
, Cmd.none
|
|
)
|
|
, NoOp
|
|
)
|
|
|
|
LoginCompleted (Ok user) ->
|
|
( ( model
|
|
, Cmd.batch [ storeSession user, Route.modifyUrl Route.Home ]
|
|
)
|
|
, SetUser user
|
|
)
|
|
|
|
|
|
|
|
-- VALIDATION --
|
|
|
|
|
|
type Field
|
|
= Form
|
|
| Email
|
|
| Password
|
|
|
|
|
|
{-| Recording validation errors on a per-field basis facilitates displaying
|
|
them inline next to the field where the error occurred.
|
|
|
|
I implemented it this way out of habit, then realized the spec called for
|
|
displaying all the errors at the top. I thought about simplifying it, but then
|
|
figured it'd be useful to show how I would normally model this data - assuming
|
|
the intended UX was to render errors per field.
|
|
|
|
(The other part of this is having a view function like this:
|
|
|
|
viewFormErrors : Field -> List Error -> Html msg
|
|
|
|
...and it filters the list of errors to render only the ones for the given
|
|
Field. This way you can call this:
|
|
|
|
viewFormErrors Email model.errors
|
|
|
|
...next to the `email` field, and call `viewFormErrors Password model.errors`
|
|
next to the `password` field, and so on.
|
|
|
|
-}
|
|
type alias Error =
|
|
( Field, String )
|
|
|
|
|
|
modelValidator : Validator Error Model
|
|
modelValidator =
|
|
Validate.all
|
|
[ ifBlank .email ( Email, "email can't be blank." )
|
|
, ifBlank .password ( Password, "password can't be blank." )
|
|
]
|
|
|
|
|
|
errorsDecoder : Decoder (List String)
|
|
errorsDecoder =
|
|
decode (\emailOrPassword email username password -> List.concat [ emailOrPassword, email, username, password ])
|
|
|> optionalError "email or password"
|
|
|> optionalError "email"
|
|
|> optionalError "username"
|
|
|> optionalError "password"
|
|
|
|
|
|
optionalError : String -> Decoder (List String -> a) -> Decoder a
|
|
optionalError fieldName =
|
|
let
|
|
errorToString errorMessage =
|
|
String.join " " [ fieldName, errorMessage ]
|
|
in
|
|
optional fieldName (Decode.list (Decode.map errorToString string)) []
|