all repos

onasty @ f537564e35029c36ae25b3a91eb9d2023458585c

a one-time notes service
19 files changed, 247 insertions(+), 316 deletions(-)
web: general refactor (#158)

* refactor(web): unify input component

* web: make the input component even more complex

* refactor(web): update comments, type annotations, formatting

- the aim of the commit is to reduce the about of LOCs of the project

* web: dont show note password when typed in

* web: refactor how header is shown

* refactor(web): change way reminding time before user can resend the
veification email

* refactor web

* refactor(web): add viewMaybe, and viewIf helpers

* fixup! web: refactor how header is shown

* fixup! refactor(web): add viewMaybe, and viewIf helpers

* fixup! fixup! refactor(web): add viewMaybe, and viewIf helpers

* refactor(web): header couldn't care less

The header cares only if user is authorized, all other cases are
considered to be unauthorized state

* web: i like this formatting more

* web: there's no need in this doc comment

* refactor(web): serve svgs from static file, and not elm code

* add TODO comments
Author: Olexandr Smirnov ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-07-07 12:52:31 +0300
Parent: f6a62ba
M web/elm.json

@@ -12,7 +12,6 @@ "elm/core": "1.0.5",

"elm/html": "1.0.0", "elm/http": "2.0.0", "elm/json": "1.1.3", - "elm/svg": "1.0.1", "elm/time": "1.0.0", "elm/url": "1.0.0", "rtfeldman/elm-iso8601-date-strings": "1.1.4",
M web/src/Api/Auth.elm

@@ -63,10 +63,8 @@ }

-> Effect msg refreshToken options = let - body : Encode.Value body = - Encode.object - [ ( "refresh_token", Encode.string options.refreshToken ) ] + Encode.object [ ( "refresh_token", Encode.string options.refreshToken ) ] in Effect.sendApiRequest { endpoint = "/api/v1/auth/refresh-tokens"
M web/src/Api/Note.elm

@@ -21,16 +21,15 @@ }

-> Effect msg create options = let - encodeMaybe : Maybe a -> b -> (a -> E.Value) -> ( b, E.Value ) - encodeMaybe maybeData field value = - case maybeData of + encodeMaybe : Maybe a -> String -> (a -> E.Value) -> ( String, E.Value ) + encodeMaybe maybe field value = + case maybe of Just data -> ( field, value data ) Nothing -> ( field, E.null ) - body : E.Value body = E.object [ ( "content", E.string options.content )

@@ -41,11 +40,7 @@ , if options.expiresAt == Time.millisToPosix 0 then

( "expires_at", E.null ) else - ( "expires_at" - , options.expiresAt - |> Iso8601.fromTime - |> E.string - ) + ( "expires_at", options.expiresAt |> Iso8601.fromTime |> E.string ) ] in Effect.sendApiRequest
A web/src/Components/Form.elm

@@ -0,0 +1,61 @@

+module Components.Form exposing (input) + +import Html as H exposing (Html) +import Html.Attributes as A +import Html.Events as E + + +input : + -- TODO: add `error : Maybe String`, to input to show that field is not correct and message + { id : String + , field : field + , label : String + , type_ : String + , value : String + , placeholder : String + , required : Bool + , helpText : Maybe String + , prefix : Maybe String + , onInput : String -> msg + } + -> Html msg +input opts = + H.div [ A.class "space-y-2" ] + [ H.label + [ A.for opts.id + , A.class "block text-sm font-medium text-gray-700" + ] + [ H.text opts.label ] + , H.div + [ A.class + (if opts.prefix /= Nothing then + "flex items-center" + + else + "" + ) + ] + [ case opts.prefix of + Just prefix -> + H.span [ A.class "text-gray-500 text-md mr-2 whitespace-nowrap" ] [ H.text prefix ] + + Nothing -> + H.text "" + , H.input + [ A.class "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-black focus:border-transparent" + , A.type_ opts.type_ + , A.value opts.value + , A.id opts.id + , A.placeholder opts.placeholder + , A.required opts.required + , E.onInput opts.onInput + ] + [] + ] + , case opts.helpText of + Just help -> + H.p [ A.class "text-xs text-gray-500 mt-1" ] [ H.text help ] + + Nothing -> + H.text "" + ]
D

@@ -1,65 +0,0 @@

-module Components.Note exposing (noteIconSvg, noteNotFoundSvg, warningSvg) - -import Svg exposing (Svg) -import Svg.Attributes as A - - -noteIconSvg : Svg msg -noteIconSvg = - Svg.svg - [ A.class "w-8 h-8 text-gray-400" - , A.fill "none" - , A.stroke "currentColor" - , A.viewBox "0 0 24 24" - ] - [ Svg.path - [ A.d "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - , A.strokeWidth "2" - , A.strokeLinecap "round" - , A.strokeLinejoin "round" - ] - [] - ] - - -noteNotFoundSvg : Svg msg -noteNotFoundSvg = - Svg.svg - [ A.class "w-8 h-8 text-red-500" - , A.fill "none" - , A.stroke "currentColor" - , A.viewBox "0 0 24 24" - ] - [ Svg.path - [ A.d "M9.172 16.172a4 4 0 015.656 0M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - , A.strokeWidth "2" - , A.strokeLinecap "round" - , A.strokeLinejoin "round" - ] - [] - , Svg.path - [ A.d "M6 18L18 6M6 6l12 12" - , A.strokeWidth "2" - , A.strokeLinecap "round" - , A.strokeLinejoin "round" - ] - [] - ] - - -warningSvg : Svg msg -warningSvg = - Svg.svg - [ A.class "w-4 h-4 text-orange-600" - , A.fill "none" - , A.stroke "currentColor" - , A.viewBox "0 0 24 24" - ] - [ Svg.path - [ A.d "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z" - , A.strokeWidth "2" - , A.strokeLinecap "round" - , A.strokeLinejoin "round" - ] - [] - ]
A web/src/Components/Utils.elm

@@ -0,0 +1,28 @@

+module Components.Utils exposing (loadSvg, viewIf, viewMaybe) + +import Html as H exposing (Html) +import Html.Attributes as A + + +viewIf : Bool -> Html msg -> Html msg +viewIf condition html = + if condition then + html + + else + H.text "" + + +viewMaybe : Maybe a -> (a -> Html msg) -> Html msg +viewMaybe maybeValue toHtml = + case maybeValue of + Just value -> + toHtml value + + Nothing -> + H.text "" + + +loadSvg : { path : String, class : String } -> Html msg +loadSvg { path, class } = + H.img [ A.src ("/static/" ++ path), A.class class ] []
M web/src/Data/Credentials.elm

@@ -1,14 +1,4 @@

-module Data.Credentials exposing - ( Credentials - , decode - ) - -{-| - -@docs Credentials -@docs decode - --} +module Data.Credentials exposing (Credentials, decode) import Json.Decode as Decode exposing (Decoder)
M web/src/Effect.elm

@@ -1,31 +1,17 @@

module Effect exposing - ( Effect - , none, batch - , sendCmd, sendMsg - , pushRoute, replaceRoute - , pushRoutePath, replaceRoutePath - , loadExternalUrl, back - , sendApiRequest, refreshTokens - , sendToClipboard - , signin, logout, saveUser, clearUser + ( Effect, none, batch, sendCmd, sendMsg + , pushRoute, replaceRoute, pushRoutePath, replaceRoutePath, loadExternalUrl, back + , sendApiRequest, sendToClipboard + , signin, logout, refreshTokens, saveUser, clearUser , map, toCmd ) {-| -@docs Effect - -@docs none, batch -@docs sendCmd, sendMsg - -@docs pushRoute, replaceRoute -@docs pushRoutePath, replaceRoutePath -@docs loadExternalUrl, back - -@docs sendApiRequest, refreshTokens -@docs sendToClipboard -@docs signin, logout, saveUser, clearUser - +@docs Effect, none, batch, sendCmd, sendMsg +@docs pushRoute, replaceRoute, pushRoutePath, replaceRoutePath, loadExternalUrl, back +@docs sendApiRequest, sendToClipboard +@docs signin, logout, refreshTokens, saveUser, clearUser @docs map, toCmd -}

@@ -62,16 +48,13 @@ -- SHARED

| SendSharedMsg Shared.Msg.Msg | SendToLocalStorage { key : String, value : Json.Encode.Value } | SendToClipboard String - | SendApiRequest (HttpRequestDetails msg) - - -type alias HttpRequestDetails msg = - { endpoint : String - , method : String - , body : Http.Body - , decoder : Json.Decode.Decoder msg - , onHttpError : Api.Error -> msg - } + | SendApiRequest + { endpoint : String + , method : String + , body : Http.Body + , decoder : Json.Decode.Decoder msg + , onHttpError : Api.Error -> msg + }

@@ -230,9 +213,6 @@

-- INTERNALS -{-| Elm Land depends on this function to connect pages and layouts -together into the overall app. --} map : (msg1 -> msg2) -> Effect msg1 -> Effect msg2 map fn effect = case effect of

@@ -276,8 +256,6 @@ , onHttpError = \err -> fn (opts.onHttpError err)

} -{-| Elm Land depends on this function to perform your effects. --} toCmd : { key : Browser.Navigation.Key , url : Url

@@ -351,14 +329,14 @@

Err err -> opts.onHttpError err ) - (\resp -> fromHttpResponseToCustomError opts.decoder resp) + (\resp -> httpResponseToCustomError opts.decoder resp) , timeout = Just (1000 * 60) -- 60 second timeout , tracker = Nothing } -fromHttpResponseToCustomError : Json.Decode.Decoder msg -> Http.Response String -> Result Api.Error msg -fromHttpResponseToCustomError decoder response = +httpResponseToCustomError : Json.Decode.Decoder msg -> Http.Response String -> Result Api.Error msg +httpResponseToCustomError decoder response = case response of Http.GoodStatus_ _ body -> case
M web/src/ExpirationOptions.elm

@@ -1,11 +1,7 @@

-module ExpirationOptions exposing (ExpiresAt, expirationOptions) - - -type alias ExpiresAt = - { text : String, value : Int } +module ExpirationOptions exposing (expirationOptions) -expirationOptions : List ExpiresAt +expirationOptions : List { text : String, value : Int } expirationOptions = [ { text = "Never expires (default)", value = 0 } , { text = "1 hour", value = 60 * 60 * 1000 }
M web/src/JwtUtil.elm

@@ -9,11 +9,9 @@ -}

isExpired : Time.Posix -> String -> Bool isExpired now token = let - expirationThreshold : number expirationThreshold = 40 * 1000 - timeDiff : Int timeDiff = getTokenExpiration token |> (\expiration -> expiration - Time.posixToMillis now)

@@ -21,10 +19,6 @@ in

timeDiff <= expirationThreshold -{-| Extracts the expiration time (in millis) from a JWT token. -Returns 0 if cannot parse token. --} getTokenExpiration : String -> Int getTokenExpiration token = - Jwt.getTokenExpirationMillis token - |> Result.withDefault 0 + Jwt.getTokenExpirationMillis token |> Result.withDefault 0
M web/src/Layouts/Header.elm

@@ -93,44 +93,26 @@

viewNav : Auth.User.SignInStatus -> List (Html Msg) viewNav user = + let + viewLink text path = + H.a [ A.class "text-gray-600 hover:text-black transition-colors", Route.Path.href path ] + [ H.text text ] + in case user of Auth.User.SignedIn _ -> - viewSignedInNav - - Auth.User.NotSignedIn -> - viewNotSignedInNav - - Auth.User.RefreshingTokens -> - viewNotSignedInNav - - -viewSignedInNav : List (Html Msg) -viewSignedInNav = - [ viewLink "Profile" Route.Path.Profile_Me - , H.button - [ A.class "text-gray-600 hover:text-red-600 transition-colors" - , E.onClick UserClickedLogout - ] - [ H.text "Logout" ] - ] - - -viewNotSignedInNav : List (Html Msg) -viewNotSignedInNav = - -- TODO: or add about page, or delete the link - [ viewLink "About" Route.Path.Home_ - , H.a - [ A.class "px-4 py-2 border border-gray-300 rounded-md text-black hover:bg-gray-50 transition-colors" - , Route.Path.href Route.Path.Auth - ] - [ H.text "Sign In/Up" ] - ] + [ viewLink "Profile" Route.Path.Profile_Me + , H.button + [ A.class "text-gray-600 hover:text-red-600 transition-colors" + , E.onClick UserClickedLogout + ] + [ H.text "Logout" ] + ] - -viewLink : String -> Route.Path.Path -> Html Msg -viewLink text path = - H.a - [ A.class "text-gray-600 hover:text-black transition-colors" - , Route.Path.href path - ] - [ H.text text ] + _ -> + [ viewLink "About" Route.Path.Home_ -- TODO: or add about page, or delete the link + , H.a + [ A.class "px-4 py-2 border border-gray-300 rounded-md text-black hover:bg-gray-50 transition-colors" + , Route.Path.href Route.Path.Auth + ] + [ H.text "Sign In/Up" ] + ]
M web/src/Pages/Auth.elm

@@ -4,6 +4,8 @@ import Api

import Api.Auth import Auth.User import Components.Error +import Components.Form +import Components.Utils import Data.Credentials exposing (Credentials) import Effect exposing (Effect) import Html as H exposing (Html)

@@ -142,6 +144,7 @@ ApiSignInResponded (Ok credentials) ->

( { model | isSubmittingForm = False }, Effect.signin credentials ) ApiSignInResponded (Err error) -> + -- TODO: check if error is Unauthorized and prompt use to activate account ( { model | isSubmittingForm = False, apiError = Just error }, Effect.none ) ApiSignUpResponded (Ok ()) ->

@@ -225,14 +228,10 @@ timeLeftSeconds =

case ( now, lastClicked ) of ( Just now_, Just last ) -> let - remainingMs = - 30 * 1000 - (Time.posixToMillis now_ - Time.posixToMillis last) + elapsedMs = + Time.posixToMillis now_ - Time.posixToMillis last in - if remainingMs > 0 then - remainingMs // 1000 - - else - 0 + max 0 ((30 * 1000 - elapsedMs) // 1000) _ -> 0

@@ -250,17 +249,11 @@ , E.onClick UserClickedResendActivationEmail

, A.disabled (not canClick) ] [ H.text "Resend verification email" ] - , if canClick then - H.text "" - - else - H.p [ A.class "text-gray-600 text-xs mt-2" ] - [ H.text - ("You can request a new verification email in " - ++ String.fromInt timeLeftSeconds - ++ " seconds." - ) - ] + , Components.Utils.viewIf (not canClick) + (H.p + [ A.class "text-gray-600 text-xs mt-2" ] + [ H.text ("You can request a new verification email in " ++ String.fromInt timeLeftSeconds ++ " seconds.") ] + ) ]

@@ -284,11 +277,11 @@

viewChangeVariant : Variant -> Html Msg viewChangeVariant variant = let - base = - "flex-1 px-4 py-2 rounded-md font-medium transition-colors" - - buttonClasses : Bool -> String buttonClasses active = + let + base = + "flex-1 px-4 py-2 rounded-md font-medium transition-colors" + in if active then base ++ " bg-black text-white"

@@ -336,22 +329,18 @@

viewFormInput : { field : Field, value : String } -> Html Msg viewFormInput opts = - H.div [ A.class "space-y-2" ] - [ H.label - [ A.class "block text-sm font-medium text-gray-700" ] - [ H.text (fromFieldToLabel opts.field) ] - , H.div [] - [ H.input - [ A.class "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-black focus:border-transparent" - , A.type_ (fromFieldToInputType opts.field) - , A.value opts.value - , A.placeholder (fromFieldToLabel opts.field) - , A.required True - , E.onInput (UserUpdatedInput opts.field) - ] - [] - ] - ] + Components.Form.input + { id = fromFieldToInputType opts.field + , field = opts.field + , label = fromFieldToLabel opts.field + , type_ = fromFieldToInputType opts.field + , value = opts.value + , placeholder = fromFieldToLabel opts.field + , required = True + , onInput = UserUpdatedInput opts.field + , helpText = Nothing + , prefix = Nothing + } viewForgotPassword : Html Msg
M web/src/Pages/Home_.elm

@@ -3,6 +3,8 @@

import Api import Api.Note import Components.Error +import Components.Form +import Components.Utils import Data.Note as Note import Effect exposing (Effect) import ExpirationOptions exposing (expirationOptions)

@@ -209,12 +211,7 @@ [ H.div [ A.class "w-full max-w-4xl mx-auto" ]

[ H.div [ A.class "bg-white rounded-lg border border-gray-200 shadow-sm" ] [ viewHeader model.pageVariant , H.div [ A.class "p-6 space-y-6" ] - [ case model.apiError of - Just error -> - Components.Error.error (Api.errorMessage error) - - Nothing -> - H.text "" + [ Components.Utils.viewMaybe model.apiError (\e -> Components.Error.error (Api.errorMessage e)) , case model.pageVariant of CreateNote -> viewCreateNoteForm model shared.appURL

@@ -257,23 +254,31 @@ [ E.onSubmit UserClickedSubmit

, A.class "space-y-6" ] [ viewTextarea - , viewFormInput - { field = Slug + , Components.Form.input + { id = "slug" + , field = Slug , label = "Custom URL Slug (optional)" , placeholder = "my-unique-slug" , type_ = "text" - , help = "Leave empty to generate a random slug" + , helpText = Just "Leave empty to generate a random slug" , prefix = Just (secretUrl appUrl "") + , onInput = UserUpdatedInput Slug + , required = False + , value = Maybe.withDefault "" model.slug } , H.div [ A.class "grid grid-cols-1 md:grid-cols-2 gap-6" ] [ H.div [ A.class "space-y-6" ] - [ viewFormInput - { field = Password + [ Components.Form.input + { id = "password" + , field = Password , label = "Password Protection (optional)" - , type_ = "text" + , type_ = "password" , placeholder = "Enter password to protect this paste" - , help = "Viewers will need this password to access the paste" + , helpText = Just "Viewers will need this password to access the paste" , prefix = Nothing + , onInput = UserUpdatedInput Password + , required = False + , value = Maybe.withDefault "" model.password } ] , H.div [ A.class "space-y-6" ]

@@ -302,34 +307,6 @@ , A.rows 20

, A.class "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-black focus:border-transparent resize-vertical font-mono text-sm" ] [] - ] - - -viewFormInput : { field : Field, label : String, placeholder : String, type_ : String, prefix : Maybe String, help : String } -> Html Msg -viewFormInput options = - H.div [ A.class "space-y-2" ] - [ H.label - [ A.for (fromFieldToName options.field) - , A.class "block text-sm font-medium text-gray-700 mb-2" - ] - [ H.text options.label ] - , H.div [ A.class "flex items-center" ] - [ case options.prefix of - Just prefix -> - H.span [ A.class "text-gray-500 text-md mr-2 whitespace-nowrap" ] [ H.text prefix ] - - Nothing -> - H.text "" - , H.input - [ E.onInput (UserUpdatedInput options.field) - , A.id (fromFieldToName options.field) - , A.type_ options.type_ - , A.placeholder options.placeholder - , A.class "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-black focus:border-transparent" - ] - [] - ] - , H.p [ A.class "text-xs text-gray-500 mt-1" ] [ H.text options.help ] ]
M web/src/Pages/Secret/Slug_.elm

@@ -3,7 +3,7 @@

import Api import Api.Note import Components.Error -import Components.Note +import Components.Utils import Data.Note exposing (Metadata, Note) import Effect exposing (Effect) import Html as H exposing (Html)

@@ -199,30 +199,23 @@

viewShowNoteHeader : Zone -> String -> Note -> Html Msg viewShowNoteHeader zone slug note = H.div [] - [ if note.burnBeforeExpiration then - H.div [ A.class "bg-orange-50 border-b border-orange-200 p-4" ] + [ Components.Utils.viewIf note.burnBeforeExpiration + (H.div [ A.class "bg-orange-50 border-b border-orange-200 p-4" ] [ H.div [ A.class "flex items-center gap-3" ] [ H.div [ A.class "w-6 h-6 bg-orange-100 rounded-full flex items-center justify-center flex-shrink-0" ] - [ Components.Note.warningSvg ] + [ Components.Utils.loadSvg { path = "warning.svg", class = "w-4 h-4 text-orange-600" } ] , H.p [ A.class "text-orange-800 text-sm font-medium" ] [ H.text "This note was destroyed. If you need to keep it, copy it before closing this window." ] ] ] - - else - H.text "" + ) , H.div [ A.class "p-6 pb-4 border-b border-gray-200" ] [ H.div [ A.class "flex justify-between items-start" ] [ H.div [] [ H.h1 [ A.class "text-2xl font-bold text-gray-900" ] [ H.text ("Note: " ++ slug) ] , H.div [ A.class "text-sm text-gray-500 mt-2 space-y-1" ] [ H.p [] [ H.text ("Created: " ++ T.toString zone note.createdAt) ] - , case note.expiresAt of - Just expiresAt -> - H.p [] [ H.text ("Expires at: " ++ T.toString zone expiresAt) ] - - Nothing -> - H.text "" + , Components.Utils.viewMaybe note.expiresAt (\n -> H.p [] [ H.text ("Expires at: " ++ T.toString zone n) ]) ] ] , H.div [ A.class "flex gap-2" ]

@@ -246,7 +239,7 @@ viewNoteNotFound slug =

H.div [ A.class "p-6" ] [ H.div [ A.class "text-center py-12" ] [ H.div [ A.class "w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4" ] - [ Components.Note.noteNotFoundSvg ] + [ Components.Utils.loadSvg { path = "note-not-found.svg", class = "w-8 h-8 text-red-500" } ] , H.h2 [ A.class "text-xl font-semibold text-gray-900 mb-2" ] [ H.text ("Note " ++ slug ++ " Not Found") ] , H.div [ A.class "text-gray-600 mb-6 space-y-2" ]

@@ -265,13 +258,7 @@ ]

] -viewOpenNote : - { slug : String - , hasPassword : Bool - , isLoading : Bool - , password : Maybe String - } - -> Html Msg +viewOpenNote : { slug : String, hasPassword : Bool, isLoading : Bool, password : Maybe String } -> Html Msg viewOpenNote opts = let isDisabled =

@@ -295,7 +282,7 @@ H.div [ A.class "p-6" ]

[ H.div [ A.class "text-center py-12" ] [ H.div [ A.class "mb-6" ] [ H.div [ A.class "w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4" ] - [ Components.Note.noteIconSvg ] + [ Components.Utils.loadSvg { path = "note-icon.svg", class = "w-8 h-8 text-gray-400" } ] , H.h2 [ A.class "text-lg font-semibold text-gray-900 mb-2" ] [ H.text opts.slug ] , H.p [ A.class "text-gray-600 mb-6" ] [ H.text "You're about read and destroy the note." ] ]

@@ -303,8 +290,8 @@ , H.form

[ E.onSubmit UserClickedViewNote , A.class "max-w-sm mx-auto space-y-4" ] - [ if opts.hasPassword then - H.div + [ Components.Utils.viewIf opts.hasPassword + (H.div [ A.class "space-y-2" ] [ H.label [ A.class "block text-sm font-medium text-gray-700 text-left" ]

@@ -315,9 +302,7 @@ , A.class "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-black focus:border-transparent"

] [] ] - - else - H.text "" + ) , H.button [ A.class buttonData.class , A.type_ "submit"
M web/src/Shared.elm

@@ -1,20 +1,7 @@

-module Shared exposing - ( Flags, decoder - , Model, Msg - , init, update, subscriptions - ) - -{-| - -@docs Flags, decoder -@docs Model, Msg -@docs init, update, subscriptions - --} +module Shared exposing (Flags, Model, Msg, decoder, init, subscriptions, update) import Api.Auth import Auth.User -import Data.Credentials exposing (Credentials) import Dict import Effect exposing (Effect) import Json.Decode

@@ -57,18 +44,14 @@

init : Result Json.Decode.Error Flags -> Route () -> ( Model, Effect Msg ) init flagsResult _ = let - flags : Flags flags = flagsResult |> Result.withDefault { accessToken = Nothing, refreshToken = Nothing, appUrl = "" } - maybeCredentials : Maybe Credentials maybeCredentials = - Maybe.map2 - (\access refresh -> { accessToken = access, refreshToken = refresh }) + Maybe.map2 (\access refresh -> { accessToken = access, refreshToken = refresh }) flags.accessToken flags.refreshToken - user : Auth.User.SignInStatus user = case maybeCredentials of Just credentials ->

@@ -76,15 +59,11 @@ Auth.User.SignedIn credentials

Nothing -> Auth.User.NotSignedIn - - initModel : Model - initModel = - { user = user - , timeZone = Time.utc - , appURL = flags.appUrl - } in - ( initModel + ( { user = user + , timeZone = Time.utc + , appURL = flags.appUrl + } , Effect.batch [ Time.now |> Task.perform Shared.Msg.CheckTokenExpiration |> Effect.sendCmd , Time.here |> Task.perform Shared.Msg.GotZone |> Effect.sendCmd
M web/src/Shared/Msg.elm

@@ -2,15 +2,15 @@ module Shared.Msg exposing (Msg(..))

import Api import Data.Credentials exposing (Credentials) -import Time +import Time exposing (Posix, Zone) type Msg - = GotZone Time.Zone + = GotZone Zone -- Auth | Logout | SignedIn Credentials -- Session - | CheckTokenExpiration Time.Posix + | CheckTokenExpiration Posix | TriggerTokenRefresh | ApiRefreshTokensResponded (Result Api.Error Credentials)
A web/static/note-icon.svg

@@ -0,0 +1,13 @@

+<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" +> + <path + d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + /> +</svg>
A web/static/note-not-found.svg

@@ -0,0 +1,19 @@

+<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" +> + <path + d="M9.172 16.172a4 4 0 015.656 0M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + /> + <path + d="M6 18L18 6M6 6l12 12" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + /> +</svg>
A web/static/warning.svg

@@ -0,0 +1,13 @@

+<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" +> + <path + d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + /> +</svg>