all repos

onasty @ c3cd668

a one-time notes service
11 files changed, 178 insertions(+), 64 deletions(-)
web: forgot password (#169)

* web: implement forgot password button

* mailer: update the reset-password link for the web app

* fix(mailer): fix the url in email

* refactor(web): rename component file for boxes

* refactor(web/auth): change the logic behind how banner is shown
Author: Olexandr Smirnov ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-07-22 14:16:33 +0300
Parent: b650519
M mailer/.env.example
···
        1
        1
         APP_URL=http://localhost:8000

      
        
        2
        +FRONTEND_URL=http://localhost:1234

      
        2
        3
         NATS_URL="nats:4222"

      
        3
        4
         METRICS_ENABLED=true

      
        4
        5
         METRICS_PORT=8001

      
M mailer/config.go
···
        6
        6
         )

      
        7
        7
         

      
        8
        8
         type Config struct {

      
        9
        
        -	AppURL        string

      
        
        9
        +	AppURL      string

      
        
        10
        +	FrontendURL string

      
        
        11
        +

      
        10
        12
         	NatsURL       string

      
        11
        13
         	MailgunFrom   string

      
        12
        14
         	MailgunDomain string

      ···
        23
        25
         func NewConfig() *Config {

      
        24
        26
         	return &Config{

      
        25
        27
         		AppURL:         getenvOrDefault("APP_URL", ""),

      
        
        28
        +		FrontendURL:    getenvOrDefault("FRONTEND_URL", ""),

      
        26
        29
         		NatsURL:        getenvOrDefault("NATS_URL", ""),

      
        27
        30
         		MailgunFrom:    getenvOrDefault("MAILGUN_FROM", ""),

      
        28
        31
         		MailgunDomain:  getenvOrDefault("MAILGUN_DOMAIN", ""),

      
M mailer/main.go
···
        51
        51
         	}

      
        52
        52
         

      
        53
        53
         	mg := NewMailgun(cfg.MailgunFrom, cfg.MailgunDomain, cfg.MailgunAPIKey)

      
        54
        
        -	service := NewService(cfg.AppURL, mg)

      
        
        54
        +	service := NewService(cfg.AppURL, cfg.FrontendURL, mg)

      
        55
        55
         	handlers := NewHandlers(service)

      
        56
        56
         

      
        57
        57
         	if err := handlers.RegisterAll(svc); err != nil {

      
M mailer/service.go
···
        6
        6
         )

      
        7
        7
         

      
        8
        8
         type Service struct {

      
        9
        
        -	appURL string

      
        10
        
        -	mg     *Mailgun

      
        
        9
        +	appURL      string

      
        
        10
        +	frontendURL string

      
        
        11
        +

      
        
        12
        +	mg *Mailgun

      
        11
        13
         }

      
        12
        14
         

      
        13
        
        -func NewService(appURL string, mg *Mailgun) *Service {

      
        
        15
        +func NewService(appURL, frontendURL string, mg *Mailgun) *Service {

      
        14
        16
         	return &Service{

      
        15
        
        -		appURL: appURL,

      
        16
        
        -		mg:     mg,

      
        
        17
        +		appURL:      appURL,

      
        
        18
        +		frontendURL: frontendURL,

      
        
        19
        +		mg:          mg,

      
        17
        20
         	}

      
        18
        21
         }

      
        19
        22
         

      ···
        23
        26
         	receiver, templateName string,

      
        24
        27
         	templateOpts map[string]string,

      
        25
        28
         ) error {

      
        26
        
        -	tmpl, err := getTemplate(s.appURL, templateName)

      
        
        29
        +	tmpl, err := getTemplate(s.appURL, s.frontendURL, templateName)

      
        27
        30
         	if err != nil {

      
        28
        31
         		return err

      
        29
        32
         	}

      
M mailer/template.go
···
        5
        5
         	"fmt"

      
        6
        6
         )

      
        7
        7
         

      
        
        8
        +var ErrInvalidTemplate = errors.New("failed to get template")

      
        
        9
        +

      
        8
        10
         type Template struct {

      
        9
        11
         	Subject string

      
        10
        12
         	Body    string

      ···
        12
        14
         

      
        13
        15
         type TemplateFunc func(args map[string]string) Template

      
        14
        16
         

      
        15
        
        -func getTemplate(appURL string, templateName string) (TemplateFunc, error) {

      
        
        17
        +func getTemplate(appURL, frontendURL string, templateName string) (TemplateFunc, error) {

      
        16
        18
         	switch templateName {

      
        17
        19
         	case "email_verification":

      
        18
        20
         		return emailVerificationTemplate(appURL), nil

      
        19
        21
         	case "reset_password":

      
        20
        
        -		return passwordResetTemplate(appURL), nil

      
        
        22
        +		return passwordResetTemplate(frontendURL), nil

      
        21
        23
         	default:

      
        22
        
        -		return nil, errors.New("failed to get template") //nolint:err113

      
        
        24
        +		return nil, ErrInvalidTemplate

      
        23
        25
         	}

      
        24
        26
         }

      
        25
        27
         

      ···
        36
        38
         	}

      
        37
        39
         }

      
        38
        40
         

      
        39
        
        -func passwordResetTemplate(appURL string) TemplateFunc {

      
        
        41
        +func passwordResetTemplate(frontendURL string) TemplateFunc {

      
        40
        42
         	return func(opts map[string]string) Template {

      
        41
        43
         		return Template{

      
        42
        44
         			Subject: "Onasty: reset your password",

      
        43
        
        -			// TODO: when ui is ready, change the link to the ui

      
        44
        45
         			Body: fmt.Sprintf(`To reset your password, use this api:

      
        45
        
        -<a href="%[1]s/api/v1/auth/reset-password/%[2]s">%[1]s/api/v1/auth/reset-password/%[2]s</a>

      
        
        46
        +<a href="%[1]s/auth?token=%[2]s">%[1]s/auth?token=%[2]s</a>

      
        46
        47
         <br />

      
        47
        48
         <br />

      
        48
        
        -This link will expire after an hour.`, appURL, opts["token"]),

      
        
        49
        +This link will expire after an hour.`, frontendURL, opts["token"]),

      
        49
        50
         		}

      
        50
        51
         	}

      
        51
        52
         }

      
M web/src/Api/Auth.elm
···
        1
        
        -module Api.Auth exposing (refreshToken, resendVerificationEmail, signin, signup)

      
        
        1
        +module Api.Auth exposing (forgotPassword, refreshToken, resendVerificationEmail, resetPassword, signin, signup)

      
        2
        2
         

      
        3
        3
         import Api

      
        4
        4
         import Data.Credentials as Credentials exposing (Credentials)

      ···
        72
        72
                 , body = Http.jsonBody body

      
        73
        73
                 , onResponse = options.onResponse

      
        74
        74
                 , decoder = Credentials.decode

      
        
        75
        +        }

      
        
        76
        +

      
        
        77
        +

      
        
        78
        +forgotPassword : { onResponse : Result Api.Error () -> msg, email : String } -> Effect msg

      
        
        79
        +forgotPassword options =

      
        
        80
        +    Effect.sendApiRequest

      
        
        81
        +        { endpoint = "/api/v1/auth/reset-password"

      
        
        82
        +        , method = "POST"

      
        
        83
        +        , body = Encode.object [ ( "email", Encode.string options.email ) ] |> Http.jsonBody

      
        
        84
        +        , onResponse = options.onResponse

      
        
        85
        +        , decoder = Decode.succeed ()

      
        
        86
        +        }

      
        
        87
        +

      
        
        88
        +

      
        
        89
        +resetPassword : { onResponse : Result Api.Error () -> msg, token : String, password : String } -> Effect msg

      
        
        90
        +resetPassword options =

      
        
        91
        +    Effect.sendApiRequest

      
        
        92
        +        { endpoint = "/api/v1/auth/reset-password/" ++ options.token

      
        
        93
        +        , method = "POST"

      
        
        94
        +        , body = Encode.object [ ( "password", Encode.string options.password ) ] |> Http.jsonBody

      
        
        95
        +        , onResponse = options.onResponse

      
        
        96
        +        , decoder = Decode.succeed ()

      
        75
        97
                 }

      
        76
        98
         

      
        77
        99
         

      
A web/src/Components/Box.elm
···
        
        1
        +module Components.Box exposing (error, success, successBox)

      
        
        2
        +

      
        
        3
        +import Html as H exposing (Html)

      
        
        4
        +import Html.Attributes as A

      
        
        5
        +

      
        
        6
        +

      
        
        7
        +error : String -> Html msg

      
        
        8
        +error errorMsg =

      
        
        9
        +    H.div [ A.class "bg-red-50 border border-red-200 rounded-md p-4" ]

      
        
        10
        +        [ H.p [ A.class "text-red-800 text-sm" ] [ H.text errorMsg ] ]

      
        
        11
        +

      
        
        12
        +

      
        
        13
        +success : { header : String, body : String } -> Html msg

      
        
        14
        +success opts =

      
        
        15
        +    successBox

      
        
        16
        +        [ H.div [ A.class "font-medium text-green-800 mb-2" ] [ H.text opts.header ]

      
        
        17
        +        , H.p [ A.class "text-green-800 text-sm" ] [ H.text opts.body ]

      
        
        18
        +        ]

      
        
        19
        +

      
        
        20
        +

      
        
        21
        +successBox : List (Html msg) -> Html msg

      
        
        22
        +successBox child =

      
        
        23
        +    H.div [ A.class "bg-green-50 border border-green-200 rounded-md p-4" ] child

      
D web/src/Components/Error.elm
···
        1
        
        -module Components.Error exposing (error)

      
        2
        
        -

      
        3
        
        -import Html as H exposing (Html)

      
        4
        
        -import Html.Attributes as A

      
        5
        
        -

      
        6
        
        -

      
        7
        
        -error : String -> Html msg

      
        8
        
        -error errorMsg =

      
        9
        
        -    H.div [ A.class "bg-red-50 border border-red-200 rounded-md p-4" ]

      
        10
        
        -        [ H.p [ A.class "text-red-800 text-sm" ] [ H.text errorMsg ] ]

      
M web/src/Pages/Auth.elm
···
        1
        
        -module Pages.Auth exposing (Model, Msg, Variant, page)

      
        
        1
        +module Pages.Auth exposing (Banner, Model, Msg, Variant, page)

      
        2
        2
         

      
        3
        3
         import Api

      
        4
        4
         import Api.Auth

      
        5
        5
         import Auth.User

      
        6
        
        -import Components.Error

      
        
        6
        +import Components.Box

      
        7
        7
         import Components.Form

      
        8
        8
         import Components.Utils

      
        9
        9
         import Data.Credentials exposing (Credentials)

      
        
        10
        +import Dict

      
        10
        11
         import Effect exposing (Effect)

      
        11
        12
         import Html as H exposing (Html)

      
        12
        13
         import Html.Attributes as A

      ···
        21
        22
         

      
        22
        23
         

      
        23
        24
         page : Shared.Model -> Route () -> Page Model Msg

      
        24
        
        -page shared _ =

      
        
        25
        +page shared route =

      
        25
        26
             Page.new

      
        26
        
        -        { init = init shared

      
        
        27
        +        { init = init shared route

      
        27
        28
                 , update = update

      
        28
        29
                 , subscriptions = subscriptions

      
        29
        30
                 , view = view

      ···
        41
        42
             , passwordAgain : String

      
        42
        43
             , isSubmittingForm : Bool

      
        43
        44
             , formVariant : Variant

      
        44
        
        -    , showVerifyBanner : Bool

      
        
        45
        +    , banner : Banner

      
        45
        46
             , lastClicked : Maybe Posix

      
        46
        
        -    , apiError : Maybe Api.Error

      
        47
        47
             , now : Maybe Posix

      
        48
        48
             }

      
        49
        49
         

      
        50
        50
         

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

      
        52
        
        -init shared _ =

      
        53
        
        -    ( { formVariant = SignIn

      
        
        51
        +init : Shared.Model -> Route () -> () -> ( Model, Effect Msg )

      
        
        52
        +init shared route () =

      
        
        53
        +    let

      
        
        54
        +        formVariant =

      
        
        55
        +            case Dict.get "token" route.query of

      
        
        56
        +                Just token ->

      
        
        57
        +                    SetNewPassword token

      
        
        58
        +

      
        
        59
        +                Nothing ->

      
        
        60
        +                    SignIn

      
        
        61
        +    in

      
        
        62
        +    ( { formVariant = formVariant

      
        54
        63
               , isSubmittingForm = False

      
        55
        64
               , email = ""

      
        56
        65
               , password = ""

      
        57
        66
               , passwordAgain = ""

      
        58
        
        -      , showVerifyBanner = False

      
        59
        67
               , lastClicked = Nothing

      
        60
        
        -      , apiError = Nothing

      
        
        68
        +      , banner = Hidden

      
        61
        69
               , now = Nothing

      
        62
        70
               }

      
        63
        71
             , case shared.user of

      ···
        81
        89
             | UserClickedResendActivationEmail

      
        82
        90
             | ApiSignInResponded (Result Api.Error Credentials)

      
        83
        91
             | ApiSignUpResponded (Result Api.Error ())

      
        
        92
        +    | ApiForgotPasswordResponded (Result Api.Error ())

      
        
        93
        +    | ApiSetNewPasswordResponded (Result Api.Error ())

      
        84
        94
             | ApiResendVerificationEmail (Result Api.Error ())

      
        85
        95
         

      
        86
        96
         

      ···
        90
        100
             | PasswordAgain

      
        91
        101
         

      
        92
        102
         

      
        
        103
        +type alias ResetPasswordToken =

      
        
        104
        +    String

      
        
        105
        +

      
        
        106
        +

      
        93
        107
         type Variant

      
        94
        108
             = SignIn

      
        95
        109
             | SignUp

      
        
        110
        +    | ForgotPassword

      
        
        111
        +    | SetNewPassword ResetPasswordToken

      
        
        112
        +

      
        
        113
        +

      
        
        114
        +type Banner

      
        
        115
        +    = Hidden

      
        
        116
        +    | ResendVerificationEmail

      
        
        117
        +    | Error Api.Error

      
        
        118
        +    | CheckEmail

      
        96
        119
         

      
        97
        120
         

      
        98
        121
         update : Msg -> Model -> ( Model, Effect Msg )

      ···
        102
        125
                     ( { model | now = Just now }, Effect.none )

      
        103
        126
         

      
        104
        127
                 UserClickedSubmit ->

      
        105
        
        -            ( { model | isSubmittingForm = True, apiError = Nothing }

      
        
        128
        +            ( { model | isSubmittingForm = True }

      
        106
        129
                     , case model.formVariant of

      
        107
        130
                         SignIn ->

      
        108
        131
                             Api.Auth.signin

      ···
        117
        140
                                 , email = model.email

      
        118
        141
                                 , password = model.password

      
        119
        142
                                 }

      
        
        143
        +

      
        
        144
        +                ForgotPassword ->

      
        
        145
        +                    Api.Auth.forgotPassword { onResponse = ApiForgotPasswordResponded, email = model.email }

      
        
        146
        +

      
        
        147
        +                SetNewPassword token ->

      
        
        148
        +                    Api.Auth.resetPassword { onResponse = ApiSetNewPasswordResponded, token = token, password = model.password }

      
        120
        149
                     )

      
        121
        150
         

      
        122
        151
                 UserClickedResendActivationEmail ->

      ···
        142
        171
                 ApiSignInResponded (Ok credentials) ->

      
        143
        172
                     ( { model | isSubmittingForm = False }, Effect.signin credentials )

      
        144
        173
         

      
        145
        
        -        ApiSignInResponded (Err error) ->

      
        146
        
        -            if Api.isNotVerified error then

      
        147
        
        -                ( { model | isSubmittingForm = False, apiError = Nothing, showVerifyBanner = True }, Effect.none )

      
        
        174
        +        ApiSignInResponded (Err err) ->

      
        
        175
        +            if Api.isNotVerified err then

      
        
        176
        +                ( { model | isSubmittingForm = False, banner = ResendVerificationEmail }, Effect.none )

      
        148
        177
         

      
        149
        178
                     else

      
        150
        
        -                ( { model | isSubmittingForm = False, apiError = Just error }, Effect.none )

      
        
        179
        +                ( { model | isSubmittingForm = False, banner = Error err }, Effect.none )

      
        151
        180
         

      
        152
        181
                 ApiSignUpResponded (Ok ()) ->

      
        153
        
        -            ( { model | isSubmittingForm = False, showVerifyBanner = True }, Effect.none )

      
        
        182
        +            ( { model | isSubmittingForm = False, banner = ResendVerificationEmail }, Effect.none )

      
        154
        183
         

      
        155
        
        -        ApiSignUpResponded (Err error) ->

      
        156
        
        -            ( { model | isSubmittingForm = False, apiError = Just error }, Effect.none )

      
        
        184
        +        ApiSignUpResponded (Err err) ->

      
        
        185
        +            ( { model | isSubmittingForm = False, banner = Error err }, Effect.none )

      
        157
        186
         

      
        158
        187
                 ApiResendVerificationEmail (Ok ()) ->

      
        159
        
        -            ( { model | apiError = Nothing }, Effect.none )

      
        
        188
        +            ( model, Effect.none )

      
        160
        189
         

      
        161
        190
                 ApiResendVerificationEmail (Err err) ->

      
        162
        
        -            ( { model | apiError = Just err }, Effect.none )

      
        
        191
        +            ( { model | banner = Error err }, Effect.none )

      
        163
        192
         

      
        
        193
        +        ApiSetNewPasswordResponded (Ok ()) ->

      
        
        194
        +            ( { model | isSubmittingForm = False, formVariant = SignIn, password = "", passwordAgain = "" }, Effect.replaceRoutePath Route.Path.Auth )

      
        164
        195
         

      
        
        196
        +        ApiSetNewPasswordResponded (Err err) ->

      
        
        197
        +            ( { model | isSubmittingForm = False, banner = Error err }, Effect.none )

      
        165
        198
         

      
        166
        
        --- SUBSCRIPTIONS

      
        
        199
        +        ApiForgotPasswordResponded (Ok ()) ->

      
        
        200
        +            ( { model | isSubmittingForm = False, banner = CheckEmail }, Effect.none )

      
        
        201
        +

      
        
        202
        +        ApiForgotPasswordResponded (Err err) ->

      
        
        203
        +            ( { model | isSubmittingForm = False, banner = Error err }, Effect.none )

      
        167
        204
         

      
        168
        205
         

      
        169
        206
         subscriptions : Model -> Sub Msg

      
        170
        207
         subscriptions model =

      
        171
        
        -    if model.showVerifyBanner then

      
        
        208
        +    if model.banner == ResendVerificationEmail then

      
        172
        209
                 Time.every 1000 Tick

      
        173
        210
         

      
        174
        211
             else

      ···
        201
        238
         

      
        202
        239
         viewBanner : Model -> Html Msg

      
        203
        240
         viewBanner model =

      
        204
        
        -    case ( model.apiError, model.showVerifyBanner ) of

      
        205
        
        -        ( Just error, False ) ->

      
        206
        
        -            Components.Error.error (Api.errorMessage error)

      
        
        241
        +    case model.banner of

      
        
        242
        +        Hidden ->

      
        
        243
        +            H.text ""

      
        
        244
        +

      
        
        245
        +        Error err ->

      
        
        246
        +            Components.Box.error (Api.errorMessage err)

      
        
        247
        +

      
        
        248
        +        CheckEmail ->

      
        
        249
        +            Components.Box.success { header = "Check your email!", body = "To continue with resetting your password please check the email we've sent." }

      
        207
        250
         

      
        208
        
        -        ( Nothing, True ) ->

      
        
        251
        +        ResendVerificationEmail ->

      
        209
        252
                     viewVerificationBanner model.now model.lastClicked

      
        210
        253
         

      
        211
        
        -        _ ->

      
        212
        
        -            H.text ""

      
        213
        
        -

      
        214
        254
         

      
        215
        255
         viewVerificationBanner : Maybe Posix -> Maybe Posix -> Html Msg

      
        216
        256
         viewVerificationBanner now lastClicked =

      ···
        241
        281
                 canClick =

      
        242
        282
                     timeLeftSeconds == 0

      
        243
        283
             in

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

      
        
        284
        +    Components.Box.successBox

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

      
        246
        286
                 , H.p [ A.class "text-green-800 text-sm" ] [ H.text "Please verify your account to continue. We've sent a verification link to your email — click it to activate your account." ]

      
        247
        287
                 , H.button

      ···
        268
        308
         

      
        269
        309
                         SignUp ->

      
        270
        310
                             ( "Create Account", "Enter your information to create your account" )

      
        
        311
        +

      
        
        312
        +                ForgotPassword ->

      
        
        313
        +                    ( "Forgot Password", "Enter your email to reset your password" )

      
        
        314
        +

      
        
        315
        +                SetNewPassword _ ->

      
        
        316
        +                    ( "Set New Password", "Enter your new password to reset your account" )

      
        271
        317
             in

      
        272
        318
             H.div [ A.class "p-6 pb-4" ]

      
        273
        319
                 [ H.h1 [ A.class "text-2xl font-bold text-center mb-2" ] [ H.text title ]

      ···
        325
        371
                         , viewFormInput { field = PasswordAgain, value = model.passwordAgain }

      
        326
        372
                         , viewSubmitButton model

      
        327
        373
                         ]

      
        
        374
        +

      
        
        375
        +            ForgotPassword ->

      
        
        376
        +                [ viewFormInput { field = Email, value = model.email }

      
        
        377
        +                , viewSubmitButton model

      
        
        378
        +                ]

      
        
        379
        +

      
        
        380
        +            SetNewPassword token ->

      
        
        381
        +                [ viewFormInput { field = Password, value = model.password }

      
        
        382
        +                , viewFormInput { field = PasswordAgain, value = model.passwordAgain }

      
        
        383
        +                , H.input [ A.type_ "hidden", A.value token, A.name "token" ] []

      
        
        384
        +                , viewSubmitButton model

      
        
        385
        +                ]

      
        328
        386
                 )

      
        329
        387
         

      
        330
        388
         

      ···
        350
        408
                 [ H.button

      
        351
        409
                     [ A.class "text-sm text-black hover:underline focus:outline-none"

      
        352
        410
                     , A.type_ "button"

      
        353
        
        -

      
        354
        
        -            -- TODO: implement forgot password

      
        355
        
        -            -- , E.onClick (UserChangedFormVariant ForgotPassword)

      
        
        411
        +            , E.onClick (UserChangedFormVariant ForgotPassword)

      
        356
        412
                     ]

      
        357
        413
                     [ H.text "Forgot password?" ]

      
        358
        414
                 ]

      ···
        389
        445
                         || String.isEmpty model.passwordAgain

      
        390
        446
                         || (model.password /= model.passwordAgain)

      
        391
        447
         

      
        
        448
        +        ForgotPassword ->

      
        
        449
        +            model.isSubmittingForm || String.isEmpty model.email

      
        
        450
        +

      
        
        451
        +        SetNewPassword _ ->

      
        
        452
        +            model.isSubmittingForm

      
        
        453
        +                || String.isEmpty model.password

      
        
        454
        +                || String.isEmpty model.passwordAgain

      
        
        455
        +                || (model.password /= model.passwordAgain)

      
        
        456
        +

      
        392
        457
         

      
        393
        458
         fromVariantToLabel : Variant -> String

      
        394
        459
         fromVariantToLabel variant =

      ···
        398
        463
         

      
        399
        464
                 SignUp ->

      
        400
        465
                     "Sign Up"

      
        
        466
        +

      
        
        467
        +        ForgotPassword ->

      
        
        468
        +            "Forgot Password"

      
        
        469
        +

      
        
        470
        +        SetNewPassword _ ->

      
        
        471
        +            "Set new password"

      
        401
        472
         

      
        402
        473
         

      
        403
        474
         fromFieldToLabel : Field -> String

      
M web/src/Pages/Home_.elm
···
        2
        2
         

      
        3
        3
         import Api

      
        4
        4
         import Api.Note

      
        5
        
        -import Components.Error

      
        
        5
        +import Components.Box

      
        6
        6
         import Components.Form

      
        7
        7
         import Components.Utils

      
        8
        8
         import Data.Note as Note

      ···
        211
        211
                         [ H.div [ A.class "bg-white rounded-lg border border-gray-200 shadow-sm" ]

      
        212
        212
                             [ viewHeader model.pageVariant

      
        213
        213
                             , H.div [ A.class "p-6 space-y-6" ]

      
        214
        
        -                        [ Components.Utils.viewMaybe model.apiError (\e -> Components.Error.error (Api.errorMessage e))

      
        
        214
        +                        [ Components.Utils.viewMaybe model.apiError (\e -> Components.Box.error (Api.errorMessage e))

      
        215
        215
                                 , case model.pageVariant of

      
        216
        216
                                     CreateNote ->

      
        217
        217
                                         viewCreateNoteForm model shared.appURL

      
M web/src/Pages/Secret/Slug_.elm
···
        2
        2
         

      
        3
        3
         import Api

      
        4
        4
         import Api.Note

      
        5
        
        -import Components.Error

      
        
        5
        +import Components.Box

      
        6
        6
         import Components.Utils

      
        7
        7
         import Data.Note exposing (Metadata, Note)

      
        8
        8
         import Effect exposing (Effect)

      ···
        145
        145
                                     viewNoteNotFound model.slug

      
        146
        146
         

      
        147
        147
                                   else

      
        148
        
        -                            Components.Error.error (Api.errorMessage error)

      
        
        148
        +                            Components.Box.error (Api.errorMessage error)

      
        149
        149
                                 ]

      
        150
        150
                         )

      
        151
        151
                     ]