@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
package/lib/mailto.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encodes values to be used in mailto protocol.
|
|
3
|
+
*
|
|
4
|
+
* Note that we cannot simply use URLSeachParams because they, for example, encode a space
|
|
5
|
+
* as plus (+), which cannot be used in mailto if we want to have consistent behaviour in
|
|
6
|
+
* all email clients.
|
|
7
|
+
*/
|
|
8
|
+
function encodeValuesForMailto<T extends object>(object: T) {
|
|
9
|
+
return Object.entries(object)
|
|
10
|
+
.filter(([_, v]) => !!v)
|
|
11
|
+
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
|
12
|
+
.join("&");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function serializeArray(value?: string | string[] | null) {
|
|
16
|
+
return Array.isArray(value) ? value.join(",") : value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type MailtoPayload = {
|
|
20
|
+
body?: string | null;
|
|
21
|
+
subject?: string | null;
|
|
22
|
+
cc?: string | string[] | null;
|
|
23
|
+
bcc?: string | string[] | null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function mailto(recipient: string | null, payload?: MailtoPayload) {
|
|
27
|
+
// If the recipient is falsy the user can choose who to send the email to.
|
|
28
|
+
const to = recipient ?? "";
|
|
29
|
+
|
|
30
|
+
const serialized = payload
|
|
31
|
+
? `?${encodeValuesForMailto({
|
|
32
|
+
body: payload.body,
|
|
33
|
+
subject: payload.subject,
|
|
34
|
+
cc: serializeArray(payload.cc),
|
|
35
|
+
bcc: serializeArray(payload.bcc),
|
|
36
|
+
})}`
|
|
37
|
+
: "";
|
|
38
|
+
|
|
39
|
+
return `mailto:${to}${serialized}`;
|
|
40
|
+
}
|
package/lib/types.ts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import type { Infer } from "superstruct";
|
|
2
2
|
import { currentUser, redeemedPledge, sessionInfo } from "./structs.js";
|
|
3
3
|
|
|
4
|
+
// Generic type helpers
|
|
5
|
+
|
|
6
|
+
type Brand<B> = { __brand: B };
|
|
7
|
+
|
|
8
|
+
export type Branded<T, B> = T & Brand<B>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A branded string.
|
|
12
|
+
*
|
|
13
|
+
* Use this type to make a HTML string as trusted. This can be either HTML
|
|
14
|
+
* coming from a source that we know is safe (statically generated markdown
|
|
15
|
+
* that exists in our codebase) or sanitized user-generated content.
|
|
16
|
+
*/
|
|
17
|
+
export type TrustedHtml = Branded<string, "TrustedHtml">;
|
|
18
|
+
|
|
19
|
+
// Common ITC types
|
|
20
|
+
|
|
4
21
|
export type CurrentUser = Infer<ReturnType<typeof currentUser>>;
|
|
5
22
|
|
|
6
23
|
export type SessionInfo = Infer<ReturnType<typeof sessionInfo>>;
|
|
@@ -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/lib/utm.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Given an object with keys that might contain undefined values, returns a new
|
|
3
|
+
* object only with keys that are not undefined.
|
|
4
|
+
*/
|
|
5
|
+
function omitUndefinedKeys<T>(record: Record<string, T | undefined>) {
|
|
6
|
+
return Object.fromEntries(
|
|
7
|
+
Object.entries(record).filter(([_, v]) => v !== undefined),
|
|
8
|
+
) as Record<string, T>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Full UTM configuration object.
|
|
13
|
+
*/
|
|
14
|
+
export type UtmParams = {
|
|
15
|
+
/**
|
|
16
|
+
* The website/platform the visitor is coming from.
|
|
17
|
+
*/
|
|
18
|
+
source: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The type of marketing channel (e.g. cpc, email, affiliate, social,
|
|
22
|
+
* or similar).
|
|
23
|
+
*/
|
|
24
|
+
medium: string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The name of the campaign or product description. In our case, this is
|
|
28
|
+
* usually the name of the app.
|
|
29
|
+
*/
|
|
30
|
+
campaign: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Optionally, provide an identifier for the place from which the link was
|
|
34
|
+
* clicked. E.g. footer, join, etc.
|
|
35
|
+
*/
|
|
36
|
+
content?: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Optionally, identify paid keywords. We usually do not use this.
|
|
40
|
+
*/
|
|
41
|
+
term?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* UTM Params configuration that is appropriate to set at the link level,
|
|
46
|
+
* if app-level defaults have been set.
|
|
47
|
+
*/
|
|
48
|
+
export type LinkUtmParams = Pick<UtmParams, "content" | "term">;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Returns URL Search params with provided UTM configuration.
|
|
52
|
+
*
|
|
53
|
+
* Most of the time, you probably want to set up some defaults using
|
|
54
|
+
* {@link createUtm}. This function is intended for special cases.
|
|
55
|
+
*/
|
|
56
|
+
export function utm(params: UtmParams) {
|
|
57
|
+
return new URLSearchParams(
|
|
58
|
+
omitUndefinedKeys({
|
|
59
|
+
utm_source: params.source,
|
|
60
|
+
utm_medium: params.medium,
|
|
61
|
+
utm_campaign: params.campaign,
|
|
62
|
+
utm_content: params.content,
|
|
63
|
+
utm_term: params.term,
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A factory for the {@link utm} function. Use it to set sensible defaults for
|
|
70
|
+
* further use of the returned function.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* const utm = createUtm({
|
|
75
|
+
* source: "indietabletopclub",
|
|
76
|
+
* medium: "referral",
|
|
77
|
+
* campaign: "spacegitsapp",
|
|
78
|
+
* });
|
|
79
|
+
*
|
|
80
|
+
* utm() // => URLSearchParams that inlude the above config
|
|
81
|
+
*
|
|
82
|
+
* utm({ campaign: "foo", content: "bar" }) // Sets optional fiels and overrides
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export function createUtm(defaults: UtmParams) {
|
|
86
|
+
return function (params?: Partial<UtmParams>) {
|
|
87
|
+
return utm({ ...defaults, ...params });
|
|
88
|
+
};
|
|
89
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indietabletop/appkit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.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
|
|
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
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
import { http, HttpResponse } from "msw";
|
|
3
|
-
import { fn } from "storybook/test";
|
|
4
|
-
import { IndieTabletopClient } from "../client.ts";
|
|
5
|
-
import { sleep } from "../sleep.ts";
|
|
6
|
-
import { LoginPage } from "./LoginPage.tsx";
|
|
7
|
-
|
|
8
|
-
const userA = {
|
|
9
|
-
id: "a",
|
|
10
|
-
email: "a@example.com",
|
|
11
|
-
isVerified: true,
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const userB = {
|
|
15
|
-
id: "b",
|
|
16
|
-
email: "b@example.com",
|
|
17
|
-
isVerified: true,
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const meta = {
|
|
21
|
-
title: "Pages/Login Page",
|
|
22
|
-
component: LoginPage,
|
|
23
|
-
tags: ["autodocs"],
|
|
24
|
-
args: {
|
|
25
|
-
client: new IndieTabletopClient({ apiOrigin: "" }),
|
|
26
|
-
onSuccess: fn(),
|
|
27
|
-
onLogout: fn(),
|
|
28
|
-
localUser: null,
|
|
29
|
-
},
|
|
30
|
-
parameters: {
|
|
31
|
-
msw: {
|
|
32
|
-
handlers: {
|
|
33
|
-
getCurrentUser: http.get("/v1/users/me", async () => {
|
|
34
|
-
await sleep(2000);
|
|
35
|
-
|
|
36
|
-
return HttpResponse.json(userA);
|
|
37
|
-
}),
|
|
38
|
-
|
|
39
|
-
createNewSession: http.post("/v1/sessions", async () => {
|
|
40
|
-
await sleep(2000);
|
|
41
|
-
|
|
42
|
-
return HttpResponse.json({
|
|
43
|
-
currentUser: userA,
|
|
44
|
-
sessionInfo: {
|
|
45
|
-
createdTs: 123,
|
|
46
|
-
expiresTs: 123,
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
}),
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
} satisfies Meta<typeof LoginPage>;
|
|
54
|
-
|
|
55
|
-
export default meta;
|
|
56
|
-
|
|
57
|
-
type Story = StoryObj<typeof meta>;
|
|
58
|
-
|
|
59
|
-
// There is no local user and server response reports user not authenticated.
|
|
60
|
-
// This is the majority case.
|
|
61
|
-
export const Default: Story = {
|
|
62
|
-
parameters: {
|
|
63
|
-
msw: {
|
|
64
|
-
handlers: {
|
|
65
|
-
getCurrentUser: http.get("/v1/users/me", async () => {
|
|
66
|
-
await sleep(2000);
|
|
67
|
-
|
|
68
|
-
return HttpResponse.text("Not authenticated", { status: 401 });
|
|
69
|
-
}),
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
// User is logged in via a server cookie. They can proceed to app.
|
|
76
|
-
export const AlreadyLoggedIn: Story = {
|
|
77
|
-
args: {
|
|
78
|
-
localUser: userA,
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// App has local data that doesn't match server-data. User has to choose
|
|
83
|
-
// how to proceed.
|
|
84
|
-
|
|
85
|
-
// TODO Style UserMismatch and add handler.
|
|
86
|
-
export const UserMismatch: Story = {
|
|
87
|
-
args: {
|
|
88
|
-
localUser: userB,
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
// TODO: Implement Not Found story (user was probably deleted)
|
|
93
|
-
export const UserNotFound: Story = {
|
|
94
|
-
args: {
|
|
95
|
-
localUser: null,
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
// TODO: Implement no connection story
|
|
100
|
-
export const NoConnection: Story = {};
|
|
101
|
-
|
|
102
|
-
// TODO: Implement fallback failure (all other cases)
|
|
103
|
-
export const FallbackFailure: Story = {
|
|
104
|
-
args: {
|
|
105
|
-
localUser: userB,
|
|
106
|
-
},
|
|
107
|
-
};
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import { Form } from "@ariakit/react";
|
|
2
|
-
import { Button } from "@ariakit/react/button";
|
|
3
|
-
|
|
4
|
-
import { useCallback, useEffect, useState } from "react";
|
|
5
|
-
import { Link, useLocation } from "wouter";
|
|
6
|
-
import { Pending, type Failure, type Success } from "../async-op.js";
|
|
7
|
-
import { IndieTabletopClient } from "../client.ts";
|
|
8
|
-
import { interactiveText } from "../common.css.ts";
|
|
9
|
-
import { getSubmitFailureMessage } from "../failureMessages.ts";
|
|
10
|
-
import { LoadingPage } from "../InfoPage/index.tsx";
|
|
11
|
-
import {
|
|
12
|
-
Letterhead,
|
|
13
|
-
LetterheadHeading,
|
|
14
|
-
LetterheadParagraph,
|
|
15
|
-
LetterheadSubmitButton,
|
|
16
|
-
} from "../Letterhead/index.tsx";
|
|
17
|
-
import {
|
|
18
|
-
LetterheadSubmitError,
|
|
19
|
-
LetterheadTextField,
|
|
20
|
-
} from "../LetterheadForm/index.tsx";
|
|
21
|
-
import { Title } from "../Title/index.tsx";
|
|
22
|
-
import type { CurrentUser, FailurePayload } from "../types.ts";
|
|
23
|
-
import { useForm } from "../use-form.ts";
|
|
24
|
-
import { validEmail } from "../validations.ts";
|
|
25
|
-
import * as css from "./style.css.ts";
|
|
26
|
-
|
|
27
|
-
type EventHandler = () => Promise<void> | void;
|
|
28
|
-
|
|
29
|
-
function LoginForm(props: {
|
|
30
|
-
client: IndieTabletopClient;
|
|
31
|
-
onSuccess: EventHandler;
|
|
32
|
-
}) {
|
|
33
|
-
const { client, onSuccess } = props;
|
|
34
|
-
|
|
35
|
-
const [_, navigate] = useLocation();
|
|
36
|
-
const { form, submitName } = useForm({
|
|
37
|
-
defaultValues: { email: "", password: "" },
|
|
38
|
-
validate: { email: validEmail },
|
|
39
|
-
async onSubmit({ values }) {
|
|
40
|
-
const result = await client.login(values);
|
|
41
|
-
|
|
42
|
-
return result.mapFailure((failure) => {
|
|
43
|
-
return getSubmitFailureMessage(failure, {
|
|
44
|
-
401: "Username and password do not match. Please try again.",
|
|
45
|
-
404: "Could not find a user with this email.",
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
},
|
|
49
|
-
|
|
50
|
-
async onSuccess() {
|
|
51
|
-
await onSuccess();
|
|
52
|
-
navigate("~/");
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
return (
|
|
57
|
-
<Form store={form} resetOnSubmit={false}>
|
|
58
|
-
<div
|
|
59
|
-
style={{
|
|
60
|
-
display: "flex",
|
|
61
|
-
flexDirection: "column",
|
|
62
|
-
gap: "1.125rem",
|
|
63
|
-
}}
|
|
64
|
-
>
|
|
65
|
-
<LetterheadTextField
|
|
66
|
-
name={form.names.email}
|
|
67
|
-
placeholder="james.workshop@example.com"
|
|
68
|
-
label="Email"
|
|
69
|
-
type="email"
|
|
70
|
-
required
|
|
71
|
-
/>
|
|
72
|
-
<LetterheadTextField
|
|
73
|
-
name={form.names.password}
|
|
74
|
-
label="Password"
|
|
75
|
-
placeholder="Your password"
|
|
76
|
-
type="password"
|
|
77
|
-
required
|
|
78
|
-
/>
|
|
79
|
-
</div>
|
|
80
|
-
<div
|
|
81
|
-
style={{
|
|
82
|
-
marginBlockStart: "2rem",
|
|
83
|
-
display: "flex",
|
|
84
|
-
flexDirection: "column",
|
|
85
|
-
gap: "1.125rem",
|
|
86
|
-
}}
|
|
87
|
-
>
|
|
88
|
-
<LetterheadSubmitError name={submitName} />
|
|
89
|
-
<LetterheadSubmitButton>Log in</LetterheadSubmitButton>
|
|
90
|
-
</div>
|
|
91
|
-
</Form>
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function useCurrentUserResult(client: IndieTabletopClient) {
|
|
96
|
-
const getCurrentUser = useCallback(() => client.getCurrentUser(), [client]);
|
|
97
|
-
const [result, setResult] = useState<
|
|
98
|
-
Success<CurrentUser> | Failure<FailurePayload> | Pending
|
|
99
|
-
>(new Pending());
|
|
100
|
-
|
|
101
|
-
useEffect(() => {
|
|
102
|
-
getCurrentUser().then((result) => setResult(result));
|
|
103
|
-
}, [getCurrentUser]);
|
|
104
|
-
|
|
105
|
-
return result;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function LoginPage(props: {
|
|
109
|
-
client: IndieTabletopClient;
|
|
110
|
-
localUser: CurrentUser | null;
|
|
111
|
-
onSuccess: EventHandler;
|
|
112
|
-
onLogout: EventHandler;
|
|
113
|
-
}) {
|
|
114
|
-
const { client, localUser, onLogout } = props;
|
|
115
|
-
const currentUserResult = useCurrentUserResult(client);
|
|
116
|
-
|
|
117
|
-
return (
|
|
118
|
-
<div className={css.page}>
|
|
119
|
-
<Title>Login</Title>
|
|
120
|
-
|
|
121
|
-
{currentUserResult.unpack(
|
|
122
|
-
(serverUser) => {
|
|
123
|
-
if (localUser && localUser.id !== serverUser.id) {
|
|
124
|
-
return (
|
|
125
|
-
<Letterhead>
|
|
126
|
-
<header className={css.header}>
|
|
127
|
-
<LetterheadHeading>User mismatch</LetterheadHeading>
|
|
128
|
-
|
|
129
|
-
<LetterheadParagraph>
|
|
130
|
-
You are logged into Indie Tabletop Club as{" "}
|
|
131
|
-
{serverUser.email}, but the account currently logged in on
|
|
132
|
-
this device is {localUser.email}.
|
|
133
|
-
</LetterheadParagraph>
|
|
134
|
-
|
|
135
|
-
<LetterheadParagraph>
|
|
136
|
-
Please pick which user account to continue with to prevent
|
|
137
|
-
syncing issues.
|
|
138
|
-
</LetterheadParagraph>
|
|
139
|
-
|
|
140
|
-
<div>
|
|
141
|
-
<Button type="button">
|
|
142
|
-
<div>Continue as {serverUser.email}</div>
|
|
143
|
-
<div>Local data will be deleted.</div>
|
|
144
|
-
</Button>
|
|
145
|
-
|
|
146
|
-
<Button type="button">
|
|
147
|
-
<div>Continue as {localUser.email}</div>
|
|
148
|
-
<div>You will be asked for your credentials again.</div>
|
|
149
|
-
</Button>
|
|
150
|
-
</div>
|
|
151
|
-
</header>
|
|
152
|
-
</Letterhead>
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return (
|
|
157
|
-
<Letterhead>
|
|
158
|
-
<header className={css.header}>
|
|
159
|
-
<LetterheadHeading>Logged in</LetterheadHeading>
|
|
160
|
-
<LetterheadParagraph>
|
|
161
|
-
You are already logged into Indie Tabletop Club as{" "}
|
|
162
|
-
{serverUser.email}.
|
|
163
|
-
</LetterheadParagraph>
|
|
164
|
-
|
|
165
|
-
<LetterheadParagraph>
|
|
166
|
-
<Link className={interactiveText} href="~/">
|
|
167
|
-
Continue
|
|
168
|
-
</Link>
|
|
169
|
-
{` as current user, or `}
|
|
170
|
-
<Button className={interactiveText} onClick={onLogout}>
|
|
171
|
-
log out
|
|
172
|
-
</Button>
|
|
173
|
-
.
|
|
174
|
-
</LetterheadParagraph>
|
|
175
|
-
</header>
|
|
176
|
-
</Letterhead>
|
|
177
|
-
);
|
|
178
|
-
},
|
|
179
|
-
(failure) => {
|
|
180
|
-
if (failure.type === "API_ERROR" && failure.code === 401) {
|
|
181
|
-
return (
|
|
182
|
-
<Letterhead>
|
|
183
|
-
<header className={css.header}>
|
|
184
|
-
<LetterheadHeading>Log in</LetterheadHeading>
|
|
185
|
-
<LetterheadParagraph>
|
|
186
|
-
Log into your Indie Tabletop Club account to access creator
|
|
187
|
-
features.
|
|
188
|
-
</LetterheadParagraph>
|
|
189
|
-
</header>
|
|
190
|
-
|
|
191
|
-
<LoginForm {...props} />
|
|
192
|
-
</Letterhead>
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return null;
|
|
197
|
-
},
|
|
198
|
-
() => (
|
|
199
|
-
<LoadingPage />
|
|
200
|
-
),
|
|
201
|
-
)}
|
|
202
|
-
</div>
|
|
203
|
-
);
|
|
204
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { MinWidth } from "@indietabletop/appkit";
|
|
2
|
-
import { style } from "@vanilla-extract/css";
|
|
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 header = style({
|
|
16
|
-
marginBlockEnd: "3rem",
|
|
17
|
-
});
|
package/lib/Title/index.tsx
DELETED