4 files changed,
134 insertions(+),
79 deletions(-)
Author:
Olexandr Smirnov
ss2316544@gmail.com
Committed by:
GitHub
noreply@github.com
Committed at:
2025-08-09 17:50:55 +0300
Parent:
7a1da0f
M
web/src/Components/Form.elm
··· 1 -module Components.Form exposing (ButtonStyle(..), CanBeClicked, button, input, submitButton) 1 +module Components.Form exposing (ButtonStyle(..), CanBeClicked, InputStyle(..), button, input, submitButton) 2 2 3 3 import Html as H exposing (Html) 4 4 import Html.Attributes as A ··· 9 9 -- INPUT 10 10 11 11 12 +type InputStyle 13 + = Simple 14 + | Complex 15 + { prefix : String 16 + , helpText : String 17 + } 18 + 19 + 12 20 input : 13 - -- TODO: add `error : Maybe String`, to show that field is not correct and message 14 21 { id : String 15 22 , field : field 16 - , label : String 17 23 , type_ : String 18 24 , value : String 25 + , label : String 19 26 , placeholder : String 20 27 , required : Bool 21 - , helpText : Maybe String 22 - , prefix : Maybe String 23 28 , onInput : String -> msg 29 + , style : InputStyle 30 + , error : Maybe String 24 31 } 25 32 -> Html msg 26 33 input opts = 34 + let 35 + style = 36 + case opts.style of 37 + Simple -> 38 + { prefix = H.text "", help = H.text "" } 39 + 40 + Complex complex -> 41 + { prefix = H.span [ A.class "text-gray-500 text-md whitespace-nowrap" ] [ H.text complex.prefix ] 42 + , help = H.p [ A.class "text-xs text-gray-500 mt-1" ] [ H.text complex.helpText ] 43 + } 44 + 45 + error = 46 + case opts.error of 47 + Nothing -> 48 + { element = H.text "", inputAdditionalClasses = "border-gray-300 focus:ring-black " } 49 + 50 + Just err -> 51 + { element = H.p [ A.class "text-red-600 text-xs mt-1" ] [ H.text err ] 52 + , inputAdditionalClasses = " border-red-400 focus:ring-red-500" 53 + } 54 + in 27 55 H.div [ A.class "space-y-2" ] 28 56 [ H.label 29 57 [ A.for opts.id 30 58 , A.class "block text-sm font-medium text-gray-700" 31 59 ] 32 60 [ H.text opts.label ] 33 - , H.div 34 - [ A.class 35 - (if opts.prefix /= Nothing then 36 - "flex items-center" 37 - 38 - else 39 - "" 40 - ) 41 - ] 42 - [ case opts.prefix of 43 - Just prefix -> 44 - H.span [ A.class "text-gray-500 text-md mr-2 whitespace-nowrap" ] [ H.text prefix ] 45 - 46 - Nothing -> 47 - H.text "" 61 + , H.div [ A.class "flex items-center" ] 62 + [ style.prefix 48 63 , H.input 49 - [ 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" 64 + [ A.class ("w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:border-transparent transition-colors" ++ error.inputAdditionalClasses) 50 65 , A.type_ opts.type_ 51 66 , A.value opts.value 52 67 , A.id opts.id ··· 56 71 ] 57 72 [] 58 73 ] 59 - , case opts.helpText of 60 - Just help -> 61 - H.p [ A.class "text-xs text-gray-500 mt-1" ] [ H.text help ] 62 - 63 - Nothing -> 64 - H.text "" 74 + , error.element 75 + , style.help 65 76 ] 66 77 67 78
M
web/src/Pages/Auth.elm
··· 336 336 ] 337 337 (case model.formVariant of 338 338 SignIn -> 339 - [ viewFormInput { field = Email, value = model.email } 340 - , viewFormInput { field = Password, value = model.password } 339 + [ viewFormInput { field = Email, value = model.email, error = validateEmail model.email } 340 + , viewFormInput { field = Password, value = model.password, error = validatePassword model.password } 341 341 , viewForgotPassword 342 342 , viewSubmitButton model 343 343 ] 344 344 345 345 SignUp -> 346 - [ viewFormInput { field = Email, value = model.email } 347 - , viewFormInput { field = Password, value = model.password } 348 - , viewFormInput { field = PasswordAgain, value = model.passwordAgain } 346 + [ viewFormInput { field = Email, value = model.email, error = validateEmail model.email } 347 + , viewFormInput { field = Password, value = model.password, error = validatePassword model.password } 348 + , viewFormInput { field = PasswordAgain, value = model.passwordAgain, error = validatePasswords model.password model.passwordAgain } 349 349 , viewSubmitButton model 350 350 ] 351 351 352 352 ForgotPassword -> 353 - [ viewFormInput { field = Email, value = model.email } 353 + [ viewFormInput { field = Email, value = model.email, error = validateEmail model.email } 354 354 , viewSubmitButton model 355 355 ] 356 356 357 - SetNewPassword token -> 358 - [ viewFormInput { field = Password, value = model.password } 359 - , viewFormInput { field = PasswordAgain, value = model.passwordAgain } 360 - , H.input [ A.type_ "hidden", A.value token, A.name "token" ] [] 357 + SetNewPassword _ -> 358 + [ viewFormInput { field = Password, value = model.password, error = validatePassword model.password } 359 + , viewFormInput { field = PasswordAgain, value = model.passwordAgain, error = validatePasswords model.password model.passwordAgain } 361 360 , viewSubmitButton model 362 361 ] 363 362 ) 364 363 365 364 366 -viewFormInput : { field : Field, value : String } -> Html Msg 365 +viewFormInput : { field : Field, value : String, error : Maybe String } -> Html Msg 367 366 viewFormInput opts = 368 367 Components.Form.input 369 - { id = (fromFieldToFieldInfo opts.field).label 370 - , field = opts.field 368 + { style = Components.Form.Simple 369 + , id = (fromFieldToFieldInfo opts.field).label 370 + , error = opts.error 371 371 , label = (fromFieldToFieldInfo opts.field).label 372 372 , type_ = (fromFieldToFieldInfo opts.field).type_ 373 - , value = opts.value 374 373 , placeholder = (fromFieldToFieldInfo opts.field).label 375 - , required = True 376 374 , onInput = UserUpdatedInput opts.field 377 - , helpText = Nothing 378 - , prefix = Nothing 375 + , field = opts.field 376 + , value = opts.value 377 + , required = True 379 378 } 380 379 381 380 ··· 406 405 case model.formVariant of 407 406 SignIn -> 408 407 model.isSubmittingForm 409 - || String.isEmpty model.email 410 - || String.isEmpty model.password 408 + || (validateEmail model.email /= Nothing) 409 + || (validatePassword model.password /= Nothing) 411 410 412 411 SignUp -> 413 412 model.isSubmittingForm 414 - || String.isEmpty model.email 415 - || String.isEmpty model.password 416 - || String.isEmpty model.passwordAgain 417 - || (model.password /= model.passwordAgain) 413 + || (validateEmail model.email /= Nothing) 414 + || (validatePassword model.password /= Nothing) 415 + || (validatePasswords model.password model.passwordAgain /= Nothing) 418 416 419 417 ForgotPassword -> 420 - model.isSubmittingForm || String.isEmpty model.email 418 + model.isSubmittingForm || (validateEmail model.email /= Nothing) 421 419 422 420 SetNewPassword _ -> 423 421 model.isSubmittingForm 424 - || String.isEmpty model.password 425 - || String.isEmpty model.passwordAgain 426 - || (model.password /= model.passwordAgain) 422 + || (validateEmail model.email /= Nothing) 423 + || (validatePassword model.password /= Nothing) 424 + || (validatePasswords model.password model.passwordAgain /= Nothing) 425 + 426 + 427 +validateEmail : String -> Maybe String 428 +validateEmail email = 429 + if 430 + not (String.isEmpty email) 431 + && (not (String.contains "@" email) || not (String.contains "." email)) 432 + then 433 + Just "Please enter a valid email address." 434 + 435 + else 436 + Nothing 437 + 438 + 439 +validatePassword : String -> Maybe String 440 +validatePassword passwd = 441 + if not (String.isEmpty passwd) && String.length passwd < 8 then 442 + Just "Password must be at least 8 characters long." 443 + 444 + else 445 + Nothing 446 + 447 + 448 +validatePasswords : String -> String -> Maybe String 449 +validatePasswords passowrd1 password2 = 450 + if not (String.isEmpty passowrd1) && passowrd1 /= password2 then 451 + Just "Passwords do not match." 452 + 453 + else 454 + Nothing 427 455 428 456 429 457 fromVariantToLabel : FormVariant -> String
M
web/src/Pages/Home_.elm
··· 241 241 242 242 243 243 -- VIEW CREATE NOTE 244 --- TODO: validate the form 245 244 246 245 247 246 viewCreateNoteForm : Model -> (String -> String) -> Html Msg ··· 252 251 ] 253 252 [ viewTextarea 254 253 , Components.Form.input 255 - { id = "slug" 254 + { style = 255 + Components.Form.Complex 256 + { prefix = appUrl "" 257 + , helpText = "Leave empty to generate a random slug" 258 + } 259 + , error = validateSlugInput model.slug 256 260 , field = Slug 261 + , id = "slug" 257 262 , label = "Custom URL Slug (optional)" 258 - , placeholder = "my-unique-slug" 259 - , type_ = "text" 260 - , helpText = Just "Leave empty to generate a random slug" 261 - , prefix = Just (appUrl "") 262 263 , onInput = UserUpdatedInput Slug 264 + , placeholder = "my-unique-slug" 263 265 , required = False 266 + , type_ = "text" 264 267 , value = Maybe.withDefault "" model.slug 265 268 } 266 269 , H.div [ A.class "grid grid-cols-1 md:grid-cols-2 gap-6" ] 267 270 [ H.div [ A.class "space-y-6" ] 268 271 [ Components.Form.input 269 - { id = "password" 272 + { style = 273 + Components.Form.Complex 274 + { prefix = "" 275 + , helpText = "Viewers will need this password to access the paste" 276 + } 270 277 , field = Password 278 + , id = "password" 279 + , error = Nothing 271 280 , label = "Password Protection (optional)" 272 - , type_ = "password" 281 + , onInput = UserUpdatedInput Password 273 282 , placeholder = "Enter password to protect this paste" 274 - , helpText = Just "Viewers will need this password to access the paste" 275 - , prefix = Nothing 276 - , onInput = UserUpdatedInput Password 277 283 , required = False 284 + , type_ = "password" 278 285 , value = Maybe.withDefault "" model.password 279 286 } 280 287 ] ··· 287 294 [ Components.Form.submitButton 288 295 { text = "Create note" 289 296 , style = Components.Form.Primary (isFormDisabled model) 290 - , disabled = False 297 + , disabled = isFormDisabled model 291 298 , class = "" 292 299 } 293 300 ] ··· 356 363 isFormDisabled : Model -> Bool 357 364 isFormDisabled model = 358 365 String.isEmpty model.content 366 + || (validateSlugInput model.slug /= Nothing) 367 + 368 + 369 +validateSlugInput : Maybe String -> Maybe String 370 +validateSlugInput slug = 371 + let 372 + value = 373 + Maybe.withDefault "" slug 374 + in 375 + if not (String.isEmpty value) && String.contains " " value then 376 + Just "Slug cannot contain spaces." 377 + 378 + else 379 + Nothing 359 380 360 381 361 382 fromFieldToName : Field -> String