@indietabletop/appkit 3.5.0 → 3.6.0-0
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/ClientContext/ClientContext.tsx +25 -0
- package/lib/InfoPage/index.tsx +46 -0
- package/lib/InfoPage/pages.tsx +36 -0
- package/lib/InfoPage/style.css.ts +36 -0
- package/lib/Letterhead/index.tsx +3 -3
- package/lib/LetterheadForm/index.tsx +1 -1
- package/lib/LoginPage/LoginPage.stories.tsx +107 -0
- package/lib/LoginPage/LoginPage.tsx +204 -0
- package/lib/LoginPage/style.css.ts +17 -0
- package/lib/ModernIDB/Cursor.ts +91 -0
- package/lib/ModernIDB/ModernIDB.ts +337 -0
- package/lib/ModernIDB/ModernIDBError.ts +9 -0
- package/lib/ModernIDB/ObjectStore.ts +195 -0
- package/lib/ModernIDB/ObjectStoreIndex.ts +102 -0
- package/lib/ModernIDB/README.md +9 -0
- package/lib/ModernIDB/Transaction.ts +40 -0
- package/lib/ModernIDB/VersionChangeManager.ts +57 -0
- package/lib/ModernIDB/bindings/factory.tsx +160 -0
- package/lib/ModernIDB/bindings/index.ts +2 -0
- package/lib/ModernIDB/bindings/types.ts +56 -0
- package/lib/ModernIDB/bindings/utils.tsx +32 -0
- package/lib/ModernIDB/index.ts +10 -0
- package/lib/ModernIDB/types.ts +77 -0
- package/lib/ModernIDB/utils.ts +51 -0
- package/lib/ReleaseInfo/index.tsx +29 -0
- package/lib/RulesetResolver.ts +214 -0
- package/lib/SubscribeCard/SubscribeCard.stories.tsx +133 -0
- package/lib/SubscribeCard/SubscribeCard.tsx +107 -0
- package/lib/SubscribeCard/style.css.ts +14 -0
- package/lib/Title/index.tsx +4 -0
- package/lib/append-copy-to-text.ts +1 -1
- package/lib/async-op.ts +8 -0
- package/lib/client.ts +37 -2
- package/lib/copyrightRange.ts +6 -0
- package/lib/groupBy.ts +25 -0
- package/lib/ids.ts +6 -0
- package/lib/index.ts +12 -0
- package/lib/random.ts +12 -0
- package/lib/result/swr.ts +18 -0
- package/lib/structs.ts +10 -0
- package/lib/typeguards.ts +12 -0
- package/lib/types.ts +3 -1
- package/lib/unique.test.ts +22 -0
- package/lib/unique.ts +24 -0
- package/package.json +16 -6
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { http, HttpResponse } from "msw";
|
|
3
|
+
import { LetterheadParagraph } from "../Letterhead/index.tsx";
|
|
4
|
+
import { sleep } from "../sleep.ts";
|
|
5
|
+
import { SubscribeCard } from "./SubscribeCard.tsx";
|
|
6
|
+
|
|
7
|
+
const pledge = {
|
|
8
|
+
id: "1",
|
|
9
|
+
email: "test@example.com",
|
|
10
|
+
contactSubscribed: false,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const subscribeByPledgeId = {
|
|
14
|
+
success() {
|
|
15
|
+
return http.post(
|
|
16
|
+
"http://localhost:8000/v1/newsletters/changelog/subscriptions",
|
|
17
|
+
async () => {
|
|
18
|
+
await sleep(2000);
|
|
19
|
+
|
|
20
|
+
return HttpResponse.json(
|
|
21
|
+
{ message: "Added to newsletter" },
|
|
22
|
+
{ status: 201 },
|
|
23
|
+
);
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
},
|
|
27
|
+
noConnection() {
|
|
28
|
+
return http.post(
|
|
29
|
+
"http://localhost:8000/v1/newsletters/changelog/subscriptions",
|
|
30
|
+
async () => {
|
|
31
|
+
return HttpResponse.error();
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
},
|
|
35
|
+
error() {
|
|
36
|
+
return http.post(
|
|
37
|
+
"http://localhost:8000/v1/newsletters/changelog/subscriptions",
|
|
38
|
+
async () => {
|
|
39
|
+
await sleep(2000);
|
|
40
|
+
|
|
41
|
+
return HttpResponse.text("Application error", { status: 500 });
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const meta = {
|
|
48
|
+
title: "Pages/Subscribe Card",
|
|
49
|
+
component: SubscribeCard,
|
|
50
|
+
tags: ["autodocs"],
|
|
51
|
+
args: {
|
|
52
|
+
pledge,
|
|
53
|
+
content: {
|
|
54
|
+
SUBSCRIBE: {
|
|
55
|
+
title: "Space Gits on Indie Tabletop Club",
|
|
56
|
+
description: (
|
|
57
|
+
<LetterheadParagraph>
|
|
58
|
+
Space Gits is getting a fancy <strong>gang builder</strong> and{" "}
|
|
59
|
+
<strong>smart rules</strong> in late 2025 in collaboration with
|
|
60
|
+
Indie Tabletop Club. Subscribe to ITC's{" "}
|
|
61
|
+
<em>at‑most‑one‑a‑month</em> newsletter and be among the first to
|
|
62
|
+
gain early access. It's gonna be rad.
|
|
63
|
+
</LetterheadParagraph>
|
|
64
|
+
),
|
|
65
|
+
},
|
|
66
|
+
SUBSCRIBE_SUCCESS: {
|
|
67
|
+
title: "Subscribed!",
|
|
68
|
+
description: (
|
|
69
|
+
<LetterheadParagraph>
|
|
70
|
+
Thank you for being awesome. Once Space Gits is available for early
|
|
71
|
+
access, you will be among the first to know!
|
|
72
|
+
</LetterheadParagraph>
|
|
73
|
+
),
|
|
74
|
+
},
|
|
75
|
+
PREVIOUSLY_SUBSCRIBED: {
|
|
76
|
+
title: "Space Gits on Indie Tabletop Club",
|
|
77
|
+
description: (
|
|
78
|
+
<>
|
|
79
|
+
<LetterheadParagraph>
|
|
80
|
+
Space Gits is getting a fancy <strong>gang builder</strong> and{" "}
|
|
81
|
+
<strong>smart rules</strong> in late 2025 in collaboration with
|
|
82
|
+
Indie Tabletop Club.
|
|
83
|
+
</LetterheadParagraph>
|
|
84
|
+
|
|
85
|
+
<LetterheadParagraph>
|
|
86
|
+
Early access will be announced via ITC's newsletter, which
|
|
87
|
+
— according to our records — you are are already
|
|
88
|
+
subscribed to! Nothing left to do but await the good news in your
|
|
89
|
+
inbox!
|
|
90
|
+
</LetterheadParagraph>
|
|
91
|
+
</>
|
|
92
|
+
),
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
parameters: {
|
|
97
|
+
msw: {
|
|
98
|
+
handlers: {
|
|
99
|
+
subscribeByPledgeId: subscribeByPledgeId.success(),
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
} satisfies Meta<typeof SubscribeCard>;
|
|
104
|
+
|
|
105
|
+
export default meta;
|
|
106
|
+
|
|
107
|
+
type Story = StoryObj<typeof meta>;
|
|
108
|
+
|
|
109
|
+
export const Default: Story = {};
|
|
110
|
+
|
|
111
|
+
export const AlreadySubscribed: Story = {
|
|
112
|
+
args: { pledge: { ...pledge, contactSubscribed: true } },
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const NoConnection: Story = {
|
|
116
|
+
parameters: {
|
|
117
|
+
msw: {
|
|
118
|
+
handlers: {
|
|
119
|
+
subscribeByPledgeId: subscribeByPledgeId.noConnection(),
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const FallbackFailure: Story = {
|
|
126
|
+
parameters: {
|
|
127
|
+
msw: {
|
|
128
|
+
handlers: {
|
|
129
|
+
subscribeByPledgeId: subscribeByPledgeId.error(),
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Form } from "@ariakit/react";
|
|
2
|
+
import {
|
|
3
|
+
getSubmitFailureMessage,
|
|
4
|
+
Letterhead,
|
|
5
|
+
LetterheadHeading,
|
|
6
|
+
LetterheadParagraph,
|
|
7
|
+
LetterheadSubmitButton,
|
|
8
|
+
LetterheadSubmitError,
|
|
9
|
+
useForm,
|
|
10
|
+
type RedeemedPledge,
|
|
11
|
+
} from "@indietabletop/appkit";
|
|
12
|
+
import { useState, type ReactNode } from "react";
|
|
13
|
+
import { useClient } from "../ClientContext/ClientContext.tsx";
|
|
14
|
+
import * as css from "./style.css.ts";
|
|
15
|
+
|
|
16
|
+
type Pledge = Pick<RedeemedPledge, "id" | "email" | "contactSubscribed">;
|
|
17
|
+
|
|
18
|
+
function SubscribeView(
|
|
19
|
+
props: ViewContent & {
|
|
20
|
+
onSuccess: () => void;
|
|
21
|
+
pledge: Pledge;
|
|
22
|
+
},
|
|
23
|
+
) {
|
|
24
|
+
const { pledge, title, description, onSuccess } = props;
|
|
25
|
+
|
|
26
|
+
const client = useClient();
|
|
27
|
+
const { form, submitName } = useForm({
|
|
28
|
+
defaultValues: {},
|
|
29
|
+
onSuccess,
|
|
30
|
+
async onSubmit() {
|
|
31
|
+
const result = await client.subscribeToNewsletterByPledgeId(pledge.id);
|
|
32
|
+
return result.mapFailure(getSubmitFailureMessage);
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Letterhead textAlign="center">
|
|
38
|
+
<LetterheadHeading className={css.card.heading}>
|
|
39
|
+
{title}
|
|
40
|
+
</LetterheadHeading>
|
|
41
|
+
|
|
42
|
+
{description}
|
|
43
|
+
|
|
44
|
+
<Form store={form} className={css.card.form}>
|
|
45
|
+
<LetterheadSubmitError name={submitName} />
|
|
46
|
+
|
|
47
|
+
<LetterheadSubmitButton>Subscribe</LetterheadSubmitButton>
|
|
48
|
+
|
|
49
|
+
<LetterheadParagraph size="small">
|
|
50
|
+
Using {pledge.email} from your pledge.
|
|
51
|
+
</LetterheadParagraph>
|
|
52
|
+
</Form>
|
|
53
|
+
</Letterhead>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function InfoView(props: ViewContent) {
|
|
58
|
+
return (
|
|
59
|
+
<Letterhead textAlign="center">
|
|
60
|
+
<LetterheadHeading className={css.card.heading}>
|
|
61
|
+
{props.title}
|
|
62
|
+
</LetterheadHeading>
|
|
63
|
+
|
|
64
|
+
{props.description}
|
|
65
|
+
</Letterhead>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type SubscribeStep =
|
|
70
|
+
| "PREVIOUSLY_SUBSCRIBED"
|
|
71
|
+
| "SUBSCRIBE"
|
|
72
|
+
| "SUBSCRIBE_SUCCESS";
|
|
73
|
+
|
|
74
|
+
type ViewContent = {
|
|
75
|
+
title: ReactNode;
|
|
76
|
+
description: ReactNode;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export function SubscribeCard(props: {
|
|
80
|
+
pledge: Pledge;
|
|
81
|
+
content: Record<SubscribeStep, ViewContent>;
|
|
82
|
+
}) {
|
|
83
|
+
const { pledge } = props;
|
|
84
|
+
|
|
85
|
+
const [step, setStep] = useState<SubscribeStep>(
|
|
86
|
+
pledge.contactSubscribed ? "PREVIOUSLY_SUBSCRIBED" : "SUBSCRIBE",
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const content = props.content[step];
|
|
90
|
+
|
|
91
|
+
switch (step) {
|
|
92
|
+
case "SUBSCRIBE": {
|
|
93
|
+
return (
|
|
94
|
+
<SubscribeView
|
|
95
|
+
{...props}
|
|
96
|
+
{...content}
|
|
97
|
+
onSuccess={() => void setStep("SUBSCRIBE_SUCCESS")}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case "PREVIOUSLY_SUBSCRIBED":
|
|
103
|
+
case "SUBSCRIBE_SUCCESS": {
|
|
104
|
+
return <InfoView {...content} />;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { style } from "@vanilla-extract/css";
|
|
2
|
+
|
|
3
|
+
export const card = {
|
|
4
|
+
heading: style({
|
|
5
|
+
maxInlineSize: "20rem",
|
|
6
|
+
marginInline: "auto",
|
|
7
|
+
}),
|
|
8
|
+
form: style({
|
|
9
|
+
marginBlockStart: "2.5rem",
|
|
10
|
+
display: "flex",
|
|
11
|
+
flexDirection: "column",
|
|
12
|
+
gap: "1rem",
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
@@ -18,7 +18,7 @@ export function appendCopyToText(input: string): string {
|
|
|
18
18
|
// with the copy suffix, but it doesn't contain count.
|
|
19
19
|
const nextCount = !count ? 2 : parseInt(count, 10) + 1;
|
|
20
20
|
|
|
21
|
-
return `${value
|
|
21
|
+
return `${value?.trim()} (Copy ${nextCount})`;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
package/lib/async-op.ts
CHANGED
|
@@ -240,6 +240,14 @@ export function fold<Ops extends readonly AsyncOp<unknown, unknown>[] | []>(
|
|
|
240
240
|
return new Pending() as never;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
export function isAsyncOp(value: unknown): value is AsyncOp<unknown, unknown> {
|
|
244
|
+
return (
|
|
245
|
+
value instanceof Pending ||
|
|
246
|
+
value instanceof Success ||
|
|
247
|
+
value instanceof Failure
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
243
251
|
export type AsyncOp<SuccessValue, FailureValue> =
|
|
244
252
|
| Pending
|
|
245
253
|
| Success<SuccessValue>
|
package/lib/client.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type Infer, mask, object, string, Struct } from "superstruct";
|
|
1
|
+
import { type Infer, mask, object, string, Struct, unknown } from "superstruct";
|
|
2
2
|
import { Failure, Success } from "./async-op.js";
|
|
3
|
-
import { currentUser, sessionInfo } from "./structs.js";
|
|
3
|
+
import { currentUser, redeemedPledge, sessionInfo } from "./structs.js";
|
|
4
4
|
import type { CurrentUser, FailurePayload, SessionInfo } from "./types.js";
|
|
5
5
|
|
|
6
6
|
export class IndieTabletopClient {
|
|
@@ -276,6 +276,22 @@ export class IndieTabletopClient {
|
|
|
276
276
|
return req;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
async getSnapshot<T, S>(
|
|
280
|
+
gameCode: string,
|
|
281
|
+
snapshotId: string,
|
|
282
|
+
struct: Struct<T, S>,
|
|
283
|
+
) {
|
|
284
|
+
return await this.fetch(`/v1/snapshots/${gameCode}/${snapshotId}`, struct);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async createSnapshot(gameCode: string, payload: object) {
|
|
288
|
+
return await this.fetch(
|
|
289
|
+
`/v1/snapshots/${gameCode}`,
|
|
290
|
+
object({ snapshotId: string() }),
|
|
291
|
+
{ method: "POST", json: payload },
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
279
295
|
async getCurrentUser() {
|
|
280
296
|
const result = await this.fetchWithAuth(`/v1/users/me`, currentUser());
|
|
281
297
|
|
|
@@ -286,6 +302,10 @@ export class IndieTabletopClient {
|
|
|
286
302
|
return result;
|
|
287
303
|
}
|
|
288
304
|
|
|
305
|
+
getRuleset(game: string, version: string) {
|
|
306
|
+
return this.fetch(`/v1/rulesets/${game}/${version}`, unknown());
|
|
307
|
+
}
|
|
308
|
+
|
|
289
309
|
/**
|
|
290
310
|
* Uploads a file given S3 presigned config.
|
|
291
311
|
*/
|
|
@@ -316,4 +336,19 @@ export class IndieTabletopClient {
|
|
|
316
336
|
return new Failure({ type: "NETWORK_ERROR" });
|
|
317
337
|
}
|
|
318
338
|
}
|
|
339
|
+
|
|
340
|
+
async redeemPledge(id: string) {
|
|
341
|
+
return await this.fetch(
|
|
342
|
+
`/v1/redemptions/spacegits/pledges/${id}`,
|
|
343
|
+
redeemedPledge(),
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async subscribeToNewsletterByPledgeId(pledgeId: string) {
|
|
348
|
+
return await this.fetch(
|
|
349
|
+
`/v1/newsletters/changelog/subscriptions`,
|
|
350
|
+
object({ message: string() }),
|
|
351
|
+
{ method: "POST", json: { pledgeId, type: "PLEDGE" } },
|
|
352
|
+
);
|
|
353
|
+
}
|
|
319
354
|
}
|
package/lib/groupBy.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function groupBy<T, K extends string>(
|
|
2
|
+
items: T[],
|
|
3
|
+
getKey: (item: T) => K,
|
|
4
|
+
) {
|
|
5
|
+
const groups: Partial<Record<K, T[]>> = {};
|
|
6
|
+
|
|
7
|
+
for (const item of items) {
|
|
8
|
+
const key = getKey(item);
|
|
9
|
+
const group = groups[key];
|
|
10
|
+
if (group) {
|
|
11
|
+
group.push(item);
|
|
12
|
+
} else {
|
|
13
|
+
groups[key] = [item];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Using a Proxy to make sure that even if one of the possible group keys
|
|
18
|
+
// was not included in the provided list, that group will still return
|
|
19
|
+
// an empty list rather than `undefined`.
|
|
20
|
+
return new Proxy(groups, {
|
|
21
|
+
get(target, prop, receiver) {
|
|
22
|
+
return Reflect.get(target, prop, receiver) ?? [];
|
|
23
|
+
},
|
|
24
|
+
}) as Record<K, T[]>;
|
|
25
|
+
}
|
package/lib/ids.ts
ADDED
package/lib/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Components
|
|
2
|
+
export * from "./ClientContext/ClientContext.tsx";
|
|
2
3
|
export * from "./DialogTrigger/index.tsx";
|
|
3
4
|
export * from "./ExternalLink.tsx";
|
|
4
5
|
export * from "./FormSubmitButton.tsx";
|
|
@@ -8,9 +9,12 @@ export * from "./Letterhead/index.tsx";
|
|
|
8
9
|
export * from "./LetterheadForm/index.tsx";
|
|
9
10
|
export * from "./LoadingIndicator.tsx";
|
|
10
11
|
export * from "./ModalDialog/index.tsx";
|
|
12
|
+
export * from "./ReleaseInfo/index.tsx";
|
|
11
13
|
export * from "./ServiceWorkerHandler.tsx";
|
|
14
|
+
export * from "./SubscribeCard/SubscribeCard.tsx";
|
|
12
15
|
|
|
13
16
|
// Hooks
|
|
17
|
+
export * from "./RulesetResolver.ts";
|
|
14
18
|
export * from "./use-async-op.ts";
|
|
15
19
|
export * from "./use-document-background-color.ts";
|
|
16
20
|
export * from "./use-form.ts";
|
|
@@ -26,10 +30,18 @@ export * from "./async-op.ts";
|
|
|
26
30
|
export * from "./caught-value.ts";
|
|
27
31
|
export * from "./class-names.ts";
|
|
28
32
|
export * from "./client.ts";
|
|
33
|
+
export * from "./copyrightRange.ts";
|
|
29
34
|
export * from "./failureMessages.ts";
|
|
35
|
+
export * from "./groupBy.ts";
|
|
36
|
+
export * from "./ids.ts";
|
|
30
37
|
export * from "./media.ts";
|
|
31
38
|
export * from "./random.ts";
|
|
32
39
|
export * from "./sleep.ts";
|
|
33
40
|
export * from "./structs.ts";
|
|
41
|
+
export * from "./typeguards.ts";
|
|
34
42
|
export * from "./types.ts";
|
|
43
|
+
export * from "./unique.ts";
|
|
35
44
|
export * from "./validations.ts";
|
|
45
|
+
|
|
46
|
+
// Other
|
|
47
|
+
export * from "./ModernIDB/index.ts";
|
package/lib/random.ts
CHANGED
|
@@ -5,3 +5,15 @@ export function random(max: number) {
|
|
|
5
5
|
export function randomItem<T>(array: T[]) {
|
|
6
6
|
return array[random(array.length)];
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
export function randomItemOrThrow<T>(array: T[]) {
|
|
10
|
+
const item = array[random(array.length)];
|
|
11
|
+
|
|
12
|
+
if (!item) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
"Could not select a random item from list. Perhaps the list is empty?",
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return item;
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SWRResponse } from "swr";
|
|
2
|
+
import { Failure, Pending } from "../async-op.ts";
|
|
3
|
+
import type { FailurePayload } from "../types.ts";
|
|
4
|
+
|
|
5
|
+
export function swrResponseToResult<T>(response: SWRResponse<T, unknown>) {
|
|
6
|
+
const { data, error } = response;
|
|
7
|
+
|
|
8
|
+
if (data !== undefined) {
|
|
9
|
+
return data;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (error !== undefined) {
|
|
13
|
+
console.error(error);
|
|
14
|
+
return new Failure<FailurePayload>({ type: "UNKNOWN_ERROR" });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return new Pending();
|
|
18
|
+
}
|
package/lib/structs.ts
CHANGED
|
@@ -15,3 +15,13 @@ export function sessionInfo() {
|
|
|
15
15
|
createdTs: number(),
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
export function redeemedPledge() {
|
|
20
|
+
return object({
|
|
21
|
+
id: string(),
|
|
22
|
+
downloadUrl: string(),
|
|
23
|
+
downloadUrlExpiresTs: number(),
|
|
24
|
+
contactSubscribed: boolean(),
|
|
25
|
+
email: string(),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkes whether value is `null` or `undefined`.
|
|
3
|
+
*
|
|
4
|
+
* In some cases, this is preferrable to using `if (!value)`, as that will
|
|
5
|
+
* include `""`, `0`, and `false`, which is not always desirable.
|
|
6
|
+
*
|
|
7
|
+
* This function uses the same semantics like the nullish coalescing operators
|
|
8
|
+
* like `??` and `??=`.
|
|
9
|
+
*/
|
|
10
|
+
export function isNullish(value: unknown): value is null | undefined {
|
|
11
|
+
return value === null || value === undefined;
|
|
12
|
+
}
|
package/lib/types.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Infer } from "superstruct";
|
|
2
|
-
import { currentUser, sessionInfo } from "./structs.js";
|
|
2
|
+
import { currentUser, redeemedPledge, sessionInfo } from "./structs.js";
|
|
3
3
|
|
|
4
4
|
export type CurrentUser = Infer<ReturnType<typeof currentUser>>;
|
|
5
5
|
|
|
6
6
|
export type SessionInfo = Infer<ReturnType<typeof sessionInfo>>;
|
|
7
7
|
|
|
8
|
+
export type RedeemedPledge = Infer<ReturnType<typeof redeemedPledge>>;
|
|
9
|
+
|
|
8
10
|
export type FailurePayload =
|
|
9
11
|
| {
|
|
10
12
|
type: "API_ERROR";
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
});
|
package/lib/unique.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type UniqueKey = string | number;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns an array of unique items, determining uniqueness via the getKey
|
|
5
|
+
* function.
|
|
6
|
+
*
|
|
7
|
+
* Note that the first unique item is returned, all others are omitted
|
|
8
|
+
* (assuming that they are unique, so it shouldn't matter).
|
|
9
|
+
*/
|
|
10
|
+
export function uniqueBy<T>(items: T[], getKey: (item: T) => UniqueKey): T[] {
|
|
11
|
+
const seen = new Set<UniqueKey>();
|
|
12
|
+
const returnItems: T[] = [];
|
|
13
|
+
|
|
14
|
+
for (const item of items) {
|
|
15
|
+
const uniqueKey = getKey(item);
|
|
16
|
+
|
|
17
|
+
if (!seen.has(uniqueKey)) {
|
|
18
|
+
returnItems.push(item);
|
|
19
|
+
seen.add(uniqueKey);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return returnItems;
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indietabletop/appkit",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0-0",
|
|
4
4
|
"description": "A collection of modules used in apps built by Indie Tabletop Club",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"build": "tsc",
|
|
10
10
|
"dev": "tsc --watch",
|
|
11
11
|
"test": "vitest",
|
|
12
|
-
"storybook": "storybook dev -p
|
|
12
|
+
"storybook": "storybook dev -p 6000"
|
|
13
13
|
},
|
|
14
14
|
"exports": {
|
|
15
15
|
".": "./lib/index.ts",
|
|
@@ -25,11 +25,13 @@
|
|
|
25
25
|
"react": "^18.0.0 || ^19.0.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@storybook/addon-docs": "^9.
|
|
29
|
-
"@storybook/react-vite": "^9.
|
|
28
|
+
"@storybook/addon-docs": "^9.1.6",
|
|
29
|
+
"@storybook/react-vite": "^9.1.6",
|
|
30
30
|
"@types/react": "^19.1.8",
|
|
31
|
+
"msw": "^2.11.2",
|
|
32
|
+
"msw-storybook-addon": "^2.0.5",
|
|
31
33
|
"np": "^10.1.0",
|
|
32
|
-
"storybook": "^9.
|
|
34
|
+
"storybook": "^9.1.6",
|
|
33
35
|
"typescript": "^5.8.2",
|
|
34
36
|
"vite": "^6.3.5",
|
|
35
37
|
"vitest": "^3.0.5"
|
|
@@ -41,6 +43,14 @@
|
|
|
41
43
|
"@vanilla-extract/dynamic": "^2.1.3",
|
|
42
44
|
"@vanilla-extract/recipes": "^0.5.7",
|
|
43
45
|
"@vanilla-extract/sprinkles": "^1.6.3",
|
|
44
|
-
"
|
|
46
|
+
"nanoid": "^5.1.5",
|
|
47
|
+
"superstruct": "^2.0.2",
|
|
48
|
+
"swr": "^2.3.6",
|
|
49
|
+
"wouter": "^3.7.1"
|
|
50
|
+
},
|
|
51
|
+
"msw": {
|
|
52
|
+
"workerDirectory": [
|
|
53
|
+
"public"
|
|
54
|
+
]
|
|
45
55
|
}
|
|
46
56
|
}
|