about summary refs log tree commit diff
path: root/client/src
diff options
context:
space:
mode:
authorWilliam Carroll <wpcarro@gmail.com>2020-08-01T22·04+0100
committerWilliam Carroll <wpcarro@gmail.com>2020-08-01T22·04+0100
commit249e3113ffbcda047bd9461f01aaa64aa2dd94f1 (patch)
tree6f3edd19086b88a60e48fa66a74cbc501e776b35 /client/src
parent54eb29eae0398dd19f5fdaed278f29453b0b7e44 (diff)
Support creating Trips from the frontend
*sigh* ... spent way too much time encoding/decoding date types...

I need my database, server, client, and JSON need to agree on types.

TL;DR:
- Add CSS for elm/datepicker library
- Create Common.allErrors to display UI errors
- Prefer Data.Time.Calendar.Day instead of newtype Date wrapper around Text
Diffstat (limited to 'client/src')
-rw-r--r--client/src/Admin.elm20
-rw-r--r--client/src/Common.elm27
-rw-r--r--client/src/Login.elm30
-rw-r--r--client/src/Manager.elm11
-rw-r--r--client/src/State.elm242
-rw-r--r--client/src/UI.elm25
-rw-r--r--client/src/User.elm113
7 files changed, 373 insertions, 95 deletions
diff --git a/client/src/Admin.elm b/client/src/Admin.elm
index 3c0f221d93ed..e8e33bde617b 100644
--- a/client/src/Admin.elm
+++ b/client/src/Admin.elm
@@ -5,6 +5,7 @@ import Html.Attributes exposing (..)
 import Html.Events exposing (..)
 import RemoteData
 import State
+import Common
 import Tailwind
 import UI
 import Utils
@@ -78,22 +79,5 @@ render model =
         , case model.adminTab of
             State.Users ->
                 allUsers model
-        , case model.logoutError of
-            Nothing ->
-                text ""
-
-            Just e ->
-                UI.errorBanner
-                    { title = "Error logging out"
-                    , body = Utils.explainHttpError e
-                    }
-        , case model.deleteUserError of
-            Nothing ->
-                text ""
-
-            Just e ->
-                UI.errorBanner
-                    { title = "Error attempting to delete user"
-                    , body = Utils.explainHttpError e
-                    }
+        , Common.allErrors model
         ]
diff --git a/client/src/Common.elm b/client/src/Common.elm
new file mode 100644
index 000000000000..a3106114d43a
--- /dev/null
+++ b/client/src/Common.elm
@@ -0,0 +1,27 @@
+module Common exposing (..)
+
+import Html exposing (..)
+import Maybe.Extra as ME
+import State
+import UI
+import Utils
+
+
+allErrors : State.Model -> Html State.Msg
+allErrors model =
+    div []
+        (State.allErrors
+            model
+            |> List.map
+                (\( mError, title ) ->
+                    case mError of
+                        Nothing ->
+                            text ""
+
+                        Just err ->
+                            UI.errorBanner
+                                { title = title
+                                , body = Utils.explainHttpError err
+                                }
+                )
+        )
diff --git a/client/src/Login.elm b/client/src/Login.elm
index 60a45e7fca77..083c4705609d 100644
--- a/client/src/Login.elm
+++ b/client/src/Login.elm
@@ -1,5 +1,6 @@
 module Login exposing (render)
 
+import Common
 import Html exposing (..)
 import Html.Attributes exposing (..)
 import Html.Events exposing (..)
@@ -137,24 +138,7 @@ login model =
         ]
         [ UI.header 3 "Welcome to Trip Planner"
         , loginForm model
-        , case model.loginError of
-            Nothing ->
-                text ""
-
-            Just e ->
-                UI.errorBanner
-                    { title = "Error logging in"
-                    , body = Utils.explainHttpError e
-                    }
-        , case model.signUpError of
-            Nothing ->
-                text ""
-
-            Just e ->
-                UI.errorBanner
-                    { title = "Error creating account"
-                    , body = Utils.explainHttpError e
-                    }
+        , Common.allErrors model
         ]
 
 
@@ -174,15 +158,7 @@ logout model =
             { label = "Logout"
             , handleClick = State.AttemptLogout
             }
-        , case model.logoutError of
-            Nothing ->
-                text ""
-
-            Just e ->
-                UI.errorBanner
-                    { title = "Error logging out"
-                    , body = Utils.explainHttpError e
-                    }
+        , Common.allErrors model
         ]
 
 
diff --git a/client/src/Manager.elm b/client/src/Manager.elm
index b7f36cfd46bb..7cf5dc3107c3 100644
--- a/client/src/Manager.elm
+++ b/client/src/Manager.elm
@@ -1,6 +1,7 @@
 module Manager exposing (render)
 
 import Array
+import Common
 import Html exposing (..)
 import Html.Attributes exposing (..)
 import Html.Events exposing (..)
@@ -33,14 +34,6 @@ render model =
                         { label = "Logout"
                         , handleClick = State.AttemptLogout
                         }
-                    , case model.logoutError of
-                        Nothing ->
-                            text ""
-
-                        Just e ->
-                            UI.errorBanner
-                                { title = "Error logging out"
-                                , body = Utils.explainHttpError e
-                                }
+                    , Common.allErrors model
                     ]
                 ]
diff --git a/client/src/State.elm b/client/src/State.elm
index e23580a05ebb..66b3e57f0c79 100644
--- a/client/src/State.elm
+++ b/client/src/State.elm
@@ -3,6 +3,8 @@ module State exposing (..)
 import Array exposing (Array)
 import Browser
 import Browser.Navigation as Nav
+import Date
+import DatePicker
 import Http
 import Json.Decode as JD
 import Json.Decode.Extra as JDE
@@ -31,6 +33,10 @@ type Msg
     | UpdatePassword String
     | UpdateRole String
     | UpdateAdminTab AdminTab
+    | UpdateTripDestination String
+    | UpdateTripStartDate DatePicker.Msg
+    | UpdateTripEndDate DatePicker.Msg
+    | UpdateTripComment String
     | ClearErrors
     | ToggleLoginForm
       -- SPA
@@ -42,12 +48,15 @@ type Msg
     | AttemptLogin
     | AttemptLogout
     | AttemptDeleteUser String
+    | AttemptCreateTrip Date.Date Date.Date
       -- Inbound network
     | GotUsers (WebData AllUsers)
+    | GotTrips (WebData (List Trip))
     | GotSignUp (Result Http.Error Session)
     | GotLogin (Result Http.Error Session)
     | GotLogout (Result Http.Error String)
     | GotDeleteUser (Result Http.Error String)
+    | CreatedTrip (Result Http.Error ())
 
 
 type Route
@@ -85,13 +94,6 @@ type alias Review =
     }
 
 
-type alias Reviews =
-    { hi : Maybe Review
-    , lo : Maybe Review
-    , all : List Review
-    }
-
-
 type AdminTab
     = Users
 
@@ -101,6 +103,14 @@ type LoginTab
     | SignUpForm
 
 
+type alias Trip =
+    { destination : String
+    , startDate : Date.Date
+    , endDate : Date.Date
+    , comment : String
+    }
+
+
 type alias Model =
     { route : Maybe Route
     , url : Url.Url
@@ -111,15 +121,33 @@ type alias Model =
     , password : String
     , role : Maybe Role
     , users : WebData AllUsers
+    , startDatePicker : DatePicker.DatePicker
+    , endDatePicker : DatePicker.DatePicker
+    , tripDestination : String
+    , tripStartDate : Maybe Date.Date
+    , tripEndDate : Maybe Date.Date
+    , tripComment : String
+    , trips : WebData (List Trip)
     , adminTab : AdminTab
     , loginTab : LoginTab
     , loginError : Maybe Http.Error
     , logoutError : Maybe Http.Error
     , signUpError : Maybe Http.Error
     , deleteUserError : Maybe Http.Error
+    , createTripError : Maybe Http.Error
     }
 
 
+allErrors : Model -> List ( Maybe Http.Error, String )
+allErrors model =
+    [ ( model.loginError, "Error attempting to authenticate" )
+    , ( model.logoutError, "Error attempting to log out" )
+    , ( model.signUpError, "Error attempting to create your account" )
+    , ( model.deleteUserError, "Error attempting to delete a user" )
+    , ( model.createTripError, "Error attempting to create a trip" )
+    ]
+
+
 
 --------------------------------------------------------------------------------
 -- Functions
@@ -220,6 +248,31 @@ signUp { username, email, password } =
         }
 
 
+createTrip :
+    { username : String
+    , destination : String
+    , startDate : Date.Date
+    , endDate : Date.Date
+    , comment : String
+    }
+    -> Cmd Msg
+createTrip { username, destination, startDate, endDate, comment } =
+    Utils.postWithCredentials
+        { url = endpoint [ "trips" ] []
+        , body =
+            Http.jsonBody
+                (JE.object
+                    [ ( "username", JE.string username )
+                    , ( "destination", JE.string destination )
+                    , ( "startDate", encodeDate startDate )
+                    , ( "endDate", encodeDate endDate )
+                    , ( "comment", JE.string comment )
+                    ]
+                )
+        , expect = Http.expectWhatever CreatedTrip
+        }
+
+
 deleteUser : String -> Cmd Msg
 deleteUser username =
     Utils.deleteWithCredentials
@@ -239,6 +292,35 @@ decodeReview =
         (JD.field "timestamp" JD.string)
 
 
+encodeDate : Date.Date -> JE.Value
+encodeDate date =
+    date |> Date.toIsoString |> JE.string
+
+
+decodeDate : JD.Decoder Date.Date
+decodeDate =
+    JD.string |> JD.andThen (Date.fromIsoString >> JDE.fromResult)
+
+
+fetchTrips : Cmd Msg
+fetchTrips =
+    Utils.getWithCredentials
+        { url = endpoint [ "trips" ] []
+        , expect =
+            Http.expectJson
+                (RemoteData.fromResult >> GotTrips)
+                (JD.list
+                    (JD.map4
+                        Trip
+                        (JD.field "destination" JD.string)
+                        (JD.field "startDate" decodeDate)
+                        (JD.field "endDate" decodeDate)
+                        (JD.field "comment" JD.string)
+                    )
+                )
+        }
+
+
 fetchUsers : Cmd Msg
 fetchUsers =
     Utils.getWithCredentials
@@ -301,6 +383,13 @@ routeParser =
 -}
 init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
 init _ url key =
+    let
+        ( startDatePicker, startDatePickerCmd ) =
+            DatePicker.init
+
+        ( endDatePicker, endDatePickerCmd ) =
+            DatePicker.init
+    in
     ( { route = Nothing
       , url = url
       , key = key
@@ -310,14 +399,25 @@ init _ url key =
       , password = ""
       , role = Nothing
       , users = RemoteData.NotAsked
+      , tripDestination = ""
+      , tripStartDate = Nothing
+      , tripEndDate = Nothing
+      , tripComment = ""
+      , trips = RemoteData.NotAsked
+      , startDatePicker = startDatePicker
+      , endDatePicker = endDatePicker
       , adminTab = Users
       , loginTab = LoginForm
       , loginError = Nothing
       , logoutError = Nothing
       , signUpError = Nothing
       , deleteUserError = Nothing
+      , createTripError = Nothing
       }
-    , Cmd.none
+    , Cmd.batch
+        [ Cmd.map UpdateTripStartDate startDatePickerCmd
+        , Cmd.map UpdateTripEndDate endDatePickerCmd
+        ]
     )
 
 
@@ -359,12 +459,59 @@ update msg model =
             in
             ( { model | role = maybeRole }, Cmd.none )
 
+        UpdateTripDestination x ->
+            ( { model | tripDestination = x }, Cmd.none )
+
+        UpdateTripStartDate dpMsg ->
+            let
+                ( newDatePicker, dateEvent ) =
+                    DatePicker.update DatePicker.defaultSettings dpMsg model.startDatePicker
+
+                newDate =
+                    case dateEvent of
+                        DatePicker.Picked changedDate ->
+                            Just changedDate
+
+                        _ ->
+                            model.tripStartDate
+            in
+            ( { model
+                | tripStartDate = newDate
+                , startDatePicker = newDatePicker
+              }
+            , Cmd.none
+            )
+
+        UpdateTripEndDate dpMsg ->
+            let
+                ( newDatePicker, dateEvent ) =
+                    DatePicker.update DatePicker.defaultSettings dpMsg model.endDatePicker
+
+                newDate =
+                    case dateEvent of
+                        DatePicker.Picked changedDate ->
+                            Just changedDate
+
+                        _ ->
+                            model.tripEndDate
+            in
+            ( { model
+                | tripEndDate = newDate
+                , endDatePicker = newDatePicker
+              }
+            , Cmd.none
+            )
+
+        UpdateTripComment x ->
+            ( { model | tripComment = x }, Cmd.none )
+
         ClearErrors ->
             ( { model
                 | loginError = Nothing
                 , logoutError = Nothing
                 , signUpError = Nothing
                 , deleteUserError = Nothing
+                , createTripError = Nothing
               }
             , Cmd.none
             )
@@ -400,27 +547,18 @@ update msg model =
                     ( { model
                         | url = url
                         , route = route
+                        , trips = RemoteData.Loading
                       }
-                    , Cmd.none
+                    , fetchTrips
                     )
 
                 Just ManagerHome ->
-                    case model.session of
-                        Nothing ->
-                            ( { model
-                                | url = url
-                                , route = route
-                              }
-                            , Cmd.none
-                            )
-
-                        Just session ->
-                            ( { model
-                                | url = url
-                                , route = route
-                              }
-                            , Cmd.none
-                            )
+                    ( { model
+                        | url = url
+                        , route = route
+                      }
+                    , Cmd.none
+                    )
 
                 Just AdminHome ->
                     ( { model
@@ -439,14 +577,14 @@ update msg model =
                     , Cmd.none
                     )
 
-        -- GET /all-usernames
+        -- GET /accounts
         AttemptGetUsers ->
             ( { model | users = RemoteData.Loading }, fetchUsers )
 
         GotUsers xs ->
             ( { model | users = xs }, Cmd.none )
 
-        -- DELETE /user/:username
+        -- DELETE /accounts
         AttemptDeleteUser username ->
             ( model, deleteUser username )
 
@@ -460,7 +598,47 @@ update msg model =
                     , sleepAndClearErrors
                     )
 
-        -- /create-account
+        -- POST /trips
+        AttemptCreateTrip startDate endDate ->
+            ( model
+            , case model.session of
+                Nothing ->
+                    Cmd.none
+
+                Just session ->
+                    createTrip
+                        { username = session.username
+                        , destination = model.tripDestination
+                        , startDate = startDate
+                        , endDate = endDate
+                        , comment = model.tripComment
+                        }
+            )
+
+        CreatedTrip result ->
+            case result of
+                Ok _ ->
+                    ( { model
+                        | tripDestination = ""
+                        , tripStartDate = Nothing
+                        , tripEndDate = Nothing
+                        , tripComment = ""
+                      }
+                    , fetchTrips
+                    )
+
+                Err e ->
+                    ( { model
+                        | createTripError = Just e
+                        , tripDestination = ""
+                        , tripStartDate = Nothing
+                        , tripEndDate = Nothing
+                        , tripComment = ""
+                      }
+                    , sleepAndClearErrors
+                    )
+
+        -- POST /accounts
         AttemptSignUp ->
             ( model
             , signUp
@@ -482,7 +660,11 @@ update msg model =
                     , sleepAndClearErrors
                     )
 
-        -- /login
+        -- GET /trips
+        GotTrips xs ->
+            ( { model | trips = xs }, Cmd.none )
+
+        -- POST /login
         AttemptLogin ->
             ( model, login model.username model.password )
 
@@ -498,7 +680,7 @@ update msg model =
                     , sleepAndClearErrors
                     )
 
-        -- / logout
+        -- GET /logout
         AttemptLogout ->
             ( model, logout )
 
diff --git a/client/src/UI.elm b/client/src/UI.elm
index 482c6ebe9dd8..1de137fcaf4f 100644
--- a/client/src/UI.elm
+++ b/client/src/UI.elm
@@ -1,5 +1,7 @@
 module UI exposing (..)
 
+import Date
+import DatePicker exposing (defaultSettings)
 import Html exposing (..)
 import Html.Attributes exposing (..)
 import Html.Events exposing (..)
@@ -284,3 +286,26 @@ absentData { handleFetch } =
                 }
             ]
         ]
+
+
+datePicker :
+    { mDate : Maybe Date.Date
+    , prompt : String
+    , prefix : String
+    , picker : DatePicker.DatePicker
+    , onUpdate : DatePicker.Msg -> State.Msg
+    }
+    -> Html State.Msg
+datePicker { mDate, prompt, prefix, picker, onUpdate } =
+    let
+        settings =
+            { defaultSettings
+                | placeholder = prompt
+                , inputClassList =
+                    [ ( "text-center", True )
+                    , ( "py-2", True )
+                    ]
+            }
+    in
+    div [ [ "w-1/2", "py-4", "mx-auto" ] |> Tailwind.use |> class ]
+        [ DatePicker.view mDate settings picker |> Html.map onUpdate ]
diff --git a/client/src/User.elm b/client/src/User.elm
index 7139d2028368..5216eeada03e 100644
--- a/client/src/User.elm
+++ b/client/src/User.elm
@@ -1,9 +1,12 @@
 module User exposing (render)
 
+import Common
+import Date
+import DatePicker
 import Html exposing (..)
 import Html.Attributes exposing (..)
 import Html.Events exposing (..)
-import Maybe.Extra
+import Maybe.Extra as ME
 import RemoteData
 import State
 import Tailwind
@@ -11,6 +14,100 @@ import UI
 import Utils
 
 
+createTrip : State.Model -> Html State.Msg
+createTrip model =
+    div []
+        [ UI.header 3 "Plan Upcoming Trip"
+        , UI.textField
+            { pholder = "Where are you going?"
+            , inputId = "destination"
+            , handleInput = State.UpdateTripDestination
+            , inputValue = model.tripDestination
+            }
+        , div [ [ "flex" ] |> Tailwind.use |> class ]
+            [ UI.datePicker
+                { mDate = model.tripStartDate
+                , prompt = "Set departure date"
+                , prefix = "Departure: "
+                , picker = model.startDatePicker
+                , onUpdate = State.UpdateTripStartDate
+                }
+            , UI.datePicker
+                { mDate = model.tripEndDate
+                , prompt = "Set return date"
+                , prefix = "Return: "
+                , picker = model.endDatePicker
+                , onUpdate = State.UpdateTripEndDate
+                }
+            ]
+        , UI.textField
+            { pholder = "Comments?"
+            , inputId = "comment"
+            , handleInput = State.UpdateTripComment
+            , inputValue = model.tripComment
+            }
+        , UI.baseButton
+            { enabled =
+                List.all
+                    identity
+                    [ String.length model.tripDestination > 0
+                    , String.length model.tripComment > 0
+                    , ME.isJust model.tripStartDate
+                    , ME.isJust model.tripEndDate
+                    ]
+            , extraClasses = [ "my-4" ]
+            , handleClick =
+                case ( model.tripStartDate, model.tripEndDate ) of
+                    ( Nothing, _ ) ->
+                        State.DoNothing
+
+                    ( _, Nothing ) ->
+                        State.DoNothing
+
+                    ( Just startDate, Just endDate ) ->
+                        State.AttemptCreateTrip startDate endDate
+            , label = "Schedule trip"
+            }
+        ]
+
+
+trips : State.Model -> Html msg
+trips model =
+    div []
+        [ UI.header 3 "Upcoming Trips"
+        , case model.trips of
+            RemoteData.NotAsked ->
+                UI.paragraph "Somehow we've reached the user home page without requesting your trips data. Please report this to our engineering team at bugs@tripplaner.tld"
+
+            RemoteData.Loading ->
+                UI.paragraph "Loading your trips..."
+
+            RemoteData.Failure e ->
+                UI.paragraph ("Error: " ++ Utils.explainHttpError e)
+
+            RemoteData.Success xs ->
+                ul []
+                    (xs
+                        |> List.map
+                            (\trip ->
+                                li
+                                    [ [ "py-2" ]
+                                        |> Tailwind.use
+                                        |> class
+                                    ]
+                                    [ text
+                                        (Date.toIsoString trip.startDate
+                                            ++ " - "
+                                            ++ Date.toIsoString trip.endDate
+                                            ++ " -> "
+                                            ++ trip.destination
+                                        )
+                                    ]
+                            )
+                    )
+        ]
+
+
 render : State.Model -> Html State.Msg
 render model =
     div
@@ -23,17 +120,11 @@ render model =
             )
         ]
         [ UI.header 2 ("Welcome, " ++ model.username ++ "!")
-        , UI.simpleButton
+        , createTrip model
+        , trips model
+        , UI.textButton
             { label = "Logout"
             , handleClick = State.AttemptLogout
             }
-        , case model.logoutError of
-            Nothing ->
-                text ""
-
-            Just e ->
-                UI.errorBanner
-                    { title = "Error logging out"
-                    , body = Utils.explainHttpError e
-                    }
+        , Common.allErrors model
         ]