@indietabletop/appkit 4.0.0-0 → 4.0.0-1
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/lib/AppConfig/AppConfig.tsx +1 -0
- package/lib/DocumentTitle/DocumentTitle.tsx +4 -3
- package/lib/account/CurrentUserFetcher.tsx +2 -0
- package/lib/account/VerifyPage.tsx +29 -54
- package/lib/account/useCurrentUserResult.tsx +16 -1
- package/lib/copyrightRange.ts +7 -3
- package/lib/types.ts +17 -0
- package/lib/utm.ts +4 -1
- package/package.json +6 -6
- package/lib/append-copy-to-text.test.ts +0 -29
- package/lib/failureMessages.test.ts +0 -169
- package/lib/mailto.test.ts +0 -66
- package/lib/unique.test.ts +0 -22
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useAppConfig } from "../AppConfig/AppConfig.tsx";
|
|
2
2
|
|
|
3
3
|
export function DocumentTitle(props: { children: string }) {
|
|
4
|
-
const { children:
|
|
5
|
-
const { appName } = useAppConfig();
|
|
4
|
+
const { children: children } = props;
|
|
5
|
+
const { appName, isDev } = useAppConfig();
|
|
6
6
|
const itc = `${appName} · Indie Tabletop Club`;
|
|
7
|
+
const title = children ? `${children} | ${itc}` : itc;
|
|
7
8
|
|
|
8
|
-
return <title>{
|
|
9
|
+
return <title>{isDev ? `[DEV] ${title}` : title}</title>;
|
|
9
10
|
}
|
|
@@ -2,6 +2,7 @@ import { Button, Form } from "@ariakit/react";
|
|
|
2
2
|
import { useState, type Dispatch, type SetStateAction } from "react";
|
|
3
3
|
import { Link } from "wouter";
|
|
4
4
|
import { useAppConfig, useClient } from "../AppConfig/AppConfig.tsx";
|
|
5
|
+
import { interactiveText } from "../common.css.ts";
|
|
5
6
|
import { getSubmitFailureMessage } from "../failureMessages.ts";
|
|
6
7
|
import {
|
|
7
8
|
Letterhead,
|
|
@@ -18,13 +19,20 @@ import {
|
|
|
18
19
|
} from "../LetterheadForm/index.tsx";
|
|
19
20
|
import type { CurrentUser } from "../types.ts";
|
|
20
21
|
import { useForm } from "../use-form.ts";
|
|
21
|
-
import type { EventHandler } from "./types.ts";
|
|
22
|
+
import type { EventHandler, EventHandlerWithReload } from "./types.ts";
|
|
22
23
|
|
|
23
|
-
type SetStep = Dispatch<SetStateAction<
|
|
24
|
+
type SetStep = Dispatch<SetStateAction<VerifyStep>>;
|
|
25
|
+
|
|
26
|
+
function InitialStep(props: {
|
|
27
|
+
setStep: SetStep;
|
|
28
|
+
currentUser: CurrentUser;
|
|
29
|
+
onLogout: EventHandlerWithReload;
|
|
30
|
+
reload: () => void;
|
|
31
|
+
}) {
|
|
32
|
+
const { setStep, onLogout, currentUser, reload } = props;
|
|
33
|
+
const { email } = currentUser;
|
|
24
34
|
|
|
25
|
-
function InitialStep(props: { setStep: SetStep; currentUser: CurrentUser }) {
|
|
26
35
|
const client = useClient();
|
|
27
|
-
const { email } = props.currentUser;
|
|
28
36
|
const { form, submitName } = useForm({
|
|
29
37
|
defaultValues: {},
|
|
30
38
|
async onSubmit() {
|
|
@@ -32,7 +40,7 @@ function InitialStep(props: { setStep: SetStep; currentUser: CurrentUser }) {
|
|
|
32
40
|
return op.mapFailure(getSubmitFailureMessage);
|
|
33
41
|
},
|
|
34
42
|
onSuccess(value) {
|
|
35
|
-
|
|
43
|
+
setStep({ type: "SUBMIT_CODE", tokenId: value.tokenId });
|
|
36
44
|
},
|
|
37
45
|
});
|
|
38
46
|
|
|
@@ -50,6 +58,16 @@ function InitialStep(props: { setStep: SetStep; currentUser: CurrentUser }) {
|
|
|
50
58
|
<LetterheadFormActions>
|
|
51
59
|
<LetterheadSubmitError name={submitName} />
|
|
52
60
|
<LetterheadSubmitButton>Send code</LetterheadSubmitButton>
|
|
61
|
+
<LetterheadParagraph>
|
|
62
|
+
Cannot complete verification?{" "}
|
|
63
|
+
<Button
|
|
64
|
+
className={interactiveText}
|
|
65
|
+
onClick={() => void onLogout({ reload })}
|
|
66
|
+
>
|
|
67
|
+
Log out
|
|
68
|
+
</Button>
|
|
69
|
+
.
|
|
70
|
+
</LetterheadParagraph>
|
|
53
71
|
</LetterheadFormActions>
|
|
54
72
|
</Letterhead>
|
|
55
73
|
</Form>
|
|
@@ -138,13 +156,15 @@ function SuccessStep(props: { onClose?: EventHandler }) {
|
|
|
138
156
|
);
|
|
139
157
|
}
|
|
140
158
|
|
|
141
|
-
type
|
|
159
|
+
type VerifyStep =
|
|
142
160
|
| { type: "INITIAL" }
|
|
143
161
|
| { type: "SUBMIT_CODE"; tokenId: string }
|
|
144
162
|
| { type: "SUCCESS" };
|
|
145
163
|
|
|
146
164
|
export function VerifyAccountView(props: {
|
|
147
165
|
currentUser: CurrentUser;
|
|
166
|
+
onLogout: EventHandlerWithReload;
|
|
167
|
+
reload: () => void;
|
|
148
168
|
|
|
149
169
|
/**
|
|
150
170
|
* If provided, will cause the success step to render a close dialog button
|
|
@@ -154,18 +174,15 @@ export function VerifyAccountView(props: {
|
|
|
154
174
|
*/
|
|
155
175
|
onClose?: EventHandler;
|
|
156
176
|
}) {
|
|
157
|
-
const {
|
|
158
|
-
const [step, setStep] = useState<Steps>({ type: "INITIAL" });
|
|
177
|
+
const [step, setStep] = useState<VerifyStep>({ type: "INITIAL" });
|
|
159
178
|
|
|
160
179
|
switch (step.type) {
|
|
161
180
|
case "INITIAL": {
|
|
162
|
-
return <InitialStep
|
|
181
|
+
return <InitialStep {...props} setStep={setStep} />;
|
|
163
182
|
}
|
|
164
183
|
|
|
165
184
|
case "SUBMIT_CODE": {
|
|
166
|
-
return
|
|
167
|
-
<SubmitCodeStep {...step} setStep={setStep} currentUser={currentUser} />
|
|
168
|
-
);
|
|
185
|
+
return <SubmitCodeStep {...props} {...step} setStep={setStep} />;
|
|
169
186
|
}
|
|
170
187
|
|
|
171
188
|
case "SUCCESS": {
|
|
@@ -173,45 +190,3 @@ export function VerifyAccountView(props: {
|
|
|
173
190
|
}
|
|
174
191
|
}
|
|
175
192
|
}
|
|
176
|
-
|
|
177
|
-
// TODO: Decide if to remove this?
|
|
178
|
-
function AlreadyVerifiedView() {
|
|
179
|
-
const { hrefs } = useAppConfig();
|
|
180
|
-
|
|
181
|
-
return (
|
|
182
|
-
<Letterhead>
|
|
183
|
-
<LetterheadHeader>
|
|
184
|
-
<LetterheadHeading>Already Verified</LetterheadHeading>
|
|
185
|
-
<LetterheadParagraph>
|
|
186
|
-
Your user account has already been verified. You're all good!
|
|
187
|
-
</LetterheadParagraph>
|
|
188
|
-
</LetterheadHeader>
|
|
189
|
-
|
|
190
|
-
<Link href={hrefs.dashboard()} className={button()}>
|
|
191
|
-
Back to dashboard
|
|
192
|
-
</Link>
|
|
193
|
-
</Letterhead>
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// TODO: Decide if to remove this?
|
|
198
|
-
export function VerifyCard(props: { currentUser: CurrentUser | null }) {
|
|
199
|
-
const { currentUser } = props;
|
|
200
|
-
|
|
201
|
-
if (!currentUser) {
|
|
202
|
-
return (
|
|
203
|
-
<Letterhead>
|
|
204
|
-
<LetterheadHeading>Cannot verify</LetterheadHeading>
|
|
205
|
-
<LetterheadParagraph>
|
|
206
|
-
You must be logged in to verify your account.
|
|
207
|
-
</LetterheadParagraph>
|
|
208
|
-
</Letterhead>
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (currentUser.isVerified) {
|
|
213
|
-
return <AlreadyVerifiedView />;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return <VerifyAccountView currentUser={currentUser} />;
|
|
217
|
-
}
|
|
@@ -23,7 +23,22 @@ export function useCurrentUserResult(options?: {
|
|
|
23
23
|
// with caching or any of that business.
|
|
24
24
|
useEffect(() => {
|
|
25
25
|
if (performFetch) {
|
|
26
|
-
|
|
26
|
+
const fetchUserAndStoreResult = async () => {
|
|
27
|
+
setResult(new Pending());
|
|
28
|
+
|
|
29
|
+
const result = await client.getCurrentUser();
|
|
30
|
+
setResult(result);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Invoke the fetch action
|
|
34
|
+
fetchUserAndStoreResult();
|
|
35
|
+
|
|
36
|
+
// Set up listeners for data revalidation
|
|
37
|
+
window.addEventListener("focus", fetchUserAndStoreResult);
|
|
38
|
+
|
|
39
|
+
return () => {
|
|
40
|
+
window.removeEventListener("focus", fetchUserAndStoreResult);
|
|
41
|
+
};
|
|
27
42
|
}
|
|
28
43
|
}, [client, latestAttemptTs, performFetch]);
|
|
29
44
|
|
package/lib/copyrightRange.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export function copyrightRange(yearSince: number) {
|
|
2
2
|
const currentYear = new Date().getFullYear();
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
|
|
4
|
+
// Handle edge-case in which yearSince is greater than currentYear.
|
|
5
|
+
const clampedYearSince = Math.min(yearSince, currentYear);
|
|
6
|
+
|
|
7
|
+
return currentYear === clampedYearSince
|
|
8
|
+
? `© ${clampedYearSince}`
|
|
9
|
+
: `© ${clampedYearSince}–${currentYear}`;
|
|
6
10
|
}
|
package/lib/types.ts
CHANGED
|
@@ -7,6 +7,23 @@ type Brand<B> = { __brand: B };
|
|
|
7
7
|
|
|
8
8
|
export type Branded<T, B> = T & Brand<B>;
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Make properties in union K required in T.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* interface User {
|
|
16
|
+
* id: string;
|
|
17
|
+
* name?: string;
|
|
18
|
+
* email?: string;
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* type UserWithRequiredName = RequiredPick<User, 'name'>;
|
|
22
|
+
* // Result: { id: string; name: string; email?: string; }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export type RequiredPick<T, K extends keyof T> = T & Required<Pick<T, K>>;
|
|
26
|
+
|
|
10
27
|
/**
|
|
11
28
|
* A branded string.
|
|
12
29
|
*
|
package/lib/utm.ts
CHANGED
|
@@ -84,6 +84,9 @@ export function utm(params: UtmParams) {
|
|
|
84
84
|
*/
|
|
85
85
|
export function createUtm(defaults: UtmParams) {
|
|
86
86
|
return function (params?: Partial<UtmParams>) {
|
|
87
|
-
return utm({
|
|
87
|
+
return utm({
|
|
88
|
+
...defaults,
|
|
89
|
+
...(params && omitUndefinedKeys(params)),
|
|
90
|
+
});
|
|
88
91
|
};
|
|
89
92
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indietabletop/appkit",
|
|
3
|
-
"version": "4.0.0-
|
|
3
|
+
"version": "4.0.0-1",
|
|
4
4
|
"description": "A collection of modules used in apps built by Indie Tabletop Club",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -25,17 +25,17 @@
|
|
|
25
25
|
"react": "^18.0.0 || ^19.0.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@storybook/addon-docs": "^9.1.
|
|
29
|
-
"@storybook/addon-links": "^9.1.
|
|
30
|
-
"@storybook/react-vite": "^9.1.
|
|
28
|
+
"@storybook/addon-docs": "^9.1.10",
|
|
29
|
+
"@storybook/addon-links": "^9.1.10",
|
|
30
|
+
"@storybook/react-vite": "^9.1.10",
|
|
31
31
|
"@types/react": "^19.1.8",
|
|
32
32
|
"msw": "^2.11.2",
|
|
33
33
|
"msw-storybook-addon": "^2.0.5",
|
|
34
34
|
"np": "^10.1.0",
|
|
35
|
-
"storybook": "^9.1.
|
|
35
|
+
"storybook": "^9.1.10",
|
|
36
36
|
"typescript": "^5.8.2",
|
|
37
37
|
"vite": "^6.3.5",
|
|
38
|
-
"vitest": "^3.
|
|
38
|
+
"vitest": "^3.2.4"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@ariakit/react": "^0.4.17",
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
appendCopyToText,
|
|
4
|
-
maybeAppendCopyToText,
|
|
5
|
-
} from "./append-copy-to-text.ts";
|
|
6
|
-
|
|
7
|
-
describe("appendCopyToText", () => {
|
|
8
|
-
test("Appends ' (Copy)' to provided string", () => {
|
|
9
|
-
const returnValue = appendCopyToText("Zangrad Raiders");
|
|
10
|
-
expect(returnValue).toBe("Zangrad Raiders (Copy)");
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
test("Adds a copy count number if string already ends in ' (Copy)'", () => {
|
|
14
|
-
const returnValue = appendCopyToText("Zangrad Raiders (Copy)");
|
|
15
|
-
expect(returnValue).toBe("Zangrad Raiders (Copy 2)");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test("Increments a copy count number if one already exists", () => {
|
|
19
|
-
const returnValue = appendCopyToText("Zangrad Raiders (Copy 2)");
|
|
20
|
-
expect(returnValue).toBe("Zangrad Raiders (Copy 3)");
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe("maybeAppendCopyToText", () => {
|
|
25
|
-
test("Ignores empty strings", () => {
|
|
26
|
-
const returnValue = maybeAppendCopyToText("");
|
|
27
|
-
expect(returnValue).toBe("");
|
|
28
|
-
});
|
|
29
|
-
});
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
getFetchFailureMessages,
|
|
4
|
-
getSubmitFailureMessage,
|
|
5
|
-
} from "./failureMessages.ts";
|
|
6
|
-
|
|
7
|
-
describe("getFetchFailureMessages", () => {
|
|
8
|
-
test("Returns correct message for API_ERROR with code 404", () => {
|
|
9
|
-
const result = getFetchFailureMessages({ type: "API_ERROR", code: 404 });
|
|
10
|
-
|
|
11
|
-
expect(result).toMatchInlineSnapshot(`
|
|
12
|
-
{
|
|
13
|
-
"action": {
|
|
14
|
-
"href": "~/",
|
|
15
|
-
"label": "Go back",
|
|
16
|
-
"type": "LINK",
|
|
17
|
-
},
|
|
18
|
-
"description": "The link you have followed might be broken.",
|
|
19
|
-
"title": "Not found",
|
|
20
|
-
}
|
|
21
|
-
`);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test("Returns correct message for API_ERROR with code 500", () => {
|
|
25
|
-
const result = getFetchFailureMessages({ type: "API_ERROR", code: 500 });
|
|
26
|
-
|
|
27
|
-
expect(result).toMatchInlineSnapshot(`
|
|
28
|
-
{
|
|
29
|
-
"action": {
|
|
30
|
-
"label": "Reload app",
|
|
31
|
-
"type": "RELOAD",
|
|
32
|
-
},
|
|
33
|
-
"description": "This is probably an issue with our servers. You can try refreshing.",
|
|
34
|
-
"title": "Ooops, something went wrong",
|
|
35
|
-
}
|
|
36
|
-
`);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("Returns correct message for API_ERROR with partial override", () => {
|
|
40
|
-
const result = getFetchFailureMessages(
|
|
41
|
-
{ type: "API_ERROR", code: 404 },
|
|
42
|
-
{ 404: { title: `Army not found` } },
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
expect(result).toMatchInlineSnapshot(`
|
|
46
|
-
{
|
|
47
|
-
"action": {
|
|
48
|
-
"href": "~/",
|
|
49
|
-
"label": "Go back",
|
|
50
|
-
"type": "LINK",
|
|
51
|
-
},
|
|
52
|
-
"description": "The link you have followed might be broken.",
|
|
53
|
-
"title": "Army not found",
|
|
54
|
-
}
|
|
55
|
-
`);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("Returns correct message for API_ERROR with override", () => {
|
|
59
|
-
const result = getFetchFailureMessages(
|
|
60
|
-
{ type: "API_ERROR", code: 404 },
|
|
61
|
-
{
|
|
62
|
-
404: {
|
|
63
|
-
title: `Army not found`,
|
|
64
|
-
description: `It might have been deleted.`,
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
expect(result).toMatchInlineSnapshot(`
|
|
70
|
-
{
|
|
71
|
-
"action": {
|
|
72
|
-
"href": "~/",
|
|
73
|
-
"label": "Go back",
|
|
74
|
-
"type": "LINK",
|
|
75
|
-
},
|
|
76
|
-
"description": "It might have been deleted.",
|
|
77
|
-
"title": "Army not found",
|
|
78
|
-
}
|
|
79
|
-
`);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("Returns correct message for NETWORK_ERROR", () => {
|
|
83
|
-
const result = getFetchFailureMessages({ type: "NETWORK_ERROR" });
|
|
84
|
-
|
|
85
|
-
expect(result).toMatchInlineSnapshot(`
|
|
86
|
-
{
|
|
87
|
-
"action": {
|
|
88
|
-
"label": "Retry request",
|
|
89
|
-
"type": "REFETCH",
|
|
90
|
-
},
|
|
91
|
-
"description": "Check your interent connection and try again.",
|
|
92
|
-
"title": "No connection",
|
|
93
|
-
}
|
|
94
|
-
`);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("Returns correct message for UNKNOWN_ERROR", () => {
|
|
98
|
-
const result = getFetchFailureMessages({ type: "UNKNOWN_ERROR" });
|
|
99
|
-
|
|
100
|
-
expect(result).toMatchInlineSnapshot(`
|
|
101
|
-
{
|
|
102
|
-
"action": {
|
|
103
|
-
"label": "Reload app",
|
|
104
|
-
"type": "RELOAD",
|
|
105
|
-
},
|
|
106
|
-
"description": "This is probably an issue on our side. You can try refreshing.",
|
|
107
|
-
"title": "Ooops, something went wrong",
|
|
108
|
-
}
|
|
109
|
-
`);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("Returns correct message for an unrecognised error type", () => {
|
|
113
|
-
const result = getFetchFailureMessages({ type: "FOO" as any });
|
|
114
|
-
|
|
115
|
-
expect(result).toMatchInlineSnapshot(`
|
|
116
|
-
{
|
|
117
|
-
"action": {
|
|
118
|
-
"label": "Reload app",
|
|
119
|
-
"type": "RELOAD",
|
|
120
|
-
},
|
|
121
|
-
"description": "This is probably an issue on our side. You can try refreshing.",
|
|
122
|
-
"title": "Ooops, something went wrong",
|
|
123
|
-
}
|
|
124
|
-
`);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
describe("getSubmitFailureMessage", () => {
|
|
129
|
-
test("Returns correct message for API_ERROR with code 500", () => {
|
|
130
|
-
const message = getSubmitFailureMessage({ type: "API_ERROR", code: 500 });
|
|
131
|
-
expect(message).toMatchInlineSnapshot(
|
|
132
|
-
`"Could not submit form due to an unexpected server error. Please refresh the page and try again."`,
|
|
133
|
-
);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("Returns correct message for API_ERROR with override", () => {
|
|
137
|
-
const message = getSubmitFailureMessage(
|
|
138
|
-
{ type: "API_ERROR", code: 401 },
|
|
139
|
-
{ 401: `Username and password do not match. Please try again.` },
|
|
140
|
-
);
|
|
141
|
-
expect(message).toMatchInlineSnapshot(
|
|
142
|
-
`"Username and password do not match. Please try again."`,
|
|
143
|
-
);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("Returns correct message for NETWORK_ERROR", () => {
|
|
147
|
-
const result = getSubmitFailureMessage({ type: "NETWORK_ERROR" });
|
|
148
|
-
|
|
149
|
-
expect(result).toMatchInlineSnapshot(
|
|
150
|
-
`"Could not submit form due to network error. Make sure you are connected to the internet and try again."`,
|
|
151
|
-
);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("Returns correct message for UNKNOWN_ERROR", () => {
|
|
155
|
-
const result = getSubmitFailureMessage({ type: "UNKNOWN_ERROR" });
|
|
156
|
-
|
|
157
|
-
expect(result).toMatchInlineSnapshot(
|
|
158
|
-
`"Could not submit form due to an unexpected error. Please refresh the page and try again."`,
|
|
159
|
-
);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test("Returns correct message for an unrecognised error type", () => {
|
|
163
|
-
const result = getSubmitFailureMessage({ type: "FOO" as any });
|
|
164
|
-
|
|
165
|
-
expect(result).toMatchInlineSnapshot(
|
|
166
|
-
`"Could not submit form due to an unexpected error. Please refresh the page and try again."`,
|
|
167
|
-
);
|
|
168
|
-
});
|
|
169
|
-
});
|
package/lib/mailto.test.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "vitest";
|
|
2
|
-
import { mailto as emailTemplateToMailto } from "./mailto.ts";
|
|
3
|
-
|
|
4
|
-
describe("emailTempalteToMailto", () => {
|
|
5
|
-
test("correctly serializes data", () => {
|
|
6
|
-
expect(
|
|
7
|
-
emailTemplateToMailto(null, {
|
|
8
|
-
body: `Hello world!\n\nI am URL escaped.`,
|
|
9
|
-
subject: `This is a subject?`,
|
|
10
|
-
cc: null,
|
|
11
|
-
}),
|
|
12
|
-
).toMatchInlineSnapshot(
|
|
13
|
-
`"mailto:?body=Hello%20world!%0A%0AI%20am%20URL%20escaped.&subject=This%20is%20a%20subject%3F"`,
|
|
14
|
-
);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test("includes recipient when provided", () => {
|
|
18
|
-
expect(
|
|
19
|
-
emailTemplateToMailto("hi@example.com", {
|
|
20
|
-
body: `Hello world!\n\nI am URL escaped.`,
|
|
21
|
-
subject: `This is a subject?`,
|
|
22
|
-
cc: null,
|
|
23
|
-
}),
|
|
24
|
-
).toMatchInlineSnapshot(
|
|
25
|
-
`"mailto:hi@example.com?body=Hello%20world!%0A%0AI%20am%20URL%20escaped.&subject=This%20is%20a%20subject%3F"`,
|
|
26
|
-
);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("includes cc when provided", () => {
|
|
30
|
-
expect(
|
|
31
|
-
emailTemplateToMailto("hi@example.com", {
|
|
32
|
-
body: `Hello world!\n\nI am URL escaped.`,
|
|
33
|
-
subject: `This is a subject?`,
|
|
34
|
-
cc: "cc@example.com",
|
|
35
|
-
}),
|
|
36
|
-
).toMatchInlineSnapshot(
|
|
37
|
-
`"mailto:hi@example.com?body=Hello%20world!%0A%0AI%20am%20URL%20escaped.&subject=This%20is%20a%20subject%3F&cc=cc%40example.com"`,
|
|
38
|
-
);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("includes bcc when provided", () => {
|
|
42
|
-
expect(
|
|
43
|
-
emailTemplateToMailto("hi@example.com", {
|
|
44
|
-
body: `Hello world!\n\nI am URL escaped.`,
|
|
45
|
-
subject: `This is a subject?`,
|
|
46
|
-
cc: "cc@example.com",
|
|
47
|
-
bcc: "bcc@example.com",
|
|
48
|
-
}),
|
|
49
|
-
).toMatchInlineSnapshot(
|
|
50
|
-
`"mailto:hi@example.com?body=Hello%20world!%0A%0AI%20am%20URL%20escaped.&subject=This%20is%20a%20subject%3F&cc=cc%40example.com&bcc=bcc%40example.com"`,
|
|
51
|
-
);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("cc and bcc allow array values", () => {
|
|
55
|
-
expect(
|
|
56
|
-
emailTemplateToMailto("hi@example.com", {
|
|
57
|
-
body: `Hello world!\n\nI am URL escaped.`,
|
|
58
|
-
subject: `This is a subject?`,
|
|
59
|
-
cc: ["cc@example.com", "cc2@example.com"],
|
|
60
|
-
bcc: ["bcc@example.com", "bcc2@example.com"],
|
|
61
|
-
}),
|
|
62
|
-
).toMatchInlineSnapshot(
|
|
63
|
-
`"mailto:hi@example.com?body=Hello%20world!%0A%0AI%20am%20URL%20escaped.&subject=This%20is%20a%20subject%3F&cc=cc%40example.com%2Ccc2%40example.com&bcc=bcc%40example.com%2Cbcc2%40example.com"`,
|
|
64
|
-
);
|
|
65
|
-
});
|
|
66
|
-
});
|
package/lib/unique.test.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "vitest";
|
|
2
|
-
import { uniqueBy } from "./unique.ts";
|
|
3
|
-
|
|
4
|
-
describe("unique", () => {
|
|
5
|
-
test("Returns unique items based on getKey", () => {
|
|
6
|
-
const result = uniqueBy(
|
|
7
|
-
[{ id: "zxcvbn" }, { id: "qwerty" }, { id: "zxcvbn" }],
|
|
8
|
-
(item) => item.id,
|
|
9
|
-
);
|
|
10
|
-
|
|
11
|
-
expect(result).toMatchInlineSnapshot(`
|
|
12
|
-
[
|
|
13
|
-
{
|
|
14
|
-
"id": "zxcvbn",
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
"id": "qwerty",
|
|
18
|
-
},
|
|
19
|
-
]
|
|
20
|
-
`);
|
|
21
|
-
});
|
|
22
|
-
});
|