subtree(users/wpcarro): docking briefcase at '24f5a642'

git-subtree-dir: users/wpcarro
git-subtree-mainline: 464bbcb15c
git-subtree-split: 24f5a642af
Change-Id: I6105b3762b79126b3488359c95978cadb3efa789
This commit is contained in:
Vincent Ambo 2021-12-14 01:51:19 +03:00
commit 019f8fd211
766 changed files with 175420 additions and 0 deletions

View file

@ -0,0 +1,189 @@
module Admin exposing (render)
import Common
import Date
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Maybe.Extra as ME
import RemoteData
import State
import Tailwind
import UI
import Utils
roleToggle : State.Model -> State.Role -> Html State.Msg
roleToggle model role =
div [ [ "px-1", "inline" ] |> Tailwind.use |> class ]
[ UI.toggleButton
{ toggled = model.inviteRole == Just role
, label = State.roleToString role
, handleEnable = State.UpdateInviteRole (Just role)
, handleDisable = State.UpdateInviteRole Nothing
}
]
inviteUser : State.Model -> Html State.Msg
inviteUser model =
div [ [ "pb-6" ] |> Tailwind.use |> class ]
[ UI.header 3 "Invite a user"
, UI.textField
{ handleInput = State.UpdateInviteEmail
, inputId = "invite-email"
, inputValue = model.inviteEmail
, pholder = "Email..."
}
, div [ [ "pt-4" ] |> Tailwind.use |> class ]
[ roleToggle model State.User
, roleToggle model State.Manager
, roleToggle model State.Admin
]
, UI.baseButton
{ enabled =
List.all
identity
[ String.length model.inviteEmail > 0
, ME.isJust model.inviteRole
]
, extraClasses = [ "my-4" ]
, label =
case model.inviteResponseStatus of
RemoteData.Loading ->
"Sending..."
_ ->
"Send invitation"
, handleClick =
case model.inviteRole of
Nothing ->
State.DoNothing
Just role ->
State.AttemptInviteUser role
}
]
allTrips : State.Model -> Html State.Msg
allTrips model =
case model.trips of
RemoteData.NotAsked ->
UI.absentData { handleFetch = State.AttemptGetTrips }
RemoteData.Loading ->
UI.paragraph "Loading..."
RemoteData.Failure e ->
UI.paragraph ("Error: " ++ Utils.explainHttpError e)
RemoteData.Success xs ->
ul []
(xs
|> List.map
(\trip ->
li []
[ UI.paragraph (Date.toIsoString trip.startDate ++ " - " ++ Date.toIsoString trip.endDate ++ ", " ++ trip.username ++ " is going " ++ trip.destination)
, UI.textButton
{ label = "delete"
, handleClick = State.AttemptDeleteTrip trip
}
]
)
)
allUsers : State.Model -> Html State.Msg
allUsers model =
case model.accounts of
RemoteData.NotAsked ->
UI.absentData { handleFetch = State.AttemptGetAccounts }
RemoteData.Loading ->
UI.paragraph "Loading..."
RemoteData.Failure e ->
UI.paragraph ("Error: " ++ Utils.explainHttpError e)
RemoteData.Success xs ->
ul []
(xs
|> List.map
(\account ->
li []
[ UI.paragraph
(account.username
++ " - "
++ State.roleToString account.role
)
, UI.textButton
{ label = "delete"
, handleClick = State.AttemptDeleteAccount account.username
}
]
)
)
users : List String -> Html State.Msg
users xs =
ul []
(xs
|> List.map
(\x ->
li [ [ "py-4", "flex" ] |> Tailwind.use |> class ]
[ p [ [ "flex-1" ] |> Tailwind.use |> class ] [ text x ]
, div [ [ "flex-1" ] |> Tailwind.use |> class ]
[ UI.simpleButton
{ label = "Delete"
, handleClick = State.AttemptDeleteAccount x
}
]
]
)
)
render : State.Model -> Html State.Msg
render model =
div
[ [ "container"
, "mx-auto"
, "text-center"
]
|> Tailwind.use
|> class
]
[ UI.header 2 "Welcome!"
, div []
[ UI.textButton
{ label = "Logout"
, handleClick = State.AttemptLogout
}
]
, div [ [ "py-3" ] |> Tailwind.use |> class ]
[ case model.adminTab of
State.Accounts ->
UI.textButton
{ label = "Switch to trips"
, handleClick = State.UpdateAdminTab State.Trips
}
State.Trips ->
UI.textButton
{ label = "Switch to accounts"
, handleClick = State.UpdateAdminTab State.Accounts
}
]
, case model.adminTab of
State.Accounts ->
div []
[ inviteUser model
, allUsers model
]
State.Trips ->
allTrips model
, Common.allErrors model
]

View file

@ -0,0 +1,37 @@
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
}
)
)
withSession : State.Model -> (State.Session -> Html State.Msg) -> Html State.Msg
withSession model renderWithSession =
case model.session of
Nothing ->
div [] [ UI.paragraph "You need a valid session to view this page. Please attempt to log in." ]
Just session ->
renderWithSession session

View file

@ -0,0 +1,199 @@
module Login exposing (render)
import Common
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import State
import Tailwind
import UI
import Utils
googleSignIn : Html State.Msg
googleSignIn =
div
[ class "g-signin2"
, attribute "onsuccess" "onSignIn"
, onClick State.GoogleSignIn
]
[]
loginForm : State.Model -> Html State.Msg
loginForm model =
div
[ [ "w-full"
, "max-w-xs"
, "mx-auto"
]
|> Tailwind.use
|> class
]
[ div
[ [ "bg-white"
, "shadow-md"
, "rounded"
, "px-8"
, "pt-6"
, "pb-8"
, "mb-4"
, "text-left"
]
|> Tailwind.use
|> class
]
[ div [ [ "text-center", "pb-6" ] |> Tailwind.use |> class ]
[ UI.textButton
{ handleClick = State.ToggleLoginForm
, label =
case model.loginTab of
State.LoginForm ->
"Switch to sign up"
State.SignUpForm ->
"Switch to login"
}
]
, div
[ [ "mb-4" ] |> Tailwind.use |> class ]
[ UI.label_ { for_ = "username", text_ = "Username" }
, UI.textField
{ inputId = "Username"
, pholder = "Username"
, handleInput = State.UpdateUsername
, inputValue = model.username
}
]
, case model.loginTab of
State.LoginForm ->
text ""
State.SignUpForm ->
div
[ [ "mb-4" ] |> Tailwind.use |> class ]
[ UI.label_ { for_ = "email", text_ = "Email" }
, input
[ [ "shadow"
, "appearance-none"
, "border"
, "rounded"
, "w-full"
, "py-2"
, "px-3"
, "text-gray-700"
, "leading-tight"
, "focus:outline-none"
, "focus:shadow-outline"
]
|> Tailwind.use
|> class
, id "email"
, placeholder "who@domain.tld"
, onInput State.UpdateEmail
]
[]
]
, div
[ [ "mb-4" ] |> Tailwind.use |> class ]
[ UI.label_ { for_ = "password", text_ = "Password" }
, input
[ [ "shadow"
, "appearance-none"
, "border"
, "rounded"
, "w-full"
, "py-2"
, "px-3"
, "text-gray-700"
, "leading-tight"
, "focus:outline-none"
, "focus:shadow-outline"
]
|> Tailwind.use
|> class
, id "password"
, type_ "password"
, placeholder "******************"
, onInput State.UpdatePassword
]
[]
]
, case model.loginTab of
State.LoginForm ->
div [ [ "flex", "space-around" ] |> Tailwind.use |> class ]
[ UI.simpleButton
{ handleClick = State.AttemptLogin
, label = "Login"
}
, div [ [ "pl-4" ] |> Tailwind.use |> class ] [ googleSignIn ]
]
State.SignUpForm ->
if
List.all identity
[ String.length model.username > 0
, String.length model.email > 0
, String.length model.password > 0
]
then
div []
[ UI.simpleButton
{ handleClick = State.AttemptSignUp
, label = "Sign up"
}
]
else
UI.disabledButton { label = "Sign up" }
]
]
login :
State.Model
-> Html State.Msg
login model =
div
[ [ "text-center"
, "py-20"
, "bg-gray-200"
, "h-screen"
]
|> Tailwind.use
|> class
]
[ UI.header 3 "Welcome to Trip Planner"
, loginForm model
, Common.allErrors model
]
logout : State.Model -> Html State.Msg
logout model =
div
[ [ "text-center"
, "py-20"
, "bg-gray-200"
, "h-screen"
]
|> Tailwind.use
|> class
]
[ UI.header 3 "Looks like you're already signed in..."
, UI.simpleButton
{ label = "Logout"
, handleClick = State.AttemptLogout
}
, Common.allErrors model
]
render : State.Model -> Html State.Msg
render model =
case model.session of
Nothing ->
login model
Just x ->
logout model

View file

@ -0,0 +1,62 @@
module Main exposing (main)
import Admin
import Browser
import Html exposing (..)
import Login
import Manager
import State
import Url
import User
viewForRoute : State.Route -> (State.Model -> Html State.Msg)
viewForRoute route =
case route of
State.Login ->
Login.render
State.UserHome ->
User.render
State.ManagerHome ->
Manager.render
State.AdminHome ->
Admin.render
view : State.Model -> Browser.Document State.Msg
view model =
{ title = "TripPlanner"
, body =
[ case ( model.session, model.route ) of
-- Redirect to /login when someone is not authenticated.
-- TODO(wpcarro): We should ensure that /login shows in the URL
-- bar.
( Nothing, _ ) ->
Login.render model
( Just session, Nothing ) ->
Login.render model
-- Authenticated
( Just session, Just route ) ->
if State.isAuthorized session.role route then
viewForRoute route model
else
text "Access denied. You are not authorized to be here. Evacuate the area immediately"
]
}
main =
Browser.application
{ init = State.init
, onUrlChange = State.UrlChanged
, onUrlRequest = State.LinkClicked
, subscriptions = \_ -> Sub.none
, update = State.update
, view = view
}

View file

@ -0,0 +1,70 @@
module Manager exposing (render)
import Array
import Common
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import RemoteData
import State
import Tailwind
import UI
import Utils
allUsers : State.Model -> Html State.Msg
allUsers model =
case model.accounts of
RemoteData.NotAsked ->
UI.absentData { handleFetch = State.AttemptGetAccounts }
RemoteData.Loading ->
UI.paragraph "Loading..."
RemoteData.Failure e ->
UI.paragraph ("Error: " ++ Utils.explainHttpError e)
RemoteData.Success xs ->
ul []
(xs
|> List.map
(\account ->
li []
[ UI.paragraph
(account.username
++ " - "
++ State.roleToString account.role
)
, UI.textButton
{ label = "delete"
, handleClick = State.AttemptDeleteAccount account.username
}
]
)
)
render : State.Model -> Html State.Msg
render model =
Common.withSession model
(\session ->
div
[ class
([ "container"
, "mx-auto"
, "text-center"
]
|> Tailwind.use
)
]
[ h1 []
[ UI.header 2 ("Welcome back, " ++ session.username ++ "!")
, UI.textButton
{ label = "Logout"
, handleClick = State.AttemptLogout
}
, allUsers model
, Common.allErrors model
]
]
)

View file

@ -0,0 +1,7 @@
module Shared exposing (..)
clientOrigin =
"http://localhost:8000"
serverOrigin =
"http://localhost:3000"

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
module Tailwind exposing (..)
{-| Functions to make Tailwind development in Elm even more pleasant.
-}
{-| Conditionally use `class` selection when `condition` is true.
-}
when : Bool -> String -> String
when condition class =
if condition then
class
else
""
if_ : Bool -> String -> String -> String
if_ condition whenTrue whenFalse =
if condition then
whenTrue
else
whenFalse
use : List String -> String
use styles =
String.join " " styles

View file

@ -0,0 +1,318 @@
module UI exposing (..)
import Date
import DatePicker exposing (defaultSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import State
import Tailwind
label_ : { for_ : String, text_ : String } -> Html msg
label_ { for_, text_ } =
label
[ [ "block"
, "text-gray-700"
, "text-sm"
, "font-bold"
, "mb-2"
]
|> Tailwind.use
|> class
, for for_
]
[ text text_ ]
errorBanner : { title : String, body : String } -> Html msg
errorBanner { title, body } =
div
[ [ "text-left"
, "fixed"
, "container"
, "top-0"
, "mt-6"
]
|> Tailwind.use
|> class
, style "left" "50%"
-- TODO(wpcarro): Consider supporting breakpoints, but for now
-- don't.
, style "width" "800px"
, style "margin-left" "-400px"
]
[ div
[ [ "bg-red-500"
, "text-white"
, "font-bold"
, "rounded-t"
, "px-4"
, "py-2"
]
|> Tailwind.use
|> class
]
[ text title ]
, div
[ [ "border"
, "border-t-0"
, "border-red-400"
, "rounded-b"
, "bg-red-100"
, "px-4"
, "py-3"
, "text-red-700"
]
|> Tailwind.use
|> class
]
[ p [] [ text body ] ]
]
baseButton :
{ label : String
, enabled : Bool
, handleClick : msg
, extraClasses : List String
}
-> Html msg
baseButton { label, enabled, handleClick, extraClasses } =
button
[ [ if enabled then
"bg-blue-500"
else
"bg-gray-500"
, if enabled then
"hover:bg-blue-700"
else
""
, if enabled then
""
else
"cursor-not-allowed"
, "text-white"
, "font-bold"
, "py-1"
, "shadow-lg"
, "px-4"
, "rounded"
, "focus:outline-none"
, "focus:shadow-outline"
]
++ extraClasses
|> Tailwind.use
|> class
, onClick handleClick
, disabled (not enabled)
]
[ text label ]
simpleButton :
{ label : String
, handleClick : msg
}
-> Html msg
simpleButton { label, handleClick } =
baseButton
{ label = label
, enabled = True
, handleClick = handleClick
, extraClasses = []
}
disabledButton :
{ label : String }
-> Html State.Msg
disabledButton { label } =
baseButton
{ label = label
, enabled = False
, handleClick = State.DoNothing
, extraClasses = []
}
textButton :
{ label : String
, handleClick : msg
}
-> Html msg
textButton { label, handleClick } =
button
[ [ "text-blue-600"
, "hover:text-blue-500"
, "font-bold"
, "hover:underline"
, "focus:outline-none"
]
|> Tailwind.use
|> class
, onClick handleClick
]
[ text label ]
textField :
{ pholder : String
, inputId : String
, handleInput : String -> msg
, inputValue : String
}
-> Html msg
textField { pholder, inputId, handleInput, inputValue } =
input
[ [ "shadow"
, "appearance-none"
, "border"
, "rounded"
, "w-full"
, "py-2"
, "px-3"
, "text-gray-700"
, "leading-tight"
, "focus:outline-none"
, "focus:shadow-outline"
]
|> Tailwind.use
|> class
, id inputId
, value inputValue
, placeholder pholder
, onInput handleInput
]
[]
toggleButton :
{ toggled : Bool
, label : String
, handleEnable : msg
, handleDisable : msg
}
-> Html msg
toggleButton { toggled, label, handleEnable, handleDisable } =
button
[ [ if toggled then
"bg-blue-700"
else
"bg-blue-500"
, "hover:bg-blue-700"
, "text-white"
, "font-bold"
, "py-2"
, "px-4"
, "rounded"
, "focus:outline-none"
, "focus:shadow-outline"
]
|> Tailwind.use
|> class
, onClick
(if toggled then
handleDisable
else
handleEnable
)
]
[ text label ]
paragraph : String -> Html msg
paragraph x =
p [ [ "text-xl" ] |> Tailwind.use |> class ] [ text x ]
header : Int -> String -> Html msg
header which x =
let
hStyles =
case which of
1 ->
[ "text-6xl"
, "py-12"
]
2 ->
[ "text-3xl"
, "py-6"
]
_ ->
[ "text-2xl"
, "py-2"
]
in
h1
[ hStyles
++ [ "font-bold"
, "text-gray-700"
]
|> Tailwind.use
|> class
]
[ text x ]
link : String -> String -> Html msg
link path label =
a
[ href path
, [ "underline"
, "text-blue-600"
, "text-xl"
]
|> Tailwind.use
|> class
]
[ text label ]
absentData : { handleFetch : msg } -> Html msg
absentData { handleFetch } =
div []
[ paragraph "Welp... it looks like you've caught us in a state that we considered impossible: we did not fetch the data upon which this page depends. Maybe you can help us out by clicking the super secret, highly privileged \"Fetch data\" button below (we don't normally show people this)."
, div [ [ "py-4" ] |> Tailwind.use |> class ]
[ simpleButton
{ label = "Fetch data"
, handleClick = 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 ]
wrapNoPrint : Html State.Msg -> Html State.Msg
wrapNoPrint component =
div [ [ "no-print" ] |> Tailwind.use |> class ] [ component ]

View file

@ -0,0 +1,245 @@
module User exposing (render)
import Common
import Date
import DatePicker
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Maybe.Extra as ME
import RemoteData
import State
import Tailwind
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"
}
]
renderEditTrip : State.Model -> State.Trip -> Html State.Msg
renderEditTrip model trip =
li []
[ div []
[ UI.textField
{ handleInput = State.UpdateEditTripDestination
, inputId = "edit-trip-destination"
, inputValue = model.editTripDestination
, pholder = "Destination"
}
, UI.textField
{ handleInput = State.UpdateEditTripComment
, inputId = "edit-trip-comment"
, inputValue = model.editTripComment
, pholder = "Comment"
}
]
, div []
[ UI.baseButton
{ enabled =
case model.updateTripStatus of
RemoteData.Loading ->
False
_ ->
True
, extraClasses = []
, label =
case model.updateTripStatus of
RemoteData.Loading ->
"Saving..."
_ ->
"Save"
, handleClick =
State.AttemptUpdateTrip
{ username = trip.username
, destination = trip.destination
, startDate = trip.startDate
}
{ username = trip.username
, destination = model.editTripDestination
, startDate = trip.startDate
, endDate = trip.endDate
, comment = model.editTripComment
}
}
, UI.simpleButton
{ label = "Cancel"
, handleClick = State.CancelEditTrip
}
]
]
renderTrip : Date.Date -> State.Trip -> Html State.Msg
renderTrip today trip =
li
[ [ "py-2" ]
|> Tailwind.use
|> class
]
[ if Date.compare today trip.startDate == GT then
UI.paragraph
(String.fromInt (Date.diff Date.Days trip.startDate today)
++ " days until you're travelling to "
++ trip.destination
++ " for "
++ String.fromInt
(Date.diff
Date.Days
trip.startDate
trip.endDate
)
++ " days."
)
else
UI.paragraph
(String.fromInt (Date.diff Date.Days today trip.endDate)
++ " days ago you returned from your trip to "
++ trip.destination
)
, UI.paragraph ("\"" ++ trip.comment ++ "\"")
, UI.wrapNoPrint
(UI.textButton
{ label = "Edit"
, handleClick = State.EditTrip trip
}
)
, UI.wrapNoPrint
(UI.textButton
{ label = "Delete"
, handleClick = State.AttemptDeleteTrip trip
}
)
]
trips : State.Model -> Html State.Msg
trips model =
div []
[ UI.header 3 "Your 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 ->
case model.todaysDate of
Nothing ->
text ""
Just today ->
div [ [ "mb-10" ] |> Tailwind.use |> class ]
[ ul [ [ "my-4" ] |> Tailwind.use |> class ]
(xs
|> List.sortWith (\x y -> Date.compare y.startDate x.startDate)
|> List.map
(\trip ->
case model.editingTrip of
Nothing ->
renderTrip today trip
Just x ->
if x == trip then
renderEditTrip model trip
else
renderTrip today trip
)
)
, UI.wrapNoPrint
(UI.simpleButton
{ label = "Print iternary"
, handleClick = State.PrintPage
}
)
]
]
render : State.Model -> Html State.Msg
render model =
Common.withSession model
(\session ->
div
[ class
([ "container"
, "mx-auto"
, "text-center"
]
|> Tailwind.use
)
]
[ UI.wrapNoPrint (UI.header 2 ("Welcome, " ++ session.username ++ "!"))
, UI.wrapNoPrint (createTrip model)
, trips model
, UI.wrapNoPrint
(UI.textButton
{ label = "Logout"
, handleClick = State.AttemptLogout
}
)
, Common.allErrors model
]
)

View file

@ -0,0 +1,109 @@
module Utils exposing (..)
import DateFormat
import Http
import Time
import Shared
explainHttpError : Http.Error -> String
explainHttpError e =
case e of
Http.BadUrl _ ->
"Bad URL: you may have supplied an improperly formatted URL"
Http.Timeout ->
"Timeout: the resource you requested did not arrive within the interval of time that you claimed it should"
Http.BadStatus s ->
"Bad Status: the server returned a bad status code: " ++ String.fromInt s
Http.BadBody b ->
"Bad Body: our application had trouble decoding the body of the response from the server: " ++ b
Http.NetworkError ->
"Network Error: something went awry in the network stack. I recommend checking the server logs if you can."
getWithCredentials :
{ url : String
, expect : Http.Expect msg
}
-> Cmd msg
getWithCredentials { url, expect } =
Http.riskyRequest
{ url = url
, headers = [ Http.header "Origin" Shared.clientOrigin ]
, method = "GET"
, timeout = Nothing
, tracker = Nothing
, body = Http.emptyBody
, expect = expect
}
postWithCredentials :
{ url : String
, body : Http.Body
, expect : Http.Expect msg
}
-> Cmd msg
postWithCredentials { url, body, expect } =
Http.riskyRequest
{ url = url
, headers = [ Http.header "Origin" Shared.clientOrigin ]
, method = "POST"
, timeout = Nothing
, tracker = Nothing
, body = body
, expect = expect
}
deleteWithCredentials :
{ url : String
, body : Http.Body
, expect : Http.Expect msg
}
-> Cmd msg
deleteWithCredentials { url, body, expect } =
Http.riskyRequest
{ url = url
, headers = [ Http.header "Origin" Shared.clientOrigin ]
, method = "DELETE"
, timeout = Nothing
, tracker = Nothing
, body = body
, expect = expect
}
putWithCredentials :
{ url : String
, body : Http.Body
, expect : Http.Expect msg
}
-> Cmd msg
putWithCredentials { url, body, expect } =
Http.riskyRequest
{ url = url
, headers = [ Http.header "Origin" Shared.clientOrigin ]
, method = "PUT"
, timeout = Nothing
, tracker = Nothing
, body = body
, expect = expect
}
formatTime : Time.Posix -> String
formatTime ts =
DateFormat.format
[ DateFormat.monthNameFull
, DateFormat.text " "
, DateFormat.dayOfMonthSuffix
, DateFormat.text ", "
, DateFormat.yearNumber
]
Time.utc
ts