@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.
Files changed (44) hide show
  1. package/lib/AppConfig/AppConfig.tsx +54 -0
  2. package/lib/HistoryState.ts +21 -0
  3. package/lib/Letterhead/style.css.ts +2 -0
  4. package/lib/LetterheadForm/index.tsx +53 -0
  5. package/lib/LetterheadForm/style.css.ts +8 -0
  6. package/lib/QRCode/QRCode.stories.tsx +47 -0
  7. package/lib/QRCode/QRCode.tsx +49 -0
  8. package/lib/QRCode/style.css.ts +19 -0
  9. package/lib/ShareButton/ShareButton.tsx +141 -0
  10. package/lib/SubscribeCard/LetterheadInfoCard.tsx +23 -0
  11. package/lib/SubscribeCard/SubscribeByEmailCard.stories.tsx +83 -0
  12. package/lib/SubscribeCard/SubscribeByEmailCard.tsx +177 -0
  13. package/lib/SubscribeCard/SubscribeCard.stories.tsx +27 -23
  14. package/lib/SubscribeCard/SubscribeCard.tsx +22 -17
  15. package/lib/Title/index.tsx +7 -2
  16. package/lib/account/AccountIssueView.tsx +40 -0
  17. package/lib/account/AlreadyLoggedInView.tsx +44 -0
  18. package/lib/account/CurrentUserFetcher.stories.tsx +339 -0
  19. package/lib/account/CurrentUserFetcher.tsx +119 -0
  20. package/lib/account/FailureFallbackView.tsx +36 -0
  21. package/lib/account/JoinPage.stories.tsx +270 -0
  22. package/lib/account/JoinPage.tsx +288 -0
  23. package/lib/account/LoadingView.tsx +14 -0
  24. package/lib/account/LoginPage.stories.tsx +318 -0
  25. package/lib/account/LoginPage.tsx +138 -0
  26. package/lib/account/LoginView.tsx +136 -0
  27. package/lib/account/NoConnectionView.tsx +34 -0
  28. package/lib/account/PasswordResetPage.stories.tsx +250 -0
  29. package/lib/account/PasswordResetPage.tsx +291 -0
  30. package/lib/account/UserMismatchView.tsx +61 -0
  31. package/lib/account/VerifyPage.tsx +217 -0
  32. package/lib/account/style.css.ts +57 -0
  33. package/lib/account/types.ts +9 -0
  34. package/lib/account/useCurrentUserResult.tsx +38 -0
  35. package/lib/class-names.ts +1 -1
  36. package/lib/client.ts +54 -7
  37. package/lib/globals.css.ts +5 -0
  38. package/lib/index.ts +11 -1
  39. package/lib/useEnsureValue.ts +31 -0
  40. package/package.json +3 -2
  41. package/lib/ClientContext/ClientContext.tsx +0 -25
  42. package/lib/LoginPage/LoginPage.stories.tsx +0 -107
  43. package/lib/LoginPage/LoginPage.tsx +0 -204
  44. package/lib/LoginPage/style.css.ts +0 -17
@@ -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://localhost:8000/v1/newsletters/changelog/subscriptions",
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://localhost:8000/v1/newsletters/changelog/subscriptions",
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://localhost:8000/v1/newsletters/changelog/subscriptions",
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
- <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
- ),
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
- export const AlreadySubscribed: Story = {
112
- args: { pledge: { ...pledge, contactSubscribed: true } },
113
- };
114
-
115
- export const NoConnection: Story = {
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.noConnection(),
125
+ subscribeByPledgeId: subscribeByPledgeId.error(),
120
126
  },
121
127
  },
122
128
  },
123
129
  };
124
130
 
125
- export const FallbackFailure: Story = {
126
- parameters: {
127
- msw: {
128
- handlers: {
129
- subscribeByPledgeId: subscribeByPledgeId.error(),
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 "../ClientContext/ClientContext.tsx";
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">;
@@ -28,13 +29,17 @@ function SubscribeView(
28
29
  defaultValues: {},
29
30
  onSuccess,
30
31
  async onSubmit() {
31
- const result = await client.subscribeToNewsletterByPledgeId(pledge.id);
32
+ const result = await client.subscribeToNewsletterByPledgeId(
33
+ "changelog",
34
+ pledge.id,
35
+ );
36
+
32
37
  return result.mapFailure(getSubmitFailureMessage);
33
38
  },
34
39
  });
35
40
 
36
41
  return (
37
- <Letterhead textAlign="center">
42
+ <Letterhead>
38
43
  <LetterheadHeading className={css.card.heading}>
39
44
  {title}
40
45
  </LetterheadHeading>
@@ -54,30 +59,30 @@ function SubscribeView(
54
59
  );
55
60
  }
56
61
 
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
62
  type SubscribeStep =
70
63
  | "PREVIOUSLY_SUBSCRIBED"
71
64
  | "SUBSCRIBE"
72
65
  | "SUBSCRIBE_SUCCESS";
73
66
 
74
- type ViewContent = {
67
+ export type ViewContent = {
75
68
  title: ReactNode;
76
69
  description: ReactNode;
77
70
  };
78
71
 
79
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
+ */
80
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
+ */
81
86
  content: Record<SubscribeStep, ViewContent>;
82
87
  }) {
83
88
  const { pledge } = props;
@@ -101,7 +106,7 @@ export function SubscribeCard(props: {
101
106
 
102
107
  case "PREVIOUSLY_SUBSCRIBED":
103
108
  case "SUBSCRIBE_SUCCESS": {
104
- return <InfoView {...content} />;
109
+ return <LetterheadInfoCard {...content} />;
105
110
  }
106
111
  }
107
112
  }
@@ -1,4 +1,9 @@
1
+ import { useAppConfig } from "../AppConfig/AppConfig.tsx";
2
+
1
3
  export function Title(props: { children: string }) {
2
- // TODO: Pick out an app name from context
3
- return <title>{`${props.children} | Indie Tabletop Club`}</title>;
4
+ const { appName } = useAppConfig();
5
+
6
+ return (
7
+ <title>{`${props.children} | ${appName} · Indie Tabletop Club`}</title>
8
+ );
4
9
  }
@@ -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
+ }