finish register and login flow + suspense fallback fix

Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
mrjvs 2023-11-18 15:12:31 +01:00
parent 567c6a3894
commit 061c944034
14 changed files with 251 additions and 100 deletions

View file

@ -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>
); );
} }

View file

@ -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",

View 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}
/>
);
});

View 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>
);
}

View 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>
);
}

View file

@ -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>

View file

@ -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

View file

@ -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;
} }

View file

@ -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>,

View file

@ -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>
); );

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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"
}, }
}, }
}, }
}, }
}, }
}, }
}), })
], ]
}; };