@indietabletop/appkit 3.6.0-1 → 3.6.0-3
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 +54 -0
- package/lib/HistoryState.ts +21 -0
- package/lib/Letterhead/style.css.ts +2 -0
- package/lib/LetterheadForm/index.tsx +53 -0
- package/lib/LetterheadForm/style.css.ts +8 -0
- package/lib/QRCode/QRCode.stories.tsx +47 -0
- package/lib/QRCode/QRCode.tsx +49 -0
- package/lib/QRCode/style.css.ts +19 -0
- package/lib/ShareButton/ShareButton.tsx +141 -0
- package/lib/SubscribeCard/LetterheadInfoCard.tsx +23 -0
- package/lib/SubscribeCard/SubscribeByEmailCard.stories.tsx +83 -0
- package/lib/SubscribeCard/SubscribeByEmailCard.tsx +177 -0
- package/lib/SubscribeCard/SubscribeCard.stories.tsx +27 -23
- package/lib/SubscribeCard/SubscribeCard.tsx +22 -17
- package/lib/Title/index.tsx +7 -2
- package/lib/account/AccountIssueView.tsx +40 -0
- package/lib/account/AlreadyLoggedInView.tsx +44 -0
- package/lib/account/CurrentUserFetcher.stories.tsx +339 -0
- package/lib/account/CurrentUserFetcher.tsx +119 -0
- package/lib/account/FailureFallbackView.tsx +36 -0
- package/lib/account/JoinPage.stories.tsx +270 -0
- package/lib/account/JoinPage.tsx +288 -0
- package/lib/account/LoadingView.tsx +14 -0
- package/lib/account/LoginPage.stories.tsx +318 -0
- package/lib/account/LoginPage.tsx +138 -0
- package/lib/account/LoginView.tsx +136 -0
- package/lib/account/NoConnectionView.tsx +34 -0
- package/lib/account/PasswordResetPage.stories.tsx +250 -0
- package/lib/account/PasswordResetPage.tsx +291 -0
- package/lib/account/UserMismatchView.tsx +61 -0
- package/lib/account/VerifyPage.tsx +217 -0
- package/lib/account/style.css.ts +57 -0
- package/lib/account/types.ts +9 -0
- package/lib/account/useCurrentUserResult.tsx +38 -0
- package/lib/class-names.ts +1 -1
- package/lib/client.ts +54 -7
- package/lib/globals.css.ts +5 -0
- package/lib/index.ts +11 -1
- package/lib/useEnsureValue.ts +31 -0
- package/package.json +3 -2
- package/lib/ClientContext/ClientContext.tsx +0 -25
- package/lib/LoginPage/LoginPage.stories.tsx +0 -107
- package/lib/LoginPage/LoginPage.tsx +0 -204
- package/lib/LoginPage/style.css.ts +0 -17
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { Button, Form } from "@ariakit/react";
|
|
2
|
+
import { useState, type Dispatch, type SetStateAction } from "react";
|
|
3
|
+
import { Link } from "wouter";
|
|
4
|
+
import { useAppConfig, useClient } from "../AppConfig/AppConfig.tsx";
|
|
5
|
+
import { getSubmitFailureMessage } from "../failureMessages.ts";
|
|
6
|
+
import {
|
|
7
|
+
Letterhead,
|
|
8
|
+
LetterheadHeading,
|
|
9
|
+
LetterheadParagraph,
|
|
10
|
+
LetterheadSubmitButton,
|
|
11
|
+
} from "../Letterhead/index.tsx";
|
|
12
|
+
import { button } from "../Letterhead/style.css.ts";
|
|
13
|
+
import {
|
|
14
|
+
LetterheadFormActions,
|
|
15
|
+
LetterheadHeader,
|
|
16
|
+
LetterheadSubmitError,
|
|
17
|
+
LetterheadTextField,
|
|
18
|
+
} from "../LetterheadForm/index.tsx";
|
|
19
|
+
import type { CurrentUser } from "../types.ts";
|
|
20
|
+
import { useForm } from "../use-form.ts";
|
|
21
|
+
import type { EventHandler } from "./types.ts";
|
|
22
|
+
|
|
23
|
+
type SetStep = Dispatch<SetStateAction<Steps>>;
|
|
24
|
+
|
|
25
|
+
function InitialStep(props: { setStep: SetStep; currentUser: CurrentUser }) {
|
|
26
|
+
const client = useClient();
|
|
27
|
+
const { email } = props.currentUser;
|
|
28
|
+
const { form, submitName } = useForm({
|
|
29
|
+
defaultValues: {},
|
|
30
|
+
async onSubmit() {
|
|
31
|
+
const op = await client.requestUserVerification();
|
|
32
|
+
return op.mapFailure(getSubmitFailureMessage);
|
|
33
|
+
},
|
|
34
|
+
onSuccess(value) {
|
|
35
|
+
props.setStep({ type: "SUBMIT_CODE", tokenId: value.tokenId });
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Form store={form}>
|
|
41
|
+
<Letterhead>
|
|
42
|
+
<LetterheadHeader>
|
|
43
|
+
<LetterheadHeading>Verify account</LetterheadHeading>
|
|
44
|
+
<LetterheadParagraph>
|
|
45
|
+
Your account is currently not verified. We will send a one-time code
|
|
46
|
+
to your email ({email}) to complete the verification.
|
|
47
|
+
</LetterheadParagraph>
|
|
48
|
+
</LetterheadHeader>
|
|
49
|
+
|
|
50
|
+
<LetterheadFormActions>
|
|
51
|
+
<LetterheadSubmitError name={submitName} />
|
|
52
|
+
<LetterheadSubmitButton>Send code</LetterheadSubmitButton>
|
|
53
|
+
</LetterheadFormActions>
|
|
54
|
+
</Letterhead>
|
|
55
|
+
</Form>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function SubmitCodeStep(props: {
|
|
60
|
+
tokenId: string;
|
|
61
|
+
setStep: SetStep;
|
|
62
|
+
currentUser: CurrentUser;
|
|
63
|
+
}) {
|
|
64
|
+
const client = useClient();
|
|
65
|
+
const { email } = props.currentUser;
|
|
66
|
+
const { form, submitName } = useForm({
|
|
67
|
+
defaultValues: {
|
|
68
|
+
code: "",
|
|
69
|
+
},
|
|
70
|
+
async onSubmit({ values }) {
|
|
71
|
+
const op = await client.verifyUser({
|
|
72
|
+
...values,
|
|
73
|
+
tokenId: props.tokenId,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return op.mapFailure((failure) => {
|
|
77
|
+
return getSubmitFailureMessage(failure, {
|
|
78
|
+
404: "🚫 This code is incorrect or expired. Please try again.",
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
onSuccess() {
|
|
83
|
+
props.setStep({ type: "SUCCESS" });
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<Form store={form}>
|
|
89
|
+
<Letterhead>
|
|
90
|
+
<LetterheadHeader>
|
|
91
|
+
<LetterheadHeading>Verify Account</LetterheadHeading>
|
|
92
|
+
<LetterheadParagraph>
|
|
93
|
+
We've sent a one-time code to {email}. Please, enter the code in the
|
|
94
|
+
field below to complete verification.
|
|
95
|
+
</LetterheadParagraph>
|
|
96
|
+
</LetterheadHeader>
|
|
97
|
+
|
|
98
|
+
<LetterheadTextField
|
|
99
|
+
name={form.names.code}
|
|
100
|
+
label="Code"
|
|
101
|
+
placeholder="E.g. 123123"
|
|
102
|
+
autoComplete="one-time-code"
|
|
103
|
+
required
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
<LetterheadFormActions>
|
|
107
|
+
<LetterheadSubmitError name={submitName} />
|
|
108
|
+
<LetterheadSubmitButton>Verify</LetterheadSubmitButton>
|
|
109
|
+
</LetterheadFormActions>
|
|
110
|
+
</Letterhead>
|
|
111
|
+
</Form>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function SuccessStep(props: { onClose?: EventHandler }) {
|
|
116
|
+
const { onClose } = props;
|
|
117
|
+
const { hrefs } = useAppConfig();
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<Letterhead>
|
|
121
|
+
<LetterheadHeader>
|
|
122
|
+
<LetterheadHeading>Success!</LetterheadHeading>
|
|
123
|
+
<LetterheadParagraph>
|
|
124
|
+
Your Indie Tabletop Club account has been verified. Yay!
|
|
125
|
+
</LetterheadParagraph>
|
|
126
|
+
</LetterheadHeader>
|
|
127
|
+
|
|
128
|
+
{onClose ? (
|
|
129
|
+
<Button onClick={() => onClose()} className={button()}>
|
|
130
|
+
Done
|
|
131
|
+
</Button>
|
|
132
|
+
) : (
|
|
133
|
+
<Link href={hrefs.dashboard()} className={button()}>
|
|
134
|
+
Go to dashboard
|
|
135
|
+
</Link>
|
|
136
|
+
)}
|
|
137
|
+
</Letterhead>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
type Steps =
|
|
142
|
+
| { type: "INITIAL" }
|
|
143
|
+
| { type: "SUBMIT_CODE"; tokenId: string }
|
|
144
|
+
| { type: "SUCCESS" };
|
|
145
|
+
|
|
146
|
+
export function VerifyAccountView(props: {
|
|
147
|
+
currentUser: CurrentUser;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* If provided, will cause the success step to render a close dialog button
|
|
151
|
+
* instead of "Go to Dashboard" button.
|
|
152
|
+
*
|
|
153
|
+
* This is useful if this view is used within a modal dialog context.
|
|
154
|
+
*/
|
|
155
|
+
onClose?: EventHandler;
|
|
156
|
+
}) {
|
|
157
|
+
const { currentUser } = props;
|
|
158
|
+
const [step, setStep] = useState<Steps>({ type: "INITIAL" });
|
|
159
|
+
|
|
160
|
+
switch (step.type) {
|
|
161
|
+
case "INITIAL": {
|
|
162
|
+
return <InitialStep setStep={setStep} currentUser={currentUser} />;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
case "SUBMIT_CODE": {
|
|
166
|
+
return (
|
|
167
|
+
<SubmitCodeStep {...step} setStep={setStep} currentUser={currentUser} />
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
case "SUCCESS": {
|
|
172
|
+
return <SuccessStep {...props} />;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { style } from "@vanilla-extract/css";
|
|
2
|
+
import { Hover, MinWidth } from "../media.ts";
|
|
3
|
+
|
|
4
|
+
export const page = style({
|
|
5
|
+
backgroundColor: "white",
|
|
6
|
+
|
|
7
|
+
"@media": {
|
|
8
|
+
[MinWidth.SMALL]: {
|
|
9
|
+
backgroundColor: "transparent",
|
|
10
|
+
padding: "clamp(1rem, 5vw, 5rem)",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const accountPicker = {
|
|
16
|
+
container: style({
|
|
17
|
+
backgroundColor: "#fafafa",
|
|
18
|
+
borderRadius: "0.5rem",
|
|
19
|
+
}),
|
|
20
|
+
|
|
21
|
+
button: style({
|
|
22
|
+
padding: "1rem 1.5rem",
|
|
23
|
+
textAlign: "start",
|
|
24
|
+
inlineSize: "100%",
|
|
25
|
+
borderRadius: "inherit",
|
|
26
|
+
|
|
27
|
+
"@media": {
|
|
28
|
+
[Hover.HOVER]: {
|
|
29
|
+
transition: "200ms background-color",
|
|
30
|
+
|
|
31
|
+
":hover": {
|
|
32
|
+
backgroundColor: "hsl(from #fafafa h s calc(l - 3))",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
|
|
38
|
+
buttonLabel: style({
|
|
39
|
+
fontWeight: 600,
|
|
40
|
+
fontSize: "1.125rem",
|
|
41
|
+
}),
|
|
42
|
+
|
|
43
|
+
divider: style({
|
|
44
|
+
marginInline: "1.5rem",
|
|
45
|
+
backgroundColor: "#e2e2e2",
|
|
46
|
+
blockSize: "1px",
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const loadingView = {
|
|
51
|
+
container: style({
|
|
52
|
+
display: "flex",
|
|
53
|
+
justifyContent: "center",
|
|
54
|
+
alignItems: "center",
|
|
55
|
+
height: "12rem",
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useClient } from "../AppConfig/AppConfig.tsx";
|
|
3
|
+
import { type Failure, Pending, type Success } from "../async-op.ts";
|
|
4
|
+
import type { CurrentUser, FailurePayload } from "../types.ts";
|
|
5
|
+
|
|
6
|
+
export function useCurrentUserResult(options?: {
|
|
7
|
+
/**
|
|
8
|
+
* Optionally disable immediate fetch action.
|
|
9
|
+
*
|
|
10
|
+
* @default true
|
|
11
|
+
*/
|
|
12
|
+
performFetch?: boolean;
|
|
13
|
+
}) {
|
|
14
|
+
const { performFetch = true } = options ?? {};
|
|
15
|
+
const client = useClient();
|
|
16
|
+
const [latestAttemptTs, setLatestAttemptTs] = useState(Date.now());
|
|
17
|
+
|
|
18
|
+
const [result, setResult] = useState<
|
|
19
|
+
Success<CurrentUser> | Failure<FailurePayload> | Pending
|
|
20
|
+
>(new Pending());
|
|
21
|
+
|
|
22
|
+
// We are intentionally not using SWR in this case, as we don't want to deal
|
|
23
|
+
// with caching or any of that business.
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (performFetch) {
|
|
26
|
+
client.getCurrentUser().then((result) => setResult(result));
|
|
27
|
+
}
|
|
28
|
+
}, [client, latestAttemptTs, performFetch]);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
result,
|
|
32
|
+
latestAttemptTs,
|
|
33
|
+
reload: () => {
|
|
34
|
+
setResult(new Pending());
|
|
35
|
+
setLatestAttemptTs(Date.now());
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
package/lib/class-names.ts
CHANGED
|
@@ -23,7 +23,7 @@ export function classNames(...classNames: ClassName[]) {
|
|
|
23
23
|
* be filtered out.
|
|
24
24
|
*
|
|
25
25
|
* @example
|
|
26
|
-
* <h1 {...
|
|
26
|
+
* <h1 {...cx(props, 'heading', 'bold')}>Hello world!</h1>
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
export function cx(...cns: ClassName[]) {
|
package/lib/client.ts
CHANGED
|
@@ -3,6 +3,15 @@ import { Failure, Success } from "./async-op.js";
|
|
|
3
3
|
import { currentUser, redeemedPledge, sessionInfo } from "./structs.js";
|
|
4
4
|
import type { CurrentUser, FailurePayload, SessionInfo } from "./types.js";
|
|
5
5
|
|
|
6
|
+
const logLevelToInt = {
|
|
7
|
+
off: 0,
|
|
8
|
+
error: 1,
|
|
9
|
+
warn: 2,
|
|
10
|
+
info: 3,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type LogLevel = keyof typeof logLevelToInt;
|
|
14
|
+
|
|
6
15
|
export class IndieTabletopClient {
|
|
7
16
|
origin: string;
|
|
8
17
|
private onCurrentUser?: (currentUser: CurrentUser) => void;
|
|
@@ -11,6 +20,7 @@ export class IndieTabletopClient {
|
|
|
11
20
|
private refreshTokenPromise?: Promise<
|
|
12
21
|
Success<{ sessionInfo: SessionInfo }> | Failure<FailurePayload>
|
|
13
22
|
>;
|
|
23
|
+
private maxLogLevel: number;
|
|
14
24
|
|
|
15
25
|
constructor(props: {
|
|
16
26
|
apiOrigin: string;
|
|
@@ -31,11 +41,28 @@ export class IndieTabletopClient {
|
|
|
31
41
|
* Runs when token refresh is attempted, but fails due to 401 error.
|
|
32
42
|
*/
|
|
33
43
|
onSessionExpired?: () => void;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Controls how much to log to the console.
|
|
47
|
+
*
|
|
48
|
+
* This is useful e.g. in Storybook, where errors are reported by MSW, and
|
|
49
|
+
* we don't want to pollute the console with duplicate data.
|
|
50
|
+
*
|
|
51
|
+
* @default 'info'
|
|
52
|
+
*/
|
|
53
|
+
logLevel?: LogLevel;
|
|
34
54
|
}) {
|
|
35
55
|
this.origin = props.apiOrigin;
|
|
36
56
|
this.onCurrentUser = props.onCurrentUser;
|
|
37
57
|
this.onSessionInfo = props.onSessionInfo;
|
|
38
58
|
this.onSessionExpired = props.onSessionExpired;
|
|
59
|
+
this.maxLogLevel = props.logLevel ? logLevelToInt[props.logLevel] : 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private log(level: Exclude<LogLevel, "off">, ...messages: unknown[]) {
|
|
63
|
+
if (logLevelToInt[level] <= this.maxLogLevel) {
|
|
64
|
+
console[level](...messages);
|
|
65
|
+
}
|
|
39
66
|
}
|
|
40
67
|
|
|
41
68
|
protected async fetch<T, S>(
|
|
@@ -63,7 +90,6 @@ export class IndieTabletopClient {
|
|
|
63
90
|
});
|
|
64
91
|
|
|
65
92
|
if (!res.ok) {
|
|
66
|
-
console.error(res);
|
|
67
93
|
return new Failure({ type: "API_ERROR", code: res.status });
|
|
68
94
|
}
|
|
69
95
|
|
|
@@ -71,12 +97,12 @@ export class IndieTabletopClient {
|
|
|
71
97
|
const data = mask(await res.json(), struct);
|
|
72
98
|
return new Success(data);
|
|
73
99
|
} catch (error) {
|
|
74
|
-
|
|
100
|
+
this.log("error", error);
|
|
75
101
|
|
|
76
102
|
return new Failure({ type: "VALIDATION_ERROR" });
|
|
77
103
|
}
|
|
78
104
|
} catch (error) {
|
|
79
|
-
|
|
105
|
+
this.log("error", error);
|
|
80
106
|
|
|
81
107
|
if (error instanceof Error) {
|
|
82
108
|
return new Failure({ type: "NETWORK_ERROR" });
|
|
@@ -101,15 +127,15 @@ export class IndieTabletopClient {
|
|
|
101
127
|
}
|
|
102
128
|
|
|
103
129
|
if (op.failure.type === "API_ERROR" && op.failure.code === 401) {
|
|
104
|
-
|
|
130
|
+
this.log("info", "API request failed with error 401. Refreshing tokens.");
|
|
105
131
|
|
|
106
132
|
const refreshOp = await this.refreshTokens();
|
|
107
133
|
|
|
108
134
|
if (refreshOp.isSuccess) {
|
|
109
|
-
|
|
135
|
+
this.log("info", "Tokens refreshed. Retrying request.");
|
|
110
136
|
return await this.fetch(path, struct, init);
|
|
111
137
|
} else {
|
|
112
|
-
|
|
138
|
+
this.log("info", "Could not refresh tokens.");
|
|
113
139
|
}
|
|
114
140
|
}
|
|
115
141
|
|
|
@@ -189,7 +215,7 @@ export class IndieTabletopClient {
|
|
|
189
215
|
const ongoingRequest = this.refreshTokenPromise;
|
|
190
216
|
|
|
191
217
|
if (ongoingRequest) {
|
|
192
|
-
|
|
218
|
+
this.log("info", "Token refresh ongoing. Reusing existing promise.");
|
|
193
219
|
return await ongoingRequest;
|
|
194
220
|
}
|
|
195
221
|
|
|
@@ -354,4 +380,25 @@ export class IndieTabletopClient {
|
|
|
354
380
|
{ method: "POST", json: { pledgeId, type: "PLEDGE" } },
|
|
355
381
|
);
|
|
356
382
|
}
|
|
383
|
+
|
|
384
|
+
async subscribeToNewsletterByEmail(newsletterCode: string, email: string) {
|
|
385
|
+
return await this.fetch(
|
|
386
|
+
`/v1/newsletters/${newsletterCode}/subscriptions`,
|
|
387
|
+
object({ message: string(), tokenId: string() }),
|
|
388
|
+
{ method: "POST", json: { type: "EMAIL", email } },
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async confirmNewsletterSignup(
|
|
393
|
+
newsletterCode: string,
|
|
394
|
+
tokenId: string,
|
|
395
|
+
plaintextCode: string,
|
|
396
|
+
) {
|
|
397
|
+
const queryParams = new URLSearchParams({ plaintextCode });
|
|
398
|
+
return await this.fetch(
|
|
399
|
+
`/v1/newsletters/${newsletterCode}/newsletter-signup-tokens/${tokenId}?${queryParams}`,
|
|
400
|
+
object({ message: string() }),
|
|
401
|
+
{ method: "PUT" },
|
|
402
|
+
);
|
|
403
|
+
}
|
|
357
404
|
}
|
package/lib/globals.css.ts
CHANGED
|
@@ -3,7 +3,12 @@ import { globalStyle } from "@vanilla-extract/css";
|
|
|
3
3
|
// Apply global vars
|
|
4
4
|
import "./vars.css.ts";
|
|
5
5
|
|
|
6
|
+
const futureCss = {
|
|
7
|
+
interpolateSize: "allow-keywords",
|
|
8
|
+
};
|
|
9
|
+
|
|
6
10
|
globalStyle(":root", {
|
|
11
|
+
...futureCss,
|
|
7
12
|
fontSynthesis: "none",
|
|
8
13
|
textRendering: "optimizeLegibility",
|
|
9
14
|
WebkitFontSmoothing: "antialiased",
|
package/lib/index.ts
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
// Components
|
|
2
|
-
export * from "./
|
|
2
|
+
export * from "./account/CurrentUserFetcher.tsx";
|
|
3
|
+
export * from "./account/JoinPage.tsx";
|
|
4
|
+
export * from "./account/LoginPage.tsx";
|
|
5
|
+
export * from "./account/PasswordResetPage.tsx";
|
|
6
|
+
export * from "./AppConfig/AppConfig.tsx";
|
|
3
7
|
export * from "./DialogTrigger/index.tsx";
|
|
4
8
|
export * from "./ExternalLink.tsx";
|
|
5
9
|
export * from "./FormSubmitButton.tsx";
|
|
6
10
|
export * from "./FullscreenDismissBlocker.tsx";
|
|
11
|
+
export * from "./IndieTabletopClubLogo.tsx";
|
|
7
12
|
export * from "./IndieTabletopClubSymbol.tsx";
|
|
8
13
|
export * from "./Letterhead/index.tsx";
|
|
9
14
|
export * from "./LetterheadForm/index.tsx";
|
|
10
15
|
export * from "./LoadingIndicator.tsx";
|
|
11
16
|
export * from "./ModalDialog/index.tsx";
|
|
17
|
+
export * from "./QRCode/QRCode.tsx";
|
|
12
18
|
export * from "./ReleaseInfo/index.tsx";
|
|
13
19
|
export * from "./ServiceWorkerHandler.tsx";
|
|
20
|
+
export * from "./ShareButton/ShareButton.tsx";
|
|
21
|
+
export * from "./SubscribeCard/SubscribeByEmailCard.tsx";
|
|
14
22
|
export * from "./SubscribeCard/SubscribeCard.tsx";
|
|
15
23
|
|
|
16
24
|
// Hooks
|
|
@@ -22,6 +30,7 @@ export * from "./use-is-installed.ts";
|
|
|
22
30
|
export * from "./use-media-query.ts";
|
|
23
31
|
export * from "./use-reverting-state.ts";
|
|
24
32
|
export * from "./use-scroll-restoration.ts";
|
|
33
|
+
export * from "./useEnsureValue.ts";
|
|
25
34
|
export * from "./useIsVisible.ts";
|
|
26
35
|
|
|
27
36
|
// Utils
|
|
@@ -33,6 +42,7 @@ export * from "./client.ts";
|
|
|
33
42
|
export * from "./copyrightRange.ts";
|
|
34
43
|
export * from "./failureMessages.ts";
|
|
35
44
|
export * from "./groupBy.ts";
|
|
45
|
+
export * from "./HistoryState.ts";
|
|
36
46
|
export * from "./ids.ts";
|
|
37
47
|
export * from "./media.ts";
|
|
38
48
|
export * from "./random.ts";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Failure, Pending, Success, type AsyncOp } from "./async-op.ts";
|
|
3
|
+
import { caughtValueToString } from "./caught-value.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checks if the provided value is non-nullish, otherwise calls provided
|
|
7
|
+
* getValue function to obtain the value (presumably from some remote location).
|
|
8
|
+
*/
|
|
9
|
+
export function useEnsureValue<T>(
|
|
10
|
+
value: T | null | undefined,
|
|
11
|
+
getValue: () => Promise<Success<T> | Failure<string>>,
|
|
12
|
+
) {
|
|
13
|
+
const [result, setShareLink] = useState<AsyncOp<T, string>>(
|
|
14
|
+
value != null ? new Success(value) : new Pending(),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (value == null) {
|
|
19
|
+
getValue().then(
|
|
20
|
+
(success) => {
|
|
21
|
+
setShareLink(success);
|
|
22
|
+
},
|
|
23
|
+
(error: unknown) => {
|
|
24
|
+
setShareLink(new Failure(caughtValueToString(error)));
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}, [value, getValue]);
|
|
29
|
+
|
|
30
|
+
return result;
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indietabletop/appkit",
|
|
3
|
-
"version": "3.6.0-
|
|
3
|
+
"version": "3.6.0-3",
|
|
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
|
|
12
|
+
"storybook": "storybook dev"
|
|
13
13
|
},
|
|
14
14
|
"exports": {
|
|
15
15
|
".": "./lib/index.ts",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@storybook/addon-docs": "^9.1.6",
|
|
29
|
+
"@storybook/addon-links": "^9.1.7",
|
|
29
30
|
"@storybook/react-vite": "^9.1.6",
|
|
30
31
|
"@types/react": "^19.1.8",
|
|
31
32
|
"msw": "^2.11.2",
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { createContext, type ReactNode, useContext } from "react";
|
|
2
|
-
import type { IndieTabletopClient } from "../client.ts";
|
|
3
|
-
|
|
4
|
-
const ClientContext = createContext<IndieTabletopClient | null>(null);
|
|
5
|
-
|
|
6
|
-
export function ClientProvider(props: {
|
|
7
|
-
client: IndieTabletopClient;
|
|
8
|
-
children: ReactNode;
|
|
9
|
-
}) {
|
|
10
|
-
const { client, children } = props;
|
|
11
|
-
return <ClientContext value={client}>{children}</ClientContext>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function useClient() {
|
|
15
|
-
const client = useContext(ClientContext);
|
|
16
|
-
|
|
17
|
-
if (!client) {
|
|
18
|
-
throw new Error(
|
|
19
|
-
`Attempting to retrieve ITC client, but none was found within context. ` +
|
|
20
|
-
`Make sure that ClientProvider is used in the component hierarchy.`,
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return client;
|
|
25
|
-
}
|