2 files changed,
70 insertions(+),
42 deletions(-)
Author:
Smirnov Oleksandr
ss2316544@gmail.com
Committed by:
GitHub
noreply@github.com
Committed at:
2025-06-22 14:20:34 +0300
Parent:
336ceb2
jump to
| M | web/src/Effect.elm |
| M | web/src/Pages/Auth.elm |
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