all repos

onasty @ 3b5e67f9c8eb6a915c65d5a30d0fffe901227f2e

a one-time notes service

onasty/web/src/Pages/Secret/Slug_.elm (view raw)

Olexandr Smirnov Olexandr Smirnov
ss2316544@gmail.com
web: button component; improve code consistency (#168)..., 10 months ago
1
module Pages.Secret.Slug_ exposing (Model, Msg, PageVariant, page)
2
3
import Api
4
import Api.Note
5
import Components.Box
6
import Components.Form
7
import Components.Icon
8
import Components.Utils
9
import Data.Note exposing (Metadata, Note)
10
import Effect exposing (Effect)
11
import Html as H exposing (Html)
12
import Html.Attributes as A
13
import Html.Events as E
14
import Layouts
15
import Page exposing (Page)
16
import Route exposing (Route)
17
import Shared
18
import Time exposing (Zone)
19
import Time.Format as T
20
import View exposing (View)
21
22
23
page : Shared.Model -> Route { slug : String } -> Page Model Msg
24
page shared route =
25
    Page.new
26
        { init = init route.params.slug
27
        , update = update
28
        , subscriptions = subscriptions
29
        , view = view shared
30
        }
31
        |> Page.withLayout (\_ -> Layouts.Header {})
32
33
34
35
-- INIT
36
37
38
type PageVariant
39
    = RequestNote
40
    | ShowNote (Api.Response Note)
41
    | NotFound
42
43
44
type alias Model =
45
    { page : PageVariant
46
    , metadata : Api.Response Metadata
47
    , slug : String
48
    , password : Maybe String
49
    }
50
51
52
init : String -> () -> ( Model, Effect Msg )
53
init slug () =
54
    ( { page = RequestNote
55
      , metadata = Api.Loading
56
      , slug = slug
57
      , password = Nothing
58
      }
59
    , Api.Note.getMetadata
60
        { onResponse = ApiGetMetadataResponded
61
        , slug = slug
62
        }
63
    )
64
65
66
67
-- UPDATE
68
69
70
type Msg
71
    = UserClickedViewNote
72
    | UserClickedCopyContent
73
    | UserUpdatedPassword String
74
    | ApiGetNoteResponded (Result Api.Error Note)
75
    | ApiGetMetadataResponded (Result Api.Error Metadata)
76
77
78
update : Msg -> Model -> ( Model, Effect Msg )
79
update msg model =
80
    case msg of
81
        UserClickedViewNote ->
82
            ( { model | page = ShowNote Api.Loading }
83
            , Api.Note.get
84
                { onResponse = ApiGetNoteResponded
85
                , password = model.password
86
                , slug = model.slug
87
                }
88
            )
89
90
        UserClickedCopyContent ->
91
            case model.page of
92
                ShowNote (Api.Success note) ->
93
                    ( model, Effect.sendToClipboard note.content )
94
95
                _ ->
96
                    ( model, Effect.none )
97
98
        UserUpdatedPassword password ->
99
            ( { model | password = Just password }, Effect.none )
100
101
        ApiGetNoteResponded (Ok note) ->
102
            ( { model | page = ShowNote (Api.Success note) }, Effect.none )
103
104
        ApiGetNoteResponded (Err error) ->
105
            ( { model | page = ShowNote (Api.Failure error) }, Effect.none )
106
107
        ApiGetMetadataResponded (Ok metadata) ->
108
            ( { model | metadata = Api.Success metadata }, Effect.none )
109
110
        ApiGetMetadataResponded (Err error) ->
111
            ( { model | page = NotFound, metadata = Api.Failure error }, Effect.none )
112
113
114
subscriptions : Model -> Sub Msg
115
subscriptions _ =
116
    Sub.none
117
118
119
120
-- VIEW
121
122
123
view : Shared.Model -> Model -> View Msg
124
view shared model =
125
    { title = "View note"
126
    , body =
127
        [ Components.Utils.commonContainer
128
            (case model.metadata of
129
                Api.Success metadata ->
130
                    viewPage shared.timeZone model.slug model.page metadata model.password
131
132
                Api.Loading ->
133
                    [ viewHeader { title = "View note", subtitle = "Loading note metadata..." }
134
                    , viewOpenNote { slug = model.slug, hasPassword = False, password = Nothing, isLoading = True }
135
                    ]
136
137
                Api.Failure error ->
138
                    [ viewHeader { title = "Note Not Found", subtitle = "The note you're looking for doesn't exist or has expired" }
139
                    , if Api.is404 error then
140
                        viewNoteNotFound
141
142
                      else
143
                        Components.Box.error (Api.errorMessage error)
144
                    ]
145
            )
146
        ]
147
    }
148
149
150
viewPage : Zone -> String -> PageVariant -> Metadata -> Maybe String -> List (Html Msg)
151
viewPage zone slug variant metadata password =
152
    case variant of
153
        RequestNote ->
154
            [ viewHeader { title = "View note", subtitle = "Click the button below to view the note content" }
155
            , viewOpenNote { slug = slug, hasPassword = metadata.hasPassword, password = password, isLoading = False }
156
            ]
157
158
        ShowNote apiResp ->
159
            case apiResp of
160
                Api.Success note ->
161
                    [ viewShowNoteHeader zone slug note
162
                    , viewNoteContent note
163
                    ]
164
165
                Api.Loading ->
166
                    [ viewHeader { title = "View note", subtitle = "Click the button below to view the note content" }
167
                    , viewOpenNote { slug = slug, hasPassword = metadata.hasPassword, password = password, isLoading = True }
168
                    ]
169
170
                Api.Failure _ ->
171
                    [ viewHeader { title = "Note Not Found", subtitle = "The note you're looking for doesn't exist or has expired" }
172
                    , viewNoteNotFound
173
                    ]
174
175
        NotFound ->
176
            [ viewNoteNotFound ]
177
178
179
180
-- HEADER
181
182
183
viewHeader : { title : String, subtitle : String } -> Html msg
184
viewHeader options =
185
    H.div [ A.class "p-6 pb-4 border-b border-gray-200" ]
186
        [ H.h1
187
            [ A.class "text-2xl font-bold text-gray-900" ]
188
            [ H.text options.title ]
189
        , H.p [ A.class "text-gray-600 mt-2" ] [ H.text options.subtitle ]
190
        ]
191
192
193
viewShowNoteHeader : Zone -> String -> Note -> Html Msg
194
viewShowNoteHeader zone slug note =
195
    H.div []
196
        [ Components.Utils.viewIf note.burnBeforeExpiration
197
            (H.div [ A.class "bg-orange-50 border-b border-orange-200 p-4" ]
198
                [ H.div [ A.class "flex items-center gap-3" ]
199
                    [ H.div [ A.class "w-6 h-6 bg-orange-100 rounded-full flex items-center justify-center flex-shrink-0" ]
200
                        [ Components.Icon.view Components.Icon.Warning "w-4 h-4 text-orange-600" ]
201
                    , H.p [ A.class "text-orange-800 text-sm font-medium" ]
202
                        [ H.text "This note was destroyed. If you need to keep it, copy it before closing this window." ]
203
                    ]
204
                ]
205
            )
206
        , H.div [ A.class "p-6 pb-4 border-b border-gray-200" ]
207
            [ H.div [ A.class "flex justify-between items-start" ]
208
                [ H.div []
209
                    [ H.h1 [ A.class "text-2xl font-bold text-gray-900" ] [ H.text ("Note: " ++ slug) ]
210
                    , H.div [ A.class "text-sm text-gray-500 mt-2 space-y-1" ]
211
                        [ H.p [] [ H.text ("Created: " ++ T.toString zone note.createdAt) ]
212
                        , Components.Utils.viewMaybe note.expiresAt (\n -> H.p [] [ H.text ("Expires at: " ++ T.toString zone n) ])
213
                        ]
214
                    ]
215
                , H.div [ A.class "flex gap-2" ]
216
                    [ Components.Form.button
217
                        { text = "Copy Content"
218
                        , style = Components.Form.SecondaryDisabled False
219
                        , onClick = UserClickedCopyContent
220
                        , disabled = False
221
                        }
222
                    ]
223
                ]
224
            ]
225
        ]
226
227
228
229
-- NOTE
230
231
232
viewNoteNotFound : Html msg
233
viewNoteNotFound =
234
    H.div [ A.class "p-6" ]
235
        [ H.div [ A.class "text-center py-12" ]
236
            [ H.div [ A.class "w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4" ]
237
                [ Components.Icon.view Components.Icon.NotFound "w-8 h-8 text-red-500" ]
238
            , H.h2 [ A.class "text-xl font-semibold text-gray-900 mb-2" ]
239
                [ H.text "Note not found" ]
240
            ]
241
        ]
242
243
244
viewOpenNote : { slug : String, hasPassword : Bool, isLoading : Bool, password : Maybe String } -> Html Msg
245
viewOpenNote opts =
246
    let
247
        isDisabled =
248
            (opts.hasPassword && Maybe.withDefault "" opts.password == "") || opts.isLoading
249
    in
250
    H.div [ A.class "p-6" ]
251
        [ H.div [ A.class "text-center py-12" ]
252
            [ H.div [ A.class "mb-6" ]
253
                [ H.div [ A.class "w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4" ]
254
                    [ Components.Icon.view Components.Icon.NoteIcon "w-8 h-8 text-gray-400" ]
255
                , H.h2 [ A.class "text-lg font-semibold text-gray-900 mb-2" ] [ H.text opts.slug ]
256
                , H.p [ A.class "text-gray-600 mb-6" ] [ H.text "You're about read and destroy the note." ]
257
                ]
258
            , H.form
259
                [ E.onSubmit UserClickedViewNote
260
                , A.class "max-w-sm mx-auto space-y-4"
261
                ]
262
                [ Components.Utils.viewIf opts.hasPassword
263
                    (H.div [ A.class "space-y-2" ]
264
                        [ H.label [ A.class "block text-sm font-medium text-gray-700 text-left" ] [ H.text "Password" ]
265
                        , H.input
266
                            [ E.onInput UserUpdatedPassword
267
                            , 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"
268
                            ]
269
                            []
270
                        ]
271
                    )
272
                , Components.Form.submitButton
273
                    { text =
274
                        if opts.isLoading then
275
                            "Loading Note..."
276
277
                        else
278
                            "View Note"
279
                    , style = Components.Form.Primary isDisabled
280
                    , disabled = isDisabled
281
                    , class = "py-3"
282
                    }
283
                ]
284
            ]
285
        ]
286
287
288
viewNoteContent : Note -> Html msg
289
viewNoteContent note =
290
    H.div [ A.class "p-6" ]
291
        [ H.div [ A.class "bg-gray-50 border border-gray-200 rounded-md p-4" ]
292
            [ H.pre
293
                [ A.class "whitespace-pre-wrap font-mono text-sm text-gray-800 overflow-x-auto" ]
294
                [ H.text note.content ]
295
            ]
296
        ]