@nymphjs/tilmeld-components 1.0.0-beta.13 → 1.0.0-beta.130

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,309 @@
1
+ <svelte:options runes />
2
+
3
+ {#if $user != null}
4
+ <Dialog
5
+ {use}
6
+ bind:open
7
+ aria-labelledby="tilmeld-password-title"
8
+ aria-describedby="tilmeld-password-content"
9
+ surface$class="tilmeld-password-dialog-surface"
10
+ {...exclude(restProps, [
11
+ 'password$',
12
+ 'newPassword$',
13
+ 'newPassword2$',
14
+ 'revokeTokens$',
15
+ 'closeButton$',
16
+ 'saveButton$',
17
+ 'progress$',
18
+ ])}
19
+ >
20
+ <!-- Title cannot contain leading whitespace due to mdc-typography-baseline-top() -->
21
+ <Title id="tilmeld-password-title">{title}</Title>
22
+ <Content id="tilmeld-password-content">
23
+ <div>
24
+ <Textfield
25
+ bind:value={currentPassword}
26
+ label="Current Password"
27
+ type="password"
28
+ style="width: 100%;"
29
+ input$autocomplete="current-password"
30
+ {...prefixFilter(restProps, 'password$')}
31
+ />
32
+ </div>
33
+
34
+ <div>
35
+ <Textfield
36
+ bind:value={newPassword}
37
+ label="New Password"
38
+ type="password"
39
+ style="width: 100%;"
40
+ input$autocomplete="new-password"
41
+ {...prefixFilter(restProps, 'newPassword$')}
42
+ />
43
+ </div>
44
+
45
+ <div>
46
+ <Textfield
47
+ bind:value={newPassword2}
48
+ label="Re-enter New Password"
49
+ type="password"
50
+ style="width: 100%;"
51
+ input$autocomplete="new-password"
52
+ {...prefixFilter(restProps, 'newPassword2$')}
53
+ />
54
+ </div>
55
+
56
+ <div style="margin: 1em 0;">
57
+ <FormField>
58
+ <Checkbox
59
+ bind:checked={revokeCurrentTokens}
60
+ {...prefixFilter(restProps, 'revokeTokens$')}
61
+ />
62
+ {#snippet label()}
63
+ Log out of all other sessions.
64
+ <span
65
+ style="display: block; font-size: 0.7em; height: 0; overflow: visible;"
66
+ >
67
+ Use this if you think you've been hacked.
68
+ </span>
69
+ {/snippet}
70
+ </FormField>
71
+ </div>
72
+
73
+ {@render additional?.()}
74
+
75
+ {#if failureMessage}
76
+ <div class="tilmeld-password-failure">
77
+ {failureMessage}
78
+ </div>
79
+ {/if}
80
+
81
+ {#if loading}
82
+ <div class="tilmeld-password-loading">
83
+ <CircularProgress
84
+ style="height: 24px; width: 24px;"
85
+ indeterminate
86
+ {...prefixFilter(restProps, 'progress$')}
87
+ />
88
+ </div>
89
+ {/if}
90
+ </Content>
91
+ <Actions>
92
+ <Button disabled={loading} {...prefixFilter(restProps, 'closeButton$')}>
93
+ <Label>Close</Label>
94
+ </Button>
95
+ <Button
96
+ onclick={preventDefault(stopPropagation(changePassword))}
97
+ disabled={loading}
98
+ {...prefixFilter(restProps, 'saveButton$')}
99
+ >
100
+ <Label>Change Password</Label>
101
+ </Button>
102
+ </Actions>
103
+ </Dialog>
104
+ {/if}
105
+
106
+ <script lang="ts">
107
+ import type { ComponentProps, Snippet } from 'svelte';
108
+ import { onMount, onDestroy } from 'svelte';
109
+ import type { Writable } from 'svelte/store';
110
+ import { writable } from 'svelte/store';
111
+ import CircularProgress from '@smui/circular-progress';
112
+ import Dialog, { Title, Content, Actions } from '@smui/dialog';
113
+ import Textfield from '@smui/textfield';
114
+ import Checkbox from '@smui/checkbox';
115
+ import FormField from '@smui/form-field';
116
+ import Button, { Label } from '@smui/button';
117
+ import type { ActionArray } from '@smui/common/internal';
118
+ import { exclude, prefixFilter } from '@smui/common/internal';
119
+ import { preventDefault, stopPropagation } from '@smui/common/events';
120
+ import type { CurrentUserData } from '@nymphjs/tilmeld-client';
121
+ import type { User as UserClass } from '@nymphjs/tilmeld-client';
122
+
123
+ type OwnProps = {
124
+ /**
125
+ * An array of Action or [Action, ActionProps] to be applied to the element.
126
+ */
127
+ use?: ActionArray;
128
+ /**
129
+ * Whether the dialog is open.
130
+ */
131
+ open?: boolean;
132
+ /**
133
+ * The title of the dialog.
134
+ */
135
+ title?: string;
136
+ /**
137
+ * The User class from Nymph.
138
+ */
139
+ User: typeof UserClass;
140
+ /**
141
+ * A writable store of the current user.
142
+ *
143
+ * It will be retrieved from the server if not provided.
144
+ */
145
+ user?: Writable<(UserClass & CurrentUserData) | null | undefined>;
146
+ /**
147
+ * User provided. You can bind to it if you need to.
148
+ */
149
+ currentPassword?: string;
150
+ /**
151
+ * User provided. You can bind to it if you need to.
152
+ */
153
+ newPassword?: string;
154
+ /**
155
+ * User provided. You can bind to it if you need to.
156
+ */
157
+ newPassword2?: string;
158
+ /**
159
+ * User provided. You can bind to it if you need to.
160
+ */
161
+ revokeCurrentTokens?: boolean;
162
+
163
+ /**
164
+ * A spot for additional content.
165
+ */
166
+ additional?: Snippet;
167
+ };
168
+ let {
169
+ use = [],
170
+ open = $bindable(false),
171
+ title = 'Change Your Password',
172
+ User,
173
+ user = $bindable(writable(false as unknown as undefined)),
174
+ currentPassword = $bindable(''),
175
+ newPassword = $bindable(''),
176
+ newPassword2 = $bindable(''),
177
+ revokeCurrentTokens = $bindable(false),
178
+ additional,
179
+ ...restProps
180
+ }: OwnProps & {
181
+ [
182
+ k in keyof ComponentProps<typeof Textfield> as `password\$${k}`
183
+ ]?: ComponentProps<typeof Textfield>[k];
184
+ } & {
185
+ [
186
+ k in keyof ComponentProps<typeof Textfield> as `newPassword\$${k}`
187
+ ]?: ComponentProps<typeof Textfield>[k];
188
+ } & {
189
+ [
190
+ k in keyof ComponentProps<typeof Textfield> as `newPassword2\$${k}`
191
+ ]?: ComponentProps<typeof Textfield>[k];
192
+ } & {
193
+ [
194
+ k in keyof ComponentProps<typeof Checkbox> as `revokeTokens\$${k}`
195
+ ]?: ComponentProps<typeof Checkbox>[k];
196
+ } & {
197
+ [
198
+ k in keyof ComponentProps<typeof CircularProgress> as `progress\$${k}`
199
+ ]?: ComponentProps<typeof CircularProgress>[k];
200
+ } & {
201
+ [
202
+ k in keyof ComponentProps<
203
+ typeof Button<undefined, 'button'>
204
+ > as `closeButton\$${k}`
205
+ ]?: ComponentProps<typeof Button<undefined, 'button'>>[k];
206
+ } & {
207
+ [
208
+ k in keyof ComponentProps<
209
+ typeof Button<undefined, 'button'>
210
+ > as `saveButton\$${k}`
211
+ ]?: ComponentProps<typeof Button<undefined, 'button'>>[k];
212
+ } = $props();
213
+
214
+ let loading = $state(false);
215
+ let failureMessage: string | undefined = $state();
216
+
217
+ $effect(() => {
218
+ if (!open) {
219
+ loading = false;
220
+ failureMessage = undefined;
221
+ currentPassword = '';
222
+ newPassword = '';
223
+ newPassword2 = '';
224
+ revokeCurrentTokens = false;
225
+ }
226
+ });
227
+
228
+ const onLogin = (currentUser: UserClass & CurrentUserData) => {
229
+ $user = currentUser;
230
+ };
231
+ const onLogout = () => {
232
+ $user = null;
233
+ };
234
+
235
+ onMount(async () => {
236
+ User.on('login', onLogin);
237
+ User.on('logout', onLogout);
238
+ if ($user === (false as unknown as undefined)) {
239
+ $user = undefined;
240
+ $user = await User.current();
241
+ }
242
+ });
243
+
244
+ onDestroy(() => {
245
+ User.off('login', onLogin);
246
+ User.off('logout', onLogout);
247
+ });
248
+
249
+ async function changePassword() {
250
+ if (currentPassword === '') {
251
+ failureMessage = 'You need to enter your current password';
252
+ return;
253
+ }
254
+ if (newPassword !== newPassword2) {
255
+ failureMessage = 'Your passwords do not match.';
256
+ return;
257
+ }
258
+ if (newPassword === '') {
259
+ failureMessage = 'You need to enter a new password';
260
+ return;
261
+ }
262
+
263
+ failureMessage = undefined;
264
+ loading = true;
265
+
266
+ // Get the current user again, in case their data has changed.
267
+ $user = await User.current();
268
+
269
+ if ($user == null) {
270
+ failureMessage = 'You must be logged in.';
271
+ loading = false;
272
+ return;
273
+ }
274
+
275
+ try {
276
+ // Change the user's password.
277
+ const data = await $user.$changePassword({
278
+ currentPassword,
279
+ newPassword,
280
+ revokeCurrentTokens,
281
+ });
282
+
283
+ if (!data.result) {
284
+ failureMessage = data.message;
285
+ } else {
286
+ open = false;
287
+ }
288
+ } catch (e: any) {
289
+ failureMessage = e?.message;
290
+ }
291
+ loading = false;
292
+ }
293
+ </script>
294
+
295
+ <style>
296
+ :global(.mdc-dialog .mdc-dialog__surface.tilmeld-password-dialog-surface) {
297
+ width: 360px;
298
+ max-width: calc(100vw - 32px);
299
+ }
300
+ .tilmeld-password-failure {
301
+ margin-top: 1em;
302
+ color: var(--mdc-theme-error, #f00);
303
+ }
304
+ .tilmeld-password-loading {
305
+ display: flex;
306
+ justify-content: center;
307
+ align-items: center;
308
+ }
309
+ </style>