@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.
package/jest.config.js ADDED
@@ -0,0 +1,6 @@
1
+ /** @type {import('@ts-jest/dist/types').InitialOptionsTsJest} */
2
+ module.exports = {
3
+ preset: 'ts-jest',
4
+ testEnvironment: 'node',
5
+ rootDir: 'src/',
6
+ };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@nymphjs/tilmeld-components",
3
+ "version": "1.0.0-alpha.2",
4
+ "description": "NymphJS - Tilmeld Front End Components",
5
+ "type": "commonjs",
6
+ "main": "dist/index.js",
7
+ "types": "src/index.d.ts",
8
+ "svelte": "src/index.ts",
9
+ "keywords": [
10
+ "nymph",
11
+ "ORM",
12
+ "object relational mapper"
13
+ ],
14
+ "scripts": {
15
+ "clean": "test -d dist && rm -r dist || true",
16
+ "build": "webpack",
17
+ "watch": "webpack --watch",
18
+ "prepare": "npm run clean && npm run build",
19
+ "test": "jest",
20
+ "test:watch": "jest --watch"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/sciactive/nymphjs.git"
28
+ },
29
+ "author": "Hunter Perrin <hperrin@gmail.com>",
30
+ "bugs": {
31
+ "url": "https://github.com/sciactive/nymphjs/issues"
32
+ },
33
+ "license": "Apache-2.0",
34
+ "devDependencies": {
35
+ "@nymphjs/tilmeld-client": "^1.0.0-alpha.2",
36
+ "@smui/button": "^5.0.0-beta.5",
37
+ "@smui/circular-progress": "^5.0.0-beta.5",
38
+ "@smui/dialog": "^5.0.0-beta.5",
39
+ "@smui/form-field": "^5.0.0-beta.5",
40
+ "@smui/radio": "^5.0.0-beta.5",
41
+ "@smui/textfield": "^5.0.0-beta.5",
42
+ "@tsconfig/recommended": "^1.0.1",
43
+ "@tsconfig/svelte": "^2.0.1",
44
+ "@types/jest": "^27.0.1",
45
+ "jest": "^27.0.6",
46
+ "svelte": "^3.42.4",
47
+ "svelte-check": "^2.2.5",
48
+ "svelte-loader": "^3.1.2",
49
+ "svelte-preprocess": "^4.8.0",
50
+ "ts-jest": "^27.0.5",
51
+ "ts-loader": "^9.2.5",
52
+ "typescript": "^4.3.5",
53
+ "webpack": "^5.51.1",
54
+ "webpack-cli": "^4.8.0"
55
+ },
56
+ "gitHead": "9596354bffaa484f3002f3063f0963f6261040be"
57
+ }
@@ -0,0 +1,317 @@
1
+ {#if clientConfig != null && user != null}
2
+ <Dialog
3
+ bind:open
4
+ aria-labelledby="tilmeld-account-title"
5
+ aria-describedby="tilmeld-account-content"
6
+ surface$class="tilmeld-account-dialog-surface"
7
+ >
8
+ <!-- Title cannot contain leading whitespace due to mdc-typography-baseline-top() -->
9
+ <Title id="tilmeld-account-title">Your Account</Title>
10
+ <Content id="tilmeld-account-content">
11
+ {#if !clientConfig.emailUsernames && clientConfig.allowUsernameChange}
12
+ <div>
13
+ <Textfield
14
+ bind:value={user.username}
15
+ label="Username"
16
+ type="text"
17
+ style="width: 100%;"
18
+ helperLine$style="width: 100%;"
19
+ invalid={usernameVerified === false}
20
+ input$autocomplete="username"
21
+ input$autocapitalize="off"
22
+ input$spellcheck="false"
23
+ >
24
+ <HelperText persistent slot="helper">
25
+ {usernameVerifiedMessage ?? ''}
26
+ </HelperText>
27
+ </Textfield>
28
+ </div>
29
+ {/if}
30
+
31
+ {#if clientConfig.emailUsernames || clientConfig.userFields.indexOf('email') !== -1}
32
+ <div>
33
+ <Textfield
34
+ bind:value={user.email}
35
+ label="Email"
36
+ type="email"
37
+ style="width: 100%;"
38
+ helperLine$style="width: 100%;"
39
+ invalid={emailVerified === false}
40
+ input$autocomplete="email"
41
+ input$autocapitalize="off"
42
+ input$spellcheck="false"
43
+ >
44
+ <HelperText persistent slot="helper">
45
+ {emailVerifiedMessage ?? ''}
46
+ </HelperText>
47
+ </Textfield>
48
+ </div>
49
+ {/if}
50
+
51
+ {#if clientConfig.userFields.indexOf('name') !== -1}
52
+ <div>
53
+ <Textfield
54
+ bind:value={user.nameFirst}
55
+ label="First Name"
56
+ type="text"
57
+ style="width: 100%;"
58
+ input$autocomplete="given-name"
59
+ />
60
+ </div>
61
+
62
+ <div>
63
+ <Textfield
64
+ bind:value={user.nameMiddle}
65
+ label="Middle Name"
66
+ type="text"
67
+ style="width: 100%;"
68
+ input$autocomplete="additional-name"
69
+ />
70
+ </div>
71
+
72
+ <div>
73
+ <Textfield
74
+ bind:value={user.nameLast}
75
+ label="Last Name"
76
+ type="text"
77
+ style="width: 100%;"
78
+ input$autocomplete="family-name"
79
+ />
80
+ </div>
81
+ {/if}
82
+
83
+ {#if clientConfig.userFields.indexOf('phone') !== -1}
84
+ <div>
85
+ <Textfield
86
+ bind:value={user.phone}
87
+ label="Phone Number"
88
+ type="tel"
89
+ style="width: 100%;"
90
+ input$autocomplete="tel"
91
+ input$name="phone"
92
+ />
93
+ </div>
94
+ {/if}
95
+
96
+ <div class="tilmeld-account-action">
97
+ <a
98
+ href="javascript:void(0);"
99
+ on:click={() => {
100
+ open = false;
101
+ changePasswordOpen = true;
102
+ }}
103
+ >
104
+ Change your password.
105
+ </a>
106
+ </div>
107
+
108
+ {#if failureMessage}
109
+ <div class="tilmeld-account-failure">
110
+ {failureMessage}
111
+ </div>
112
+ {/if}
113
+
114
+ {#if saving}
115
+ <div class="tilmeld-account-loading">
116
+ <CircularProgress style="height: 24px; width: 24px;" indeterminate />
117
+ </div>
118
+ {/if}
119
+ </Content>
120
+ <Actions>
121
+ <Button disabled={saving}>
122
+ <Label>Close</Label>
123
+ </Button>
124
+ <Button on:click$preventDefault$stopPropagation={save} disabled={saving}>
125
+ <Label>Save Changes</Label>
126
+ </Button>
127
+ </Actions>
128
+ </Dialog>
129
+ <ChangePassword bind:open={changePasswordOpen} />
130
+ {/if}
131
+
132
+ <script lang="ts">
133
+ import { onMount, onDestroy } from 'svelte';
134
+ import CircularProgress from '@smui/circular-progress';
135
+ import Dialog, { Title, Content, Actions } from '@smui/dialog';
136
+ import Textfield from '@smui/textfield';
137
+ import HelperText from '@smui/textfield/helper-text/index';
138
+ import Button, { Label } from '@smui/button';
139
+ import { ClientConfig, CurrentUserData, User } from '@nymphjs/tilmeld-client';
140
+ import ChangePassword from './ChangePassword.svelte';
141
+
142
+ export let open = false;
143
+
144
+ let clientConfig: ClientConfig | undefined = undefined;
145
+ let user: (User & CurrentUserData) | undefined = undefined;
146
+ let saving = false;
147
+ let originalUsername: string | undefined = undefined;
148
+ let originalEmail: string | undefined = undefined;
149
+ let failureMessage: string | undefined = undefined;
150
+ let usernameTimer: NodeJS.Timeout | undefined = undefined;
151
+ let usernameVerified: boolean | undefined = undefined;
152
+ let usernameVerifiedMessage: string | undefined = undefined;
153
+ let emailTimer: NodeJS.Timeout | undefined = undefined;
154
+ let emailVerified: boolean | undefined = undefined;
155
+ let emailVerifiedMessage: string | undefined = undefined;
156
+ let changePasswordOpen = false;
157
+
158
+ const onLogin = (currentUser: User & CurrentUserData) => {
159
+ user = currentUser;
160
+ readyEntity();
161
+ originalUsername = user?.username;
162
+ originalEmail = user?.email;
163
+ };
164
+ const onLogout = () => {
165
+ user = undefined;
166
+ originalUsername = undefined;
167
+ originalEmail = undefined;
168
+ };
169
+
170
+ $: if (user && user.username !== originalUsername) {
171
+ checkUsername();
172
+ } else {
173
+ if (usernameTimer) {
174
+ clearTimeout(usernameTimer);
175
+ }
176
+ usernameVerified = true;
177
+ usernameVerifiedMessage = undefined;
178
+ }
179
+
180
+ $: if (user && user.email !== originalEmail) {
181
+ checkEmail();
182
+ } else {
183
+ if (emailTimer) {
184
+ clearTimeout(emailTimer);
185
+ }
186
+ emailVerified = true;
187
+ emailVerifiedMessage = undefined;
188
+ }
189
+
190
+ onMount(async () => {
191
+ User.on('login', onLogin);
192
+ User.on('logout', onLogout);
193
+ user = (await User.current()) ?? undefined;
194
+ readyEntity();
195
+ originalUsername = user?.username;
196
+ originalEmail = user?.email;
197
+ });
198
+ onMount(async () => {
199
+ clientConfig = await User.getClientConfig();
200
+ });
201
+
202
+ onDestroy(() => {
203
+ User.off('login', onLogin);
204
+ User.off('logout', onLogout);
205
+ });
206
+
207
+ function readyEntity() {
208
+ // Make sure all fields are defined.
209
+ if (user != null && user.username == null) {
210
+ user.username = '';
211
+ }
212
+ if (user != null && user.email == null) {
213
+ user.email = '';
214
+ }
215
+ if (user != null && user.nameFirst == null) {
216
+ user.nameFirst = '';
217
+ }
218
+ if (user != null && user.nameMiddle == null) {
219
+ user.nameMiddle = '';
220
+ }
221
+ if (user != null && user.nameLast == null) {
222
+ user.nameLast = '';
223
+ }
224
+ if (user != null && user.avatar == null) {
225
+ user.avatar = '';
226
+ }
227
+ if (user != null && user.phone == null) {
228
+ user.phone = '';
229
+ }
230
+ }
231
+
232
+ async function save() {
233
+ if (user == null) {
234
+ return;
235
+ }
236
+
237
+ failureMessage = undefined;
238
+ saving = true;
239
+
240
+ if (clientConfig?.emailUsernames) {
241
+ user.username = user.email;
242
+ }
243
+
244
+ try {
245
+ if (await user.$save()) {
246
+ readyEntity();
247
+ originalEmail = user.email;
248
+ open = false;
249
+ usernameVerifiedMessage = undefined;
250
+ emailVerifiedMessage = undefined;
251
+ } else {
252
+ failureMessage = 'Error saving account changes.';
253
+ }
254
+ } catch (e: any) {
255
+ failureMessage = e?.message;
256
+ }
257
+ saving = false;
258
+ }
259
+
260
+ function checkUsername() {
261
+ usernameVerified = undefined;
262
+ usernameVerifiedMessage = undefined;
263
+ if (usernameTimer) {
264
+ clearTimeout(usernameTimer);
265
+ usernameTimer = undefined;
266
+ }
267
+ usernameTimer = setTimeout(async () => {
268
+ try {
269
+ const data = await user?.$checkUsername();
270
+ usernameVerified = data?.result ?? false;
271
+ usernameVerifiedMessage =
272
+ data?.message ?? 'Error getting verification.';
273
+ } catch (e: any) {
274
+ usernameVerified = false;
275
+ usernameVerifiedMessage = e?.message;
276
+ }
277
+ }, 400);
278
+ }
279
+
280
+ function checkEmail() {
281
+ emailVerified = undefined;
282
+ emailVerifiedMessage = undefined;
283
+ if (emailTimer) {
284
+ clearTimeout(emailTimer);
285
+ emailTimer = undefined;
286
+ }
287
+ emailTimer = setTimeout(async () => {
288
+ try {
289
+ const data = await user?.$checkEmail();
290
+ emailVerified = data?.result ?? false;
291
+ emailVerifiedMessage = data?.message ?? 'Error getting verification.';
292
+ } catch (e: any) {
293
+ emailVerified = false;
294
+ emailVerifiedMessage = e?.message;
295
+ }
296
+ }, 400);
297
+ }
298
+ </script>
299
+
300
+ <style>
301
+ :global(.mdc-dialog .mdc-dialog__surface.tilmeld-account-dialog-surface) {
302
+ width: 360px;
303
+ max-width: calc(100vw - 32px);
304
+ }
305
+ .tilmeld-account-failure {
306
+ margin-top: 1em;
307
+ color: var(--mdc-theme-error, #f00);
308
+ }
309
+ .tilmeld-account-action {
310
+ margin-top: 1em;
311
+ }
312
+ .tilmeld-account-loading {
313
+ display: flex;
314
+ justify-content: center;
315
+ align-items: center;
316
+ }
317
+ </style>
@@ -0,0 +1,176 @@
1
+ {#if user != null}
2
+ <Dialog
3
+ bind:open
4
+ aria-labelledby="tilmeld-password-title"
5
+ aria-describedby="tilmeld-password-content"
6
+ surface$class="tilmeld-password-dialog-surface"
7
+ >
8
+ <!-- Title cannot contain leading whitespace due to mdc-typography-baseline-top() -->
9
+ <Title id="tilmeld-password-title">Change Your Password</Title>
10
+ <Content id="tilmeld-password-content">
11
+ <div>
12
+ <Textfield
13
+ bind:value={currentPassword}
14
+ label="Current Password"
15
+ type="password"
16
+ style="width: 100%;"
17
+ input$autocomplete="current-password"
18
+ />
19
+ </div>
20
+
21
+ <div>
22
+ <Textfield
23
+ bind:value={newPassword}
24
+ label="New Password"
25
+ type="password"
26
+ style="width: 100%;"
27
+ input$autocomplete="new-password"
28
+ />
29
+ </div>
30
+
31
+ <div>
32
+ <Textfield
33
+ bind:value={newPassword2}
34
+ label="Re-enter New Password"
35
+ type="password"
36
+ style="width: 100%;"
37
+ input$autocomplete="new-password"
38
+ />
39
+ </div>
40
+
41
+ {#if failureMessage}
42
+ <div class="tilmeld-password-failure">
43
+ {failureMessage}
44
+ </div>
45
+ {/if}
46
+
47
+ {#if changing}
48
+ <div class="tilmeld-password-loading">
49
+ <CircularProgress style="height: 24px; width: 24px;" indeterminate />
50
+ </div>
51
+ {/if}
52
+ </Content>
53
+ <Actions>
54
+ <Button disabled={changing}>
55
+ <Label>Close</Label>
56
+ </Button>
57
+ <Button
58
+ on:click$preventDefault$stopPropagation={changePassword}
59
+ disabled={changing}
60
+ >
61
+ <Label>Change Password</Label>
62
+ </Button>
63
+ </Actions>
64
+ </Dialog>
65
+ {/if}
66
+
67
+ <script lang="ts">
68
+ import { onMount, onDestroy } from 'svelte';
69
+ import CircularProgress from '@smui/circular-progress';
70
+ import Dialog, { Title, Content, Actions } from '@smui/dialog';
71
+ import Textfield from '@smui/textfield';
72
+ import Button, { Label } from '@smui/button';
73
+ import { ClientConfig, CurrentUserData, User } from '@nymphjs/tilmeld-client';
74
+
75
+ export let open = false;
76
+
77
+ let clientConfig: ClientConfig | undefined = undefined;
78
+ let user: (User & CurrentUserData) | undefined = undefined;
79
+ let changing = false;
80
+ let failureMessage: string | undefined = undefined;
81
+
82
+ /** User provided. You can bind to it if you need to. */
83
+ export let currentPassword = '';
84
+ /** User provided. You can bind to it if you need to. */
85
+ export let newPassword = '';
86
+ /** User provided. You can bind to it if you need to. */
87
+ export let newPassword2 = '';
88
+
89
+ $: {
90
+ if (!open) {
91
+ changing = false;
92
+ failureMessage = undefined;
93
+ currentPassword = '';
94
+ newPassword = '';
95
+ newPassword2 = '';
96
+ }
97
+ }
98
+
99
+ const onLogin = (currentUser: User & CurrentUserData) => {
100
+ user = currentUser;
101
+ };
102
+ const onLogout = () => {
103
+ user = undefined;
104
+ };
105
+
106
+ onMount(async () => {
107
+ User.on('login', onLogin);
108
+ User.on('logout', onLogout);
109
+ user = (await User.current()) ?? undefined;
110
+ });
111
+
112
+ onDestroy(() => {
113
+ User.off('login', onLogin);
114
+ User.off('logout', onLogout);
115
+ });
116
+
117
+ async function changePassword() {
118
+ if (currentPassword === '') {
119
+ failureMessage = 'You need to enter your current password';
120
+ return;
121
+ }
122
+ if (newPassword !== newPassword2) {
123
+ failureMessage = 'Your passwords do not match.';
124
+ return;
125
+ }
126
+ if (newPassword === '') {
127
+ failureMessage = 'You need to enter a new password';
128
+ return;
129
+ }
130
+
131
+ failureMessage = undefined;
132
+ changing = true;
133
+
134
+ // Get the current user again, in case their data has changed.
135
+ user = (await User.current()) ?? undefined;
136
+
137
+ if (user == null) {
138
+ failureMessage = 'You must be logged in.';
139
+ changing = false;
140
+ return;
141
+ }
142
+
143
+ try {
144
+ // Change the user's password.
145
+ const data = await user.$changePassword({
146
+ currentPassword,
147
+ newPassword,
148
+ });
149
+
150
+ if (!data.result) {
151
+ failureMessage = data.message;
152
+ } else {
153
+ open = false;
154
+ }
155
+ } catch (e: any) {
156
+ failureMessage = e?.message;
157
+ }
158
+ changing = false;
159
+ }
160
+ </script>
161
+
162
+ <style>
163
+ :global(.mdc-dialog .mdc-dialog__surface.tilmeld-password-dialog-surface) {
164
+ width: 360px;
165
+ max-width: calc(100vw - 32px);
166
+ }
167
+ .tilmeld-password-failure {
168
+ margin-top: 1em;
169
+ color: var(--mdc-theme-error, #f00);
170
+ }
171
+ .tilmeld-password-loading {
172
+ display: flex;
173
+ justify-content: center;
174
+ align-items: center;
175
+ }
176
+ </style>