9 files changed,
65 insertions(+),
35 deletions(-)
Author:
Olexandr Smirnov
ss2316544@gmail.com
Committed by:
GitHub
noreply@github.com
Committed at:
2025-07-02 20:17:29 +0300
Parent:
8e14c68
M
web/elm.json
··· 15 15 "elm/svg": "1.0.1", 16 16 "elm/time": "1.0.0", 17 17 "elm/url": "1.0.0", 18 - "jweir/elm-iso8601": "7.0.1", 18 + "rtfeldman/elm-iso8601-date-strings": "1.1.4", 19 + "ryan-haskell/date-format": "1.0.0", 19 20 "simonh1000/elm-jwt": "7.1.1" 20 21 }, 21 22 "indirect": { 22 23 "danfishgold/base64-bytes": "1.1.0", 23 24 "elm/bytes": "1.0.8", 24 25 "elm/file": "1.0.5", 25 - "elm/regex": "1.0.0", 26 + "elm/parser": "1.1.0", 26 27 "elm/virtual-dom": "1.0.3" 27 28 } 28 29 },
M
web/src/Api/Note.elm
··· 4 4 import Data.Note as Note exposing (CreateResponse, Metadata, Note) 5 5 import Effect exposing (Effect) 6 6 import Http 7 -import ISO8601 7 +import Iso8601 8 8 import Json.Encode as E 9 9 import Time exposing (Posix) 10 10 import Url ··· 43 43 else 44 44 ( "expires_at" 45 45 , options.expiresAt 46 - |> ISO8601.fromPosix 47 - |> ISO8601.toString 46 + |> Iso8601.fromTime 48 47 |> E.string 49 48 ) 50 49 ]
M
web/src/Data/Me.elm
··· 1 1 module Data.Me exposing (Me, decode) 2 2 3 +import Iso8601 3 4 import Json.Decode as Decode exposing (Decoder) 5 +import Time exposing (Posix) 4 6 5 7 6 8 type alias Me = 7 9 { email : String 8 - , createdAt : String -- TODO: upgrade to elm/time 10 + , createdAt : Posix 9 11 } 10 12 11 13 ··· 13 15 decode = 14 16 Decode.map2 Me 15 17 (Decode.field "email" Decode.string) 16 - (Decode.field "created_at" Decode.string) 18 + (Decode.field "created_at" Iso8601.decoder)
M
web/src/Data/Note.elm
··· 1 1 module Data.Note exposing (CreateResponse, Metadata, Note, decode, decodeCreateResponse, decodeMetadata) 2 2 3 +import Iso8601 3 4 import Json.Decode as D exposing (Decoder) 5 +import Time exposing (Posix) 4 6 5 7 6 8 type alias CreateResponse = ··· 9 11 10 12 decodeCreateResponse : Decoder CreateResponse 11 13 decodeCreateResponse = 12 - D.map CreateResponse 13 - (D.field "slug" D.string) 14 + D.map CreateResponse (D.field "slug" D.string) 14 15 15 16 16 17 type alias Note = 17 18 { content : String 18 - , readAt : Maybe String -- TODO: use Posix 19 + , readAt : Maybe Posix 19 20 , burnBeforeExpiration : Bool 20 - , createdAt : String -- TODO: use Posix 21 - , expiresAt : Maybe String -- TODO: use Posix 21 + , createdAt : Posix 22 + , expiresAt : Maybe Posix 22 23 } 23 24 24 25 ··· 26 27 decode = 27 28 D.map5 Note 28 29 (D.field "content" D.string) 29 - (D.maybe (D.field "read_at" D.string)) 30 + (D.maybe (D.field "read_at" Iso8601.decoder)) 30 31 (D.field "burn_before_expiration" D.bool) 31 - (D.field "created_at" D.string) 32 - (D.maybe (D.field "expires_at" D.string)) 32 + (D.field "created_at" Iso8601.decoder) 33 + (D.maybe (D.field "expires_at" Iso8601.decoder)) 33 34 34 35 35 36 type alias Metadata = 36 - { createdAt : String -- TODO: use Posix 37 + { createdAt : Posix 37 38 , hasPassword : Bool 38 39 } 39 40 ··· 41 42 decodeMetadata : Decoder Metadata 42 43 decodeMetadata = 43 44 D.map2 Metadata 44 - (D.field "created_at" D.string) 45 + (D.field "created_at" Iso8601.decoder) 45 46 (D.field "has_password" D.bool)
M
web/src/Pages/Profile/Me.elm
··· 10 10 import Page exposing (Page) 11 11 import Route exposing (Route) 12 12 import Shared 13 +import Time.Format as T 13 14 import View exposing (View) 14 15 15 16 ··· 91 92 92 93 93 94 viewUserDetails : Shared.Model -> Me -> Html Msg 94 -viewUserDetails _ me = 95 +viewUserDetails shared me = 95 96 Html.div [] 96 97 [ Html.p [] [ Html.text ("Email: " ++ me.email) ] 97 - , Html.p [] [ Html.text ("Joined: " ++ me.createdAt) ] 98 + , Html.p [] [ Html.text ("Joined: " ++ T.toString shared.timeZone me.createdAt) ] 98 99 ]
M
web/src/Pages/Secret/Slug_.elm
··· 13 13 import Page exposing (Page) 14 14 import Route exposing (Route) 15 15 import Shared 16 +import Time exposing (Zone) 17 +import Time.Format as T 16 18 import View exposing (View) 17 19 18 20 19 21 page : Shared.Model -> Route { slug : String } -> Page Model Msg 20 -page _ route = 22 +page shared route = 21 23 Page.new 22 24 { init = init route.params.slug 23 25 , update = update 24 26 , subscriptions = subscriptions 25 - , view = view 27 + , view = view shared 26 28 } 27 29 |> Page.withLayout (\_ -> Layouts.Header {}) 28 30 ··· 120 122 -- VIEW 121 123 122 124 123 -view : Model -> View Msg 124 -view model = 125 +view : Shared.Model -> Model -> View Msg 126 +view shared model = 125 127 { title = "View note" 126 128 , body = 127 129 [ H.div ··· 130 132 [ A.class "bg-white rounded-lg border border-gray-200 shadow-sm" ] 131 133 (case model.metadata of 132 134 Api.Success metadata -> 133 - viewPage model.slug model.page metadata model.password 135 + viewPage shared.timeZone model.slug model.page metadata model.password 134 136 135 137 Api.Loading -> 136 138 [ viewHeader { title = "View note", subtitle = "Loading note metadata..." } ··· 151 153 } 152 154 153 155 154 -viewPage : String -> PageVariant -> Metadata -> Maybe String -> List (Html Msg) 155 -viewPage slug variant metadata password = 156 +viewPage : Zone -> String -> PageVariant -> Metadata -> Maybe String -> List (Html Msg) 157 +viewPage zone slug variant metadata password = 156 158 case variant of 157 159 RequestNote -> 158 160 [ viewHeader { title = "View note", subtitle = "Click the button below to view the note content" } ··· 162 164 ShowNote apiResp -> 163 165 case apiResp of 164 166 Api.Success note -> 165 - [ viewShowNoteHeader slug note 167 + [ viewShowNoteHeader zone slug note 166 168 , viewNoteContent note 167 169 ] 168 170 ··· 194 196 ] 195 197 196 198 197 -viewShowNoteHeader : String -> Note -> Html Msg 198 -viewShowNoteHeader slug note = 199 +viewShowNoteHeader : Zone -> String -> Note -> Html Msg 200 +viewShowNoteHeader zone slug note = 199 201 H.div [] 200 202 [ if note.burnBeforeExpiration then 201 203 H.div [ A.class "bg-orange-50 border-b border-orange-200 p-4" ] ··· 214 216 [ H.div [] 215 217 [ H.h1 [ A.class "text-2xl font-bold text-gray-900" ] [ H.text ("Note: " ++ slug) ] 216 218 , H.div [ A.class "text-sm text-gray-500 mt-2 space-y-1" ] 217 - [ H.p [] [ H.text ("Created: " ++ note.createdAt) ] 219 + [ H.p [] [ H.text ("Created: " ++ T.toString zone note.createdAt) ] 218 220 , case note.expiresAt of 219 221 Just expiresAt -> 220 - -- TODO: format time properly 221 - H.p [] [ H.text ("Expires at: " ++ expiresAt) ] 222 + H.p [] [ H.text ("Expires at: " ++ T.toString zone expiresAt) ] 222 223 223 224 Nothing -> 224 225 H.text ""
A
web/src/Time/Format.elm
··· 1 +module Time.Format exposing (toString) 2 + 3 +import DateFormat 4 +import Time exposing (Posix, Zone) 5 + 6 + 7 +{-| Formats a given `Posix` time and `Zone` into a human-readable string. 8 + 9 + toString zone posix 10 + > "July 2nd, 2025 21:05" 11 + 12 +-} 13 +toString : Zone -> Posix -> String 14 +toString = 15 + DateFormat.format 16 + [ DateFormat.monthNameFull 17 + , DateFormat.text " " 18 + , DateFormat.dayOfMonthSuffix 19 + , DateFormat.text ", " 20 + , DateFormat.yearNumber 21 + , DateFormat.text " " 22 + , DateFormat.hourMilitaryFromOneNumber 23 + , DateFormat.text ":" 24 + , DateFormat.minuteFixed 25 + ]
M
web/tests/UnitTests/Data/Note.elm
··· 13 13 (\_ -> 14 14 "{\"slug\":\"the.note-slug\"}" 15 15 |> D.decodeString Data.Note.decodeCreateResponse 16 - |> Expect.equal (Ok { slug = "the.note-slug" }) 16 + |> Expect.ok 17 17 ) 18 18 , test "decodeMetadata" 19 19 (\_ -> ··· 24 24 } 25 25 """ 26 26 |> D.decodeString Data.Note.decodeMetadata 27 - |> Expect.equal (Ok { createdAt = "2023-10-01T12:00:00Z", hasPassword = False }) 27 + |> Expect.ok 28 28 ) 29 29 ]