all repos

onasty @ 67a69fbea5f9d5d13f9d6f0d5a3be15b9351c8d8

a one-time notes service
2 files changed, 70 insertions(+), 42 deletions(-)
web: add cooldown timer for resend verification email (#143)

* web: add cooldown timer for resend verification email

* web: some small refactoring

* fixup! web: some small refactoring

* fixup! web: some small refactoring

* refactor(effect): 'reduce the amount of LOC'

* web: make error messages more "descriptive"
Author: Smirnov Oleksandr ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-06-22 14:20:34 +0300
Parent: 336ceb2
M web/src/Effect.elm
···
        175
        175
             }

      
        176
        176
             -> Effect msg

      
        177
        177
         sendApiRequest opts =

      
        178
        
        -    let

      
        179
        
        -        onHttpError : Api.Error -> msg

      
        180
        
        -        onHttpError err =

      
        181
        
        -            opts.onResponse (Err err)

      
        182
        
        -

      
        183
        
        -        decoder : Json.Decode.Decoder msg

      
        184
        
        -        decoder =

      
        185
        
        -            opts.decoder

      
        186
        
        -                |> Json.Decode.map Ok

      
        187
        
        -                |> Json.Decode.map opts.onResponse

      
        188
        
        -    in

      
        189
        178
             SendApiRequest

      
        190
        179
                 { endpoint = opts.endpoint

      
        191
        180
                 , method = opts.method

      
        192
        181
                 , body = opts.body

      
        193
        
        -        , onHttpError = onHttpError

      
        194
        
        -        , decoder = decoder

      
        
        182
        +        , onHttpError = \e -> opts.onResponse (Err e)

      
        
        183
        +        , decoder =

      
        
        184
        +            opts.decoder

      
        
        185
        +                |> Json.Decode.map Ok

      
        
        186
        +                |> Json.Decode.map opts.onResponse

      
        195
        187
                 }

      
        196
        188
         

      
        197
        189
         

      ···
        368
        360
                             Ok value

      
        369
        361
         

      
        370
        362
                         Err err ->

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

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

      
        372
        364
         

      
        373
        365
                 Http.BadStatus_ { statusCode } body ->

      
        374
        366
                     case Json.Decode.decodeString Data.Error.decode body of

      ···
        376
        368
                             Err (Api.HttpError { message = err.message, reason = Http.BadStatus statusCode })

      
        377
        369
         

      
        378
        370
                         Err err ->

      
        379
        
        -                    Err (Api.JsonDecodeError { message = "Something unexpected happened", reason = err })

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

      
        380
        372
         

      
        381
        373
                 Http.BadUrl_ url ->

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

      
M web/src/Pages/Auth.elm
···
        13
        13
         import Route exposing (Route)

      
        14
        14
         import Route.Path

      
        15
        15
         import Shared

      
        
        16
        +import Time exposing (Posix)

      
        16
        17
         import View exposing (View)

      
        17
        18
         

      
        18
        19
         

      ···
        38
        39
             , isSubmittingForm : Bool

      
        39
        40
             , formVariant : Variant

      
        40
        41
             , gotSignedUp : Bool

      
        
        42
        +    , lastClicked : Maybe Posix

      
        41
        43
             , apiError : Maybe Api.Error

      
        
        44
        +    , now : Maybe Posix

      
        42
        45
             }

      
        43
        46
         

      
        44
        47
         

      
        45
        48
         init : Shared.Model -> () -> ( Model, Effect Msg )

      
        46
        49
         init shared _ =

      
        47
        
        -    ( { isSubmittingForm = False

      
        48
        
        -      , email = ""

      
        
        50
        +    ( { email = ""

      
        49
        51
               , password = ""

      
        50
        52
               , passwordAgain = ""

      
        
        53
        +      , isSubmittingForm = False

      
        51
        54
               , formVariant = SignIn

      
        
        55
        +      , gotSignedUp = False

      
        
        56
        +      , lastClicked = Nothing

      
        52
        57
               , apiError = Nothing

      
        53
        
        -      , gotSignedUp = False

      
        
        58
        +      , now = Nothing

      
        54
        59
               }

      
        55
        60
             , case shared.user of

      
        56
        61
                 Auth.User.SignedIn _ ->

      ···
        66
        71
         

      
        67
        72
         

      
        68
        73
         type Msg

      
        69
        
        -    = UserUpdatedInput Field String

      
        
        74
        +    = Tick Posix

      
        
        75
        +    | UserUpdatedInput Field String

      
        70
        76
             | UserChangedFormVariant Variant

      
        71
        77
             | UserClickedSubmit

      
        72
        78
             | UserClickedResendActivationEmail

      ···
        89
        95
         update : Msg -> Model -> ( Model, Effect Msg )

      
        90
        96
         update msg model =

      
        91
        97
             case msg of

      
        
        98
        +        Tick now ->

      
        
        99
        +            ( { model | now = Just now }, Effect.none )

      
        
        100
        +

      
        92
        101
                 UserClickedSubmit ->

      
        93
        102
                     ( { model | isSubmittingForm = True, apiError = Nothing }

      
        94
        103
                     , case model.formVariant of

      ···
        108
        117
                     )

      
        109
        118
         

      
        110
        119
                 UserClickedResendActivationEmail ->

      
        111
        
        -            ( model

      
        
        120
        +            ( { model | lastClicked = model.now }

      
        112
        121
                     , Api.Auth.resendVerificationEmail

      
        113
        122
                         { onResponse = ApiResendVerificationEmail

      
        114
        123
                         , email = model.email

      ···
        152
        161
         

      
        153
        162
         

      
        154
        163
         subscriptions : Model -> Sub Msg

      
        155
        
        -subscriptions _ =

      
        156
        
        -    Sub.none

      
        
        164
        +subscriptions model =

      
        
        165
        +    if model.gotSignedUp then

      
        
        166
        +        Time.every 1000 Tick

      
        
        167
        +

      
        
        168
        +    else

      
        
        169
        +        Sub.none

      
        157
        170
         

      
        158
        171
         

      
        159
        172
         

      ···
        167
        180
                 [ H.div [ A.class "min-h-screen flex items-center justify-center bg-gray-50 p-4" ]

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

      
        169
        182
                         -- TODO: add oauth buttons

      
        170
        
        -                [ viewBanner model.apiError model.gotSignedUp

      
        
        183
        +                [ viewBanner model

      
        171
        184
                         , viewHeader model.formVariant

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

      
        173
        186
                             [ viewChangeVariant model.formVariant

      ···
        180
        193
             }

      
        181
        194
         

      
        182
        195
         

      
        183
        
        -viewBanner : Maybe Api.Error -> Bool -> Html Msg

      
        184
        
        -viewBanner maybeError gotSignedUp =

      
        185
        
        -    case ( maybeError, gotSignedUp ) of

      
        
        196
        +viewBanner : Model -> Html Msg

      
        
        197
        +viewBanner model =

      
        
        198
        +    case ( model.apiError, model.gotSignedUp ) of

      
        186
        199
                 ( Just error, False ) ->

      
        187
        200
                     viewBannerError error

      
        188
        201
         

      
        189
        202
                 ( Nothing, True ) ->

      
        190
        
        -            viewBannerSuccess

      
        
        203
        +            viewBannerSuccess model.now model.lastClicked

      
        191
        204
         

      
        192
        205
                 _ ->

      
        193
        206
                     H.text ""

      
        194
        207
         

      
        195
        208
         

      
        196
        
        -viewBannerSuccess : Html Msg

      
        197
        
        -viewBannerSuccess =

      
        
        209
        +viewBannerSuccess : Maybe Posix -> Maybe Posix -> Html Msg

      
        
        210
        +viewBannerSuccess now lastClicked =

      
        198
        211
             let

      
        199
        212
                 buttonClassesBase : String

      
        200
        213
                 buttonClassesBase =

      
        201
        214
                     "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"

      
        202
        215
         

      
        203
        216
                 buttonClasses : Bool -> String

      
        204
        
        -        buttonClasses disabled =

      
        205
        
        -            if disabled then

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

      
        
        217
        +        buttonClasses active =

      
        
        218
        +            if active then

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

      
        207
        220
         

      
        208
        221
                     else

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

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

      
        
        223
        +

      
        
        224
        +        timeLeftSeconds : Int

      
        
        225
        +        timeLeftSeconds =

      
        
        226
        +            case ( now, lastClicked ) of

      
        
        227
        +                ( Just now_, Just last ) ->

      
        
        228
        +                    let

      
        
        229
        +                        remainingMs : Int

      
        
        230
        +                        remainingMs =

      
        
        231
        +                            30 * 1000 - (Time.posixToMillis now_ - Time.posixToMillis last)

      
        
        232
        +                    in

      
        
        233
        +                    if remainingMs > 0 then

      
        
        234
        +                        remainingMs // 1000

      
        
        235
        +

      
        
        236
        +                    else

      
        
        237
        +                        0

      
        
        238
        +

      
        
        239
        +                _ ->

      
        
        240
        +                    0

      
        210
        241
         

      
        211
        
        -        isDisabled : Bool

      
        212
        
        -        isDisabled =

      
        213
        
        -            False

      
        
        242
        +        canClick : Bool

      
        
        243
        +        canClick =

      
        
        244
        +            timeLeftSeconds == 0

      
        214
        245
             in

      
        215
        246
             H.div [ A.class "bg-green-50 border border-green-200 rounded-md p-4 mb-4" ]

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

      
        217
        248
                 , H.p [ A.class "text-green-800 text-sm" ] [ H.text "We've sent you a verification link. Please check your email and click the link to activate your account." ]

      
        218
        249
                 , H.button

      
        219
        
        -            -- TODO: implement countdown for resend button

      
        220
        
        -            [ A.class (buttonClasses isDisabled)

      
        
        250
        +            [ A.class (buttonClasses canClick)

      
        221
        251
                     , E.onClick UserClickedResendActivationEmail

      
        222
        
        -            , A.disabled isDisabled

      
        
        252
        +            , A.disabled (not canClick)

      
        223
        253
                     ]

      
        224
        254
                     [ H.text "Resend verification email" ]

      
        225
        
        -        , if isDisabled then

      
        226
        
        -            H.p [ A.class "text-gray-600 text-xs mt-2" ] [ H.text "You can request a new verification email in N seconds" ]

      
        
        255
        +        , if canClick then

      
        
        256
        +            H.text ""

      
        227
        257
         

      
        228
        258
                   else

      
        229
        
        -            H.text ""

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

      
        
        260
        +                [ H.text

      
        
        261
        +                    ("You can request a new verification email in "

      
        
        262
        +                        ++ String.fromInt timeLeftSeconds

      
        
        263
        +                        ++ " seconds."

      
        
        264
        +                    )

      
        
        265
        +                ]

      
        230
        266
                 ]

      
        231
        267
         

      
        232
        268