19 files changed,
247 insertions(+),
316 deletions(-)
Author:
Olexandr Smirnov
ss2316544@gmail.com
Committed by:
GitHub
noreply@github.com
Committed at:
2025-07-07 12:52:31 +0300
Parent:
f6a62ba
jump to
M
web/src/Api/Auth.elm
··· 63 63 -> Effect msg 64 64 refreshToken options = 65 65 let 66 - body : Encode.Value 67 66 body = 68 - Encode.object 69 - [ ( "refresh_token", Encode.string options.refreshToken ) ] 67 + Encode.object [ ( "refresh_token", Encode.string options.refreshToken ) ] 70 68 in 71 69 Effect.sendApiRequest 72 70 { endpoint = "/api/v1/auth/refresh-tokens"
M
web/src/Api/Note.elm
··· 21 21 -> Effect msg 22 22 create options = 23 23 let 24 - encodeMaybe : Maybe a -> b -> (a -> E.Value) -> ( b, E.Value ) 25 - encodeMaybe maybeData field value = 26 - case maybeData of 24 + encodeMaybe : Maybe a -> String -> (a -> E.Value) -> ( String, E.Value ) 25 + encodeMaybe maybe field value = 26 + case maybe of 27 27 Just data -> 28 28 ( field, value data ) 29 29 30 30 Nothing -> 31 31 ( field, E.null ) 32 32 33 - body : E.Value 34 33 body = 35 34 E.object 36 35 [ ( "content", E.string options.content ) ··· 41 40 ( "expires_at", E.null ) 42 41 43 42 else 44 - ( "expires_at" 45 - , options.expiresAt 46 - |> Iso8601.fromTime 47 - |> E.string 48 - ) 43 + ( "expires_at", options.expiresAt |> Iso8601.fromTime |> E.string ) 49 44 ] 50 45 in 51 46 Effect.sendApiRequest
A
web/src/Components/Form.elm
··· 1 +module Components.Form exposing (input) 2 + 3 +import Html as H exposing (Html) 4 +import Html.Attributes as A 5 +import Html.Events as E 6 + 7 + 8 +input : 9 + -- TODO: add `error : Maybe String`, to input to show that field is not correct and message 10 + { id : String 11 + , field : field 12 + , label : String 13 + , type_ : String 14 + , value : String 15 + , placeholder : String 16 + , required : Bool 17 + , helpText : Maybe String 18 + , prefix : Maybe String 19 + , onInput : String -> msg 20 + } 21 + -> Html msg 22 +input opts = 23 + H.div [ A.class "space-y-2" ] 24 + [ H.label 25 + [ A.for opts.id 26 + , A.class "block text-sm font-medium text-gray-700" 27 + ] 28 + [ H.text opts.label ] 29 + , H.div 30 + [ A.class 31 + (if opts.prefix /= Nothing then 32 + "flex items-center" 33 + 34 + else 35 + "" 36 + ) 37 + ] 38 + [ case opts.prefix of 39 + Just prefix -> 40 + H.span [ A.class "text-gray-500 text-md mr-2 whitespace-nowrap" ] [ H.text prefix ] 41 + 42 + Nothing -> 43 + H.text "" 44 + , H.input 45 + [ 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" 46 + , A.type_ opts.type_ 47 + , A.value opts.value 48 + , A.id opts.id 49 + , A.placeholder opts.placeholder 50 + , A.required opts.required 51 + , E.onInput opts.onInput 52 + ] 53 + [] 54 + ] 55 + , case opts.helpText of 56 + Just help -> 57 + H.p [ A.class "text-xs text-gray-500 mt-1" ] [ H.text help ] 58 + 59 + Nothing -> 60 + H.text "" 61 + ]
D
web/src/Components/Note.elm
··· 1 -module Components.Note exposing (noteIconSvg, noteNotFoundSvg, warningSvg) 2 - 3 -import Svg exposing (Svg) 4 -import Svg.Attributes as A 5 - 6 - 7 -noteIconSvg : Svg msg 8 -noteIconSvg = 9 - Svg.svg 10 - [ A.class "w-8 h-8 text-gray-400" 11 - , A.fill "none" 12 - , A.stroke "currentColor" 13 - , A.viewBox "0 0 24 24" 14 - ] 15 - [ Svg.path 16 - [ A.d "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" 17 - , A.strokeWidth "2" 18 - , A.strokeLinecap "round" 19 - , A.strokeLinejoin "round" 20 - ] 21 - [] 22 - ] 23 - 24 - 25 -noteNotFoundSvg : Svg msg 26 -noteNotFoundSvg = 27 - Svg.svg 28 - [ A.class "w-8 h-8 text-red-500" 29 - , A.fill "none" 30 - , A.stroke "currentColor" 31 - , A.viewBox "0 0 24 24" 32 - ] 33 - [ Svg.path 34 - [ A.d "M9.172 16.172a4 4 0 015.656 0M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" 35 - , A.strokeWidth "2" 36 - , A.strokeLinecap "round" 37 - , A.strokeLinejoin "round" 38 - ] 39 - [] 40 - , Svg.path 41 - [ A.d "M6 18L18 6M6 6l12 12" 42 - , A.strokeWidth "2" 43 - , A.strokeLinecap "round" 44 - , A.strokeLinejoin "round" 45 - ] 46 - [] 47 - ] 48 - 49 - 50 -warningSvg : Svg msg 51 -warningSvg = 52 - Svg.svg 53 - [ A.class "w-4 h-4 text-orange-600" 54 - , A.fill "none" 55 - , A.stroke "currentColor" 56 - , A.viewBox "0 0 24 24" 57 - ] 58 - [ Svg.path 59 - [ A.d "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z" 60 - , A.strokeWidth "2" 61 - , A.strokeLinecap "round" 62 - , A.strokeLinejoin "round" 63 - ] 64 - [] 65 - ]
A
web/src/Components/Utils.elm
··· 1 +module Components.Utils exposing (loadSvg, viewIf, viewMaybe) 2 + 3 +import Html as H exposing (Html) 4 +import Html.Attributes as A 5 + 6 + 7 +viewIf : Bool -> Html msg -> Html msg 8 +viewIf condition html = 9 + if condition then 10 + html 11 + 12 + else 13 + H.text "" 14 + 15 + 16 +viewMaybe : Maybe a -> (a -> Html msg) -> Html msg 17 +viewMaybe maybeValue toHtml = 18 + case maybeValue of 19 + Just value -> 20 + toHtml value 21 + 22 + Nothing -> 23 + H.text "" 24 + 25 + 26 +loadSvg : { path : String, class : String } -> Html msg 27 +loadSvg { path, class } = 28 + H.img [ A.src ("/static/" ++ path), A.class class ] []
M
web/src/Effect.elm
··· 1 1 module Effect exposing 2 - ( Effect 3 - , none, batch 4 - , sendCmd, sendMsg 5 - , pushRoute, replaceRoute 6 - , pushRoutePath, replaceRoutePath 7 - , loadExternalUrl, back 8 - , sendApiRequest, refreshTokens 9 - , sendToClipboard 10 - , signin, logout, saveUser, clearUser 2 + ( Effect, none, batch, sendCmd, sendMsg 3 + , pushRoute, replaceRoute, pushRoutePath, replaceRoutePath, loadExternalUrl, back 4 + , sendApiRequest, sendToClipboard 5 + , signin, logout, refreshTokens, saveUser, clearUser 11 6 , map, toCmd 12 7 ) 13 8 14 9 {-| 15 10 16 -@docs Effect 17 - 18 -@docs none, batch 19 -@docs sendCmd, sendMsg 20 - 21 -@docs pushRoute, replaceRoute 22 -@docs pushRoutePath, replaceRoutePath 23 -@docs loadExternalUrl, back 24 - 25 -@docs sendApiRequest, refreshTokens 26 -@docs sendToClipboard 27 -@docs signin, logout, saveUser, clearUser 28 - 11 +@docs Effect, none, batch, sendCmd, sendMsg 12 +@docs pushRoute, replaceRoute, pushRoutePath, replaceRoutePath, loadExternalUrl, back 13 +@docs sendApiRequest, sendToClipboard 14 +@docs signin, logout, refreshTokens, saveUser, clearUser 29 15 @docs map, toCmd 30 16 31 17 -} ··· 62 48 | SendSharedMsg Shared.Msg.Msg 63 49 | SendToLocalStorage { key : String, value : Json.Encode.Value } 64 50 | SendToClipboard String 65 - | SendApiRequest (HttpRequestDetails msg) 66 - 67 - 68 -type alias HttpRequestDetails msg = 69 - { endpoint : String 70 - , method : String 71 - , body : Http.Body 72 - , decoder : Json.Decode.Decoder msg 73 - , onHttpError : Api.Error -> msg 74 - } 51 + | SendApiRequest 52 + { endpoint : String 53 + , method : String 54 + , body : Http.Body 55 + , decoder : Json.Decode.Decoder msg 56 + , onHttpError : Api.Error -> msg 57 + } 75 58 76 59 77 60 ··· 230 213 -- INTERNALS 231 214 232 215 233 -{-| Elm Land depends on this function to connect pages and layouts 234 -together into the overall app. 235 --} 236 216 map : (msg1 -> msg2) -> Effect msg1 -> Effect msg2 237 217 map fn effect = 238 218 case effect of ··· 276 256 } 277 257 278 258 279 -{-| Elm Land depends on this function to perform your effects. 280 --} 281 259 toCmd : 282 260 { key : Browser.Navigation.Key 283 261 , url : Url ··· 351 329 Err err -> 352 330 opts.onHttpError err 353 331 ) 354 - (\resp -> fromHttpResponseToCustomError opts.decoder resp) 332 + (\resp -> httpResponseToCustomError opts.decoder resp) 355 333 , timeout = Just (1000 * 60) -- 60 second timeout 356 334 , tracker = Nothing 357 335 } 358 336 359 337 360 -fromHttpResponseToCustomError : Json.Decode.Decoder msg -> Http.Response String -> Result Api.Error msg 361 -fromHttpResponseToCustomError decoder response = 338 +httpResponseToCustomError : Json.Decode.Decoder msg -> Http.Response String -> Result Api.Error msg 339 +httpResponseToCustomError decoder response = 362 340 case response of 363 341 Http.GoodStatus_ _ body -> 364 342 case
M
web/src/ExpirationOptions.elm
··· 1 -module ExpirationOptions exposing (ExpiresAt, expirationOptions) 2 - 3 - 4 -type alias ExpiresAt = 5 - { text : String, value : Int } 1 +module ExpirationOptions exposing (expirationOptions) 6 2 7 3 8 -expirationOptions : List ExpiresAt 4 +expirationOptions : List { text : String, value : Int } 9 5 expirationOptions = 10 6 [ { text = "Never expires (default)", value = 0 } 11 7 , { text = "1 hour", value = 60 * 60 * 1000 }
M
web/src/JwtUtil.elm
··· 9 9 isExpired : Time.Posix -> String -> Bool 10 10 isExpired now token = 11 11 let 12 - expirationThreshold : number 13 12 expirationThreshold = 14 13 40 * 1000 15 14 16 - timeDiff : Int 17 15 timeDiff = 18 16 getTokenExpiration token 19 17 |> (\expiration -> expiration - Time.posixToMillis now) ··· 21 19 timeDiff <= expirationThreshold 22 20 23 21 24 -{-| Extracts the expiration time (in millis) from a JWT token. 25 -Returns 0 if cannot parse token. 26 --} 27 22 getTokenExpiration : String -> Int 28 23 getTokenExpiration token = 29 - Jwt.getTokenExpirationMillis token 30 - |> Result.withDefault 0 24 + Jwt.getTokenExpirationMillis token |> Result.withDefault 0
M
web/src/Layouts/Header.elm
··· 93 93 94 94 viewNav : Auth.User.SignInStatus -> List (Html Msg) 95 95 viewNav user = 96 + let 97 + viewLink text path = 98 + H.a [ A.class "text-gray-600 hover:text-black transition-colors", Route.Path.href path ] 99 + [ H.text text ] 100 + in 96 101 case user of 97 102 Auth.User.SignedIn _ -> 98 - viewSignedInNav 99 - 100 - Auth.User.NotSignedIn -> 101 - viewNotSignedInNav 102 - 103 - Auth.User.RefreshingTokens -> 104 - viewNotSignedInNav 105 - 106 - 107 -viewSignedInNav : List (Html Msg) 108 -viewSignedInNav = 109 - [ viewLink "Profile" Route.Path.Profile_Me 110 - , H.button 111 - [ A.class "text-gray-600 hover:text-red-600 transition-colors" 112 - , E.onClick UserClickedLogout 113 - ] 114 - [ H.text "Logout" ] 115 - ] 116 - 117 - 118 -viewNotSignedInNav : List (Html Msg) 119 -viewNotSignedInNav = 120 - -- TODO: or add about page, or delete the link 121 - [ viewLink "About" Route.Path.Home_ 122 - , H.a 123 - [ A.class "px-4 py-2 border border-gray-300 rounded-md text-black hover:bg-gray-50 transition-colors" 124 - , Route.Path.href Route.Path.Auth 125 - ] 126 - [ H.text "Sign In/Up" ] 127 - ] 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" ] 109 + ] 128 110 129 - 130 -viewLink : String -> Route.Path.Path -> Html Msg 131 -viewLink text path = 132 - H.a 133 - [ A.class "text-gray-600 hover:text-black transition-colors" 134 - , Route.Path.href path 135 - ] 136 - [ H.text text ] 111 + _ -> 112 + [ viewLink "About" Route.Path.Home_ -- TODO: or add about page, or delete the link 113 + , H.a 114 + [ A.class "px-4 py-2 border border-gray-300 rounded-md text-black hover:bg-gray-50 transition-colors" 115 + , Route.Path.href Route.Path.Auth 116 + ] 117 + [ H.text "Sign In/Up" ] 118 + ]
M
web/src/Pages/Auth.elm
··· 4 4 import Api.Auth 5 5 import Auth.User 6 6 import Components.Error 7 +import Components.Form 8 +import Components.Utils 7 9 import Data.Credentials exposing (Credentials) 8 10 import Effect exposing (Effect) 9 11 import Html as H exposing (Html) ··· 142 144 ( { model | isSubmittingForm = False }, Effect.signin credentials ) 143 145 144 146 ApiSignInResponded (Err error) -> 147 + -- TODO: check if error is Unauthorized and prompt use to activate account 145 148 ( { model | isSubmittingForm = False, apiError = Just error }, Effect.none ) 146 149 147 150 ApiSignUpResponded (Ok ()) -> ··· 225 228 case ( now, lastClicked ) of 226 229 ( Just now_, Just last ) -> 227 230 let 228 - remainingMs = 229 - 30 * 1000 - (Time.posixToMillis now_ - Time.posixToMillis last) 231 + elapsedMs = 232 + Time.posixToMillis now_ - Time.posixToMillis last 230 233 in 231 - if remainingMs > 0 then 232 - remainingMs // 1000 233 - 234 - else 235 - 0 234 + max 0 ((30 * 1000 - elapsedMs) // 1000) 236 235 237 236 _ -> 238 237 0 ··· 250 249 , A.disabled (not canClick) 251 250 ] 252 251 [ H.text "Resend verification email" ] 253 - , if canClick then 254 - H.text "" 255 - 256 - else 257 - H.p [ A.class "text-gray-600 text-xs mt-2" ] 258 - [ H.text 259 - ("You can request a new verification email in " 260 - ++ String.fromInt timeLeftSeconds 261 - ++ " seconds." 262 - ) 263 - ] 252 + , Components.Utils.viewIf (not canClick) 253 + (H.p 254 + [ A.class "text-gray-600 text-xs mt-2" ] 255 + [ H.text ("You can request a new verification email in " ++ String.fromInt timeLeftSeconds ++ " seconds.") ] 256 + ) 264 257 ] 265 258 266 259 ··· 284 277 viewChangeVariant : Variant -> Html Msg 285 278 viewChangeVariant variant = 286 279 let 287 - base = 288 - "flex-1 px-4 py-2 rounded-md font-medium transition-colors" 289 - 290 - buttonClasses : Bool -> String 291 280 buttonClasses active = 281 + let 282 + base = 283 + "flex-1 px-4 py-2 rounded-md font-medium transition-colors" 284 + in 292 285 if active then 293 286 base ++ " bg-black text-white" 294 287 ··· 336 329 337 330 viewFormInput : { field : Field, value : String } -> Html Msg 338 331 viewFormInput opts = 339 - H.div [ A.class "space-y-2" ] 340 - [ H.label 341 - [ A.class "block text-sm font-medium text-gray-700" ] 342 - [ H.text (fromFieldToLabel opts.field) ] 343 - , H.div [] 344 - [ H.input 345 - [ 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" 346 - , A.type_ (fromFieldToInputType opts.field) 347 - , A.value opts.value 348 - , A.placeholder (fromFieldToLabel opts.field) 349 - , A.required True 350 - , E.onInput (UserUpdatedInput opts.field) 351 - ] 352 - [] 353 - ] 354 - ] 332 + Components.Form.input 333 + { id = fromFieldToInputType opts.field 334 + , field = opts.field 335 + , label = fromFieldToLabel opts.field 336 + , type_ = fromFieldToInputType opts.field 337 + , value = opts.value 338 + , placeholder = fromFieldToLabel opts.field 339 + , required = True 340 + , onInput = UserUpdatedInput opts.field 341 + , helpText = Nothing 342 + , prefix = Nothing 343 + } 355 344 356 345 357 346 viewForgotPassword : Html Msg
M
web/src/Pages/Home_.elm
··· 3 3 import Api 4 4 import Api.Note 5 5 import Components.Error 6 +import Components.Form 7 +import Components.Utils 6 8 import Data.Note as Note 7 9 import Effect exposing (Effect) 8 10 import ExpirationOptions exposing (expirationOptions) ··· 209 211 [ H.div [ A.class "bg-white rounded-lg border border-gray-200 shadow-sm" ] 210 212 [ viewHeader model.pageVariant 211 213 , H.div [ A.class "p-6 space-y-6" ] 212 - [ case model.apiError of 213 - Just error -> 214 - Components.Error.error (Api.errorMessage error) 215 - 216 - Nothing -> 217 - H.text "" 214 + [ Components.Utils.viewMaybe model.apiError (\e -> Components.Error.error (Api.errorMessage e)) 218 215 , case model.pageVariant of 219 216 CreateNote -> 220 217 viewCreateNoteForm model shared.appURL ··· 257 254 , A.class "space-y-6" 258 255 ] 259 256 [ viewTextarea 260 - , viewFormInput 261 - { field = Slug 257 + , Components.Form.input 258 + { id = "slug" 259 + , field = Slug 262 260 , label = "Custom URL Slug (optional)" 263 261 , placeholder = "my-unique-slug" 264 262 , type_ = "text" 265 - , help = "Leave empty to generate a random slug" 263 + , helpText = Just "Leave empty to generate a random slug" 266 264 , prefix = Just (secretUrl appUrl "") 265 + , onInput = UserUpdatedInput Slug 266 + , required = False 267 + , value = Maybe.withDefault "" model.slug 267 268 } 268 269 , H.div [ A.class "grid grid-cols-1 md:grid-cols-2 gap-6" ] 269 270 [ H.div [ A.class "space-y-6" ] 270 - [ viewFormInput 271 - { field = Password 271 + [ Components.Form.input 272 + { id = "password" 273 + , field = Password 272 274 , label = "Password Protection (optional)" 273 - , type_ = "text" 275 + , type_ = "password" 274 276 , placeholder = "Enter password to protect this paste" 275 - , help = "Viewers will need this password to access the paste" 277 + , helpText = Just "Viewers will need this password to access the paste" 276 278 , prefix = Nothing 279 + , onInput = UserUpdatedInput Password 280 + , required = False 281 + , value = Maybe.withDefault "" model.password 277 282 } 278 283 ] 279 284 , H.div [ A.class "space-y-6" ] ··· 302 307 , 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 resize-vertical font-mono text-sm" 303 308 ] 304 309 [] 305 - ] 306 - 307 - 308 -viewFormInput : { field : Field, label : String, placeholder : String, type_ : String, prefix : Maybe String, help : String } -> Html Msg 309 -viewFormInput options = 310 - H.div [ A.class "space-y-2" ] 311 - [ H.label 312 - [ A.for (fromFieldToName options.field) 313 - , A.class "block text-sm font-medium text-gray-700 mb-2" 314 - ] 315 - [ H.text options.label ] 316 - , H.div [ A.class "flex items-center" ] 317 - [ case options.prefix of 318 - Just prefix -> 319 - H.span [ A.class "text-gray-500 text-md mr-2 whitespace-nowrap" ] [ H.text prefix ] 320 - 321 - Nothing -> 322 - H.text "" 323 - , H.input 324 - [ E.onInput (UserUpdatedInput options.field) 325 - , A.id (fromFieldToName options.field) 326 - , A.type_ options.type_ 327 - , A.placeholder options.placeholder 328 - , 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" 329 - ] 330 - [] 331 - ] 332 - , H.p [ A.class "text-xs text-gray-500 mt-1" ] [ H.text options.help ] 333 310 ] 334 311 335 312
M
web/src/Pages/Secret/Slug_.elm
··· 3 3 import Api 4 4 import Api.Note 5 5 import Components.Error 6 -import Components.Note 6 +import Components.Utils 7 7 import Data.Note exposing (Metadata, Note) 8 8 import Effect exposing (Effect) 9 9 import Html as H exposing (Html) ··· 199 199 viewShowNoteHeader : Zone -> String -> Note -> Html Msg 200 200 viewShowNoteHeader zone slug note = 201 201 H.div [] 202 - [ if note.burnBeforeExpiration then 203 - H.div [ A.class "bg-orange-50 border-b border-orange-200 p-4" ] 202 + [ Components.Utils.viewIf note.burnBeforeExpiration 203 + (H.div [ A.class "bg-orange-50 border-b border-orange-200 p-4" ] 204 204 [ H.div [ A.class "flex items-center gap-3" ] 205 205 [ H.div [ A.class "w-6 h-6 bg-orange-100 rounded-full flex items-center justify-center flex-shrink-0" ] 206 - [ Components.Note.warningSvg ] 206 + [ Components.Utils.loadSvg { path = "warning.svg", class = "w-4 h-4 text-orange-600" } ] 207 207 , H.p [ A.class "text-orange-800 text-sm font-medium" ] 208 208 [ H.text "This note was destroyed. If you need to keep it, copy it before closing this window." ] 209 209 ] 210 210 ] 211 - 212 - else 213 - H.text "" 211 + ) 214 212 , H.div [ A.class "p-6 pb-4 border-b border-gray-200" ] 215 213 [ H.div [ A.class "flex justify-between items-start" ] 216 214 [ H.div [] 217 215 [ H.h1 [ A.class "text-2xl font-bold text-gray-900" ] [ H.text ("Note: " ++ slug) ] 218 216 , H.div [ A.class "text-sm text-gray-500 mt-2 space-y-1" ] 219 217 [ H.p [] [ H.text ("Created: " ++ T.toString zone note.createdAt) ] 220 - , case note.expiresAt of 221 - Just expiresAt -> 222 - H.p [] [ H.text ("Expires at: " ++ T.toString zone expiresAt) ] 223 - 224 - Nothing -> 225 - H.text "" 218 + , Components.Utils.viewMaybe note.expiresAt (\n -> H.p [] [ H.text ("Expires at: " ++ T.toString zone n) ]) 226 219 ] 227 220 ] 228 221 , H.div [ A.class "flex gap-2" ] ··· 246 239 H.div [ A.class "p-6" ] 247 240 [ H.div [ A.class "text-center py-12" ] 248 241 [ H.div [ A.class "w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4" ] 249 - [ Components.Note.noteNotFoundSvg ] 242 + [ Components.Utils.loadSvg { path = "note-not-found.svg", class = "w-8 h-8 text-red-500" } ] 250 243 , H.h2 [ A.class "text-xl font-semibold text-gray-900 mb-2" ] 251 244 [ H.text ("Note " ++ slug ++ " Not Found") ] 252 245 , H.div [ A.class "text-gray-600 mb-6 space-y-2" ] ··· 265 258 ] 266 259 267 260 268 -viewOpenNote : 269 - { slug : String 270 - , hasPassword : Bool 271 - , isLoading : Bool 272 - , password : Maybe String 273 - } 274 - -> Html Msg 261 +viewOpenNote : { slug : String, hasPassword : Bool, isLoading : Bool, password : Maybe String } -> Html Msg 275 262 viewOpenNote opts = 276 263 let 277 264 isDisabled = ··· 295 282 [ H.div [ A.class "text-center py-12" ] 296 283 [ H.div [ A.class "mb-6" ] 297 284 [ H.div [ A.class "w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4" ] 298 - [ Components.Note.noteIconSvg ] 285 + [ Components.Utils.loadSvg { path = "note-icon.svg", class = "w-8 h-8 text-gray-400" } ] 299 286 , H.h2 [ A.class "text-lg font-semibold text-gray-900 mb-2" ] [ H.text opts.slug ] 300 287 , H.p [ A.class "text-gray-600 mb-6" ] [ H.text "You're about read and destroy the note." ] 301 288 ] ··· 303 290 [ E.onSubmit UserClickedViewNote 304 291 , A.class "max-w-sm mx-auto space-y-4" 305 292 ] 306 - [ if opts.hasPassword then 307 - H.div 293 + [ Components.Utils.viewIf opts.hasPassword 294 + (H.div 308 295 [ A.class "space-y-2" ] 309 296 [ H.label 310 297 [ A.class "block text-sm font-medium text-gray-700 text-left" ] ··· 315 302 ] 316 303 [] 317 304 ] 318 - 319 - else 320 - H.text "" 305 + ) 321 306 , H.button 322 307 [ A.class buttonData.class 323 308 , A.type_ "submit"
A
web/static/note-icon.svg
··· 1 +<svg 2 + xmlns="http://www.w3.org/2000/svg" 3 + fill="none" 4 + stroke="currentColor" 5 + viewBox="0 0 24 24" 6 +> 7 + <path 8 + d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" 9 + stroke-width="2" 10 + stroke-linecap="round" 11 + stroke-linejoin="round" 12 + /> 13 +</svg>
A
web/static/note-not-found.svg
··· 1 +<svg 2 + xmlns="http://www.w3.org/2000/svg" 3 + fill="none" 4 + stroke="currentColor" 5 + viewBox="0 0 24 24" 6 +> 7 + <path 8 + d="M9.172 16.172a4 4 0 015.656 0M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" 9 + stroke-width="2" 10 + stroke-linecap="round" 11 + stroke-linejoin="round" 12 + /> 13 + <path 14 + d="M6 18L18 6M6 6l12 12" 15 + stroke-width="2" 16 + stroke-linecap="round" 17 + stroke-linejoin="round" 18 + /> 19 +</svg>
A
web/static/warning.svg
··· 1 +<svg 2 + xmlns="http://www.w3.org/2000/svg" 3 + fill="none" 4 + stroke="currentColor" 5 + viewBox="0 0 24 24" 6 +> 7 + <path 8 + d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z" 9 + stroke-width="2" 10 + stroke-linecap="round" 11 + stroke-linejoin="round" 12 + /> 13 +</svg>