all repos

onasty @ ef386ed

a one-time notes service
9 files changed, 65 insertions(+), 35 deletions(-)
web: pretty time (#157)

* web: change iso8601 libraries, and parse time from the api responses

* fix(web): unit tests

* feat(web): add time formatter helper func

* fix(web): display pretty time on pages

* refactor(web): update me decoder to use new way of time

* fixup! refactor(web): update me decoder to use new way of time
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/Error.elm
···
        17
        17
                         }

      
        18
        18
                         """

      
        19
        19
                             |> Json.decodeString Data.Error.decode

      
        20
        
        -                    |> Expect.equal (Ok { message = "some kind of an error" })

      
        
        20
        +                    |> Expect.ok

      
        21
        21
                 ]

      
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
                 ]