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,14 +15,15 @@ "elm/json": "1.1.3",

"elm/svg": "1.0.1", "elm/time": "1.0.0", "elm/url": "1.0.0", - "jweir/elm-iso8601": "7.0.1", + "rtfeldman/elm-iso8601-date-strings": "1.1.4", + "ryan-haskell/date-format": "1.0.0", "simonh1000/elm-jwt": "7.1.1" }, "indirect": { "danfishgold/base64-bytes": "1.1.0", "elm/bytes": "1.0.8", "elm/file": "1.0.5", - "elm/regex": "1.0.0", + "elm/parser": "1.1.0", "elm/virtual-dom": "1.0.3" } },
M web/src/Api/Note.elm

@@ -4,7 +4,7 @@ import Api

import Data.Note as Note exposing (CreateResponse, Metadata, Note) import Effect exposing (Effect) import Http -import ISO8601 +import Iso8601 import Json.Encode as E import Time exposing (Posix) import Url

@@ -43,8 +43,7 @@

else ( "expires_at" , options.expiresAt - |> ISO8601.fromPosix - |> ISO8601.toString + |> Iso8601.fromTime |> E.string ) ]
M web/src/Data/Me.elm

@@ -1,11 +1,13 @@

module Data.Me exposing (Me, decode) +import Iso8601 import Json.Decode as Decode exposing (Decoder) +import Time exposing (Posix) type alias Me = { email : String - , createdAt : String -- TODO: upgrade to elm/time + , createdAt : Posix }

@@ -13,4 +15,4 @@ decode : Decoder Me

decode = Decode.map2 Me (Decode.field "email" Decode.string) - (Decode.field "created_at" Decode.string) + (Decode.field "created_at" Iso8601.decoder)
M web/src/Data/Note.elm

@@ -1,6 +1,8 @@

module Data.Note exposing (CreateResponse, Metadata, Note, decode, decodeCreateResponse, decodeMetadata) +import Iso8601 import Json.Decode as D exposing (Decoder) +import Time exposing (Posix) type alias CreateResponse =

@@ -9,16 +11,15 @@

decodeCreateResponse : Decoder CreateResponse decodeCreateResponse = - D.map CreateResponse - (D.field "slug" D.string) + D.map CreateResponse (D.field "slug" D.string) type alias Note = { content : String - , readAt : Maybe String -- TODO: use Posix + , readAt : Maybe Posix , burnBeforeExpiration : Bool - , createdAt : String -- TODO: use Posix - , expiresAt : Maybe String -- TODO: use Posix + , createdAt : Posix + , expiresAt : Maybe Posix }

@@ -26,14 +27,14 @@ decode : Decoder Note

decode = D.map5 Note (D.field "content" D.string) - (D.maybe (D.field "read_at" D.string)) + (D.maybe (D.field "read_at" Iso8601.decoder)) (D.field "burn_before_expiration" D.bool) - (D.field "created_at" D.string) - (D.maybe (D.field "expires_at" D.string)) + (D.field "created_at" Iso8601.decoder) + (D.maybe (D.field "expires_at" Iso8601.decoder)) type alias Metadata = - { createdAt : String -- TODO: use Posix + { createdAt : Posix , hasPassword : Bool }

@@ -41,5 +42,5 @@

decodeMetadata : Decoder Metadata decodeMetadata = D.map2 Metadata - (D.field "created_at" D.string) + (D.field "created_at" Iso8601.decoder) (D.field "has_password" D.bool)
M web/src/Pages/Profile/Me.elm

@@ -10,6 +10,7 @@ import Layouts

import Page exposing (Page) import Route exposing (Route) import Shared +import Time.Format as T import View exposing (View)

@@ -91,8 +92,8 @@ Html.text (Api.errorMessage err)

viewUserDetails : Shared.Model -> Me -> Html Msg -viewUserDetails _ me = +viewUserDetails shared me = Html.div [] [ Html.p [] [ Html.text ("Email: " ++ me.email) ] - , Html.p [] [ Html.text ("Joined: " ++ me.createdAt) ] + , Html.p [] [ Html.text ("Joined: " ++ T.toString shared.timeZone me.createdAt) ] ]
M web/src/Pages/Secret/Slug_.elm

@@ -13,16 +13,18 @@ import Layouts

import Page exposing (Page) import Route exposing (Route) import Shared +import Time exposing (Zone) +import Time.Format as T import View exposing (View) page : Shared.Model -> Route { slug : String } -> Page Model Msg -page _ route = +page shared route = Page.new { init = init route.params.slug , update = update , subscriptions = subscriptions - , view = view + , view = view shared } |> Page.withLayout (\_ -> Layouts.Header {})

@@ -120,8 +122,8 @@

-- VIEW -view : Model -> View Msg -view model = +view : Shared.Model -> Model -> View Msg +view shared model = { title = "View note" , body = [ H.div

@@ -130,7 +132,7 @@ [ H.div

[ A.class "bg-white rounded-lg border border-gray-200 shadow-sm" ] (case model.metadata of Api.Success metadata -> - viewPage model.slug model.page metadata model.password + viewPage shared.timeZone model.slug model.page metadata model.password Api.Loading -> [ viewHeader { title = "View note", subtitle = "Loading note metadata..." }

@@ -151,8 +153,8 @@ ]

} -viewPage : String -> PageVariant -> Metadata -> Maybe String -> List (Html Msg) -viewPage slug variant metadata password = +viewPage : Zone -> String -> PageVariant -> Metadata -> Maybe String -> List (Html Msg) +viewPage zone slug variant metadata password = case variant of RequestNote -> [ viewHeader { title = "View note", subtitle = "Click the button below to view the note content" }

@@ -162,7 +164,7 @@

ShowNote apiResp -> case apiResp of Api.Success note -> - [ viewShowNoteHeader slug note + [ viewShowNoteHeader zone slug note , viewNoteContent note ]

@@ -194,8 +196,8 @@ , H.p [ A.class "text-gray-600 mt-2" ] [ H.text options.subtitle ]

] -viewShowNoteHeader : String -> Note -> Html Msg -viewShowNoteHeader slug note = +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" ]

@@ -214,11 +216,10 @@ [ 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: " ++ note.createdAt) ] + [ H.p [] [ H.text ("Created: " ++ T.toString zone note.createdAt) ] , case note.expiresAt of Just expiresAt -> - -- TODO: format time properly - H.p [] [ H.text ("Expires at: " ++ expiresAt) ] + H.p [] [ H.text ("Expires at: " ++ T.toString zone expiresAt) ] Nothing -> H.text ""
A web/src/Time/Format.elm

@@ -0,0 +1,25 @@

+module Time.Format exposing (toString) + +import DateFormat +import Time exposing (Posix, Zone) + + +{-| Formats a given `Posix` time and `Zone` into a human-readable string. + + toString zone posix + > "July 2nd, 2025 21:05" + +-} +toString : Zone -> Posix -> String +toString = + DateFormat.format + [ DateFormat.monthNameFull + , DateFormat.text " " + , DateFormat.dayOfMonthSuffix + , DateFormat.text ", " + , DateFormat.yearNumber + , DateFormat.text " " + , DateFormat.hourMilitaryFromOneNumber + , DateFormat.text ":" + , DateFormat.minuteFixed + ]
M web/tests/UnitTests/Data/Error.elm

@@ -17,5 +17,5 @@ "message": "some kind of an error"

} """ |> Json.decodeString Data.Error.decode - |> Expect.equal (Ok { message = "some kind of an error" }) + |> Expect.ok ]
M web/tests/UnitTests/Data/Note.elm

@@ -13,7 +13,7 @@ [ test "decodeCreateResponse"

(\_ -> "{\"slug\":\"the.note-slug\"}" |> D.decodeString Data.Note.decodeCreateResponse - |> Expect.equal (Ok { slug = "the.note-slug" }) + |> Expect.ok ) , test "decodeMetadata" (\_ ->

@@ -24,6 +24,6 @@ "has_password": false

} """ |> D.decodeString Data.Note.decodeMetadata - |> Expect.equal (Ok { createdAt = "2023-10-01T12:00:00Z", hasPassword = False }) + |> Expect.ok ) ]