module Page.Settings exposing (Model, Msg, init, subscriptions, toSession, update, view) import Api exposing (optionalError) import Avatar import Browser.Navigation as Nav import Email exposing (Email) import Html exposing (Html, button, div, fieldset, h1, input, li, text, textarea, ul) import Html.Attributes exposing (attribute, class, placeholder, type_, value) import Html.Events exposing (onInput, onSubmit) import Http import HttpBuilder import Json.Decode as Decode exposing (Decoder, decodeString, field, list, string) import Json.Decode.Pipeline exposing (optional) import Json.Encode as Encode import Profile exposing (Profile) import Route import Session exposing (Session) import Username as Username exposing (Username) import Validate exposing (Valid, Validator, fromValid, ifBlank, validate) import Viewer exposing (Viewer) import Viewer.Cred as Cred exposing (Cred) -- MODEL type alias Model = { session : Session , errors : List Error , form : Form } type alias Form = { avatar : Maybe String , bio : String , email : String , username : String , password : Maybe String } init : Session -> ( Model, Cmd msg ) init session = ( { session = session , errors = [] , form = case Session.viewer session of Just viewer -> let profile = Viewer.profile viewer cred = Viewer.cred viewer in { avatar = Avatar.toMaybeString (Profile.avatar profile) , email = Email.toString (Viewer.email viewer) , bio = Maybe.withDefault "" (Profile.bio profile) , username = Username.toString (Cred.username cred) , password = Nothing } Nothing -> -- It's fine to store a blank form here. You won't be -- able to submit it if you're not logged in anyway. { avatar = Nothing , email = "" , bio = "" , username = "" , password = Nothing } } , Cmd.none ) {-| A form that has been validated. Only the `edit` function uses this. Its purpose is to prevent us from forgetting to validate the form before passing it to `edit`. This doesn't create any guarantees that the form was actually validated. If we wanted to do that, we'd need to move the form data into a separate module! -} type ValidForm = Valid Form -- VIEW view : Model -> { title : String, content : Html Msg } view model = { title = "Settings" , content = case Session.cred model.session of Just cred -> div [ class "settings-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 "Your Settings" ] , model.errors |> List.map (\( _, error ) -> li [] [ text error ]) |> ul [ class "error-messages" ] , viewForm cred model.form ] ] ] ] Nothing -> text "Sign in to view your settings." } viewForm : Cred -> Form -> Html Msg viewForm cred form = Html.form [ onSubmit (SubmittedForm cred) ] [ fieldset [] [ fieldset [ class "form-group" ] [ input [ class "form-control" , placeholder "URL of profile picture" , value (Maybe.withDefault "" form.avatar) , onInput EnteredImage ] [] ] , fieldset [ class "form-group" ] [ input [ class "form-control form-control-lg" , placeholder "Username" , value form.username , onInput EnteredUsername ] [] ] , fieldset [ class "form-group" ] [ textarea [ class "form-control form-control-lg" , placeholder "Short bio about you" , attribute "rows" "8" , value form.bio , onInput EnteredBio ] [] ] , fieldset [ class "form-group" ] [ input [ class "form-control form-control-lg" , placeholder "Email" , value form.email , onInput EnteredEmail ] [] ] , fieldset [ class "form-group" ] [ input [ class "form-control form-control-lg" , type_ "password" , placeholder "Password" , value (Maybe.withDefault "" form.password) , onInput EnteredPassword ] [] ] , button [ class "btn btn-lg btn-primary pull-xs-right" ] [ text "Update Settings" ] ] ] -- UPDATE type Msg = SubmittedForm Cred | EnteredEmail String | EnteredUsername String | EnteredPassword String | EnteredBio String | EnteredImage String | CompletedSave (Result Http.Error Viewer) | GotSession Session update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of SubmittedForm cred -> case validate formValidator model.form of Ok validForm -> ( { model | errors = [] } , edit cred validForm |> Http.send CompletedSave ) Err errors -> ( { model | errors = errors } , Cmd.none ) EnteredEmail email -> updateForm (\form -> { form | email = email }) model EnteredUsername username -> updateForm (\form -> { form | username = username }) model EnteredPassword passwordStr -> let password = if String.isEmpty passwordStr then Nothing else Just passwordStr in updateForm (\form -> { form | password = password }) model EnteredBio bio -> updateForm (\form -> { form | bio = bio }) model EnteredImage avatarStr -> let avatar = if String.isEmpty avatarStr then Nothing else Just avatarStr in updateForm (\form -> { form | avatar = avatar }) model CompletedSave (Err error) -> let serverErrors = error |> Api.listErrors errorsDecoder |> List.map (\errorMessage -> ( Server, errorMessage )) in ( { model | errors = List.append model.errors serverErrors } , Cmd.none ) CompletedSave (Ok cred) -> ( model , Session.login cred ) GotSession session -> ( { model | session = session }, Cmd.none ) {-| Helper function for `update`. Updates the form and returns Cmd.none and Ignored. Useful for recording form fields! -} updateForm : (Form -> Form) -> Model -> ( Model, Cmd Msg ) updateForm transform model = ( { model | form = transform model.form }, Cmd.none ) -- SUBSCRIPTIONS subscriptions : Model -> Sub Msg subscriptions model = Session.changes GotSession (Session.navKey model.session) -- EXPORT toSession : Model -> Session toSession model = model.session -- VALIDATION type ErrorSource = Server | Username | Email | Password | ImageUrl | Bio type alias Error = ( ErrorSource, String ) formValidator : Validator Error Form formValidator = Validate.all [ ifBlank .username ( Username, "username can't be blank." ) , ifBlank .email ( Email, "email can't be blank." ) ] errorsDecoder : Decoder (List String) errorsDecoder = Decode.succeed (\email username password -> List.concat [ email, username, password ]) |> optionalError "email" |> optionalError "username" |> optionalError "password" -- HTTP {-| This takes a Valid Form as a reminder that it needs to have been validated first. -} edit : Cred -> Valid Form -> Http.Request Viewer edit cred validForm = let form = fromValid validForm updates = [ Just ( "username", Encode.string form.username ) , Just ( "email", Encode.string form.email ) , Just ( "bio", Encode.string form.bio ) , Just ( "image", Maybe.withDefault Encode.null (Maybe.map Encode.string form.avatar) ) , Maybe.map (\pass -> ( "password", Encode.string pass )) form.password ] |> List.filterMap identity body = ( "user", Encode.object updates ) |> List.singleton |> Encode.object |> Http.jsonBody expect = Decode.field "user" Viewer.decoder |> Http.expectJson in Api.url [ "user" ] |> HttpBuilder.put |> HttpBuilder.withExpect expect |> HttpBuilder.withBody body |> Cred.addHeader cred |> HttpBuilder.toRequest