all repos

onasty @ a2448ac60de5edfe7aac89ddbcf47901d19ac3ed

a one-time notes service
17 files changed, 309 insertions(+), 323 deletions(-)
web: button component; improve code consistency (#168)

* web: move repeating classes into container component

* web: rename module

* web: improve some code consistency

* web: remove link to about page

* better naming

* fixup! better naming

* linters happy good

* after rebase fixes

* chore: run frontend tests in general test suite

* web: reformat json input in tests

* web: refactor to generic button component main page

* web: add support for custom classes

* web: dont show the slug of creted note if there was an error

* web: refactor the button component

* web: refactor the submit buttons

* web: get the copy url button back

* web: fix the submitButton; refactor the view button

* web: refactor all buttons in the whole app

* web: refactor component

* refactor(web): use some tailwind utils that i didn't know existed

* web: apperantly i dont need that complex button component

* fixup! web: apperantly i dont need that complex button component

* web: rename button styles

* web: add build task

* web: i kinda looks better

* some more places where i f-ed up

* web: fix the SecondaryDisabled button style

* web: this naming makes more seance

* chore: web lint frontend too

* web: fix naming

* fixup! web: improve some code consistency
Author: Olexandr Smirnov ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-07-31 15:49:21 +0300
Parent: c3cd668
M Taskfile.yml
···
        22
        22
         

      
        23
        23
           lint:

      
        24
        24
             - golangci-lint run

      
        
        25
        +    - task: frontend:lint

      
        25
        26
         

      
        26
        27
           docker:up:

      
        27
        28
             - docker compose up -d --build --remove-orphans

      ···
        36
        37
           test:

      
        37
        38
             - task: test:unit

      
        38
        39
             - task: test:e2e

      
        
        40
        +    - task: frontend:test

      
        39
        41
         

      
        40
        42
           test:unit:

      
        41
        43
             - '{{.gotest}} --count=1 -v --short ./...'

      
M web/Taskfile.yml
···
        20
        20
           dev:

      
        21
        21
             desc: runs elm-land dev server

      
        22
        22
             cmd: bunx elm-land server

      
        
        23
        +

      
        
        24
        +  build:

      
        
        25
        +    desc: runs elm-land build

      
        
        26
        +    cmd: bunx elm-land build

      
M web/src/Api.elm
···
        4
        4
         import Json.Decode

      
        5
        5
         

      
        6
        6
         

      
        
        7
        +type Response value

      
        
        8
        +    = Loading

      
        
        9
        +    | Success value

      
        
        10
        +    | Failure Error

      
        
        11
        +

      
        
        12
        +

      
        7
        13
         type Error

      
        8
        14
             = HttpError

      
        9
        15
                 { message : String

      ···
        13
        19
                 { message : String

      
        14
        20
                 , reason : Json.Decode.Error

      
        15
        21
                 }

      
        16
        
        -

      
        17
        
        -

      
        18
        
        -type Response value

      
        19
        
        -    = Loading

      
        20
        
        -    | Success value

      
        21
        
        -    | Failure Error

      
        22
        22
         

      
        23
        23
         

      
        24
        24
         errorMessage : Error -> String

      
M web/src/Api/Auth.elm
···
        16
        16
             -> Effect msg

      
        17
        17
         signin options =

      
        18
        18
             let

      
        19
        
        -        body : Encode.Value

      
        20
        19
                 body =

      
        21
        20
                     Encode.object

      
        22
        21
                         [ ( "email", Encode.string options.email )

      ···
        40
        39
             -> Effect msg

      
        41
        40
         signup options =

      
        42
        41
             let

      
        43
        
        -        body : Encode.Value

      
        44
        42
                 body =

      
        45
        43
                     Encode.object

      
        46
        44
                         [ ( "email", Encode.string options.email )

      ···
        56
        54
                 }

      
        57
        55
         

      
        58
        56
         

      
        59
        
        -refreshToken :

      
        60
        
        -    { onResponse : Result Api.Error Credentials -> msg

      
        61
        
        -    , refreshToken : String

      
        62
        
        -    }

      
        63
        
        -    -> Effect msg

      
        
        57
        +refreshToken : { onResponse : Result Api.Error Credentials -> msg, refreshToken : String } -> Effect msg

      
        64
        58
         refreshToken options =

      
        65
        
        -    let

      
        66
        
        -        body =

      
        67
        
        -            Encode.object [ ( "refresh_token", Encode.string options.refreshToken ) ]

      
        68
        
        -    in

      
        69
        59
             Effect.sendApiRequest

      
        70
        60
                 { endpoint = "/api/v1/auth/refresh-tokens"

      
        71
        61
                 , method = "POST"

      
        72
        
        -        , body = Http.jsonBody body

      
        
        62
        +        , body = Encode.object [ ( "refresh_token", Encode.string options.refreshToken ) ] |> Http.jsonBody

      
        73
        63
                 , onResponse = options.onResponse

      
        74
        64
                 , decoder = Credentials.decode

      
        75
        65
                 }

      ···
        99
        89
         

      
        100
        90
         resendVerificationEmail : { onResponse : Result Api.Error () -> msg, email : String } -> Effect msg

      
        101
        91
         resendVerificationEmail options =

      
        102
        
        -    let

      
        103
        
        -        body =

      
        104
        
        -            Encode.object [ ( "email", Encode.string options.email ) ]

      
        105
        
        -    in

      
        106
        92
             Effect.sendApiRequest

      
        107
        93
                 { endpoint = "/api/v1/auth/resend-verification-email"

      
        108
        94
                 , method = "POST"

      
        109
        
        -        , body = Http.jsonBody body

      
        
        95
        +        , body = Encode.object [ ( "email", Encode.string options.email ) ] |> Http.jsonBody

      
        110
        96
                 , onResponse = options.onResponse

      
        111
        97
                 , decoder = Decode.succeed ()

      
        112
        98
                 }

      
M web/src/Api/Note.elm
···
        20
        20
             -> Effect msg

      
        21
        21
         create options =

      
        22
        22
             let

      
        23
        
        -        encodeMaybe : Maybe a -> String -> (a -> E.Value) -> ( String, E.Value )

      
        24
        
        -        encodeMaybe maybe field value =

      
        
        23
        +        encodeMaybe : String -> (a -> E.Value) -> Maybe a -> ( String, E.Value )

      
        
        24
        +        encodeMaybe field value maybe =

      
        25
        25
                     case maybe of

      
        26
        26
                         Just data ->

      
        27
        27
                             ( field, value data )

      ···
        32
        32
                 body =

      
        33
        33
                     E.object

      
        34
        34
                         [ ( "content", E.string options.content )

      
        35
        
        -                , encodeMaybe options.slug "slug" E.string

      
        36
        
        -                , encodeMaybe options.password "password" E.string

      
        
        35
        +                , encodeMaybe "slug" E.string options.slug

      
        
        36
        +                , encodeMaybe "password" E.string options.password

      
        37
        37
                         , ( "burn_before_expiration", E.bool options.burnBeforeExpiration )

      
        38
        38
                         , if options.expiresAt == Time.millisToPosix 0 then

      
        39
        39
                             ( "expires_at", E.null )

      
M web/src/Auth.elm
···
        13
        13
             Auth.User.User

      
        14
        14
         

      
        15
        15
         

      
        16
        
        -{-| Called before an auth-only page is loaded.

      
        17
        
        --}

      
        18
        16
         onPageLoad : Shared.Model -> Route () -> Auth.Action.Action User

      
        19
        17
         onPageLoad shared _ =

      
        20
        18
             case shared.user of

      ···
        32
        30
                     Auth.Action.loadPageWithUser credentials

      
        33
        31
         

      
        34
        32
         

      
        35
        
        -{-| Renders whenever `Auth.Action.loadCustomPage` is returned from `onPageLoad`.

      
        36
        
        --}

      
        37
        33
         viewCustomPage : Shared.Model -> Route () -> View Never

      
        38
        34
         viewCustomPage _ _ =

      
        39
        35
             View.fromString "Loading..."

      
M web/src/Components/Form.elm
···
        1
        
        -module Components.Form exposing (input)

      
        
        1
        +module Components.Form exposing (ButtonStyle(..), CanBeClicked, button, input, submitButton)

      
        2
        2
         

      
        3
        3
         import Html as H exposing (Html)

      
        4
        4
         import Html.Attributes as A

      
        5
        5
         import Html.Events as E

      
        6
        6
         

      
        7
        7
         

      
        
        8
        +

      
        
        9
        +-- INPUT

      
        
        10
        +

      
        
        11
        +

      
        8
        12
         input :

      
        9
        
        -    -- TODO: add `error : Maybe String`, to input to show that field is not correct and message

      
        
        13
        +    -- TODO: add `error : Maybe String`, to show that field is not correct and message

      
        10
        14
             { id : String

      
        11
        15
             , field : field

      
        12
        16
             , label : String

      ···
        59
        63
                     Nothing ->

      
        60
        64
                         H.text ""

      
        61
        65
                 ]

      
        
        66
        +

      
        
        67
        +

      
        
        68
        +

      
        
        69
        +-- BUTTON

      
        
        70
        +

      
        
        71
        +

      
        
        72
        +type alias CanBeClicked =

      
        
        73
        +    Bool

      
        
        74
        +

      
        
        75
        +

      
        
        76
        +type ButtonStyle

      
        
        77
        +    = Primary CanBeClicked

      
        
        78
        +    | Secondary CanBeClicked

      
        
        79
        +    | SecondaryDisabled CanBeClicked

      
        
        80
        +    | SecondaryDanger

      
        
        81
        +

      
        
        82
        +

      
        
        83
        +button : { text : String, disabled : Bool, onClick : msg, style : ButtonStyle } -> Html msg

      
        
        84
        +button opts =

      
        
        85
        +    H.button

      
        
        86
        +        [ A.type_ "button"

      
        
        87
        +        , E.onClick opts.onClick

      
        
        88
        +        , A.class (buttonStyleToClass opts.style "")

      
        
        89
        +        , A.disabled opts.disabled

      
        
        90
        +        ]

      
        
        91
        +        [ H.text opts.text ]

      
        
        92
        +

      
        
        93
        +

      
        
        94
        +submitButton : { text : String, disabled : Bool, class : String, style : ButtonStyle } -> Html msg

      
        
        95
        +submitButton opts =

      
        
        96
        +    H.button

      
        
        97
        +        [ A.type_ "submit"

      
        
        98
        +        , A.class (buttonStyleToClass opts.style opts.class)

      
        
        99
        +        , A.disabled opts.disabled

      
        
        100
        +        ]

      
        
        101
        +        [ H.text opts.text ]

      
        
        102
        +

      
        
        103
        +

      
        
        104
        +buttonStyleToClass : ButtonStyle -> String -> String

      
        
        105
        +buttonStyleToClass style appendClasses =

      
        
        106
        +    case style of

      
        
        107
        +        Primary canBeClicked ->

      
        
        108
        +            getButtonClasses canBeClicked

      
        
        109
        +                appendClasses

      
        
        110
        +                "px-6 py-2 bg-gray-300 text-gray-500 rounded-md cursor-not-allowed transition-colors"

      
        
        111
        +                "px-6 py-2 bg-black text-white rounded-md hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2 transition-colors"

      
        
        112
        +

      
        
        113
        +        SecondaryDanger ->

      
        
        114
        +            "text-gray-600 hover:text-red-600 transition-colors"

      
        
        115
        +

      
        
        116
        +        Secondary canBeClicked ->

      
        
        117
        +            getButtonClasses canBeClicked

      
        
        118
        +                appendClasses

      
        
        119
        +                "px-4 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2 transition-colors bg-green-100 border-green-300 text-green-700"

      
        
        120
        +                "px-4 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2 transition-colors border-gray-300 text-gray-700 hover:bg-gray-50"

      
        
        121
        +

      
        
        122
        +        SecondaryDisabled canBeClicked ->

      
        
        123
        +            getButtonClasses canBeClicked

      
        
        124
        +                appendClasses

      
        
        125
        +                "w-full px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2 transition-colors mt-3 border border-gray-300 text-gray-400 cursor-not-allowed"

      
        
        126
        +                "w-full px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2 transition-colors mt-3 border border-gray-300 text-gray-700 hover:bg-gray-50"

      
        
        127
        +

      
        
        128
        +

      
        
        129
        +getButtonClasses : Bool -> String -> String -> String -> String

      
        
        130
        +getButtonClasses cond extend whenTrue whenFalse =

      
        
        131
        +    let

      
        
        132
        +        cls =

      
        
        133
        +            if String.isEmpty extend then

      
        
        134
        +                ""

      
        
        135
        +

      
        
        136
        +            else

      
        
        137
        +                " " ++ extend

      
        
        138
        +    in

      
        
        139
        +    if cond then

      
        
        140
        +        whenTrue ++ cls

      
        
        141
        +

      
        
        142
        +    else

      
        
        143
        +        whenFalse ++ cls

      
A web/src/Components/Icon.elm
···
        
        1
        +module Components.Icon exposing (IconType(..), view)

      
        
        2
        +

      
        
        3
        +import Html as H exposing (Html)

      
        
        4
        +import Html.Attributes as A

      
        
        5
        +

      
        
        6
        +

      
        
        7
        +type IconType

      
        
        8
        +    = NoteIcon

      
        
        9
        +    | NotFound

      
        
        10
        +    | Warning

      
        
        11
        +

      
        
        12
        +

      
        
        13
        +view : IconType -> String -> Html msg

      
        
        14
        +view t cls =

      
        
        15
        +    let

      
        
        16
        +        getHtml img =

      
        
        17
        +            H.img [ A.src ("/static/" ++ img ++ ".svg"), A.class cls ] []

      
        
        18
        +    in

      
        
        19
        +    case t of

      
        
        20
        +        NoteIcon ->

      
        
        21
        +            getHtml "note-icon"

      
        
        22
        +

      
        
        23
        +        NotFound ->

      
        
        24
        +            getHtml "note-not-found"

      
        
        25
        +

      
        
        26
        +        Warning ->

      
        
        27
        +            getHtml "warning"

      
M web/src/Components/Utils.elm
···
        1
        
        -module Components.Utils exposing (loadSvg, viewIf, viewMaybe)

      
        
        1
        +module Components.Utils exposing (commonContainer, viewIf, viewMaybe)

      
        2
        2
         

      
        3
        3
         import Html as H exposing (Html)

      
        4
        4
         import Html.Attributes as A

      ···
        23
        23
                     H.text ""

      
        24
        24
         

      
        25
        25
         

      
        26
        
        -loadSvg : { path : String, class : String } -> Html msg

      
        27
        
        -loadSvg { path, class } =

      
        28
        
        -    H.img [ A.src ("/static/" ++ path), A.class class ] []

      
        
        26
        +commonContainer : List (Html msg) -> Html msg

      
        
        27
        +commonContainer child =

      
        
        28
        +    H.div [ A.class "py-8 w-full max-w-4xl mx-auto " ]

      
        
        29
        +        [ H.div [ A.class "rounded-lg border border-gray-200 shadow-sm" ] child ]

      
M web/src/Effect.elm
···
        1
        1
         module Effect exposing

      
        2
        
        -    ( Effect, none, batch, sendCmd, sendMsg

      
        
        2
        +    ( Effect, none, batch, map, toCmd, sendCmd, sendMsg

      
        3
        3
             , pushRoute, replaceRoute, pushRoutePath, replaceRoutePath, loadExternalUrl, back

      
        4
        4
             , sendApiRequest, sendToClipboard

      
        5
        5
             , signin, logout, refreshTokens, saveUser, clearUser

      
        6
        
        -    , map, toCmd

      
        7
        6
             )

      
        8
        7
         

      
        9
        8
         {-|

      
        10
        9
         

      
        11
        
        -@docs Effect, none, batch, sendCmd, sendMsg

      
        
        10
        +@docs Effect, none, batch, map, toCmd, sendCmd, sendMsg

      
        12
        11
         @docs pushRoute, replaceRoute, pushRoutePath, replaceRoutePath, loadExternalUrl, back

      
        13
        12
         @docs sendApiRequest, sendToClipboard

      
        14
        13
         @docs signin, logout, refreshTokens, saveUser, clearUser

      
        15
        
        -@docs map, toCmd

      
        16
        14
         

      
        17
        15
         -}

      
        18
        16
         

      ···
        355
        353
                             Err (Api.JsonDecodeError { message = "Failed to decode response", reason = err })

      
        356
        354
         

      
        357
        355
                 Http.BadStatus_ { statusCode } body ->

      
        358
        
        -            case body of

      
        359
        
        -                "" ->

      
        360
        
        -                    Err (Api.HttpError { message = "Unexpected empty response", reason = Http.BadStatus statusCode })

      
        
        356
        +            if String.isEmpty body then

      
        
        357
        +                Err (Api.HttpError { message = "Unexpected empty response", reason = Http.BadStatus statusCode })

      
        361
        358
         

      
        362
        
        -                _ ->

      
        363
        
        -                    case Json.Decode.decodeString Data.Error.decode body of

      
        364
        
        -                        Ok err ->

      
        365
        
        -                            Err (Api.HttpError { message = err.message, reason = Http.BadStatus statusCode })

      
        
        359
        +            else

      
        
        360
        +                case Json.Decode.decodeString Data.Error.decode body of

      
        
        361
        +                    Ok err ->

      
        
        362
        +                        Err (Api.HttpError { message = err.message, reason = Http.BadStatus statusCode })

      
        366
        363
         

      
        367
        
        -                        Err err ->

      
        368
        
        -                            Err (Api.JsonDecodeError { message = "Failed to decode response", reason = err })

      
        
        364
        +                    Err err ->

      
        
        365
        +                        Err (Api.JsonDecodeError { message = "Failed to decode response", reason = err })

      
        369
        366
         

      
        370
        367
                 Http.BadUrl_ url ->

      
        371
        368
                     Err (Api.HttpError { message = "Unexpected URL format", reason = Http.BadUrl url })

      
M web/src/ExpirationOptions.elmweb/src/Constants.elm
···
        1
        
        -module ExpirationOptions exposing (expirationOptions)

      
        
        1
        +module Constants exposing (expirationOptions)

      
        2
        2
         

      
        3
        3
         

      
        4
        4
         expirationOptions : List { text : String, value : Int }

      
M web/src/Layouts/Header.elm
···
        1
        1
         module Layouts.Header exposing (Model, Msg, Props, layout)

      
        2
        2
         

      
        3
        3
         import Auth.User

      
        
        4
        +import Components.Form

      
        4
        5
         import Effect exposing (Effect)

      
        5
        6
         import Html as H exposing (Html)

      
        6
        7
         import Html.Attributes as A

      
        7
        
        -import Html.Events as E

      
        8
        8
         import Layout exposing (Layout)

      
        9
        9
         import Route exposing (Route)

      
        10
        10
         import Route.Path

      ···
        101
        101
             case user of

      
        102
        102
                 Auth.User.SignedIn _ ->

      
        103
        103
                     [ viewLink "Profile" Route.Path.Profile_Me

      
        104
        
        -            , H.button

      
        105
        
        -                [ A.class "text-gray-600 hover:text-red-600 transition-colors"

      
        106
        
        -                , E.onClick UserClickedLogout

      
        107
        
        -                ]

      
        108
        
        -                [ H.text "Logout" ]

      
        
        104
        +            , Components.Form.button

      
        
        105
        +                { text = "Logout"

      
        
        106
        +                , onClick = UserClickedLogout

      
        
        107
        +                , style = Components.Form.SecondaryDanger

      
        
        108
        +                , disabled = False

      
        
        109
        +                }

      
        109
        110
                     ]

      
        110
        111
         

      
        111
        112
                 _ ->

      
        112
        
        -            [ viewLink "About" Route.Path.Home_ -- TODO: or add about page, or delete the link

      
        113
        
        -            , H.a

      
        
        113
        +            [ H.a

      
        114
        114
                         [ A.class "px-4 py-2 border border-gray-300 rounded-md text-black hover:bg-gray-50 transition-colors"

      
        115
        115
                         , Route.Path.href Route.Path.Auth

      
        116
        116
                         ]

      
M web/src/Pages/Auth.elm
···
        1
        
        -module Pages.Auth exposing (Banner, Model, Msg, Variant, page)

      
        
        1
        +module Pages.Auth exposing (Banner, FormVariant, Model, Msg, page)

      
        2
        2
         

      
        3
        3
         import Api

      
        4
        4
         import Api.Auth

      ···
        41
        41
             , password : String

      
        42
        42
             , passwordAgain : String

      
        43
        43
             , isSubmittingForm : Bool

      
        44
        
        -    , formVariant : Variant

      
        45
        44
             , banner : Banner

      
        
        45
        +    , formVariant : FormVariant

      
        46
        46
             , lastClicked : Maybe Posix

      
        47
        47
             , now : Maybe Posix

      
        48
        48
             }

      ···
        84
        84
         type Msg

      
        85
        85
             = Tick Posix

      
        86
        86
             | UserUpdatedInput Field String

      
        87
        
        -    | UserChangedFormVariant Variant

      
        
        87
        +    | UserChangedFormVariant FormVariant

      
        88
        88
             | UserClickedSubmit

      
        89
        89
             | UserClickedResendActivationEmail

      
        90
        90
             | ApiSignInResponded (Result Api.Error Credentials)

      ···
        104
        104
             String

      
        105
        105
         

      
        106
        106
         

      
        107
        
        -type Variant

      
        
        107
        +type FormVariant

      
        108
        108
             = SignIn

      
        109
        109
             | SignUp

      
        110
        110
             | ForgotPassword

      ···
        224
        224
                     [ H.div [ A.class "w-full max-w-md bg-white rounded-lg border border-gray-200 shadow-sm" ]

      
        225
        225
                         -- TODO: add oauth buttons

      
        226
        226
                         [ viewBanner model

      
        227
        
        -                , viewHeader model.formVariant

      
        
        227
        +                , viewBoxHeader model.formVariant

      
        228
        228
                         , H.div [ A.class "px-6 pb-6 space-y-4" ]

      
        229
        229
                             [ viewChangeVariant model.formVariant

      
        230
        230
                             , H.div [ A.class "border-t border-gray-200" ] []

      ···
        255
        255
         viewVerificationBanner : Maybe Posix -> Maybe Posix -> Html Msg

      
        256
        256
         viewVerificationBanner now lastClicked =

      
        257
        257
             let

      
        258
        
        -        buttonClassesBase =

      
        259
        
        -            "w-full px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2 transition-colors mt-3"

      
        260
        
        -

      
        261
        
        -        buttonClasses active =

      
        262
        
        -            if active then

      
        263
        
        -                buttonClassesBase ++ " border border-gray-300 text-gray-700 hover:bg-gray-50"

      
        264
        
        -

      
        265
        
        -            else

      
        266
        
        -                buttonClassesBase ++ " border border-gray-300 text-gray-400 cursor-not-allowed"

      
        267
        
        -

      
        268
        258
                 timeLeftSeconds =

      
        269
        259
                     case ( now, lastClicked ) of

      
        270
        260
                         ( Just now_, Just last ) ->

      ···
        284
        274
             Components.Box.successBox

      
        285
        275
                 [ H.div [ A.class "font-medium text-green-800 mb-2" ] [ H.text "Check your email!" ]

      
        286
        276
                 , H.p [ A.class "text-green-800 text-sm" ] [ H.text "Please verify your account to continue. We've sent a verification link to your email — click it to activate your account." ]

      
        287
        
        -        , H.button

      
        288
        
        -            [ A.class (buttonClasses canClick)

      
        289
        
        -            , E.onClick UserClickedResendActivationEmail

      
        290
        
        -            , A.disabled (not canClick)

      
        291
        
        -            ]

      
        292
        
        -            [ H.text "Resend verification email" ]

      
        
        277
        +        , Components.Form.button

      
        
        278
        +            { text = "Resend verification email"

      
        
        279
        +            , onClick = UserClickedResendActivationEmail

      
        
        280
        +            , disabled = not canClick

      
        
        281
        +            , style = Components.Form.SecondaryDisabled canClick

      
        
        282
        +            }

      
        293
        283
                 , Components.Utils.viewIf (not canClick)

      
        294
        
        -            (H.p

      
        295
        
        -                [ A.class "text-gray-600 text-xs mt-2" ]

      
        
        284
        +            (H.p [ A.class "text-gray-600 text-xs mt-2" ]

      
        296
        285
                         [ H.text ("You can request a new verification email in " ++ String.fromInt timeLeftSeconds ++ " seconds.") ]

      
        297
        286
                     )

      
        298
        287
                 ]

      
        299
        288
         

      
        300
        289
         

      
        301
        
        -viewHeader : Variant -> Html Msg

      
        302
        
        -viewHeader variant =

      
        
        290
        +viewBoxHeader : FormVariant -> Html Msg

      
        
        291
        +viewBoxHeader variant =

      
        303
        292
             let

      
        304
        293
                 ( title, description ) =

      
        305
        294
                     case variant of

      ···
        321
        310
                 ]

      
        322
        311
         

      
        323
        312
         

      
        324
        
        -viewChangeVariant : Variant -> Html Msg

      
        
        313
        +viewChangeVariant : FormVariant -> Html Msg

      
        325
        314
         viewChangeVariant variant =

      
        326
        
        -    let

      
        327
        
        -        buttonClasses active =

      
        328
        
        -            let

      
        329
        
        -                base =

      
        330
        
        -                    "flex-1 px-4 py-2 rounded-md font-medium transition-colors"

      
        331
        
        -            in

      
        332
        
        -            if active then

      
        333
        
        -                base ++ " bg-black text-white"

      
        334
        
        -

      
        335
        
        -            else

      
        336
        
        -                base ++ " bg-white text-black border border-gray-300 hover:bg-gray-50"

      
        337
        
        -    in

      
        338
        
        -    H.div [ A.class "flex gap-2" ]

      
        339
        
        -        [ H.button

      
        340
        
        -            [ A.class (buttonClasses (variant == SignIn))

      
        341
        
        -            , A.disabled (variant == SignIn)

      
        342
        
        -            , E.onClick (UserChangedFormVariant SignIn)

      
        343
        
        -            ]

      
        344
        
        -            [ H.text "Sign In" ]

      
        345
        
        -        , H.button

      
        346
        
        -            [ A.class (buttonClasses (variant == SignUp))

      
        347
        
        -            , A.disabled (variant == SignUp)

      
        348
        
        -            , E.onClick (UserChangedFormVariant SignUp)

      
        349
        
        -            ]

      
        350
        
        -            [ H.text "Sign Up" ]

      
        
        315
        +    H.div [ A.class "flex [&>*]:flex-1 gap-2" ]

      
        
        316
        +        [ Components.Form.button

      
        
        317
        +            { text = "Sign In"

      
        
        318
        +            , onClick = UserChangedFormVariant SignIn

      
        
        319
        +            , style = Components.Form.Primary (variant == SignIn)

      
        
        320
        +            , disabled = variant == SignIn

      
        
        321
        +            }

      
        
        322
        +        , Components.Form.button

      
        
        323
        +            { text = "Sign Up"

      
        
        324
        +            , disabled = variant == SignUp

      
        
        325
        +            , style = Components.Form.Primary (variant == SignUp)

      
        
        326
        +            , onClick = UserChangedFormVariant SignUp

      
        
        327
        +            }

      
        351
        328
                 ]

      
        352
        329
         

      
        353
        330
         

      ···
        389
        366
         viewFormInput : { field : Field, value : String } -> Html Msg

      
        390
        367
         viewFormInput opts =

      
        391
        368
             Components.Form.input

      
        392
        
        -        { id = fromFieldToInputType opts.field

      
        
        369
        +        { id = (fromFieldToFieldInfo opts.field).label

      
        393
        370
                 , field = opts.field

      
        394
        
        -        , label = fromFieldToLabel opts.field

      
        395
        
        -        , type_ = fromFieldToInputType opts.field

      
        
        371
        +        , label = (fromFieldToFieldInfo opts.field).label

      
        
        372
        +        , type_ = (fromFieldToFieldInfo opts.field).type_

      
        396
        373
                 , value = opts.value

      
        397
        
        -        , placeholder = fromFieldToLabel opts.field

      
        
        374
        +        , placeholder = (fromFieldToFieldInfo opts.field).label

      
        398
        375
                 , required = True

      
        399
        376
                 , onInput = UserUpdatedInput opts.field

      
        400
        377
                 , helpText = Nothing

      ···
        416
        393
         

      
        417
        394
         viewSubmitButton : Model -> Html Msg

      
        418
        395
         viewSubmitButton model =

      
        419
        
        -    H.button

      
        420
        
        -        [ A.type_ "submit"

      
        421
        
        -        , A.disabled (isFormDisabled model)

      
        422
        
        -        , A.class

      
        423
        
        -            (if isFormDisabled model then

      
        424
        
        -                "w-full px-4 py-2 bg-gray-300 text-gray-500 rounded-md cursor-not-allowed transition-colors"

      
        425
        
        -

      
        426
        
        -             else

      
        427
        
        -                "w-full px-4 py-2 bg-black text-white rounded-md hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2 transition-colors"

      
        428
        
        -            )

      
        429
        
        -        ]

      
        430
        
        -        [ H.text (fromVariantToLabel model.formVariant) ]

      
        
        396
        +    Components.Form.submitButton

      
        
        397
        +        { class = "w-full"

      
        
        398
        +        , text = fromVariantToLabel model.formVariant

      
        
        399
        +        , style = Components.Form.Primary (isFormDisabled model)

      
        
        400
        +        , disabled = isFormDisabled model

      
        
        401
        +        }

      
        431
        402
         

      
        432
        403
         

      
        433
        404
         isFormDisabled : Model -> Bool

      ···
        455
        426
                         || (model.password /= model.passwordAgain)

      
        456
        427
         

      
        457
        428
         

      
        458
        
        -fromVariantToLabel : Variant -> String

      
        
        429
        +fromVariantToLabel : FormVariant -> String

      
        459
        430
         fromVariantToLabel variant =

      
        460
        431
             case variant of

      
        461
        432
                 SignIn ->

      ···
        471
        442
                     "Set new password"

      
        472
        443
         

      
        473
        444
         

      
        474
        
        -fromFieldToLabel : Field -> String

      
        475
        
        -fromFieldToLabel field =

      
        476
        
        -    case field of

      
        477
        
        -        Email ->

      
        478
        
        -            "Email address"

      
        479
        
        -

      
        480
        
        -        Password ->

      
        481
        
        -            "Password"

      
        482
        
        -

      
        483
        
        -        PasswordAgain ->

      
        484
        
        -            "Confirm password"

      
        485
        
        -

      
        486
        
        -

      
        487
        
        -fromFieldToInputType : Field -> String

      
        488
        
        -fromFieldToInputType field =

      
        
        445
        +fromFieldToFieldInfo : Field -> { label : String, type_ : String }

      
        
        446
        +fromFieldToFieldInfo field =

      
        489
        447
             case field of

      
        490
        448
                 Email ->

      
        491
        
        -            "email"

      
        
        449
        +            { label = "Email address", type_ = "email" }

      
        492
        450
         

      
        493
        451
                 Password ->

      
        494
        
        -            "password"

      
        
        452
        +            { label = "Password", type_ = "password" }

      
        495
        453
         

      
        496
        454
                 PasswordAgain ->

      
        497
        
        -            "password"

      
        
        455
        +            { label = "Confirm password", type_ = "password" }

      
M web/src/Pages/Home_.elm
···
        5
        5
         import Components.Box

      
        6
        6
         import Components.Form

      
        7
        7
         import Components.Utils

      
        
        8
        +import Constants exposing (expirationOptions)

      
        8
        9
         import Data.Note as Note

      
        9
        10
         import Effect exposing (Effect)

      
        10
        
        -import ExpirationOptions exposing (expirationOptions)

      
        11
        11
         import Html as H exposing (Html)

      
        12
        12
         import Html.Attributes as A

      
        13
        13
         import Html.Events as E

      ···
        49
        49
             }

      
        50
        50
         

      
        51
        51
         

      
        52
        
        -

      
        53
        
        --- TODO: store slug as Slug type

      
        54
        
        -

      
        55
        
        -

      
        56
        52
         type PageVariant

      
        57
        53
             = CreateNote

      
        58
        54
             | NoteCreated String

      ···
        79
        75
         

      
        80
        76
         

      
        81
        77
         type Msg

      
        82
        
        -    = CopyButtonReset

      
        83
        
        -    | Tick Posix

      
        
        78
        +    = Tick Posix

      
        
        79
        +    | CopyButtonReset

      
        84
        80
             | UserUpdatedInput Field String

      
        85
        81
             | UserClickedCheckbox Bool

      
        86
        82
             | UserClickedSubmit

      ···
        149
        145
                     ( { model | content = content }, Effect.none )

      
        150
        146
         

      
        151
        147
                 UserUpdatedInput Slug slug ->

      
        152
        
        -            if slug == "" then

      
        
        148
        +            if String.isEmpty slug then

      
        153
        149
                         ( { model | slug = Nothing }, Effect.none )

      
        154
        150
         

      
        155
        151
                     else

      
        156
        152
                         ( { model | slug = Just slug }, Effect.none )

      
        157
        153
         

      
        158
        154
                 UserUpdatedInput Password password ->

      
        159
        
        -            if password == "" then

      
        
        155
        +            if String.isEmpty password then

      
        160
        156
                         ( { model | password = Nothing }, Effect.none )

      
        161
        157
         

      
        162
        158
                     else

      ···
        177
        173
         

      
        178
        174
                 ApiCreateNoteResponded (Err error) ->

      
        179
        175
                     ( { model | apiError = Just error }, Effect.none )

      
        180
        
        -

      
        181
        
        -

      
        182
        
        -

      
        183
        
        --- SUBSCRIPTIONS

      
        184
        176
         

      
        185
        177
         

      
        186
        178
         subscriptions : Model -> Sub Msg

      ···
        204
        196
         

      
        205
        197
         view : Shared.Model -> Model -> View Msg

      
        206
        198
         view shared model =

      
        
        199
        +    let

      
        
        200
        +        appUrl =

      
        
        201
        +            secretUrl shared.appURL

      
        
        202
        +    in

      
        207
        203
             { title = "Onasty"

      
        208
        204
             , body =

      
        209
        
        -        [ H.div [ A.class "py-8 px-4 " ]

      
        210
        
        -            [ H.div [ A.class "w-full max-w-4xl mx-auto" ]

      
        211
        
        -                [ H.div [ A.class "bg-white rounded-lg border border-gray-200 shadow-sm" ]

      
        212
        
        -                    [ viewHeader model.pageVariant

      
        213
        
        -                    , H.div [ A.class "p-6 space-y-6" ]

      
        214
        
        -                        [ Components.Utils.viewMaybe model.apiError (\e -> Components.Box.error (Api.errorMessage e))

      
        215
        
        -                        , case model.pageVariant of

      
        216
        
        -                            CreateNote ->

      
        217
        
        -                                viewCreateNoteForm model shared.appURL

      
        
        205
        +        [ Components.Utils.commonContainer

      
        
        206
        +            [ viewHeader model.pageVariant model.apiError

      
        
        207
        +            , H.div [ A.class "p-6 space-y-6" ]

      
        
        208
        +                [ Components.Utils.viewMaybe model.apiError (\e -> Components.Box.error (Api.errorMessage e))

      
        
        209
        +                , case model.pageVariant of

      
        
        210
        +                    CreateNote ->

      
        
        211
        +                        viewCreateNoteForm model appUrl

      
        218
        212
         

      
        219
        
        -                            NoteCreated slug ->

      
        220
        
        -                                viewNoteCreated model.userClickedCopyLink shared.appURL slug

      
        221
        
        -                        ]

      
        222
        
        -                    ]

      
        
        213
        +                    NoteCreated slug ->

      
        
        214
        +                        Components.Utils.viewIf (model.apiError == Nothing)

      
        
        215
        +                            (viewNoteCreated model.userClickedCopyLink appUrl slug)

      
        223
        216
                         ]

      
        224
        217
                     ]

      
        225
        218
                 ]

      
        226
        219
             }

      
        227
        220
         

      
        228
        221
         

      
        229
        
        -viewHeader : PageVariant -> Html Msg

      
        230
        
        -viewHeader pageVariant =

      
        
        222
        +viewHeader : PageVariant -> Maybe Api.Error -> Html Msg

      
        
        223
        +viewHeader pageVariant apiError =

      
        231
        224
             H.div [ A.class "p-6 pb-4 border-b border-gray-200" ]

      
        232
        225
                 [ H.h1 [ A.class "text-2xl font-bold text-gray-900" ]

      
        233
        226
                     [ H.text

      ···
        236
        229
                                 "Create a new note"

      
        237
        230
         

      
        238
        231
                             NoteCreated _ ->

      
        239
        
        -                        "Paste Created Successfully!"

      
        
        232
        +                        if apiError == Nothing then

      
        
        233
        +                            "Paste Created Successfully!"

      
        
        234
        +

      
        
        235
        +                        else

      
        
        236
        +                            "Could not create the note."

      
        240
        237
                         )

      
        241
        238
                     ]

      
        242
        239
                 ]

      ···
        244
        241
         

      
        245
        242
         

      
        246
        243
         -- VIEW CREATE NOTE

      
        247
        
        --- TODO: validate form

      
        
        244
        +-- TODO: validate the form

      
        248
        245
         

      
        249
        246
         

      
        250
        
        -viewCreateNoteForm : Model -> String -> Html Msg

      
        
        247
        +viewCreateNoteForm : Model -> (String -> String) -> Html Msg

      
        251
        248
         viewCreateNoteForm model appUrl =

      
        252
        249
             H.form

      
        253
        250
                 [ E.onSubmit UserClickedSubmit

      ···
        261
        258
                     , placeholder = "my-unique-slug"

      
        262
        259
                     , type_ = "text"

      
        263
        260
                     , helpText = Just "Leave empty to generate a random slug"

      
        264
        
        -            , prefix = Just (secretUrl appUrl "")

      
        
        261
        +            , prefix = Just (appUrl "")

      
        265
        262
                     , onInput = UserUpdatedInput Slug

      
        266
        263
                     , required = False

      
        267
        264
                     , value = Maybe.withDefault "" model.slug

      ···
        286
        283
                         , viewBurnBeforeExpirationCheckbox

      
        287
        284
                         ]

      
        288
        285
                     ]

      
        289
        
        -        , H.div [ A.class "flex justify-end" ] [ viewSubmitButton model ]

      
        
        286
        +        , H.div [ A.class "flex justify-end" ]

      
        
        287
        +            [ Components.Form.submitButton

      
        
        288
        +                { text = "Create note"

      
        
        289
        +                , style = Components.Form.Primary (isFormDisabled model)

      
        
        290
        +                , disabled = False

      
        
        291
        +                , class = ""

      
        
        292
        +                }

      
        
        293
        +            ]

      
        290
        294
                 ]

      
        291
        295
         

      
        292
        296
         

      ···
        349
        353
                 ]

      
        350
        354
         

      
        351
        355
         

      
        352
        
        -viewSubmitButton : Model -> Html Msg

      
        353
        
        -viewSubmitButton model =

      
        354
        
        -    H.button

      
        355
        
        -        [ A.type_ "submit"

      
        356
        
        -        , A.disabled (isFormDisabled model)

      
        357
        
        -        , A.class

      
        358
        
        -            (if isFormDisabled model then

      
        359
        
        -                "px-6 py-2 bg-gray-300 text-gray-500 rounded-md cursor-not-allowed transition-colors"

      
        360
        
        -

      
        361
        
        -             else

      
        362
        
        -                "px-6 py-2 bg-black text-white rounded-md hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2 transition-colors"

      
        363
        
        -            )

      
        364
        
        -        ]

      
        365
        
        -        [ H.text "Create note" ]

      
        366
        
        -

      
        367
        
        -

      
        368
        356
         isFormDisabled : Model -> Bool

      
        369
        357
         isFormDisabled model =

      
        370
        358
             String.isEmpty model.content

      
        371
        359
         

      
        372
        360
         

      
        373
        
        -viewCreateNewNoteButton : Html Msg

      
        374
        
        -viewCreateNewNoteButton =

      
        375
        
        -    H.button

      
        376
        
        -        [ A.class "px-6 py-2 bg-black text-white rounded-md hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2 transition-colors"

      
        377
        
        -        , E.onClick UserClickedCreateNewNote

      
        378
        
        -        ]

      
        379
        
        -        [ H.text "Create New Paste" ]

      
        380
        
        -

      
        381
        
        -

      
        382
        361
         fromFieldToName : Field -> String

      
        383
        362
         fromFieldToName field =

      
        384
        363
             case field of

      ···
        399
        378
         -- VIEW NOTE CREATED

      
        400
        379
         

      
        401
        380
         

      
        402
        
        -viewNoteCreated : Bool -> String -> String -> Html Msg

      
        
        381
        +viewNoteCreated : Bool -> (String -> String) -> String -> Html Msg

      
        403
        382
         viewNoteCreated userClickedCopyLink appUrl slug =

      
        404
        383
             H.div [ A.class "bg-green-50 border border-green-200 rounded-md p-6" ]

      
        405
        
        -        [ H.div [ A.class "bg-white border border-green-300 rounded-md p-4 mb-4" ]

      
        406
        
        -            [ H.p [ A.class "text-sm text-gray-600 mb-2" ]

      
        407
        
        -                [ H.text "Your paste is available at:" ]

      
        408
        
        -            , H.p [ A.class "font-mono text-sm text-gray-800 break-all" ]

      
        409
        
        -                [ H.text (secretUrl appUrl slug) ]

      
        
        384
        +        [ H.div [ A.class "border border-green-300 rounded-md p-4 mb-4" ]

      
        
        385
        +            [ H.p [ A.class "text-sm text-gray-600 mb-2" ] [ H.text "Your paste is available at:" ]

      
        
        386
        +            , H.p [ A.class "font-mono text-sm text-gray-800" ] [ H.text (appUrl slug) ]

      
        410
        387
                     ]

      
        411
        388
                 , H.div [ A.class "flex gap-3" ]

      
        412
        
        -            [ viewCopyLinkButton userClickedCopyLink

      
        413
        
        -            , viewCreateNewNoteButton

      
        
        389
        +            [ Components.Form.button

      
        
        390
        +                { text = "Create New Paste"

      
        
        391
        +                , onClick = UserClickedCreateNewNote

      
        
        392
        +                , style = Components.Form.Primary False

      
        
        393
        +                , disabled = False

      
        
        394
        +                }

      
        
        395
        +            , Components.Form.button

      
        
        396
        +                { style = Components.Form.Secondary userClickedCopyLink

      
        
        397
        +                , onClick = UserClickedCopyLink

      
        
        398
        +                , disabled = userClickedCopyLink

      
        
        399
        +                , text =

      
        
        400
        +                    if userClickedCopyLink then

      
        
        401
        +                        "Copied!"

      
        
        402
        +

      
        
        403
        +                    else

      
        
        404
        +                        "Copy URL"

      
        
        405
        +                }

      
        414
        406
                     ]

      
        415
        407
                 ]

      
        416
        
        -

      
        417
        
        -

      
        418
        
        -viewCopyLinkButton : Bool -> Html Msg

      
        419
        
        -viewCopyLinkButton isClicked =

      
        420
        
        -    let

      
        421
        
        -        base =

      
        422
        
        -            "px-4 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2 transition-colors"

      
        423
        
        -    in

      
        424
        
        -    H.button

      
        425
        
        -        [ A.class

      
        426
        
        -            (if isClicked then

      
        427
        
        -                base ++ " bg-green-100 border-green-300 text-green-700"

      
        428
        
        -

      
        429
        
        -             else

      
        430
        
        -                base ++ " border-gray-300 text-gray-700 hover:bg-gray-50"

      
        431
        
        -            )

      
        432
        
        -        , E.onClick UserClickedCopyLink

      
        433
        
        -        ]

      
        434
        
        -        [ H.text

      
        435
        
        -            (if isClicked then

      
        436
        
        -                "Copied!"

      
        437
        
        -

      
        438
        
        -             else

      
        439
        
        -                "Copy URL"

      
        440
        
        -            )

      
        441
        
        -        ]

      
M web/src/Pages/Secret/Slug_.elm
···
        3
        3
         import Api

      
        4
        4
         import Api.Note

      
        5
        5
         import Components.Box

      
        
        6
        +import Components.Form

      
        
        7
        +import Components.Icon

      
        6
        8
         import Components.Utils

      
        7
        9
         import Data.Note exposing (Metadata, Note)

      
        8
        10
         import Effect exposing (Effect)

      ···
        109
        111
                     ( { model | page = NotFound, metadata = Api.Failure error }, Effect.none )

      
        110
        112
         

      
        111
        113
         

      
        112
        
        -

      
        113
        
        --- SUBSCRIPTIONS

      
        114
        
        -

      
        115
        
        -

      
        116
        114
         subscriptions : Model -> Sub Msg

      
        117
        115
         subscriptions _ =

      
        118
        116
             Sub.none

      ···
        126
        124
         view shared model =

      
        127
        125
             { title = "View note"

      
        128
        126
             , body =

      
        129
        
        -        [ H.div

      
        130
        
        -            [ A.class "w-full max-w-4xl mx-auto" ]

      
        131
        
        -            [ H.div

      
        132
        
        -                [ A.class "bg-white rounded-lg border border-gray-200 shadow-sm" ]

      
        133
        
        -                (case model.metadata of

      
        134
        
        -                    Api.Success metadata ->

      
        135
        
        -                        viewPage shared.timeZone model.slug model.page metadata model.password

      
        
        127
        +        [ Components.Utils.commonContainer

      
        
        128
        +            (case model.metadata of

      
        
        129
        +                Api.Success metadata ->

      
        
        130
        +                    viewPage shared.timeZone model.slug model.page metadata model.password

      
        136
        131
         

      
        137
        
        -                    Api.Loading ->

      
        138
        
        -                        [ viewHeader { title = "View note", subtitle = "Loading note metadata..." }

      
        139
        
        -                        , viewOpenNote { slug = model.slug, hasPassword = False, password = Nothing, isLoading = True }

      
        140
        
        -                        ]

      
        
        132
        +                Api.Loading ->

      
        
        133
        +                    [ viewHeader { title = "View note", subtitle = "Loading note metadata..." }

      
        
        134
        +                    , viewOpenNote { slug = model.slug, hasPassword = False, password = Nothing, isLoading = True }

      
        
        135
        +                    ]

      
        141
        136
         

      
        142
        
        -                    Api.Failure error ->

      
        143
        
        -                        [ viewHeader { title = "Note Not Found", subtitle = "The note you're looking for doesn't exist or has expired" }

      
        144
        
        -                        , if Api.is404 error then

      
        145
        
        -                            viewNoteNotFound model.slug

      
        
        137
        +                Api.Failure error ->

      
        
        138
        +                    [ viewHeader { title = "Note Not Found", subtitle = "The note you're looking for doesn't exist or has expired" }

      
        
        139
        +                    , if Api.is404 error then

      
        
        140
        +                        viewNoteNotFound

      
        146
        141
         

      
        147
        
        -                          else

      
        148
        
        -                            Components.Box.error (Api.errorMessage error)

      
        149
        
        -                        ]

      
        150
        
        -                )

      
        151
        
        -            ]

      
        
        142
        +                      else

      
        
        143
        +                        Components.Box.error (Api.errorMessage error)

      
        
        144
        +                    ]

      
        
        145
        +            )

      
        152
        146
                 ]

      
        153
        147
             }

      
        154
        148
         

      ···
        175
        169
         

      
        176
        170
                         Api.Failure _ ->

      
        177
        171
                             [ viewHeader { title = "Note Not Found", subtitle = "The note you're looking for doesn't exist or has expired" }

      
        178
        
        -                    , viewNoteNotFound slug

      
        
        172
        +                    , viewNoteNotFound

      
        179
        173
                             ]

      
        180
        174
         

      
        181
        175
                 NotFound ->

      
        182
        
        -            [ viewNoteNotFound slug ]

      
        
        176
        +            [ viewNoteNotFound ]

      
        183
        177
         

      
        184
        178
         

      
        185
        179
         

      ···
        203
        197
                     (H.div [ A.class "bg-orange-50 border-b border-orange-200 p-4" ]

      
        204
        198
                         [ H.div [ A.class "flex items-center gap-3" ]

      
        205
        199
                             [ H.div [ A.class "w-6 h-6 bg-orange-100 rounded-full flex items-center justify-center flex-shrink-0" ]

      
        206
        
        -                        [ Components.Utils.loadSvg { path = "warning.svg", class = "w-4 h-4 text-orange-600" } ]

      
        
        200
        +                        [ Components.Icon.view Components.Icon.Warning "w-4 h-4 text-orange-600" ]

      
        207
        201
                             , H.p [ A.class "text-orange-800 text-sm font-medium" ]

      
        208
        202
                                 [ H.text "This note was destroyed. If you need to keep it, copy it before closing this window." ]

      
        209
        203
                             ]

      ···
        219
        213
                                 ]

      
        220
        214
                             ]

      
        221
        215
                         , H.div [ A.class "flex gap-2" ]

      
        222
        
        -                    [ H.button

      
        223
        
        -                        [ E.onClick UserClickedCopyContent

      
        224
        
        -                        , A.class "px-3 py-2 text-sm border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2 transition-colors"

      
        225
        
        -                        ]

      
        226
        
        -                        [ H.text "Copy Content" ]

      
        
        216
        +                    [ Components.Form.button

      
        
        217
        +                        { text = "Copy Content"

      
        
        218
        +                        , style = Components.Form.SecondaryDisabled False

      
        
        219
        +                        , onClick = UserClickedCopyContent

      
        
        220
        +                        , disabled = False

      
        
        221
        +                        }

      
        227
        222
                             ]

      
        228
        223
                         ]

      
        229
        224
                     ]

      ···
        234
        229
         -- NOTE

      
        235
        230
         

      
        236
        231
         

      
        237
        
        -viewNoteNotFound : String -> Html msg

      
        238
        
        -viewNoteNotFound slug =

      
        
        232
        +viewNoteNotFound : Html msg

      
        
        233
        +viewNoteNotFound =

      
        239
        234
             H.div [ A.class "p-6" ]

      
        240
        235
                 [ H.div [ A.class "text-center py-12" ]

      
        241
        236
                     [ H.div [ A.class "w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4" ]

      
        242
        
        -                [ Components.Utils.loadSvg { path = "note-not-found.svg", class = "w-8 h-8 text-red-500" } ]

      
        
        237
        +                [ Components.Icon.view Components.Icon.NotFound "w-8 h-8 text-red-500" ]

      
        243
        238
                     , H.h2 [ A.class "text-xl font-semibold text-gray-900 mb-2" ]

      
        244
        
        -                [ H.text ("Note " ++ slug ++ " Not Found") ]

      
        245
        
        -            , H.div [ A.class "text-gray-600 mb-6 space-y-2" ]

      
        246
        
        -                [ H.p []

      
        247
        
        -                    [ H.span [ A.class "font-bold" ] [ H.text "This note may have:" ]

      
        248
        
        -                    , H.ul [ A.class "text-sm space-y-1 list-disc list-inside text-left max-w-md mx-auto" ]

      
        249
        
        -                        [ H.li [] [ H.text "Expired and been deleted" ]

      
        250
        
        -                        , H.li [] [ H.text "Have different password" ]

      
        251
        
        -                        , H.li [] [ H.text "Been deleted by the creator" ]

      
        252
        
        -                        , H.li [] [ H.text "Been burned after reading" ]

      
        253
        
        -                        , H.li [] [ H.text "Never existed or the URL is incorrect" ]

      
        254
        
        -                        ]

      
        255
        
        -                    ]

      
        256
        
        -                ]

      
        
        239
        +                [ H.text "Note not found" ]

      
        257
        240
                     ]

      
        258
        241
                 ]

      
        259
        242
         

      ···
        262
        245
         viewOpenNote opts =

      
        263
        246
             let

      
        264
        247
                 isDisabled =

      
        265
        
        -            opts.hasPassword && Maybe.withDefault "" opts.password == ""

      
        266
        
        -

      
        267
        
        -        buttonData =

      
        268
        
        -            let

      
        269
        
        -                base =

      
        270
        
        -                    "px-6 py-3 rounded-md focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2 transition-colors"

      
        271
        
        -            in

      
        272
        
        -            if opts.isLoading then

      
        273
        
        -                { text = "Loading Note...", class = base ++ " bg-gray-300 text-gray-500 cursor-not-allowed" }

      
        274
        
        -

      
        275
        
        -            else if isDisabled then

      
        276
        
        -                { text = "View Note", class = base ++ " bg-gray-300 text-gray-500 cursor-not-allowed" }

      
        277
        
        -

      
        278
        
        -            else

      
        279
        
        -                { text = "View Note", class = base ++ " bg-black text-white hover:bg-gray-800" }

      
        
        248
        +            (opts.hasPassword && Maybe.withDefault "" opts.password == "") || opts.isLoading

      
        280
        249
             in

      
        281
        250
             H.div [ A.class "p-6" ]

      
        282
        251
                 [ H.div [ A.class "text-center py-12" ]

      
        283
        252
                     [ H.div [ A.class "mb-6" ]

      
        284
        253
                         [ H.div [ A.class "w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4" ]

      
        285
        
        -                    [ Components.Utils.loadSvg { path = "note-icon.svg", class = "w-8 h-8 text-gray-400" } ]

      
        
        254
        +                    [ Components.Icon.view Components.Icon.NoteIcon "w-8 h-8 text-gray-400" ]

      
        286
        255
                         , H.h2 [ A.class "text-lg font-semibold text-gray-900 mb-2" ] [ H.text opts.slug ]

      
        287
        256
                         , H.p [ A.class "text-gray-600 mb-6" ] [ H.text "You're about read and destroy the note." ]

      
        288
        257
                         ]

      ···
        291
        260
                         , A.class "max-w-sm mx-auto space-y-4"

      
        292
        261
                         ]

      
        293
        262
                         [ Components.Utils.viewIf opts.hasPassword

      
        294
        
        -                    (H.div

      
        295
        
        -                        [ A.class "space-y-2" ]

      
        296
        
        -                        [ H.label

      
        297
        
        -                            [ A.class "block text-sm font-medium text-gray-700 text-left" ]

      
        298
        
        -                            [ H.text "Password" ]

      
        
        263
        +                    (H.div [ A.class "space-y-2" ]

      
        
        264
        +                        [ H.label [ A.class "block text-sm font-medium text-gray-700 text-left" ] [ H.text "Password" ]

      
        299
        265
                                 , H.input

      
        300
        266
                                     [ E.onInput UserUpdatedPassword

      
        301
        267
                                     , 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"

      ···
        303
        269
                                     []

      
        304
        270
                                 ]

      
        305
        271
                             )

      
        306
        
        -                , H.button

      
        307
        
        -                    [ A.class buttonData.class

      
        308
        
        -                    , A.type_ "submit"

      
        309
        
        -                    , A.disabled isDisabled

      
        310
        
        -                    ]

      
        311
        
        -                    [ H.text buttonData.text ]

      
        
        272
        +                , Components.Form.submitButton

      
        
        273
        +                    { text =

      
        
        274
        +                        if opts.isLoading then

      
        
        275
        +                            "Loading Note..."

      
        
        276
        +

      
        
        277
        +                        else

      
        
        278
        +                            "View Note"

      
        
        279
        +                    , style = Components.Form.Primary isDisabled

      
        
        280
        +                    , disabled = isDisabled

      
        
        281
        +                    , class = "py-3"

      
        
        282
        +                    }

      
        312
        283
                         ]

      
        313
        284
                     ]

      
        314
        285
                 ]

      
M web/tests/UnitTests/Data/Error.elm
···
        11
        11
             describe "Data.Error"

      
        12
        12
                 [ test "decode" <|

      
        13
        13
                     \_ ->

      
        14
        
        -                """

      
        15
        
        -                {

      
        16
        
        -                    "message": "some kind of an error"

      
        17
        
        -                }

      
        18
        
        -                """

      
        
        14
        +                """ {"message": "some kind of an error"} """

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

      
        20
        16
                             |> Expect.ok

      
        21
        17
                 ]

      
M web/tests/UnitTests/Data/Note.elm
···
        11
        11
             describe "Data.Note"

      
        12
        12
                 [ test "decodeCreateResponse"

      
        13
        13
                     (\_ ->

      
        14
        
        -                "{\"slug\":\"the.note-slug\"}"

      
        
        14
        +                """ {"slug":"the.note-slug"} """

      
        15
        15
                             |> D.decodeString Data.Note.decodeCreateResponse

      
        16
        16
                             |> Expect.ok

      
        17
        17
                     )

      
        18
        18
                 , test "decodeMetadata"

      
        19
        19
                     (\_ ->

      
        20
        20
                         """

      
        21
        
        -                    {

      
        22
        
        -                        "created_at": "2023-10-01T12:00:00Z",

      
        23
        
        -                        "has_password": false

      
        24
        
        -                    }

      
        25
        
        -                    """

      
        
        21
        +                {

      
        
        22
        +                    "created_at": "2023-10-01T12:00:00Z",

      
        
        23
        +                    "has_password": false

      
        
        24
        +                }

      
        
        25
        +                """

      
        26
        26
                             |> D.decodeString Data.Note.decodeMetadata

      
        27
        27
                             |> Expect.ok

      
        28
        28
                     )