all repos

onasty @ 67a69fb

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,23 +175,15 @@ , onResponse : Result Api.Error value -> msg

} -> Effect msg sendApiRequest opts = - let - onHttpError : Api.Error -> msg - onHttpError err = - opts.onResponse (Err err) - - decoder : Json.Decode.Decoder msg - decoder = - opts.decoder - |> Json.Decode.map Ok - |> Json.Decode.map opts.onResponse - in SendApiRequest { endpoint = opts.endpoint , method = opts.method , body = opts.body - , onHttpError = onHttpError - , decoder = decoder + , onHttpError = \e -> opts.onResponse (Err e) + , decoder = + opts.decoder + |> Json.Decode.map Ok + |> Json.Decode.map opts.onResponse }

@@ -368,7 +360,7 @@ Ok value ->

Ok value Err err -> - Err (Api.JsonDecodeError { message = "Failed to decode JSON response", reason = err }) + Err (Api.JsonDecodeError { message = "Failed to decode response", reason = err }) Http.BadStatus_ { statusCode } body -> case Json.Decode.decodeString Data.Error.decode body of

@@ -376,7 +368,7 @@ Ok err ->

Err (Api.HttpError { message = err.message, reason = Http.BadStatus statusCode }) Err err -> - Err (Api.JsonDecodeError { message = "Something unexpected happened", reason = err }) + Err (Api.JsonDecodeError { message = "Failed to decode response", reason = err }) Http.BadUrl_ url -> Err (Api.HttpError { message = "Unexpected URL format", reason = Http.BadUrl url })
M web/src/Pages/Auth.elm

@@ -13,6 +13,7 @@ import Page exposing (Page)

import Route exposing (Route) import Route.Path import Shared +import Time exposing (Posix) import View exposing (View)

@@ -38,19 +39,23 @@ , passwordAgain : String

, isSubmittingForm : Bool , formVariant : Variant , gotSignedUp : Bool + , lastClicked : Maybe Posix , apiError : Maybe Api.Error + , now : Maybe Posix } init : Shared.Model -> () -> ( Model, Effect Msg ) init shared _ = - ( { isSubmittingForm = False - , email = "" + ( { email = "" , password = "" , passwordAgain = "" + , isSubmittingForm = False , formVariant = SignIn + , gotSignedUp = False + , lastClicked = Nothing , apiError = Nothing - , gotSignedUp = False + , now = Nothing } , case shared.user of Auth.User.SignedIn _ ->

@@ -66,7 +71,8 @@ -- UPDATE

type Msg - = UserUpdatedInput Field String + = Tick Posix + | UserUpdatedInput Field String | UserChangedFormVariant Variant | UserClickedSubmit | UserClickedResendActivationEmail

@@ -89,6 +95,9 @@

update : Msg -> Model -> ( Model, Effect Msg ) update msg model = case msg of + Tick now -> + ( { model | now = Just now }, Effect.none ) + UserClickedSubmit -> ( { model | isSubmittingForm = True, apiError = Nothing } , case model.formVariant of

@@ -108,7 +117,7 @@ }

) UserClickedResendActivationEmail -> - ( model + ( { model | lastClicked = model.now } , Api.Auth.resendVerificationEmail { onResponse = ApiResendVerificationEmail , email = model.email

@@ -152,8 +161,12 @@ -- SUBSCRIPTIONS

subscriptions : Model -> Sub Msg -subscriptions _ = - Sub.none +subscriptions model = + if model.gotSignedUp then + Time.every 1000 Tick + + else + Sub.none

@@ -167,7 +180,7 @@ , body =

[ H.div [ A.class "min-h-screen flex items-center justify-center bg-gray-50 p-4" ] [ H.div [ A.class "w-full max-w-md bg-white rounded-lg border border-gray-200 shadow-sm" ] -- TODO: add oauth buttons - [ viewBanner model.apiError model.gotSignedUp + [ viewBanner model , viewHeader model.formVariant , H.div [ A.class "px-6 pb-6 space-y-4" ] [ viewChangeVariant model.formVariant

@@ -180,53 +193,76 @@ ]

} -viewBanner : Maybe Api.Error -> Bool -> Html Msg -viewBanner maybeError gotSignedUp = - case ( maybeError, gotSignedUp ) of +viewBanner : Model -> Html Msg +viewBanner model = + case ( model.apiError, model.gotSignedUp ) of ( Just error, False ) -> viewBannerError error ( Nothing, True ) -> - viewBannerSuccess + viewBannerSuccess model.now model.lastClicked _ -> H.text "" -viewBannerSuccess : Html Msg -viewBannerSuccess = +viewBannerSuccess : Maybe Posix -> Maybe Posix -> Html Msg +viewBannerSuccess now lastClicked = let buttonClassesBase : String buttonClassesBase = "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" buttonClasses : Bool -> String - buttonClasses disabled = - if disabled then - buttonClassesBase ++ " border border-gray-300 text-gray-400 cursor-not-allowed" + buttonClasses active = + if active then + buttonClassesBase ++ " border border-gray-300 text-gray-700 hover:bg-gray-50" else - buttonClassesBase ++ " border border-gray-300 text-gray-700 hover:bg-gray-50" + buttonClassesBase ++ " border border-gray-300 text-gray-400 cursor-not-allowed" + + timeLeftSeconds : Int + timeLeftSeconds = + case ( now, lastClicked ) of + ( Just now_, Just last ) -> + let + remainingMs : Int + remainingMs = + 30 * 1000 - (Time.posixToMillis now_ - Time.posixToMillis last) + in + if remainingMs > 0 then + remainingMs // 1000 + + else + 0 + + _ -> + 0 - isDisabled : Bool - isDisabled = - False + canClick : Bool + canClick = + timeLeftSeconds == 0 in H.div [ A.class "bg-green-50 border border-green-200 rounded-md p-4 mb-4" ] [ H.div [ A.class "font-medium text-green-800 mb-2" ] [ H.text "Check your email!" ] , 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." ] , H.button - -- TODO: implement countdown for resend button - [ A.class (buttonClasses isDisabled) + [ A.class (buttonClasses canClick) , E.onClick UserClickedResendActivationEmail - , A.disabled isDisabled + , A.disabled (not canClick) ] [ H.text "Resend verification email" ] - , if isDisabled then - H.p [ A.class "text-gray-600 text-xs mt-2" ] [ H.text "You can request a new verification email in N seconds" ] + , if canClick then + H.text "" else - H.text "" + H.p [ A.class "text-gray-600 text-xs mt-2" ] + [ H.text + ("You can request a new verification email in " + ++ String.fromInt timeLeftSeconds + ++ " seconds." + ) + ] ]