Skip to content

Commit f9c2743

Browse files
Allow adding new emails
1 parent 53cfb21 commit f9c2743

File tree

11 files changed

+320
-56
lines changed

11 files changed

+320
-56
lines changed

src/lib/components/inputs/text-field.svelte

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,22 @@
33
44
interface Props {
55
label: string;
6-
name: string;
76
placeholder: string;
7+
name: string;
88
value?: string;
99
autofocus?: boolean;
1010
disabled?: boolean;
11+
title?: string;
1112
}
1213
1314
let {
1415
label,
15-
name,
1616
placeholder,
17+
name,
1718
value = $bindable(''),
1819
autofocus = false,
19-
disabled = false
20+
disabled = false,
21+
title = ''
2022
}: Props = $props();
2123
2224
let input: HTMLInputElement | undefined = $state();
@@ -32,14 +34,18 @@
3234
}
3335
</script>
3436

35-
<label class="text-field">
36-
<span class="label">{label}</span>
37-
<input type="text" {name} {placeholder} {disabled} bind:this={input} bind:value />
37+
<label class="text-field" {title}>
38+
{#if value}
39+
<input type="hidden" {name} bind:value />
40+
{/if}
41+
{#if label}
42+
<span class="label">{label}</span>
43+
{/if}
44+
<input type="text" {placeholder} {name} {disabled} bind:this={input} bind:value />
3845
</label>
3946

4047
<style lang="scss">
4148
.text-field {
42-
min-width: 280px;
4349
display: flex;
4450
flex-direction: column;
4551
gap: 0.25em;

src/lib/data/translations.json

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,18 +67,48 @@
6767
"sourceCode": "Source code"
6868
},
6969
"errors": {
70-
"413": {
71-
"title": "File too large",
72-
"message": "Uploaded files can only go up to 10M."
73-
},
74-
"415": {
75-
"title": "Unsupported file format",
76-
"message": "The type of file you have selected is not supported."
70+
"updateAvatar": {
71+
"413": {
72+
"title": "File too large",
73+
"message": "Uploaded files can only go up to 10M."
74+
},
75+
"415": {
76+
"title": "Unsupported file format",
77+
"message": "The type of file you have selected is not supported."
78+
}
7779
}
7880
}
7981
},
8082
"emails": {
81-
"main": "Main"
83+
"main": "Main address",
84+
"verification": {
85+
"title": "A one-time use code has been sent to this address.",
86+
"placeholder": "Verification code"
87+
},
88+
"verify": "Verify",
89+
"new": {
90+
"label": "New email",
91+
"placeholder": "email@example.org"
92+
},
93+
"add": "Add",
94+
"errors": {
95+
"addEmail": {
96+
"400": {
97+
"title": "Invalid email",
98+
"message": "This email address is not valid."
99+
},
100+
"409": {
101+
"title": "Used email",
102+
"message": "This email address is already in use."
103+
}
104+
},
105+
"verifyEmail": {
106+
"404": {
107+
"title": "Email not verified",
108+
"message": "This code is not valid. Check the code you received via email."
109+
}
110+
}
111+
}
82112
},
83113
"account": {
84114
"randomCode": {
@@ -103,13 +133,15 @@
103133
"placeholder": "Username or email"
104134
},
105135
"errors": {
106-
"403": {
107-
"title": "Use a password",
108-
"message": "This account does not use an email for logging in. Please use your password instead."
109-
},
110-
"404": {
111-
"title": "User not found",
112-
"message": "No user account was found with this username or email."
136+
"sendEmail": {
137+
"403": {
138+
"title": "Use a password",
139+
"message": "This account does not use an email for logging in. Please use your password instead."
140+
},
141+
"404": {
142+
"title": "User not found",
143+
"message": "No user account was found with this username or email."
144+
}
113145
}
114146
}
115147
},

src/lib/openapi/fakes/emails-endpoint.ts

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,36 @@ import type {
1212
SetMainEmailRequest,
1313
VerifyEmailRequest
1414
} from '../generated';
15+
import FakeTokensEndpointApi from './tokens-endpoint';
16+
import FakeUsersEndpointApi from './users-endpoint';
17+
import { fail } from './utils';
1518

1619
export default class FakeEmailsEndpointApi implements EmailsEndpointApiInterface {
1720
async countEmails(initOverrides?: RequestInit | InitOverrideFunction): Promise<number> {
1821
const emails = await this.listEmails();
1922
return emails.length;
2023
}
2124

22-
createEmail(
25+
async createEmail(
2326
emailCreation: EmailCreation,
2427
customDeepLinks?: boolean,
2528
initOverrides?: RequestInit | InitOverrideFunction
2629
): Promise<Email> {
27-
throw new Error('Method not implemented.');
30+
switch (emailCreation.email) {
31+
case FakeUsersEndpointApi.badEmail:
32+
return fail(400);
33+
34+
case FakeUsersEndpointApi.usedEmail:
35+
return fail(409);
36+
37+
default:
38+
return {
39+
id: makeId(),
40+
email: emailCreation.email,
41+
main: false,
42+
verified: false
43+
};
44+
}
2845
}
2946

3047
deleteEmail(id: string, initOverrides?: RequestInit | InitOverrideFunction): Promise<void> {
@@ -38,7 +55,11 @@ export default class FakeEmailsEndpointApi implements EmailsEndpointApiInterface
3855
switch (page) {
3956
case undefined:
4057
case 0:
41-
return [this.makeEmail(true), this.makeEmail(), this.makeEmail()];
58+
return [
59+
this.makeEmail(true, true),
60+
this.makeEmail(false, true),
61+
this.makeEmail(false, false)
62+
];
4263

4364
default:
4465
return [];
@@ -49,14 +70,20 @@ export default class FakeEmailsEndpointApi implements EmailsEndpointApiInterface
4970
throw new Error('Method not implemented.');
5071
}
5172

52-
verifyEmail(
73+
async verifyEmail(
5374
emailVerification: EmailVerification,
5475
initOverrides?: RequestInit | InitOverrideFunction
5576
): Promise<void> {
56-
throw new Error('Method not implemented.');
77+
switch (emailVerification.code) {
78+
case FakeTokensEndpointApi.goodSecret:
79+
return;
80+
81+
default:
82+
return fail(404);
83+
}
5784
}
5885

59-
private makeEmail(main = false, verified = true): Email {
86+
private makeEmail(main: boolean, verified: boolean): Email {
6087
const id = makeId();
6188
return {
6289
id,

src/lib/openapi/fakes/users-endpoint.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,12 @@ export default class FakeUsersEndpointApi implements UsersEndpointApiInterface {
9393
switch (body) {
9494
case FakeUsersEndpointApi.normalImageFile:
9595
return FakeUsersEndpointApi.normalImageFile.name;
96+
9697
case FakeUsersEndpointApi.largeImageFile:
97-
fail(413);
98+
return fail(413);
99+
98100
default:
99-
fail(415);
101+
return fail(415);
100102
}
101103
}
102104

src/routes/+layout.svelte

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,11 @@
5959
</main>
6060
</div>
6161
<Navigation />
62-
<Dialog
63-
visible={errors.length > 0}
64-
title={currentError?.title || ''}
65-
message={currentError?.message || ''}
66-
onClickOk={removeError}
67-
/>
62+
<Dialog visible={errors.length > 0} title={currentError?.title || ''} onClickOk={removeError}>
63+
{#snippet content()}
64+
<p>{currentError?.message || ''}</p>
65+
{/snippet}
66+
</Dialog>
6867
</div>
6968

7069
<style lang="scss">

src/routes/dialog.svelte

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
<script lang="ts">
2+
import type { Snippet } from 'svelte';
23
import { t } from 'i18next';
34
import Button from '$lib/components/inputs/button.svelte';
45
56
interface Props {
67
visible: boolean;
78
title: string;
8-
message: string;
9+
content: Snippet;
910
onClickOk: () => void;
1011
}
1112
12-
let { visible, title, message, onClickOk }: Props = $props();
13+
let { visible, title, content, onClickOk }: Props = $props();
1314
</script>
1415

1516
<div class="dialog" class:visible hidden={!visible}>
1617
<div class="background"></div>
1718
<dialog class="alert">
1819
<h2>{title}</h2>
19-
<p>{message}</p>
20+
<div>{@render content()}</div>
2021
<div class="buttons">
2122
<Button type="button" primary onClick={onClickOk}>{t('ok')}</Button>
2223
</div>

src/routes/login/+page.svelte

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,14 @@
6464
switch (error.response.status) {
6565
case 400:
6666
return new DisplayableError('errors.400');
67+
6768
case 403:
6869
isWaitingForRandomCode = true;
69-
return new DisplayableError('login.errors.403');
70+
return new DisplayableError('login.errors.sendEmail.403');
71+
7072
case 404:
71-
return new DisplayableError('login.errors.404');
73+
return new DisplayableError('login.errors.sendEmail.404');
74+
7275
default:
7376
return new DisplayableError();
7477
}
@@ -92,8 +95,10 @@
9295
switch (error.response.status) {
9396
case 400:
9497
return new DisplayableError('account.errors.createToken.400');
98+
9599
case 404:
96100
return new DisplayableError('login.errors.404');
101+
97102
default:
98103
return new DisplayableError();
99104
}
@@ -111,17 +116,17 @@
111116
<div class="fields">
112117
<TextField
113118
label={t('login.identifier.label')}
114-
name="username"
115119
placeholder={t('login.identifier.placeholder')}
120+
name="username"
116121
autofocus
117122
disabled={isWaitingForRandomCode}
118123
bind:value={identifier}
119124
/>
120125
{#if isWaitingForRandomCode}
121126
<TextField
122127
label={t('account.randomCode.label')}
123-
name="one-time-code"
124128
placeholder={t('account.randomCode.placeholder')}
129+
name="one-time-code"
125130
autofocus
126131
bind:value={randomCode}
127132
/>
@@ -157,6 +162,7 @@
157162
158163
.fields {
159164
width: min-content;
165+
min-width: 280px;
160166
display: flex;
161167
flex-direction: column;
162168
gap: 1em;

src/routes/register/+page.svelte

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,21 +87,26 @@
8787
switch (violationReport?.violations?.[0]?.field) {
8888
case 'createUser.input.username':
8989
return new DisplayableError('register.errors.createUser.400.username');
90+
9091
case 'createUser.input.email':
9192
return new DisplayableError('register.errors.createUser.400.email');
93+
9294
default:
9395
return new DisplayableError('errors.400');
9496
}
9597
case 403:
9698
return new DisplayableError('register.errors.createUser.403');
99+
97100
case 409:
98101
const explainedFailure = (await error.response.json()) as ExplainedFailure;
99102
100103
switch (explainedFailure.reason) {
101104
case 'username_taken':
102105
return new DisplayableError('register.errors.createUser.409.username');
106+
103107
case 'email_taken':
104108
return new DisplayableError('register.errors.createUser.409.email');
109+
105110
default:
106111
return new DisplayableError();
107112
}
@@ -129,8 +134,10 @@
129134
switch (error.response.status) {
130135
case 400:
131136
return new DisplayableError('account.errors.createToken.400');
137+
132138
case 404:
133139
return new DisplayableError('register.errors.createToken.404');
140+
134141
default:
135142
return new DisplayableError();
136143
}
@@ -150,24 +157,24 @@
150157
<div class="fields">
151158
<TextField
152159
label={t('register.username.label')}
153-
name="username"
154160
placeholder={t('register.username.placeholder')}
161+
name="username"
155162
autofocus
156163
disabled={isWaitingForRandomCode}
157164
bind:value={username}
158165
/>
159166
<TextField
160167
label={t('register.email.label')}
161-
name="email"
162168
placeholder={t('register.email.placeholder')}
169+
name="email"
163170
disabled={isWaitingForRandomCode}
164171
bind:value={email}
165172
/>
166173
{#if isWaitingForRandomCode}
167174
<TextField
168175
label={t('account.randomCode.label')}
169-
name="one-time-code"
170176
placeholder={t('account.randomCode.placeholder')}
177+
name="one-time-code"
171178
autofocus
172179
bind:value={randomCode}
173180
/>
@@ -213,6 +220,7 @@
213220
214221
.fields {
215222
width: min-content;
223+
min-width: 280px;
216224
display: flex;
217225
flex-direction: column;
218226
gap: 1em;

0 commit comments

Comments
 (0)