@indietabletop/appkit 3.6.0-2 → 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 +17 -16
- 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,54 @@
|
|
|
1
|
+
import { createContext, type ReactNode, useContext } from "react";
|
|
2
|
+
import type { IndieTabletopClient } from "../client.ts";
|
|
3
|
+
|
|
4
|
+
export type AppConfig = {
|
|
5
|
+
appName: string;
|
|
6
|
+
client: IndieTabletopClient;
|
|
7
|
+
hrefs: {
|
|
8
|
+
login: () => string;
|
|
9
|
+
password: () => string;
|
|
10
|
+
join: () => string;
|
|
11
|
+
terms: () => string;
|
|
12
|
+
verify: () => string;
|
|
13
|
+
dashboard: () => string;
|
|
14
|
+
};
|
|
15
|
+
placeholders: {
|
|
16
|
+
email: string;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const AppConfigContext = createContext<AppConfig | null>(null);
|
|
21
|
+
|
|
22
|
+
export function AppConfigProvider(props: {
|
|
23
|
+
config: AppConfig;
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
}) {
|
|
26
|
+
const { config, children } = props;
|
|
27
|
+
return <AppConfigContext value={config}>{children}</AppConfigContext>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useAppConfig() {
|
|
31
|
+
const config = useContext(AppConfigContext);
|
|
32
|
+
|
|
33
|
+
if (!config) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Attempting to retrieve app config, but none was found within context. ` +
|
|
36
|
+
`Make sure that AppConfigProvider is used in the component hierarchy.`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return config;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function useClient() {
|
|
44
|
+
const config = useContext(AppConfigContext);
|
|
45
|
+
|
|
46
|
+
if (!config?.client) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Attempting to retrieve ITC client, but none was found within context. ` +
|
|
49
|
+
`Make sure that AppConfigProvider is used in the component hierarchy.`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return config.client;
|
|
54
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History state interface. This value can be extended via the standard
|
|
3
|
+
* typescript declaration merging mechanism.
|
|
4
|
+
*
|
|
5
|
+
* Use it via {@link getHistoryState}.
|
|
6
|
+
*
|
|
7
|
+
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html
|
|
8
|
+
*/
|
|
9
|
+
export interface HistoryState {
|
|
10
|
+
emailValue?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get's window history state with correct typings.
|
|
15
|
+
*
|
|
16
|
+
* Note that this is not a reactive value. It will not trigger re-renders if
|
|
17
|
+
* it is changed.
|
|
18
|
+
*/
|
|
19
|
+
export function getHistoryState() {
|
|
20
|
+
return window.history.state as HistoryState | null;
|
|
21
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
FormCheckbox,
|
|
3
|
+
type FormCheckboxProps,
|
|
2
4
|
FormError,
|
|
3
5
|
FormInput,
|
|
4
6
|
type FormInputProps,
|
|
@@ -7,6 +9,7 @@ import {
|
|
|
7
9
|
useStoreState,
|
|
8
10
|
} from "@ariakit/react";
|
|
9
11
|
import { type ReactNode } from "react";
|
|
12
|
+
import { cx } from "../class-names.ts";
|
|
10
13
|
import * as css from "./style.css.ts";
|
|
11
14
|
|
|
12
15
|
export type LetterheadTextFieldProps = FormInputProps & {
|
|
@@ -34,6 +37,23 @@ export function LetterheadTextField(props: LetterheadTextFieldProps) {
|
|
|
34
37
|
);
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
export function LetterheadCheckboxField(
|
|
41
|
+
props: FormCheckboxProps & { label: ReactNode },
|
|
42
|
+
) {
|
|
43
|
+
const { label, name, ...inputProps } = props;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div {...cx(css.checkboxField)}>
|
|
47
|
+
<div>
|
|
48
|
+
<FormCheckbox name={name} {...inputProps} />{" "}
|
|
49
|
+
<FormLabel name={name}>{label}</FormLabel>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<FormError name={name} className={css.fieldIssue} />
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
37
57
|
type LetterheadReadonlyTextFieldProps = {
|
|
38
58
|
label: string;
|
|
39
59
|
value: string;
|
|
@@ -92,3 +112,36 @@ export function LetterheadSubmitError(props: LetterheadSubmitErrorProps) {
|
|
|
92
112
|
</div>
|
|
93
113
|
);
|
|
94
114
|
}
|
|
115
|
+
|
|
116
|
+
export function LetterheadHeader(props: { children: ReactNode }) {
|
|
117
|
+
return <header className={css.header}>{props.children}</header>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function LetterheadFormActions(props: { children: ReactNode }) {
|
|
121
|
+
return (
|
|
122
|
+
<div
|
|
123
|
+
style={{
|
|
124
|
+
marginBlockStart: "2rem",
|
|
125
|
+
display: "flex",
|
|
126
|
+
flexDirection: "column",
|
|
127
|
+
gap: "1.125rem",
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
{props.children}
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function InputsStack(props: { children: ReactNode }) {
|
|
136
|
+
return (
|
|
137
|
+
<div
|
|
138
|
+
style={{
|
|
139
|
+
display: "flex",
|
|
140
|
+
flexDirection: "column",
|
|
141
|
+
gap: "1.125rem",
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
{props.children}
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -48,6 +48,10 @@ export const fieldInput = style([
|
|
|
48
48
|
},
|
|
49
49
|
]);
|
|
50
50
|
|
|
51
|
+
export const checkboxField = style({
|
|
52
|
+
textAlign: "start",
|
|
53
|
+
});
|
|
54
|
+
|
|
51
55
|
export const fieldIssue = style({
|
|
52
56
|
color: Color.PURPLE,
|
|
53
57
|
fontSize: "0.875rem",
|
|
@@ -83,3 +87,7 @@ export const submitError = style({
|
|
|
83
87
|
display: "none",
|
|
84
88
|
},
|
|
85
89
|
});
|
|
90
|
+
|
|
91
|
+
export const header = style({
|
|
92
|
+
marginBlockEnd: "3rem",
|
|
93
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { http, HttpResponse } from "msw";
|
|
3
|
+
import { Failure, Pending, Success } from "../async-op.ts";
|
|
4
|
+
import { QRCode } from "./QRCode.tsx";
|
|
5
|
+
|
|
6
|
+
type StoryMeta = Meta<typeof QRCode>;
|
|
7
|
+
|
|
8
|
+
type Story = StoryObj<typeof meta>;
|
|
9
|
+
|
|
10
|
+
const meta = {
|
|
11
|
+
title: "Components/QR Code",
|
|
12
|
+
component: QRCode,
|
|
13
|
+
tags: ["autodocs"],
|
|
14
|
+
args: {
|
|
15
|
+
url: new Success("example.com"),
|
|
16
|
+
},
|
|
17
|
+
parameters: {
|
|
18
|
+
msw: {
|
|
19
|
+
handlers: {
|
|
20
|
+
qr: http.get("http://mock.api/qr", async () => {
|
|
21
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 41 41" shape-rendering="crispEdges"><path fill="#ffffff" d="M0 0h41v41H0z"/><path stroke="#000000" d="M4 4.5h7m2 0h2m2 0h1m3 0h1m1 0h3m1 0h1m2 0h7M4 5.5h1m5 0h1m2 0h2m1 0h4m2 0h1m1 0h3m1 0h1m1 0h1m5 0h1M4 6.5h1m1 0h3m1 0h1m1 0h1m3 0h2m1 0h1m4 0h2m1 0h1m2 0h1m1 0h3m1 0h1M4 7.5h1m1 0h3m1 0h1m1 0h5m1 0h3m3 0h5m1 0h1m1 0h3m1 0h1M4 8.5h1m1 0h3m1 0h1m1 0h2m3 0h1m1 0h1m2 0h3m2 0h1m2 0h1m1 0h3m1 0h1M4 9.5h1m5 0h1m1 0h1m1 0h1m1 0h5m3 0h1m1 0h1m3 0h1m5 0h1M4 10.5h7m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h7M12 11.5h4m3 0h1m2 0h6M4 12.5h1m1 0h5m3 0h1m2 0h5m4 0h1m1 0h1m1 0h5M4 13.5h2m2 0h1m2 0h1m2 0h3m3 0h6m1 0h1m2 0h2m1 0h2m1 0h1M4 14.5h7m1 0h1m3 0h1m2 0h3m2 0h3m1 0h1m1 0h1m1 0h1m1 0h2M4 15.5h6m1 0h4m1 0h1m1 0h1m1 0h1m1 0h1m2 0h5m2 0h4M4 16.5h1m2 0h4m1 0h2m1 0h2m5 0h2m2 0h1m1 0h1m3 0h2m1 0h1M6 17.5h2m3 0h2m1 0h3m1 0h3m1 0h3m5 0h1m3 0h3M4 18.5h3m3 0h2m3 0h1m2 0h1m3 0h1m1 0h3m2 0h2m4 0h1M4 19.5h3m1 0h1m7 0h1m3 0h1m3 0h2m2 0h3m1 0h1m1 0h1M4 20.5h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m2 0h2m2 0h1m1 0h1m3 0h1m1 0h1m1 0h2m3 0h1M6 21.5h1m2 0h1m1 0h5m4 0h5m2 0h1m2 0h2m1 0h4M4 22.5h8m1 0h3m1 0h3m1 0h1m4 0h1m2 0h4m1 0h2M4 23.5h1m1 0h2m4 0h1m2 0h8m1 0h3m2 0h6M4 24.5h3m2 0h2m1 0h1m2 0h1m1 0h1m1 0h2m5 0h1m2 0h1m1 0h3m1 0h2M4 25.5h5m3 0h3m2 0h3m2 0h3m4 0h2m2 0h1m2 0h1M4 26.5h1m2 0h1m1 0h3m3 0h2m2 0h4m1 0h1m1 0h1m2 0h1m1 0h3m1 0h1M4 27.5h1m3 0h2m1 0h2m1 0h3m1 0h2m2 0h1m1 0h2m1 0h1m1 0h1m1 0h1m1 0h2m1 0h1M4 28.5h1m1 0h2m2 0h3m2 0h7m4 0h7m2 0h1M12 29.5h2m2 0h3m1 0h1m1 0h4m2 0h1m3 0h1m1 0h1m1 0h1M4 30.5h7m2 0h1m1 0h1m3 0h2m1 0h1m3 0h3m1 0h1m1 0h1m1 0h2M4 31.5h1m5 0h1m1 0h1m3 0h2m1 0h2m2 0h6m3 0h5M4 32.5h1m1 0h3m1 0h1m1 0h1m6 0h1m2 0h5m1 0h6M4 33.5h1m1 0h3m1 0h1m1 0h2m5 0h3m1 0h1m4 0h2m2 0h1m1 0h3M4 34.5h1m1 0h3m1 0h1m1 0h1m2 0h1m1 0h1m6 0h2m1 0h5m1 0h2M4 35.5h1m5 0h1m2 0h2m1 0h3m1 0h1m1 0h1m2 0h2m5 0h3M4 36.5h7m1 0h6m3 0h1m4 0h1m1 0h4m1 0h1m1 0h1"/></svg>`;
|
|
22
|
+
const { buffer } = new TextEncoder().encode(svg);
|
|
23
|
+
|
|
24
|
+
return HttpResponse.arrayBuffer(buffer, {
|
|
25
|
+
headers: { "Content-Type": "image/svg+xml" },
|
|
26
|
+
});
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
} satisfies StoryMeta;
|
|
32
|
+
|
|
33
|
+
export default meta;
|
|
34
|
+
|
|
35
|
+
export const SuccessCase: Story = {};
|
|
36
|
+
|
|
37
|
+
export const PendingCase: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
url: new Pending(),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const FailureCase: Story = {
|
|
44
|
+
args: {
|
|
45
|
+
url: new Failure("Failed"),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useAppConfig } from "../AppConfig/AppConfig.tsx";
|
|
2
|
+
import type { AsyncOp } from "../async-op.ts";
|
|
3
|
+
import { cx } from "../class-names.ts";
|
|
4
|
+
import { LoadingIndicator } from "../LoadingIndicator.js";
|
|
5
|
+
import { code } from "./style.css.ts";
|
|
6
|
+
|
|
7
|
+
export function QRCode(props: { url: AsyncOp<string, unknown> }) {
|
|
8
|
+
const { url: result } = props;
|
|
9
|
+
const { client } = useAppConfig();
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div {...cx(code.container)}>
|
|
13
|
+
{result.unpack(
|
|
14
|
+
(url) => {
|
|
15
|
+
const params = new URLSearchParams({ url });
|
|
16
|
+
const src = `${client.origin}/qr?${params}`;
|
|
17
|
+
return (
|
|
18
|
+
<img
|
|
19
|
+
{...cx(code.image)}
|
|
20
|
+
src={src}
|
|
21
|
+
alt=""
|
|
22
|
+
width={150}
|
|
23
|
+
height={150}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
},
|
|
27
|
+
() => {
|
|
28
|
+
return (
|
|
29
|
+
<svg
|
|
30
|
+
width="24px"
|
|
31
|
+
height="24px"
|
|
32
|
+
viewBox="0 0 24 24"
|
|
33
|
+
version="1.1"
|
|
34
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
35
|
+
>
|
|
36
|
+
<g strokeWidth="2" stroke="#000000" strokeLinecap="square">
|
|
37
|
+
<line x1="7" y1="7" x2="17" y2="17" />
|
|
38
|
+
<line x1="17" y1="7" x2="7" y2="17" />
|
|
39
|
+
</g>
|
|
40
|
+
</svg>
|
|
41
|
+
);
|
|
42
|
+
},
|
|
43
|
+
() => {
|
|
44
|
+
return <LoadingIndicator />;
|
|
45
|
+
},
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { style } from "@vanilla-extract/css";
|
|
2
|
+
|
|
3
|
+
export const code = {
|
|
4
|
+
container: style({
|
|
5
|
+
display: "flex",
|
|
6
|
+
alignItems: "center",
|
|
7
|
+
justifyContent: "center",
|
|
8
|
+
backgroundColor: "white",
|
|
9
|
+
borderRadius: "0.5rem",
|
|
10
|
+
maxInlineSize: "12rem",
|
|
11
|
+
aspectRatio: "1",
|
|
12
|
+
}),
|
|
13
|
+
|
|
14
|
+
image: style({
|
|
15
|
+
inlineSize: "100%",
|
|
16
|
+
blockSize: "auto",
|
|
17
|
+
borderRadius: "inherit",
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
@@ -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 = {};
|