mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-29 15:36:11 +00:00
finish register and login flow + suspense fallback fix
Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
parent
567c6a3894
commit
061c944034
|
@ -1,4 +1,4 @@
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { UserIcon } from "@/components/UserIcon";
|
||||||
import { AccountProfile } from "@/pages/parts/auth/AccountCreatePart";
|
import { AccountProfile } from "@/pages/parts/auth/AccountCreatePart";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
|
||||||
|
@ -6,13 +6,7 @@ export interface AvatarProps {
|
||||||
profile: AccountProfile["profile"];
|
profile: AccountProfile["profile"];
|
||||||
}
|
}
|
||||||
|
|
||||||
const possibleIcons = ["bookmark"] as const;
|
|
||||||
const avatarIconMap: Record<(typeof possibleIcons)[number], Icons> = {
|
|
||||||
bookmark: Icons.BOOKMARK,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Avatar(props: AvatarProps) {
|
export function Avatar(props: AvatarProps) {
|
||||||
const icon = (avatarIconMap as any)[props.profile.icon] ?? Icons.X;
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="h-[2em] w-[2em] rounded-full overflow-hidden flex items-center justify-center text-white"
|
className="h-[2em] w-[2em] rounded-full overflow-hidden flex items-center justify-center text-white"
|
||||||
|
@ -20,7 +14,7 @@ export function Avatar(props: AvatarProps) {
|
||||||
background: `linear-gradient(to bottom right, ${props.profile.colorA}, ${props.profile.colorB})`,
|
background: `linear-gradient(to bottom right, ${props.profile.colorA}, ${props.profile.colorB})`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon={icon} />
|
<UserIcon icon={props.profile.icon as any} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode, useCallback } from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
import { Spinner } from "@/components/layout/Spinner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
icon?: Icons;
|
icon?: Icons;
|
||||||
|
@ -14,18 +15,24 @@ interface Props {
|
||||||
href?: string;
|
href?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
download?: string;
|
download?: string;
|
||||||
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Button(props: Props) {
|
export function Button(props: Props) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const { onClick, href, loading } = props;
|
||||||
|
const cb = useCallback(() => {
|
||||||
|
if (loading) return;
|
||||||
|
if (href) history.push(href);
|
||||||
|
else onClick?.();
|
||||||
|
}, [onClick, href, history, loading]);
|
||||||
|
|
||||||
let colorClasses = "bg-white hover:bg-gray-200 text-black";
|
let colorClasses = "bg-white hover:bg-gray-200 text-black";
|
||||||
if (props.theme === "purple")
|
if (props.theme === "purple")
|
||||||
colorClasses =
|
colorClasses = "bg-buttons-purple hover:bg-buttons-purpleHover text-white";
|
||||||
"bg-video-buttons-purple hover:bg-video-buttons-purpleHover text-white";
|
|
||||||
if (props.theme === "secondary")
|
if (props.theme === "secondary")
|
||||||
colorClasses =
|
colorClasses =
|
||||||
"bg-video-buttons-cancel hover:bg-video-buttons-cancelHover transition-colors duration-100 text-white";
|
"bg-buttons-cancel hover:bg-buttons-cancelHover transition-colors duration-100 text-white";
|
||||||
if (props.theme === "danger")
|
if (props.theme === "danger")
|
||||||
colorClasses = "bg-buttons-danger hover:bg-buttons-dangerHover text-white";
|
colorClasses = "bg-buttons-danger hover:bg-buttons-dangerHover text-white";
|
||||||
|
|
||||||
|
@ -48,19 +55,20 @@ export function Button(props: Props) {
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
{props.icon ? (
|
{props.icon && !props.loading ? (
|
||||||
<span className="mr-3 hidden md:inline-block">
|
<span className="mr-3 hidden md:inline-block">
|
||||||
<Icon icon={props.icon} />
|
<Icon icon={props.icon} />
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
{props.loading ? (
|
||||||
|
<span className="mr-3 inline-flex justify-center">
|
||||||
|
<Spinner className="text-lg" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
{props.children}
|
{props.children}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
function goTo(href: string) {
|
|
||||||
history.push(href);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
props.href &&
|
props.href &&
|
||||||
(props.href.startsWith("https://") || props.href?.startsWith("data:"))
|
(props.href.startsWith("https://") || props.href?.startsWith("data:"))
|
||||||
|
@ -79,13 +87,13 @@ export function Button(props: Props) {
|
||||||
|
|
||||||
if (props.href)
|
if (props.href)
|
||||||
return (
|
return (
|
||||||
<a className={classes} onClick={() => goTo(props.href || "")}>
|
<a className={classes} onClick={cb}>
|
||||||
{content}
|
{content}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button type="button" onClick={props.onClick} className={classes}>
|
<button type="button" onClick={cb} className={classes}>
|
||||||
{content}
|
{content}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@ -101,11 +109,10 @@ interface ButtonPlainProps {
|
||||||
export function ButtonPlain(props: ButtonPlainProps) {
|
export function ButtonPlain(props: ButtonPlainProps) {
|
||||||
let colorClasses = "bg-white hover:bg-gray-200 text-black";
|
let colorClasses = "bg-white hover:bg-gray-200 text-black";
|
||||||
if (props.theme === "purple")
|
if (props.theme === "purple")
|
||||||
colorClasses =
|
colorClasses = "bg-buttons-purple hover:bg-buttons-purpleHover text-white";
|
||||||
"bg-video-buttons-purple hover:bg-video-buttons-purpleHover text-white";
|
|
||||||
if (props.theme === "secondary")
|
if (props.theme === "secondary")
|
||||||
colorClasses =
|
colorClasses =
|
||||||
"bg-video-buttons-cancel hover:bg-video-buttons-cancelHover transition-colors duration-100 text-white";
|
"bg-buttons-cancel hover:bg-buttons-cancelHover transition-colors duration-100 text-white";
|
||||||
|
|
||||||
const classes = classNames(
|
const classes = classNames(
|
||||||
"cursor-pointer inline-flex items-center justify-center rounded-lg font-medium transition-[transform,background-color] duration-100 active:scale-105 md:px-8",
|
"cursor-pointer inline-flex items-center justify-center rounded-lg font-medium transition-[transform,background-color] duration-100 active:scale-105 md:px-8",
|
||||||
|
|
35
src/components/UserIcon.tsx
Normal file
35
src/components/UserIcon.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { memo } from "react";
|
||||||
|
|
||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
|
||||||
|
export enum UserIcons {
|
||||||
|
SEARCH = "search",
|
||||||
|
BOOKMARK = "bookmark",
|
||||||
|
CLOCK = "clock",
|
||||||
|
EYE_SLASH = "eyeSlash",
|
||||||
|
USER = "user",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserIconProps {
|
||||||
|
icon: UserIcons;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconList: Record<UserIcons, string> = {
|
||||||
|
search: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z"/></svg>`,
|
||||||
|
bookmark: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M384 48V512l-192-112L0 512V48C0 21.5 21.5 0 48 0h288C362.5 0 384 21.5 384 48z"/></svg>`,
|
||||||
|
clock: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512zM232 256C232 264 236 271.5 242.7 275.1L338.7 339.1C349.7 347.3 364.6 344.3 371.1 333.3C379.3 322.3 376.3 307.4 365.3 300L280 243.2V120C280 106.7 269.3 96 255.1 96C242.7 96 231.1 106.7 231.1 120L232 256z"/></svg>`,
|
||||||
|
eyeSlash: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 640 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M150.7 92.77C195 58.27 251.8 32 320 32C400.8 32 465.5 68.84 512.6 112.6C559.4 156 590.7 207.1 605.5 243.7C608.8 251.6 608.8 260.4 605.5 268.3C592.1 300.6 565.2 346.1 525.6 386.7L630.8 469.1C641.2 477.3 643.1 492.4 634.9 502.8C626.7 513.2 611.6 515.1 601.2 506.9L9.196 42.89C-1.236 34.71-3.065 19.63 5.112 9.196C13.29-1.236 28.37-3.065 38.81 5.112L150.7 92.77zM223.1 149.5L313.4 220.3C317.6 211.8 320 202.2 320 191.1C320 180.5 316.1 169.7 311.6 160.4C314.4 160.1 317.2 159.1 320 159.1C373 159.1 416 202.1 416 255.1C416 269.7 413.1 282.7 407.1 294.5L446.6 324.7C457.7 304.3 464 280.9 464 255.1C464 176.5 399.5 111.1 320 111.1C282.7 111.1 248.6 126.2 223.1 149.5zM320 480C239.2 480 174.5 443.2 127.4 399.4C80.62 355.1 49.34 304 34.46 268.3C31.18 260.4 31.18 251.6 34.46 243.7C44 220.8 60.29 191.2 83.09 161.5L177.4 235.8C176.5 242.4 176 249.1 176 255.1C176 335.5 240.5 400 320 400C338.7 400 356.6 396.4 373 389.9L446.2 447.5C409.9 467.1 367.8 480 320 480H320z"/></svg>`,
|
||||||
|
user: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-user"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserIcon = memo((props: UserIconProps) => {
|
||||||
|
const icon = iconList[props.icon];
|
||||||
|
if (!icon) return <Icon icon={Icons.X} />;
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
dangerouslySetInnerHTML={{ __html: icon }} // eslint-disable-line react/no-danger
|
||||||
|
className={props.className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
39
src/components/form/ColorPicker.tsx
Normal file
39
src/components/form/ColorPicker.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import { Icon, Icons } from "../Icon";
|
||||||
|
|
||||||
|
export function ColorPicker(props: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onInput: (v: string) => void;
|
||||||
|
}) {
|
||||||
|
// Migrate this to another file later
|
||||||
|
const colors = ["#2E65CF", "#7652DD", "#CF2E68", "#C2CF2E", "#2ECFA8"];
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{props.label ? (
|
||||||
|
<p className="font-bold text-white">{props.label}</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
{colors.map((color) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className={classNames(
|
||||||
|
"w-full h-10 rounded flex justify-center items-center text-white pointer border-2 border-opacity-10 cursor-pointer",
|
||||||
|
props.value === color ? "border-white" : "border-transparent"
|
||||||
|
)}
|
||||||
|
onClick={() => props.onInput(color)}
|
||||||
|
style={{ backgroundColor: color }}
|
||||||
|
key={color}
|
||||||
|
>
|
||||||
|
{props.value === color ? <Icon icon={Icons.CHECKMARK} /> : null}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
47
src/components/form/IconPicker.tsx
Normal file
47
src/components/form/IconPicker.tsx
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import { UserIcon, UserIcons } from "../UserIcon";
|
||||||
|
|
||||||
|
export function IconPicker(props: {
|
||||||
|
label: string;
|
||||||
|
value: UserIcons;
|
||||||
|
onInput: (v: UserIcons) => void;
|
||||||
|
}) {
|
||||||
|
// Migrate this to another file later
|
||||||
|
const icons = [
|
||||||
|
UserIcons.USER,
|
||||||
|
UserIcons.BOOKMARK,
|
||||||
|
UserIcons.CLOCK,
|
||||||
|
UserIcons.EYE_SLASH,
|
||||||
|
UserIcons.SEARCH,
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{props.label ? (
|
||||||
|
<p className="font-bold text-white">{props.label}</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
{icons.map((icon) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className={classNames(
|
||||||
|
"w-full h-10 rounded flex justify-center items-center text-white pointer border-2 border-opacity-10 cursor-pointer",
|
||||||
|
props.value === icon
|
||||||
|
? "bg-buttons-purple border-white"
|
||||||
|
: "bg-authentication-inputBg border-transparent"
|
||||||
|
)}
|
||||||
|
onClick={() => props.onInput(icon)}
|
||||||
|
key={icon}
|
||||||
|
>
|
||||||
|
<UserIcon className="text-xl" icon={icon} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -38,7 +38,7 @@ export function LargeCardText(props: {
|
||||||
|
|
||||||
export function LargeCardButtons(props: { children: React.ReactNode }) {
|
export function LargeCardButtons(props: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center mt-8">
|
<div className="flex justify-center mt-12">
|
||||||
<div className="mx-auto inline-grid grid-cols-1 gap-3 justify-center items-center">
|
<div className="mx-auto inline-grid grid-cols-1 gap-3 justify-center items-center">
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -93,14 +93,14 @@ export function NextEpisodeButton(props: {
|
||||||
])}
|
])}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
className="py-px box-content bg-video-buttons-secondary hover:bg-video-buttons-secondaryHover bg-opacity-90 text-video-buttons-secondaryText"
|
className="py-px box-content bg-buttons-secondary hover:bg-buttons-secondaryHover bg-opacity-90 text-buttons-secondaryText"
|
||||||
onClick={hideNextEpisodeButton}
|
onClick={hideNextEpisodeButton}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => loadNextEpisode()}
|
onClick={() => loadNextEpisode()}
|
||||||
className="bg-video-buttons-primary hover:bg-video-buttons-primaryHover text-video-buttons-primaryText flex justify-center items-center"
|
className="bg-buttons-primary hover:bg-buttons-primaryHover text-buttons-primaryText flex justify-center items-center"
|
||||||
>
|
>
|
||||||
<Icon className="text-xl mr-1" icon={Icons.SKIP_EPISODE} />
|
<Icon className="text-xl mr-1" icon={Icons.SKIP_EPISODE} />
|
||||||
Next episode
|
Next episode
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useRef } from "react";
|
||||||
import { useAsync, useInterval } from "react-use";
|
import { useAsync, useInterval } from "react-use";
|
||||||
|
|
||||||
import { useAuth } from "@/hooks/auth/useAuth";
|
import { useAuth } from "@/hooks/auth/useAuth";
|
||||||
|
@ -6,11 +7,18 @@ const AUTH_CHECK_INTERVAL = 12 * 60 * 60 * 1000;
|
||||||
|
|
||||||
export function useAuthRestore() {
|
export function useAuthRestore() {
|
||||||
const { restore } = useAuth();
|
const { restore } = useAuth();
|
||||||
|
const hasRestored = useRef(false);
|
||||||
|
|
||||||
useInterval(() => {
|
useInterval(() => {
|
||||||
restore();
|
restore();
|
||||||
}, AUTH_CHECK_INTERVAL);
|
}, AUTH_CHECK_INTERVAL);
|
||||||
|
|
||||||
const result = useAsync(() => restore(), [restore]);
|
const result = useAsync(async () => {
|
||||||
|
if (hasRestored.current) return;
|
||||||
|
await restore().finally(() => {
|
||||||
|
hasRestored.current = true;
|
||||||
|
});
|
||||||
|
}, []); // no deps because we don't want to it ever rerun after the first time
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import "core-js/stable";
|
import "core-js/stable";
|
||||||
import React from "react";
|
import React, { Suspense } from "react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { HelmetProvider } from "react-helmet-async";
|
import { HelmetProvider } from "react-helmet-async";
|
||||||
|
@ -32,10 +32,15 @@ registerSW({
|
||||||
immediate: true,
|
immediate: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function LoadingScreen(props: { type: "user" | "lazy" }) {
|
||||||
|
return <p>Loading: {props.type}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
function AuthWrapper() {
|
function AuthWrapper() {
|
||||||
const status = useAuthRestore();
|
const status = useAuthRestore();
|
||||||
|
|
||||||
if (status.loading) return <p>Fetching user data</p>;
|
// TODO what to do when failing to load user data?
|
||||||
|
if (status.loading) return <LoadingScreen type="user" />;
|
||||||
if (status.error) return <p>Failed to fetch user data</p>;
|
if (status.error) return <p>Failed to fetch user data</p>;
|
||||||
return <App />;
|
return <App />;
|
||||||
}
|
}
|
||||||
|
@ -62,9 +67,11 @@ ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<HelmetProvider>
|
<HelmetProvider>
|
||||||
<TheRouter>
|
<Suspense fallback={<LoadingScreen type="lazy" />}>
|
||||||
<MigrationRunner />
|
<TheRouter>
|
||||||
</TheRouter>
|
<MigrationRunner />
|
||||||
|
</TheRouter>
|
||||||
|
</Suspense>
|
||||||
</HelmetProvider>
|
</HelmetProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3";
|
import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
import { MetaResponse } from "@/backend/accounts/meta";
|
import { MetaResponse } from "@/backend/accounts/meta";
|
||||||
import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
|
import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
|
||||||
|
@ -24,13 +25,12 @@ function CaptchaProvider(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RegisterPage() {
|
export function RegisterPage() {
|
||||||
|
const history = useHistory();
|
||||||
const [step, setStep] = useState(0);
|
const [step, setStep] = useState(0);
|
||||||
const [mnemonic, setMnemonic] = useState<null | string>(null);
|
const [mnemonic, setMnemonic] = useState<null | string>(null);
|
||||||
const [account, setAccount] = useState<null | AccountProfile>(null);
|
const [account, setAccount] = useState<null | AccountProfile>(null);
|
||||||
const [siteKey, setSiteKey] = useState<string | null>(null);
|
const [siteKey, setSiteKey] = useState<string | null>(null);
|
||||||
|
|
||||||
// TODO because of user data loading (in useAuthRestore()), the register page gets unmounted before finishing the register flow
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CaptchaProvider siteKey={siteKey}>
|
<CaptchaProvider siteKey={siteKey}>
|
||||||
<SubPageLayout>
|
<SubPageLayout>
|
||||||
|
@ -68,11 +68,10 @@ export function RegisterPage() {
|
||||||
mnemonic={mnemonic}
|
mnemonic={mnemonic}
|
||||||
userData={account}
|
userData={account}
|
||||||
onNext={() => {
|
onNext={() => {
|
||||||
setStep(4);
|
history.push("/");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{step === 4 ? <p>Success, account now exists</p> : null}
|
|
||||||
</SubPageLayout>
|
</SubPageLayout>
|
||||||
</CaptchaProvider>
|
</CaptchaProvider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
|
import { ColorPicker } from "@/components/form/ColorPicker";
|
||||||
|
import { IconPicker } from "@/components/form/IconPicker";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import {
|
import {
|
||||||
LargeCard,
|
LargeCard,
|
||||||
|
@ -8,6 +10,7 @@ import {
|
||||||
LargeCardText,
|
LargeCardText,
|
||||||
} from "@/components/layout/LargeCard";
|
} from "@/components/layout/LargeCard";
|
||||||
import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
|
import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
|
||||||
|
import { UserIcons } from "@/components/UserIcon";
|
||||||
|
|
||||||
export interface AccountProfile {
|
export interface AccountProfile {
|
||||||
device: string;
|
device: string;
|
||||||
|
@ -24,18 +27,21 @@ interface AccountCreatePartProps {
|
||||||
|
|
||||||
export function AccountCreatePart(props: AccountCreatePartProps) {
|
export function AccountCreatePart(props: AccountCreatePartProps) {
|
||||||
const [device, setDevice] = useState("");
|
const [device, setDevice] = useState("");
|
||||||
|
const [colorA, setColorA] = useState("#2E65CF");
|
||||||
|
const [colorB, setColorB] = useState("#2E65CF");
|
||||||
|
const [userIcon, setUserIcon] = useState<UserIcons>(UserIcons.USER);
|
||||||
// TODO validate device and account before next step
|
// TODO validate device and account before next step
|
||||||
|
|
||||||
const nextStep = useCallback(() => {
|
const nextStep = useCallback(() => {
|
||||||
props.onNext?.({
|
props.onNext?.({
|
||||||
device,
|
device,
|
||||||
profile: {
|
profile: {
|
||||||
colorA: "#fff",
|
colorA,
|
||||||
colorB: "#000",
|
colorB,
|
||||||
icon: "brush",
|
icon: userIcon,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [device, props]);
|
}, [device, props, colorA, colorB, userIcon]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LargeCard>
|
<LargeCard>
|
||||||
|
@ -45,12 +51,17 @@ export function AccountCreatePart(props: AccountCreatePartProps) {
|
||||||
>
|
>
|
||||||
Set up your account.... OR ELSE!
|
Set up your account.... OR ELSE!
|
||||||
</LargeCardText>
|
</LargeCardText>
|
||||||
<AuthInputBox
|
<div className="space-y-6">
|
||||||
label="Device name"
|
<AuthInputBox
|
||||||
value={device}
|
label="Device name"
|
||||||
onChange={setDevice}
|
value={device}
|
||||||
placeholder="Muad'Dib's Nintendo Switch"
|
onChange={setDevice}
|
||||||
/>
|
placeholder="Muad'Dib's Nintendo Switch"
|
||||||
|
/>
|
||||||
|
<ColorPicker label="First color" value={colorA} onInput={setColorA} />
|
||||||
|
<ColorPicker label="Second color" value={colorB} onInput={setColorB} />
|
||||||
|
<IconPicker label="User icon" value={userIcon} onInput={setUserIcon} />
|
||||||
|
</div>
|
||||||
<LargeCardButtons>
|
<LargeCardButtons>
|
||||||
<Button theme="purple" onClick={() => nextStep()}>
|
<Button theme="purple" onClick={() => nextStep()}>
|
||||||
Next
|
Next
|
||||||
|
|
|
@ -27,7 +27,6 @@ export function LoginFormPart(props: LoginFormPartProps) {
|
||||||
if (!verifyValidMnemonic(inputMnemonic))
|
if (!verifyValidMnemonic(inputMnemonic))
|
||||||
throw new Error("Invalid or incomplete passphrase");
|
throw new Error("Invalid or incomplete passphrase");
|
||||||
|
|
||||||
// TODO captcha?
|
|
||||||
await login({
|
await login({
|
||||||
mnemonic: inputMnemonic,
|
mnemonic: inputMnemonic,
|
||||||
userData: {
|
userData: {
|
||||||
|
@ -64,7 +63,6 @@ export function LoginFormPart(props: LoginFormPartProps) {
|
||||||
onChange={setDevice}
|
onChange={setDevice}
|
||||||
placeholder="Device"
|
placeholder="Device"
|
||||||
/>
|
/>
|
||||||
{result.loading ? <p>Loading...</p> : null}
|
|
||||||
{result.error && !result.loading ? (
|
{result.error && !result.loading ? (
|
||||||
<p className="text-authentication-errorText">
|
<p className="text-authentication-errorText">
|
||||||
{result.error.message}
|
{result.error.message}
|
||||||
|
@ -73,7 +71,11 @@ export function LoginFormPart(props: LoginFormPartProps) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LargeCardButtons>
|
<LargeCardButtons>
|
||||||
<Button theme="purple" onClick={() => execute(mnemonic, device)}>
|
<Button
|
||||||
|
theme="purple"
|
||||||
|
loading={result.loading}
|
||||||
|
onClick={() => execute(mnemonic, device)}
|
||||||
|
>
|
||||||
LET ME IN!
|
LET ME IN!
|
||||||
</Button>
|
</Button>
|
||||||
</LargeCardButtons>
|
</LargeCardButtons>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import { GoogleReCaptcha, useGoogleReCaptcha } from "react-google-recaptcha-v3";
|
import { useGoogleReCaptcha } from "react-google-recaptcha-v3";
|
||||||
import { useAsyncFn } from "react-use";
|
import { useAsyncFn } from "react-use";
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
|
@ -77,7 +77,11 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
<LargeCardButtons>
|
<LargeCardButtons>
|
||||||
<Button theme="purple" onClick={() => execute(mnemonic)}>
|
<Button
|
||||||
|
theme="purple"
|
||||||
|
loading={result.loading}
|
||||||
|
onClick={() => execute(mnemonic)}
|
||||||
|
>
|
||||||
Register
|
Register
|
||||||
</Button>
|
</Button>
|
||||||
</LargeCardButtons>
|
</LargeCardButtons>
|
||||||
|
|
|
@ -26,23 +26,23 @@ module.exports = {
|
||||||
"ash-400": "#3D394D",
|
"ash-400": "#3D394D",
|
||||||
"ash-300": "#2C293A",
|
"ash-300": "#2C293A",
|
||||||
"ash-200": "#2B2836",
|
"ash-200": "#2B2836",
|
||||||
"ash-100": "#1E1C26",
|
"ash-100": "#1E1C26"
|
||||||
},
|
},
|
||||||
|
|
||||||
/* fonts */
|
/* fonts */
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
"open-sans": "'Open Sans'",
|
"open-sans": "'Open Sans'"
|
||||||
},
|
},
|
||||||
|
|
||||||
/* animations */
|
/* animations */
|
||||||
keyframes: {
|
keyframes: {
|
||||||
"loading-pin": {
|
"loading-pin": {
|
||||||
"0%, 40%, 100%": { height: "0.5em", "background-color": "#282336" },
|
"0%, 40%, 100%": { height: "0.5em", "background-color": "#282336" },
|
||||||
"20%": { height: "1em", "background-color": "white" },
|
"20%": { height: "1em", "background-color": "white" }
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
animation: { "loading-pin": "loading-pin 1.8s ease-in-out infinite" },
|
animation: { "loading-pin": "loading-pin 1.8s ease-in-out infinite" }
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
require("tailwind-scrollbar"),
|
require("tailwind-scrollbar"),
|
||||||
|
@ -52,18 +52,18 @@ module.exports = {
|
||||||
colors: {
|
colors: {
|
||||||
// Branding
|
// Branding
|
||||||
pill: {
|
pill: {
|
||||||
background: "#1C1C36",
|
background: "#1C1C36"
|
||||||
},
|
},
|
||||||
|
|
||||||
// meta data for the theme itself
|
// meta data for the theme itself
|
||||||
global: {
|
global: {
|
||||||
accentA: "#505DBD",
|
accentA: "#505DBD",
|
||||||
accentB: "#3440A1",
|
accentB: "#3440A1"
|
||||||
},
|
},
|
||||||
|
|
||||||
// light bar
|
// light bar
|
||||||
lightBar: {
|
lightBar: {
|
||||||
light: "#2A2A71",
|
light: "#2A2A71"
|
||||||
},
|
},
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
|
@ -72,13 +72,24 @@ module.exports = {
|
||||||
toggleDisabled: "#202836",
|
toggleDisabled: "#202836",
|
||||||
danger: "#792131",
|
danger: "#792131",
|
||||||
dangerHover: "#8a293b",
|
dangerHover: "#8a293b",
|
||||||
|
|
||||||
|
secondary: "#161F25",
|
||||||
|
secondaryText: "#8EA3B0",
|
||||||
|
secondaryHover: "#1B262E",
|
||||||
|
primary: "#fff",
|
||||||
|
primaryText: "#000",
|
||||||
|
primaryHover: "#dedede",
|
||||||
|
purple: "#6b298a",
|
||||||
|
purpleHover: "#7f35a1",
|
||||||
|
cancel: "#252533",
|
||||||
|
cancelHover: "#3C3C4A"
|
||||||
},
|
},
|
||||||
|
|
||||||
// only used for body colors/textures
|
// only used for body colors/textures
|
||||||
background: {
|
background: {
|
||||||
main: "#0A0A10",
|
main: "#0A0A10",
|
||||||
accentA: "#6E3B80",
|
accentA: "#6E3B80",
|
||||||
accentB: "#1F1F50",
|
accentB: "#1F1F50"
|
||||||
},
|
},
|
||||||
|
|
||||||
// typography
|
// typography
|
||||||
|
@ -87,7 +98,7 @@ module.exports = {
|
||||||
text: "#73739D",
|
text: "#73739D",
|
||||||
dimmed: "#926CAD",
|
dimmed: "#926CAD",
|
||||||
divider: "#262632",
|
divider: "#262632",
|
||||||
secondary: "#64647B",
|
secondary: "#64647B"
|
||||||
},
|
},
|
||||||
|
|
||||||
// search bar
|
// search bar
|
||||||
|
@ -96,7 +107,7 @@ module.exports = {
|
||||||
focused: "#24243C",
|
focused: "#24243C",
|
||||||
placeholder: "#4A4A71",
|
placeholder: "#4A4A71",
|
||||||
icon: "#545476",
|
icon: "#545476",
|
||||||
text: "#FFFFFF",
|
text: "#FFFFFF"
|
||||||
},
|
},
|
||||||
|
|
||||||
// media cards
|
// media cards
|
||||||
|
@ -108,13 +119,13 @@ module.exports = {
|
||||||
barColor: "#4B4B63",
|
barColor: "#4B4B63",
|
||||||
barFillColor: "#BA7FD6",
|
barFillColor: "#BA7FD6",
|
||||||
badge: "#151522",
|
badge: "#151522",
|
||||||
badgeText: "#5F5F7A",
|
badgeText: "#5F5F7A"
|
||||||
},
|
},
|
||||||
|
|
||||||
// Large card
|
// Large card
|
||||||
largeCard: {
|
largeCard: {
|
||||||
background: "#171728",
|
background: "#171728",
|
||||||
icon: "#6741A5",
|
icon: "#6741A5"
|
||||||
},
|
},
|
||||||
|
|
||||||
// Passphrase
|
// Passphrase
|
||||||
|
@ -124,7 +135,7 @@ module.exports = {
|
||||||
wordBackground: "#171728",
|
wordBackground: "#171728",
|
||||||
copyText: "#58587A",
|
copyText: "#58587A",
|
||||||
copyTextHover: "#8888AA",
|
copyTextHover: "#8888AA",
|
||||||
errorText: "#DB3D62",
|
errorText: "#DB3D62"
|
||||||
},
|
},
|
||||||
|
|
||||||
// Settings page
|
// Settings page
|
||||||
|
@ -137,19 +148,19 @@ module.exports = {
|
||||||
inactive: "#8D68A9",
|
inactive: "#8D68A9",
|
||||||
icon: "#926CAD",
|
icon: "#926CAD",
|
||||||
iconActivated: "#6942A8",
|
iconActivated: "#6942A8",
|
||||||
activated: "#CBA1E8",
|
activated: "#CBA1E8"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
card: {
|
card: {
|
||||||
border: "#2A243E",
|
border: "#2A243E",
|
||||||
background: "#29243D",
|
background: "#29243D",
|
||||||
altBackground: "#29243D",
|
altBackground: "#29243D"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
utils: {
|
utils: {
|
||||||
divider: "#353549",
|
divider: "#353549"
|
||||||
},
|
},
|
||||||
|
|
||||||
// Error page
|
// Error page
|
||||||
|
@ -158,20 +169,20 @@ module.exports = {
|
||||||
border: "#252534",
|
border: "#252534",
|
||||||
|
|
||||||
type: {
|
type: {
|
||||||
secondary: "#62627D",
|
secondary: "#62627D"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// About page
|
// About page
|
||||||
about: {
|
about: {
|
||||||
circle: "#262632",
|
circle: "#262632",
|
||||||
circleText: "#9A9AC3",
|
circleText: "#9A9AC3"
|
||||||
},
|
},
|
||||||
|
|
||||||
progress: {
|
progress: {
|
||||||
background: "#8787A8",
|
background: "#8787A8",
|
||||||
preloaded: "#8787A8",
|
preloaded: "#8787A8",
|
||||||
filled: "#A75FC9",
|
filled: "#A75FC9"
|
||||||
},
|
},
|
||||||
|
|
||||||
// video player
|
// video player
|
||||||
|
@ -183,24 +194,11 @@ module.exports = {
|
||||||
error: "#E44F4F",
|
error: "#E44F4F",
|
||||||
success: "#40B44B",
|
success: "#40B44B",
|
||||||
loading: "#B759D8",
|
loading: "#B759D8",
|
||||||
noresult: "#64647B",
|
noresult: "#64647B"
|
||||||
},
|
},
|
||||||
|
|
||||||
audio: {
|
audio: {
|
||||||
set: "#A75FC9",
|
set: "#A75FC9"
|
||||||
},
|
|
||||||
|
|
||||||
buttons: {
|
|
||||||
secondary: "#161F25",
|
|
||||||
secondaryText: "#8EA3B0",
|
|
||||||
secondaryHover: "#1B262E",
|
|
||||||
primary: "#fff",
|
|
||||||
primaryText: "#000",
|
|
||||||
primaryHover: "#dedede",
|
|
||||||
purple: "#6b298a",
|
|
||||||
purpleHover: "#7f35a1",
|
|
||||||
cancel: "#252533",
|
|
||||||
cancelHover: "#3C3C4A",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
context: {
|
context: {
|
||||||
|
@ -220,19 +218,19 @@ module.exports = {
|
||||||
|
|
||||||
buttons: {
|
buttons: {
|
||||||
list: "#161C26",
|
list: "#161C26",
|
||||||
active: "#0D1317",
|
active: "#0D1317"
|
||||||
},
|
},
|
||||||
|
|
||||||
type: {
|
type: {
|
||||||
main: "#617A8A",
|
main: "#617A8A",
|
||||||
secondary: "#374A56",
|
secondary: "#374A56",
|
||||||
accent: "#A570FA",
|
accent: "#A570FA"
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
}),
|
})
|
||||||
],
|
]
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue