@nymphjs/tilmeld-components 1.0.0-alpha.2

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,314 @@
1
+ {#if clientConfig == null || registering || loggingIn}
2
+ <CircularProgress style="height: 32px; width: 32px;" indeterminate />
3
+ {:else}
4
+ <div style="width: {width};">
5
+ {#if successRegisteredMessage}
6
+ <div>
7
+ {successRegisteredMessage}
8
+ </div>
9
+ {:else if successLoginMessage}
10
+ <div>
11
+ {successLoginMessage}
12
+ </div>
13
+ {:else}
14
+ <form on:submit|preventDefault>
15
+ <div>
16
+ <Textfield
17
+ bind:value={username}
18
+ bind:this={usernameElem}
19
+ label={clientConfig.emailUsernames ? 'Email' : 'Username'}
20
+ type={clientConfig.emailUsernames ? 'email' : 'text'}
21
+ style="width: 100%;"
22
+ helperLine$style="width: 100%;"
23
+ invalid={usernameVerified === false}
24
+ input$autocomplete={clientConfig.emailUsernames
25
+ ? 'email'
26
+ : 'username'}
27
+ input$name={clientConfig.emailUsernames ? 'email' : 'username'}
28
+ input$autocapitalize="off"
29
+ input$spellcheck="false"
30
+ >
31
+ <HelperText persistent slot="helper">
32
+ {#if !existingUser}
33
+ {usernameVerifiedMessage ?? ''}
34
+ {/if}
35
+ </HelperText>
36
+ </Textfield>
37
+ </div>
38
+
39
+ <div>
40
+ <Textfield
41
+ bind:value={password}
42
+ label="Password"
43
+ type="password"
44
+ style="width: 100%;"
45
+ input$autocomplete="{clientConfig.allowRegistration && !existingUser
46
+ ? 'new'
47
+ : 'current'}-password"
48
+ input$name="password"
49
+ />
50
+ </div>
51
+
52
+ {#if clientConfig.allowRegistration && !existingUser}
53
+ <div>
54
+ <Textfield
55
+ bind:value={password2}
56
+ label="Re-enter Password"
57
+ type="password"
58
+ style="width: 100%;"
59
+ input$autocomplete="new-password"
60
+ input$name="password2"
61
+ />
62
+ </div>
63
+
64
+ {#if clientConfig.regFields.indexOf('name') !== -1}
65
+ <div>
66
+ <Textfield
67
+ bind:value={name}
68
+ label="Name"
69
+ type="text"
70
+ style="width: 100%;"
71
+ input$autocomplete="name"
72
+ input$name="name"
73
+ />
74
+ </div>
75
+ {/if}
76
+
77
+ {#if !clientConfig.emailUsernames && clientConfig.regFields.indexOf('email') !== -1}
78
+ <div>
79
+ <Textfield
80
+ bind:value={email}
81
+ label="Email"
82
+ type="email"
83
+ style="width: 100%;"
84
+ input$autocomplete="email"
85
+ input$name="email"
86
+ input$autocapitalize="off"
87
+ input$spellcheck="false"
88
+ />
89
+ </div>
90
+ {/if}
91
+
92
+ {#if clientConfig.regFields.indexOf('phone') !== -1}
93
+ <div>
94
+ <Textfield
95
+ bind:value={phone}
96
+ label="Phone Number"
97
+ type="tel"
98
+ style="width: 100%;"
99
+ input$autocomplete="tel"
100
+ input$name="phone"
101
+ />
102
+ </div>
103
+ {/if}
104
+ {/if}
105
+
106
+ {#if failureMessage}
107
+ <div class="tilmeld-login-failure">
108
+ {failureMessage}
109
+ </div>
110
+ {/if}
111
+
112
+ <div class="tilmeld-login-buttons">
113
+ {#if existingUser}
114
+ <Button variant="raised" type="submit" on:click={login}>
115
+ <Label>Log In</Label>
116
+ </Button>
117
+ {:else}
118
+ <Button variant="raised" type="submit" on:click={register}>
119
+ <Label>Create Account</Label>
120
+ </Button>
121
+ {/if}
122
+ </div>
123
+
124
+ {#if clientConfig.allowRegistration && showExistingUserToggle}
125
+ <div class="tilmeld-login-action">
126
+ {#if existingUser}
127
+ <a
128
+ href="javascript:void(0);"
129
+ on:click={() => (existingUser = false)}
130
+ >
131
+ Create an account.
132
+ </a>
133
+ {:else}
134
+ <a
135
+ href="javascript:void(0);"
136
+ on:click={() => (existingUser = true)}
137
+ >
138
+ Log in to your account.
139
+ </a>
140
+ {/if}
141
+ </div>
142
+ {/if}
143
+
144
+ {#if !hideRecovery && clientConfig.pwRecovery && existingUser}
145
+ <div class="tilmeld-login-action">
146
+ <a href="javascript:void(0);" on:click={() => (recoverOpen = true)}>
147
+ I can't access my account.
148
+ </a>
149
+ </div>
150
+ <Recover bind:open={recoverOpen} />
151
+ {/if}
152
+ </form>
153
+ {/if}
154
+ </div>
155
+ {/if}
156
+
157
+ <script lang="ts">
158
+ import { onMount, createEventDispatcher } from 'svelte';
159
+ import CircularProgress from '@smui/circular-progress';
160
+ import Button, { Label } from '@smui/button';
161
+ import Textfield, { TextfieldComponentDev } from '@smui/textfield';
162
+ import HelperText from '@smui/textfield/helper-text/index';
163
+ import {
164
+ getClientConfig,
165
+ login as loginAction,
166
+ register as registerAction,
167
+ checkUsername as checkUsernameAction,
168
+ ClientConfig,
169
+ } from '@nymphjs/tilmeld-client';
170
+ import Recover from './Recover.svelte';
171
+
172
+ const dispatch = createEventDispatcher();
173
+
174
+ /** Hide the recovery link that only appears if password recovery is on. */
175
+ export let hideRecovery = false;
176
+ /** Give focus to the username box (or email box) when the form is ready. */
177
+ export let autofocus = true;
178
+ /** This determines whether the 'Log In' or 'Sign Up' button is activated and which corresponding form is shown. */
179
+ export let existingUser = true;
180
+ /** Whether to show the 'Log In'/'Sign Up' switcher buttons. */
181
+ export let showExistingUserToggle = true;
182
+ /** The width of the form. */
183
+ export let width = '220px';
184
+
185
+ /** User provided. You can bind to it if you need to. */
186
+ export let username = '';
187
+ /** User provided. You can bind to it if you need to. */
188
+ export let password = '';
189
+ /** User provided. You can bind to it if you need to. */
190
+ export let password2 = '';
191
+ /** User provided. You can bind to it if you need to. */
192
+ export let name = '';
193
+ /** User provided. You can bind to it if you need to. */
194
+ export let email = '';
195
+ /** User provided. You can bind to it if you need to. */
196
+ export let phone = '';
197
+
198
+ let clientConfig: ClientConfig | undefined = undefined;
199
+ let usernameElem: TextfieldComponentDev;
200
+ let successLoginMessage: string | undefined = undefined;
201
+ let successRegisteredMessage: string | undefined = undefined;
202
+ let failureMessage: string | undefined = undefined;
203
+ let usernameTimer: NodeJS.Timeout | undefined = undefined;
204
+ let usernameVerified: boolean | undefined = undefined;
205
+ let usernameVerifiedMessage: string | undefined = undefined;
206
+ let registering = false;
207
+ let loggingIn = false;
208
+ let recoverOpen = false;
209
+
210
+ $: nameFirst = name?.match(/^(.*?)(?: ([^ ]+))?$/)?.[1] ?? '';
211
+ $: nameLast = name?.match(/^(.*?)(?: ([^ ]+))?$/)?.[2] ?? '';
212
+
213
+ let _previousExistingUser = existingUser;
214
+ $: if (existingUser !== _previousExistingUser) {
215
+ failureMessage = '';
216
+ checkUsername(username);
217
+ _previousExistingUser = existingUser;
218
+ }
219
+
220
+ $: {
221
+ checkUsername(username);
222
+ }
223
+
224
+ onMount(async () => {
225
+ clientConfig = await getClientConfig();
226
+ if (!clientConfig.allowRegistration) {
227
+ existingUser = true;
228
+ }
229
+ if (autofocus && usernameElem) {
230
+ usernameElem.focus();
231
+ }
232
+ });
233
+
234
+ async function login() {
235
+ successLoginMessage = undefined;
236
+ failureMessage = undefined;
237
+ loggingIn = true;
238
+ try {
239
+ const data = await loginAction(username, password);
240
+ successLoginMessage = data.message;
241
+ dispatch('login', { user: data.user });
242
+ } catch (e: any) {
243
+ failureMessage = e?.message;
244
+ }
245
+ loggingIn = false;
246
+ }
247
+
248
+ async function register() {
249
+ successRegisteredMessage = undefined;
250
+ successLoginMessage = undefined;
251
+ failureMessage = undefined;
252
+ registering = true;
253
+ try {
254
+ const data = await registerAction({
255
+ username,
256
+ usernameVerified: !!usernameVerified,
257
+ password,
258
+ password2,
259
+ email,
260
+ nameFirst,
261
+ nameLast,
262
+ phone,
263
+ });
264
+ successRegisteredMessage = data.message;
265
+ dispatch('register', { user: data.user });
266
+ if (data.loggedin) {
267
+ successLoginMessage = data.message;
268
+ dispatch('login', { user: data.user });
269
+ }
270
+ } catch (e: any) {
271
+ failureMessage = e?.message;
272
+ }
273
+ registering = false;
274
+ }
275
+
276
+ function checkUsername(newValue: string) {
277
+ usernameVerified = undefined;
278
+ usernameVerifiedMessage = undefined;
279
+ if (usernameTimer) {
280
+ clearTimeout(usernameTimer);
281
+ usernameTimer = undefined;
282
+ }
283
+ if (newValue === '' || existingUser) {
284
+ return;
285
+ }
286
+ usernameTimer = setTimeout(async () => {
287
+ try {
288
+ const data = await checkUsernameAction(newValue);
289
+ usernameVerified = true;
290
+ usernameVerifiedMessage = data.message;
291
+ } catch (e: any) {
292
+ usernameVerified = false;
293
+ usernameVerifiedMessage = e?.message;
294
+ }
295
+ }, 400);
296
+ }
297
+ </script>
298
+
299
+ <style>
300
+ .tilmeld-login-buttons {
301
+ display: flex;
302
+ flex-direction: row;
303
+ justify-content: start;
304
+ align-items: center;
305
+ margin-top: 1em;
306
+ }
307
+ .tilmeld-login-action {
308
+ margin-top: 1em;
309
+ }
310
+ .tilmeld-login-failure {
311
+ margin-top: 1em;
312
+ color: var(--mdc-theme-error, #f00);
313
+ }
314
+ </style>
@@ -0,0 +1,309 @@
1
+ {#if clientConfig != null && clientConfig.pwRecovery}
2
+ <Dialog
3
+ bind:open
4
+ aria-labelledby="tilmeld-recovery-title"
5
+ aria-describedby="tilmeld-recovery-content"
6
+ surface$class="tilmeld-recover-dialog-surface"
7
+ >
8
+ <!-- Title cannot contain leading whitespace due to mdc-typography-baseline-top() -->
9
+ <Title id="tilmeld-recovery-title">Recover Your Account</Title>
10
+ <Content id="tilmeld-recovery-content">
11
+ {#if successRecoveredMessage}
12
+ {successRecoveredMessage}
13
+ {:else}
14
+ {#if !hasSentSecret}
15
+ {#if !clientConfig.emailUsernames}
16
+ <div>
17
+ <FormField style="margin-right: 1em;">
18
+ <Radio bind:group={recoveryType} value="password" />
19
+ <span slot="label">I don't know my password.</span>
20
+ </FormField>
21
+ <FormField style="margin-right: 1em;">
22
+ <Radio bind:group={recoveryType} value="username" />
23
+ <span slot="label">I don't know my username.</span>
24
+ </FormField>
25
+ </div>
26
+ {/if}
27
+
28
+ <div>
29
+ {#if recoveryType === 'password'}
30
+ <p>
31
+ To reset your password, type the {clientConfig.emailUsernames
32
+ ? 'email'
33
+ : 'username'}
34
+ you use to sign in below.
35
+ </p>
36
+ {/if}
37
+ {#if recoveryType === 'username'}
38
+ <p>
39
+ To get your username, type your email as you entered it when
40
+ creating your account.
41
+ </p>
42
+ {/if}
43
+ </div>
44
+
45
+ <div>
46
+ <Textfield
47
+ bind:this={accountElem}
48
+ bind:value={account}
49
+ label={clientConfig.emailUsernames || recoveryType === 'username'
50
+ ? 'Email Address'
51
+ : 'Username'}
52
+ type={clientConfig.emailUsernames || recoveryType === 'username'
53
+ ? 'email'
54
+ : 'text'}
55
+ input$autocomplete={clientConfig.emailUsernames ||
56
+ recoveryType === 'username'
57
+ ? 'email'
58
+ : 'username'}
59
+ input$autocapitalize="off"
60
+ input$spellcheck="false"
61
+ />
62
+ </div>
63
+
64
+ {#if recoveryType === 'password'}
65
+ <div class="tilmeld-recover-action">
66
+ <a
67
+ href="javascript:void(0);"
68
+ on:click={() => (hasSentSecret = 1)}
69
+ >
70
+ Already Got a Code?
71
+ </a>
72
+ </div>
73
+ {/if}
74
+ {:else}
75
+ <div>
76
+ <p>
77
+ A code has been sent to you by email. Enter that code here, and a
78
+ new password for your account.
79
+ </p>
80
+ </div>
81
+
82
+ {#if hasSentSecret === 1}
83
+ <div>
84
+ <Textfield
85
+ bind:this={accountElem}
86
+ bind:value={account}
87
+ label={clientConfig.emailUsernames
88
+ ? 'Email Address'
89
+ : 'Username'}
90
+ type={clientConfig.emailUsernames ? 'email' : 'text'}
91
+ input$autocomplete={clientConfig.emailUsernames
92
+ ? 'email'
93
+ : 'username'}
94
+ input$autocapitalize="off"
95
+ input$spellcheck="false"
96
+ />
97
+ </div>
98
+ {/if}
99
+
100
+ <div>
101
+ <Textfield
102
+ bind:value={secret}
103
+ label="Recovery Code"
104
+ type="text"
105
+ input$autocomplete="one-time-code"
106
+ />
107
+ </div>
108
+
109
+ <div>
110
+ <Textfield
111
+ bind:value={password}
112
+ label="Password"
113
+ type="password"
114
+ input$autocomplete="new-password"
115
+ />
116
+ </div>
117
+
118
+ <div>
119
+ <Textfield
120
+ bind:value={password2}
121
+ label="Re-enter Password"
122
+ type="password"
123
+ input$autocomplete="new-password"
124
+ />
125
+ </div>
126
+
127
+ <div class="tilmeld-recover-action">
128
+ <a
129
+ href="javascript:void(0);"
130
+ on:click={() => (hasSentSecret = false)}
131
+ >
132
+ Need a New Code?
133
+ </a>
134
+ </div>
135
+ {/if}
136
+
137
+ {#if failureMessage}
138
+ <div class="tilmeld-recover-failure">
139
+ {failureMessage}
140
+ </div>
141
+ {/if}
142
+
143
+ {#if recovering}
144
+ <div class="tilmeld-recover-loading">
145
+ <CircularProgress
146
+ style="height: 24px; width: 24px;"
147
+ indeterminate
148
+ />
149
+ </div>
150
+ {/if}
151
+ {/if}
152
+ </Content>
153
+ <Actions>
154
+ <Button on:click={() => (open = false)} disabled={recovering}>
155
+ <Label>{successRecoveredMessage ? 'Close' : 'Cancel'}</Label>
156
+ </Button>
157
+ {#if !successRecoveredMessage}
158
+ {#if !hasSentSecret}
159
+ <Button
160
+ on:click$preventDefault$stopPropagation={sendRecovery}
161
+ disabled={recovering}
162
+ >
163
+ <Label>Send Recovery</Label>
164
+ </Button>
165
+ {:else}
166
+ <Button
167
+ on:click$preventDefault$stopPropagation={recover}
168
+ disabled={recovering}
169
+ >
170
+ <Label>Reset Password</Label>
171
+ </Button>
172
+ {/if}
173
+ {/if}
174
+ </Actions>
175
+ </Dialog>
176
+ {/if}
177
+
178
+ <script lang="ts">
179
+ import { onMount } from 'svelte';
180
+ import CircularProgress from '@smui/circular-progress';
181
+ import Dialog, { Title, Content, Actions } from '@smui/dialog';
182
+ import Textfield, { TextfieldComponentDev } from '@smui/textfield';
183
+ import Button, { Label } from '@smui/button';
184
+ import FormField from '@smui/form-field';
185
+ import Radio from '@smui/radio';
186
+ import { ClientConfig, User } from '@nymphjs/tilmeld-client';
187
+
188
+ export let open = false;
189
+ // Give focus to the account box when the form is ready.
190
+ export let autofocus = true;
191
+ export let recoveryType: 'username' | 'password' = 'password';
192
+
193
+ /** User provided. You can bind to it if you need to. */
194
+ export let account = '';
195
+ /** User provided. You can bind to it if you need to. */
196
+ export let secret = '';
197
+ /** User provided. You can bind to it if you need to. */
198
+ export let password = '';
199
+ /** User provided. You can bind to it if you need to. */
200
+ export let password2 = '';
201
+
202
+ let clientConfig: ClientConfig | undefined = undefined;
203
+ let recovering = false;
204
+ let hasSentSecret: number | boolean = false;
205
+ let accountElem: TextfieldComponentDev;
206
+ let failureMessage: string | undefined = undefined;
207
+ let successRecoveredMessage: string | undefined = undefined;
208
+
209
+ $: {
210
+ if (open && autofocus && accountElem) {
211
+ accountElem.focus();
212
+ }
213
+ }
214
+
215
+ onMount(async () => {
216
+ clientConfig = await User.getClientConfig();
217
+ });
218
+
219
+ async function sendRecovery() {
220
+ if (account === '') {
221
+ failureMessage =
222
+ 'You need to enter ' +
223
+ (clientConfig?.emailUsernames || recoveryType === 'username'
224
+ ? 'an email address'
225
+ : 'a username') +
226
+ '.';
227
+ return;
228
+ }
229
+
230
+ failureMessage = undefined;
231
+ recovering = true;
232
+
233
+ try {
234
+ const data = await User.sendRecovery({
235
+ recoveryType,
236
+ account,
237
+ });
238
+ if (!data.result) {
239
+ failureMessage = data.message;
240
+ } else {
241
+ if (recoveryType === 'username') {
242
+ successRecoveredMessage = data.message;
243
+ } else if (recoveryType === 'password') {
244
+ hasSentSecret = true;
245
+ }
246
+ }
247
+ } catch (e: any) {
248
+ failureMessage = e?.message;
249
+ }
250
+ recovering = false;
251
+ }
252
+
253
+ async function recover() {
254
+ if (account === '') {
255
+ failureMessage =
256
+ 'You need to enter ' +
257
+ (clientConfig?.emailUsernames || recoveryType === 'username'
258
+ ? 'an email address'
259
+ : 'a username') +
260
+ '.';
261
+ return;
262
+ }
263
+ if (password !== password2) {
264
+ failureMessage = 'Your passwords do not match.';
265
+ return;
266
+ }
267
+ if (password === '') {
268
+ failureMessage = 'You need to enter a password.';
269
+ return;
270
+ }
271
+
272
+ failureMessage = undefined;
273
+ recovering = true;
274
+ try {
275
+ const data = await User.recover({
276
+ username: account,
277
+ secret,
278
+ password,
279
+ });
280
+ if (!data.result) {
281
+ failureMessage = data.message;
282
+ } else {
283
+ successRecoveredMessage = data.message;
284
+ }
285
+ } catch (e: any) {
286
+ failureMessage = e?.message;
287
+ }
288
+ recovering = false;
289
+ }
290
+ </script>
291
+
292
+ <style>
293
+ :global(.mdc-dialog .mdc-dialog__surface.tilmeld-recover-dialog-surface) {
294
+ width: 540px;
295
+ max-width: calc(100vw - 32px);
296
+ }
297
+ .tilmeld-recover-action {
298
+ margin-top: 1em;
299
+ }
300
+ .tilmeld-recover-failure {
301
+ margin-top: 1em;
302
+ color: var(--mdc-theme-error, #f00);
303
+ }
304
+ .tilmeld-recover-loading {
305
+ display: flex;
306
+ justify-content: center;
307
+ align-items: center;
308
+ }
309
+ </style>
@@ -0,0 +1 @@
1
+ /// <reference types="svelte" />
package/src/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import Account from './Account.svelte';
2
+ import ChangePassword from './ChangePassword.svelte';
3
+ import Login from './Login.svelte';
4
+ import Recover from './Recover.svelte';
5
+
6
+ export { Account, ChangePassword, Login, Recover };
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ import Account from './Account.svelte';
2
+ import ChangePassword from './ChangePassword.svelte';
3
+ import Login from './Login.svelte';
4
+ import Recover from './Recover.svelte';
5
+
6
+ export { Account, ChangePassword, Login, Recover };
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "@tsconfig/svelte/tsconfig.json",
3
+
4
+ "compilerOptions": {
5
+ "noImplicitAny": true,
6
+ "strict": true
7
+ },
8
+ "include": ["src/**/*"],
9
+ "exclude": ["node_modules/*"]
10
+ }
@@ -0,0 +1,35 @@
1
+ const path = require('path');
2
+ const sveltePreprocess = require('svelte-preprocess');
3
+
4
+ module.exports = {
5
+ mode: 'production',
6
+ devtool: 'source-map',
7
+ entry: {
8
+ TilmeldComponents: './src/index.ts',
9
+ },
10
+ output: {
11
+ path: path.resolve(__dirname, 'dist'),
12
+ filename: 'index.js',
13
+ },
14
+ resolve: {
15
+ extensions: ['.mjs', '.js', '.ts', '.svelte'],
16
+ mainFields: ['svelte', 'browser', 'module', 'main'],
17
+ },
18
+ module: {
19
+ rules: [
20
+ {
21
+ test: /\.svelte$/,
22
+ use: {
23
+ loader: 'svelte-loader',
24
+ options: {
25
+ preprocess: sveltePreprocess(),
26
+ },
27
+ },
28
+ },
29
+ {
30
+ test: /\.ts$/,
31
+ loader: 'ts-loader',
32
+ },
33
+ ],
34
+ },
35
+ };