@indietabletop/appkit 3.6.0-2 → 4.0.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/AppConfig/AppConfig.tsx +48 -0
- package/lib/AuthCard/AuthCard.stories.ts +38 -0
- package/lib/AuthCard/AuthCard.tsx +64 -0
- package/lib/AuthCard/style.css.ts +49 -0
- package/lib/DialogTrigger/index.tsx +2 -2
- package/lib/DocumentTitle/DocumentTitle.tsx +9 -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/LoadingIndicator.tsx +1 -0
- package/lib/MiddotSeparated/MiddotSeparated.stories.ts +32 -0
- package/lib/MiddotSeparated/MiddotSeparated.tsx +24 -0
- package/lib/MiddotSeparated/style.css.ts +10 -0
- package/lib/ModernIDB/bindings/factory.tsx +8 -3
- 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 +17 -16
- package/lib/account/AccountIssueView.tsx +40 -0
- package/lib/account/AlreadyLoggedInView.tsx +44 -0
- package/lib/account/CurrentUserFetcher.stories.tsx +325 -0
- package/lib/account/CurrentUserFetcher.tsx +133 -0
- package/lib/account/FailureFallbackView.tsx +36 -0
- package/lib/account/JoinCard.stories.tsx +264 -0
- package/lib/account/JoinCard.tsx +291 -0
- package/lib/account/LoadingView.tsx +14 -0
- package/lib/account/LoginCard.stories.tsx +316 -0
- package/lib/account/LoginCard.tsx +141 -0
- package/lib/account/LoginView.tsx +136 -0
- package/lib/account/NoConnectionView.tsx +34 -0
- package/lib/account/PasswordResetCard.stories.tsx +247 -0
- package/lib/account/PasswordResetCard.tsx +296 -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 +9 -0
- package/lib/hrefs.ts +48 -0
- package/lib/idToDate.ts +8 -0
- package/lib/index.ts +19 -1
- package/lib/mailto.test.ts +66 -0
- package/lib/mailto.ts +40 -0
- package/lib/types.ts +17 -0
- package/lib/useEnsureValue.ts +31 -0
- package/lib/utm.ts +89 -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
- package/lib/Title/index.tsx +0 -4
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Button, type ButtonProps } from "@ariakit/react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { useRevertingState } from "../use-reverting-state.ts";
|
|
4
|
+
|
|
5
|
+
type AriakitButtonProps = Omit<
|
|
6
|
+
ButtonProps,
|
|
7
|
+
"children" | "onClick" | "disabled" | "value"
|
|
8
|
+
>;
|
|
9
|
+
|
|
10
|
+
export type CopyToClipboardButtonProps = AriakitButtonProps & {
|
|
11
|
+
value: string | null;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
revertAfterMs: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function CopyToClipboardButton(props: CopyToClipboardButtonProps) {
|
|
17
|
+
const { children, value, revertAfterMs, ...buttonProps } = props;
|
|
18
|
+
const [copiedTimestamp, setCopiedTimestamp] = useRevertingState<
|
|
19
|
+
number | null
|
|
20
|
+
>(null, revertAfterMs);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Button
|
|
24
|
+
{...buttonProps}
|
|
25
|
+
disabled={!value}
|
|
26
|
+
onClick={async () => {
|
|
27
|
+
if (value) {
|
|
28
|
+
await navigator.clipboard.writeText(value);
|
|
29
|
+
setCopiedTimestamp(Date.now());
|
|
30
|
+
}
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
{copiedTimestamp ? "Copied!" : children}
|
|
34
|
+
</Button>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type WebShareButtonProps = AriakitButtonProps & {
|
|
39
|
+
children: ReactNode;
|
|
40
|
+
shareData: ShareData | null;
|
|
41
|
+
revertAfterMs: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function WebShareButton(props: WebShareButtonProps) {
|
|
45
|
+
const { shareData, children, revertAfterMs, ...buttonProps } = props;
|
|
46
|
+
|
|
47
|
+
const [copiedTimestamp, setCopiedTimestamp] = useRevertingState<
|
|
48
|
+
number | null
|
|
49
|
+
>(null, revertAfterMs);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Button
|
|
53
|
+
{...buttonProps}
|
|
54
|
+
disabled={!shareData}
|
|
55
|
+
onClick={async () => {
|
|
56
|
+
if (shareData) {
|
|
57
|
+
try {
|
|
58
|
+
await navigator.share(shareData);
|
|
59
|
+
setCopiedTimestamp(Date.now());
|
|
60
|
+
} catch {
|
|
61
|
+
console.info("Not shared");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
{copiedTimestamp ? "Shared!" : children}
|
|
67
|
+
</Button>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type ShareButtonProps = AriakitButtonProps & {
|
|
72
|
+
/**
|
|
73
|
+
* Button label to use when copy to clipboard is used.
|
|
74
|
+
*/
|
|
75
|
+
copyLabel: string;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Button label to use when web share is used.
|
|
79
|
+
*/
|
|
80
|
+
shareLabel: string;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Data to be shared by the button.
|
|
84
|
+
*
|
|
85
|
+
* If using copy to clipboard, only the URL value will be used.
|
|
86
|
+
*/
|
|
87
|
+
shareData: ShareData | null;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* How long should the confirmation message be shown within the button?
|
|
91
|
+
*
|
|
92
|
+
* @default 1500
|
|
93
|
+
*/
|
|
94
|
+
revertAfterMs?: number;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Should web share be used instead of copy to clipboard?
|
|
98
|
+
*
|
|
99
|
+
* By default, web share will be used if the browser supports it.
|
|
100
|
+
*
|
|
101
|
+
* @default !!navigator.share
|
|
102
|
+
*/
|
|
103
|
+
webShare?: boolean;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* An unstyled button that is pre-configured to either copy provided share data
|
|
108
|
+
* to clipboard, or to use web share, depending on which feature is detected
|
|
109
|
+
* in the browser where the button is running.
|
|
110
|
+
*
|
|
111
|
+
* Props that are not consumed by the Share Button itself are passed to the
|
|
112
|
+
* underlying Ariakit Button component.
|
|
113
|
+
*
|
|
114
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share
|
|
115
|
+
*/
|
|
116
|
+
export function ShareButton(props: ShareButtonProps) {
|
|
117
|
+
const {
|
|
118
|
+
copyLabel,
|
|
119
|
+
shareLabel,
|
|
120
|
+
shareData,
|
|
121
|
+
revertAfterMs = 1500,
|
|
122
|
+
webShare = !!navigator.share,
|
|
123
|
+
} = props;
|
|
124
|
+
|
|
125
|
+
if (webShare) {
|
|
126
|
+
return (
|
|
127
|
+
<WebShareButton shareData={shareData} revertAfterMs={revertAfterMs}>
|
|
128
|
+
{shareLabel}
|
|
129
|
+
</WebShareButton>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<CopyToClipboardButton
|
|
135
|
+
revertAfterMs={revertAfterMs}
|
|
136
|
+
value={shareData?.url ?? null}
|
|
137
|
+
>
|
|
138
|
+
{copyLabel}
|
|
139
|
+
</CopyToClipboardButton>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Letterhead,
|
|
3
|
+
LetterheadHeading,
|
|
4
|
+
LetterheadParagraph,
|
|
5
|
+
} from "../Letterhead/index.tsx";
|
|
6
|
+
import * as css from "./style.css.ts";
|
|
7
|
+
import type { ViewContent } from "./SubscribeCard.tsx";
|
|
8
|
+
|
|
9
|
+
export function LetterheadInfoCard(props: ViewContent) {
|
|
10
|
+
return (
|
|
11
|
+
<Letterhead>
|
|
12
|
+
<LetterheadHeading className={css.card.heading}>
|
|
13
|
+
{props.title}
|
|
14
|
+
</LetterheadHeading>
|
|
15
|
+
|
|
16
|
+
{typeof props.description === "string" ? (
|
|
17
|
+
<LetterheadParagraph>{props.description}</LetterheadParagraph>
|
|
18
|
+
) : (
|
|
19
|
+
props.description
|
|
20
|
+
)}
|
|
21
|
+
</Letterhead>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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 { SubscribeByEmailCard } from "./SubscribeByEmailCard.tsx";
|
|
6
|
+
|
|
7
|
+
const pledge = {
|
|
8
|
+
id: "1",
|
|
9
|
+
email: "test@example.com",
|
|
10
|
+
contactSubscribed: false,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const handlers = {
|
|
14
|
+
subscribe() {
|
|
15
|
+
return http.post(
|
|
16
|
+
"http://mock.api/v1/newsletters/changelog/subscriptions",
|
|
17
|
+
async () => {
|
|
18
|
+
await sleep(2000);
|
|
19
|
+
|
|
20
|
+
return HttpResponse.json(
|
|
21
|
+
{ tokenId: "1", message: "Ok" },
|
|
22
|
+
{ status: 201 },
|
|
23
|
+
);
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
},
|
|
27
|
+
confirm() {
|
|
28
|
+
return http.put(
|
|
29
|
+
"http://mock.api/v1/newsletters/changelog/newsletter-signup-tokens/:tokenId",
|
|
30
|
+
async () => {
|
|
31
|
+
await sleep(2000);
|
|
32
|
+
|
|
33
|
+
return HttpResponse.json({ message: "Ok" }, { status: 201 });
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Allows users to subscribe to ITC's newsletter using the email provided
|
|
41
|
+
* in their pledge.
|
|
42
|
+
*/
|
|
43
|
+
const meta = {
|
|
44
|
+
title: "Pages/Subscribe By Email Card",
|
|
45
|
+
component: SubscribeByEmailCard,
|
|
46
|
+
tags: ["autodocs"],
|
|
47
|
+
args: {
|
|
48
|
+
content: {
|
|
49
|
+
SUBSCRIBE: {
|
|
50
|
+
title: "Space Gits on Indie Tabletop Club",
|
|
51
|
+
description: (
|
|
52
|
+
<LetterheadParagraph>
|
|
53
|
+
Space Gits is getting a fancy <strong>gang builder</strong> and{" "}
|
|
54
|
+
<strong>smart rules</strong> in late 2025 in collaboration with
|
|
55
|
+
Indie Tabletop Club. Subscribe to ITC's{" "}
|
|
56
|
+
<em>at‑most‑one‑a‑month</em> newsletter and be among the first to
|
|
57
|
+
gain early access. It's gonna be rad.
|
|
58
|
+
</LetterheadParagraph>
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
SUBSCRIBE_SUCCESS: {
|
|
62
|
+
title: "Subscribed!",
|
|
63
|
+
description:
|
|
64
|
+
"Thank you for being awesome. Once Space Gits is available for " +
|
|
65
|
+
"early access, you will be among the first to know!",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
parameters: {
|
|
70
|
+
msw: {
|
|
71
|
+
handlers: {
|
|
72
|
+
subscribe: handlers.subscribe(),
|
|
73
|
+
confirm: handlers.confirm(),
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
} satisfies Meta<typeof SubscribeByEmailCard>;
|
|
78
|
+
|
|
79
|
+
export default meta;
|
|
80
|
+
|
|
81
|
+
type Story = StoryObj<typeof meta>;
|
|
82
|
+
|
|
83
|
+
export const Default: Story = {};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Form } from "@ariakit/react";
|
|
2
|
+
import {
|
|
3
|
+
getSubmitFailureMessage,
|
|
4
|
+
Letterhead,
|
|
5
|
+
LetterheadFormActions,
|
|
6
|
+
LetterheadHeader,
|
|
7
|
+
LetterheadHeading,
|
|
8
|
+
LetterheadParagraph,
|
|
9
|
+
LetterheadSubmitButton,
|
|
10
|
+
LetterheadSubmitError,
|
|
11
|
+
LetterheadTextField,
|
|
12
|
+
useForm,
|
|
13
|
+
validEmail,
|
|
14
|
+
} from "@indietabletop/appkit";
|
|
15
|
+
import {
|
|
16
|
+
useState,
|
|
17
|
+
type Dispatch,
|
|
18
|
+
type ReactNode,
|
|
19
|
+
type SetStateAction,
|
|
20
|
+
} from "react";
|
|
21
|
+
import { useAppConfig, useClient } from "../AppConfig/AppConfig.tsx";
|
|
22
|
+
import { LetterheadInfoCard } from "./LetterheadInfoCard.tsx";
|
|
23
|
+
import * as css from "./style.css.ts";
|
|
24
|
+
|
|
25
|
+
type SetStep = Dispatch<SetStateAction<SubscribeStep>>;
|
|
26
|
+
|
|
27
|
+
function SubscribeView(
|
|
28
|
+
props: ViewContent & {
|
|
29
|
+
setStep: SetStep;
|
|
30
|
+
},
|
|
31
|
+
) {
|
|
32
|
+
const { placeholders } = useAppConfig();
|
|
33
|
+
const { title, description, setStep } = props;
|
|
34
|
+
|
|
35
|
+
const client = useClient();
|
|
36
|
+
const { form, submitName } = useForm({
|
|
37
|
+
defaultValues: {
|
|
38
|
+
email: "",
|
|
39
|
+
},
|
|
40
|
+
validate: {
|
|
41
|
+
email: validEmail,
|
|
42
|
+
},
|
|
43
|
+
async onSubmit({ values }) {
|
|
44
|
+
const result = await client.subscribeToNewsletterByEmail(
|
|
45
|
+
"changelog",
|
|
46
|
+
values.email,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return result.mapFailure(getSubmitFailureMessage);
|
|
50
|
+
},
|
|
51
|
+
async onSuccess({ tokenId }, { values }) {
|
|
52
|
+
setStep({ type: "SUBMIT_CODE", email: values.email, tokenId });
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Form store={form} resetOnSubmit={false}>
|
|
58
|
+
<Letterhead>
|
|
59
|
+
<LetterheadHeader>
|
|
60
|
+
<LetterheadHeading className={css.card.heading}>
|
|
61
|
+
{title}
|
|
62
|
+
</LetterheadHeading>
|
|
63
|
+
|
|
64
|
+
{description}
|
|
65
|
+
</LetterheadHeader>
|
|
66
|
+
|
|
67
|
+
<LetterheadTextField
|
|
68
|
+
name={form.names.email}
|
|
69
|
+
label="Your email"
|
|
70
|
+
placeholder={placeholders.email}
|
|
71
|
+
autoComplete="email"
|
|
72
|
+
required
|
|
73
|
+
/>
|
|
74
|
+
|
|
75
|
+
<LetterheadFormActions>
|
|
76
|
+
<LetterheadSubmitError name={submitName} />
|
|
77
|
+
<LetterheadSubmitButton>Subscribe</LetterheadSubmitButton>
|
|
78
|
+
</LetterheadFormActions>
|
|
79
|
+
</Letterhead>
|
|
80
|
+
</Form>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function SubmitCodeStep(props: {
|
|
85
|
+
tokenId: string;
|
|
86
|
+
setStep: SetStep;
|
|
87
|
+
email: string;
|
|
88
|
+
}) {
|
|
89
|
+
const { tokenId, email, setStep } = props;
|
|
90
|
+
const client = useClient();
|
|
91
|
+
|
|
92
|
+
const { form, submitName } = useForm({
|
|
93
|
+
defaultValues: {
|
|
94
|
+
code: "",
|
|
95
|
+
},
|
|
96
|
+
async onSubmit({ values }) {
|
|
97
|
+
const op = await client.confirmNewsletterSignup(
|
|
98
|
+
"changelog",
|
|
99
|
+
tokenId,
|
|
100
|
+
values.code,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return op.mapFailure((failure) => {
|
|
104
|
+
return getSubmitFailureMessage(failure, {
|
|
105
|
+
404: "🚫 This code is incorrect or expired. Please try again.",
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
onSuccess() {
|
|
110
|
+
setStep({ type: "SUBSCRIBE_SUCCESS" });
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<Form store={form} resetOnSubmit={false}>
|
|
116
|
+
<Letterhead>
|
|
117
|
+
<LetterheadHeader>
|
|
118
|
+
<LetterheadHeading>Confirm subscription</LetterheadHeading>
|
|
119
|
+
<LetterheadParagraph>
|
|
120
|
+
We've sent a one-time code to <em>{email}</em>. Please, enter the
|
|
121
|
+
code in the field below to confirm your newsletter subscription.
|
|
122
|
+
</LetterheadParagraph>
|
|
123
|
+
</LetterheadHeader>
|
|
124
|
+
|
|
125
|
+
<LetterheadTextField
|
|
126
|
+
name={form.names.code}
|
|
127
|
+
label="Code"
|
|
128
|
+
placeholder="E.g. 123123"
|
|
129
|
+
autoComplete="one-time-code"
|
|
130
|
+
required
|
|
131
|
+
/>
|
|
132
|
+
|
|
133
|
+
<LetterheadFormActions>
|
|
134
|
+
<LetterheadSubmitError name={submitName} />
|
|
135
|
+
<LetterheadSubmitButton>Confirm</LetterheadSubmitButton>
|
|
136
|
+
</LetterheadFormActions>
|
|
137
|
+
</Letterhead>
|
|
138
|
+
</Form>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
type SubscribeStep =
|
|
143
|
+
| { type: "SUBSCRIBE" }
|
|
144
|
+
| { type: "SUBMIT_CODE"; email: string; tokenId: string }
|
|
145
|
+
| { type: "SUBSCRIBE_SUCCESS" };
|
|
146
|
+
|
|
147
|
+
type ViewContent = {
|
|
148
|
+
title: ReactNode;
|
|
149
|
+
description: ReactNode;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export function SubscribeByEmailCard(props: {
|
|
153
|
+
content: Record<Exclude<SubscribeStep["type"], "SUBMIT_CODE">, ViewContent>;
|
|
154
|
+
}) {
|
|
155
|
+
const { content } = props;
|
|
156
|
+
const [step, setStep] = useState<SubscribeStep>({ type: "SUBSCRIBE" });
|
|
157
|
+
|
|
158
|
+
switch (step.type) {
|
|
159
|
+
case "SUBSCRIBE": {
|
|
160
|
+
return <SubscribeView {...content[step.type]} setStep={setStep} />;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
case "SUBMIT_CODE": {
|
|
164
|
+
return (
|
|
165
|
+
<SubmitCodeStep
|
|
166
|
+
email={step.email}
|
|
167
|
+
tokenId={step.tokenId}
|
|
168
|
+
setStep={setStep}
|
|
169
|
+
/>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case "SUBSCRIBE_SUCCESS": {
|
|
174
|
+
return <LetterheadInfoCard {...content[step.type]} />;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -13,7 +13,7 @@ const pledge = {
|
|
|
13
13
|
const subscribeByPledgeId = {
|
|
14
14
|
success() {
|
|
15
15
|
return http.post(
|
|
16
|
-
"http://
|
|
16
|
+
"http://mock.api/v1/newsletters/changelog/subscriptions",
|
|
17
17
|
async () => {
|
|
18
18
|
await sleep(2000);
|
|
19
19
|
|
|
@@ -26,7 +26,7 @@ const subscribeByPledgeId = {
|
|
|
26
26
|
},
|
|
27
27
|
noConnection() {
|
|
28
28
|
return http.post(
|
|
29
|
-
"http://
|
|
29
|
+
"http://mock.api/v1/newsletters/changelog/subscriptions",
|
|
30
30
|
async () => {
|
|
31
31
|
return HttpResponse.error();
|
|
32
32
|
},
|
|
@@ -34,7 +34,7 @@ const subscribeByPledgeId = {
|
|
|
34
34
|
},
|
|
35
35
|
error() {
|
|
36
36
|
return http.post(
|
|
37
|
-
"http://
|
|
37
|
+
"http://mock.api/v1/newsletters/changelog/subscriptions",
|
|
38
38
|
async () => {
|
|
39
39
|
await sleep(2000);
|
|
40
40
|
|
|
@@ -44,6 +44,10 @@ const subscribeByPledgeId = {
|
|
|
44
44
|
},
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Allows users to subscribe to ITC's newsletter using the email provided
|
|
49
|
+
* in their pledge.
|
|
50
|
+
*/
|
|
47
51
|
const meta = {
|
|
48
52
|
title: "Pages/Subscribe Card",
|
|
49
53
|
component: SubscribeCard,
|
|
@@ -65,12 +69,9 @@ const meta = {
|
|
|
65
69
|
},
|
|
66
70
|
SUBSCRIBE_SUCCESS: {
|
|
67
71
|
title: "Subscribed!",
|
|
68
|
-
description:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
access, you will be among the first to know!
|
|
72
|
-
</LetterheadParagraph>
|
|
73
|
-
),
|
|
72
|
+
description:
|
|
73
|
+
"Thank you for being awesome. Once Space Gits is available for " +
|
|
74
|
+
"early access, you will be among the first to know!",
|
|
74
75
|
},
|
|
75
76
|
PREVIOUSLY_SUBSCRIBED: {
|
|
76
77
|
title: "Space Gits on Indie Tabletop Club",
|
|
@@ -106,28 +107,31 @@ export default meta;
|
|
|
106
107
|
|
|
107
108
|
type Story = StoryObj<typeof meta>;
|
|
108
109
|
|
|
110
|
+
/**
|
|
111
|
+
* The default case, in which the user is not yet subscribed to ITC's newsletter
|
|
112
|
+
* and the submission is successful.
|
|
113
|
+
*/
|
|
109
114
|
export const Default: Story = {};
|
|
110
115
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Same like the default case, but the form submission fails. Errors are
|
|
118
|
+
* differentiated using the `getSubmitFailureMessage` helper, so all the basics
|
|
119
|
+
* should be covered, as this page doesn't have special cases.
|
|
120
|
+
*/
|
|
121
|
+
export const FailureOnSubmit: Story = {
|
|
116
122
|
parameters: {
|
|
117
123
|
msw: {
|
|
118
124
|
handlers: {
|
|
119
|
-
subscribeByPledgeId: subscribeByPledgeId.
|
|
125
|
+
subscribeByPledgeId: subscribeByPledgeId.error(),
|
|
120
126
|
},
|
|
121
127
|
},
|
|
122
128
|
},
|
|
123
129
|
};
|
|
124
130
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
},
|
|
132
|
-
},
|
|
131
|
+
/**
|
|
132
|
+
* In this case, the email address associated with this user account is
|
|
133
|
+
* already subscribed to our newsletter.
|
|
134
|
+
*/
|
|
135
|
+
export const AlreadySubscribed: Story = {
|
|
136
|
+
args: { pledge: { ...pledge, contactSubscribed: true } },
|
|
133
137
|
};
|
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
type RedeemedPledge,
|
|
11
11
|
} from "@indietabletop/appkit";
|
|
12
12
|
import { useState, type ReactNode } from "react";
|
|
13
|
-
import { useClient } from "../
|
|
13
|
+
import { useClient } from "../AppConfig/AppConfig.tsx";
|
|
14
|
+
import { LetterheadInfoCard } from "./LetterheadInfoCard.tsx";
|
|
14
15
|
import * as css from "./style.css.ts";
|
|
15
16
|
|
|
16
17
|
type Pledge = Pick<RedeemedPledge, "id" | "email" | "contactSubscribed">;
|
|
@@ -38,7 +39,7 @@ function SubscribeView(
|
|
|
38
39
|
});
|
|
39
40
|
|
|
40
41
|
return (
|
|
41
|
-
<Letterhead
|
|
42
|
+
<Letterhead>
|
|
42
43
|
<LetterheadHeading className={css.card.heading}>
|
|
43
44
|
{title}
|
|
44
45
|
</LetterheadHeading>
|
|
@@ -58,30 +59,30 @@ function SubscribeView(
|
|
|
58
59
|
);
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
function InfoView(props: ViewContent) {
|
|
62
|
-
return (
|
|
63
|
-
<Letterhead textAlign="center">
|
|
64
|
-
<LetterheadHeading className={css.card.heading}>
|
|
65
|
-
{props.title}
|
|
66
|
-
</LetterheadHeading>
|
|
67
|
-
|
|
68
|
-
{props.description}
|
|
69
|
-
</Letterhead>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
62
|
type SubscribeStep =
|
|
74
63
|
| "PREVIOUSLY_SUBSCRIBED"
|
|
75
64
|
| "SUBSCRIBE"
|
|
76
65
|
| "SUBSCRIBE_SUCCESS";
|
|
77
66
|
|
|
78
|
-
type ViewContent = {
|
|
67
|
+
export type ViewContent = {
|
|
79
68
|
title: ReactNode;
|
|
80
69
|
description: ReactNode;
|
|
81
70
|
};
|
|
82
71
|
|
|
83
72
|
export function SubscribeCard(props: {
|
|
73
|
+
/**
|
|
74
|
+
* The pledge data that will be used to determine whether the user should
|
|
75
|
+
* be prompted for signup.
|
|
76
|
+
*/
|
|
84
77
|
pledge: Pledge;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Content for each of the subscribe steps. You probably want to customize it
|
|
81
|
+
* for each step, so it is left to be defined at point of usage.
|
|
82
|
+
*
|
|
83
|
+
* If the `description` is a string, it will be wrapped in
|
|
84
|
+
* `<LetterheadParagraph>`, otherwise it will be left as is.
|
|
85
|
+
*/
|
|
85
86
|
content: Record<SubscribeStep, ViewContent>;
|
|
86
87
|
}) {
|
|
87
88
|
const { pledge } = props;
|
|
@@ -105,7 +106,7 @@ export function SubscribeCard(props: {
|
|
|
105
106
|
|
|
106
107
|
case "PREVIOUSLY_SUBSCRIBED":
|
|
107
108
|
case "SUBSCRIBE_SUCCESS": {
|
|
108
|
-
return <
|
|
109
|
+
return <LetterheadInfoCard {...content} />;
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Button } from "@ariakit/react";
|
|
2
|
+
import { cx } from "../class-names.ts";
|
|
3
|
+
import { interactiveText } from "../common.css.ts";
|
|
4
|
+
import {
|
|
5
|
+
Letterhead,
|
|
6
|
+
LetterheadHeading,
|
|
7
|
+
LetterheadParagraph,
|
|
8
|
+
} from "../Letterhead/index.tsx";
|
|
9
|
+
import type { EventHandlerWithReload } from "./types.ts";
|
|
10
|
+
|
|
11
|
+
export function AccountIssueView(props: {
|
|
12
|
+
onLogout: EventHandlerWithReload;
|
|
13
|
+
reload: () => void;
|
|
14
|
+
}) {
|
|
15
|
+
const { onLogout, reload } = props;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Letterhead>
|
|
19
|
+
<LetterheadHeading>Account issue</LetterheadHeading>
|
|
20
|
+
|
|
21
|
+
<LetterheadParagraph>
|
|
22
|
+
You appear to be logged into an account that no longer exists, or is not
|
|
23
|
+
working as expected. Sorry about that!
|
|
24
|
+
</LetterheadParagraph>
|
|
25
|
+
|
|
26
|
+
<LetterheadParagraph>
|
|
27
|
+
{"You can try "}
|
|
28
|
+
<Button {...cx(interactiveText)} onClick={() => onLogout({ reload })}>
|
|
29
|
+
logging out
|
|
30
|
+
</Button>
|
|
31
|
+
{" and in again. "}
|
|
32
|
+
{"If the issue persists, please get in touch at "}
|
|
33
|
+
<a {...cx(interactiveText)} href="mailto:support@indietabletop.club">
|
|
34
|
+
support@indietabletop.club
|
|
35
|
+
</a>
|
|
36
|
+
{"."}
|
|
37
|
+
</LetterheadParagraph>
|
|
38
|
+
</Letterhead>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Button } from "@ariakit/react";
|
|
2
|
+
import { Link } from "wouter";
|
|
3
|
+
import { useAppConfig } from "../AppConfig/AppConfig.tsx";
|
|
4
|
+
import { interactiveText } from "../common.css.ts";
|
|
5
|
+
import {
|
|
6
|
+
Letterhead,
|
|
7
|
+
LetterheadHeading,
|
|
8
|
+
LetterheadParagraph,
|
|
9
|
+
} from "../Letterhead/index.tsx";
|
|
10
|
+
import type { CurrentUser } from "../types.ts";
|
|
11
|
+
import type { EventHandlerWithReload } from "./types.ts";
|
|
12
|
+
|
|
13
|
+
export function AlreadyLoggedInView(props: {
|
|
14
|
+
currentUser: CurrentUser;
|
|
15
|
+
onLogout: EventHandlerWithReload;
|
|
16
|
+
reload: () => void;
|
|
17
|
+
}) {
|
|
18
|
+
const { hrefs } = useAppConfig();
|
|
19
|
+
const { currentUser, onLogout, reload } = props;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Letterhead>
|
|
23
|
+
<LetterheadHeading>Logged in</LetterheadHeading>
|
|
24
|
+
<LetterheadParagraph>
|
|
25
|
+
You are already logged into Indie Tabletop Club as{" "}
|
|
26
|
+
<em>{currentUser.email}</em>.
|
|
27
|
+
</LetterheadParagraph>
|
|
28
|
+
|
|
29
|
+
<LetterheadParagraph>
|
|
30
|
+
<Link className={interactiveText} href={hrefs.dashboard()}>
|
|
31
|
+
Continue
|
|
32
|
+
</Link>
|
|
33
|
+
{` as current user, or `}
|
|
34
|
+
<Button
|
|
35
|
+
className={interactiveText}
|
|
36
|
+
onClick={() => onLogout({ reload })}
|
|
37
|
+
>
|
|
38
|
+
log out
|
|
39
|
+
</Button>
|
|
40
|
+
.
|
|
41
|
+
</LetterheadParagraph>
|
|
42
|
+
</Letterhead>
|
|
43
|
+
);
|
|
44
|
+
}
|