error handle pages + migration page

Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
mrjvs 2023-10-24 20:34:54 +02:00
parent 7731938729
commit 6a125a593d
14 changed files with 237 additions and 276 deletions

View File

@ -67,3 +67,33 @@ export function Button(props: Props) {
</button>
);
}
interface ButtonPlainProps {
onClick?: () => void;
children?: ReactNode;
theme?: "white" | "purple" | "secondary";
className?: string;
}
export function ButtonPlain(props: ButtonPlainProps) {
let colorClasses = "bg-white hover:bg-gray-200 text-black";
if (props.theme === "purple")
colorClasses =
"bg-video-buttons-purple hover:bg-video-buttons-purpleHover text-white";
if (props.theme === "secondary")
colorClasses =
"bg-video-buttons-cancel hover:bg-video-buttons-cancelHover transition-colors duration-100 text-white";
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",
"px-4 py-3",
props.className,
colorClasses
);
return (
<button type="button" onClick={props.onClick} className={classes}>
{props.children}
</button>
);
}

View File

@ -1,114 +0,0 @@
import { Component } from "react";
import { Trans, useTranslation } from "react-i18next";
import { IconPatch } from "@/components/buttons/IconPatch";
import { Icons } from "@/components/Icon";
import { Link } from "@/components/text/Link";
import { Title } from "@/components/text/Title";
import { conf } from "@/setup/config";
interface ErrorShowcaseProps {
error: {
name: string;
description: string;
path: string;
};
}
export function ErrorShowcase(props: ErrorShowcaseProps) {
return (
<div className="w-4xl mt-12 max-w-full rounded bg-denim-300 px-6 py-4">
<p className="mb-1 break-words font-bold text-white">
{props.error.name} - {props.error.description}
</p>
<p className="break-words">{props.error.path}</p>
</div>
);
}
interface ErrorMessageProps {
error?: {
name: string;
description: string;
path: string;
};
localSize?: boolean;
children?: React.ReactNode;
}
export function ErrorMessage(props: ErrorMessageProps) {
const { t } = useTranslation();
return (
<div
className={`${
props.localSize ? "h-full" : "min-h-screen"
} flex w-full flex-col items-center justify-center px-4 py-12`}
>
<div className="flex flex-col items-center justify-start text-center">
<IconPatch icon={Icons.WARNING} className="mb-6 text-red-400" />
<Title>{t("media.errors.genericTitle")}</Title>
{props.children ? (
<p className="my-6 max-w-lg">{props.children}</p>
) : (
<p className="my-6 max-w-lg">
<Trans i18nKey="media.errors.videoFailed">
<Link url={conf().DISCORD_LINK} newTab />
<Link url={conf().GITHUB_LINK} newTab />
</Trans>
</p>
)}
</div>
{props.error ? <ErrorShowcase error={props.error} /> : null}
</div>
);
}
interface ErrorBoundaryState {
hasError: boolean;
error?: {
name: string;
description: string;
path: string;
};
}
export class ErrorBoundary extends Component<
Record<string, unknown>,
ErrorBoundaryState
> {
constructor(props: { children: any }) {
super(props);
this.state = {
hasError: false,
};
}
static getDerivedStateFromError() {
return {
hasError: true,
};
}
componentDidCatch(error: any, errorInfo: any) {
console.error("Render error caught", error, errorInfo);
if (error instanceof Error) {
const realError: Error = error as Error;
this.setState((s) => ({
...s,
hasError: true,
error: {
name: realError.name,
description: realError.message,
path: errorInfo.componentStack.split("\n")[1],
},
}));
}
}
render() {
if (!this.state.hasError) return this.props.children as any;
return <ErrorMessage error={this.state.error} />;
}
}

View File

@ -144,13 +144,13 @@ export function ProgressBar() {
>
<div
className={[
"relative w-full h-1 bg-video-progress-background bg-opacity-25 rounded-full transition-[height] duration-100 group-hover:h-1.5",
"relative w-full h-1 bg-progress-background bg-opacity-25 rounded-full transition-[height] duration-100 group-hover:h-1.5",
dragging ? "!h-1.5" : "",
].join(" ")}
>
{/* Pre-loaded content bar */}
<div
className="absolute top-0 left-0 h-full rounded-full bg-video-progress-preloaded bg-opacity-50 flex justify-end items-center"
className="absolute top-0 left-0 h-full rounded-full bg-progress-preloaded bg-opacity-50 flex justify-end items-center"
style={{
width: `${(buffered / duration) * 100}%`,
}}
@ -158,7 +158,7 @@ export function ProgressBar() {
{/* Actual progress bar */}
<div
className="absolute top-0 left-0 h-full rounded-full bg-video-progress-watched flex justify-end items-center"
className="absolute top-0 left-0 h-full rounded-full bg-progress-filled flex justify-end items-center"
style={{
width: `${
Math.max(

View File

@ -31,11 +31,15 @@ export function EmbedOption(props: {
const setSource = usePlayerStore((s) => s.setSource);
const setSourceId = usePlayerStore((s) => s.setSourceId);
const progress = usePlayerStore((s) => s.progress.time);
const unknownEmbedName = "Unknown";
const embedName = useMemo(() => {
if (!props.embedId) return "...";
if (!props.embedId) return unknownEmbedName;
const sourceMeta = providers.getMetadata(props.embedId);
return sourceMeta?.name ?? "...";
return sourceMeta?.name ?? unknownEmbedName;
}, [props.embedId]);
const [request, run] = useAsyncFn(async () => {
const result = await providers.runEmbedScraper({
id: props.embedId,
@ -46,26 +50,14 @@ export function EmbedOption(props: {
router.close();
}, [props.embedId, props.sourceId, meta, router]);
let content: ReactNode = null;
if (request.loading)
content = (
<Menu.TextDisplay noIcon>
<Loading />
</Menu.TextDisplay>
);
else if (request.error)
content = (
<Menu.TextDisplay title="Failed to scrape">
We were unable to find any videos for this source. Don&apos;t come
bitchin&apos; to us about it, just try another source.
</Menu.TextDisplay>
);
return (
<SelectableLink onClick={run}>
<SelectableLink
loading={request.loading}
error={request.error}
onClick={run}
>
<span className="flex flex-col">
<span>{embedName}</span>
{content}
</span>
</SelectableLink>
);

View File

@ -6,7 +6,7 @@ import { HelmetProvider } from "react-helmet-async";
import { BrowserRouter, HashRouter } from "react-router-dom";
import { registerSW } from "virtual:pwa-register";
import { ErrorBoundary } from "@/components/layout/ErrorBoundary";
import { ErrorBoundary } from "@/pages/errors/ErrorBoundary";
import App from "@/setup/App";
import { conf } from "@/setup/config";
import i18n from "@/setup/i18n";

View File

@ -9,6 +9,7 @@ import { HomeLayout } from "@/pages/layouts/HomeLayout";
import { BookmarksPart } from "@/pages/parts/home/BookmarksPart";
import { HeroPart } from "@/pages/parts/home/HeroPart";
import { WatchingPart } from "@/pages/parts/home/WatchingPart";
import { MigrationPart } from "@/pages/parts/migrations/MigrationPart";
import { SearchListPart } from "@/pages/parts/search/SearchListPart";
import { SearchLoadingPart } from "@/pages/parts/search/SearchLoadingPart";
@ -38,6 +39,8 @@ export function HomePage() {
const [search] = searchParams;
const s = useSearch(search);
return <MigrationPart />;
return (
<HomeLayout showBg={showBg}>
<div className="mb-16 sm:mb-24">

View File

@ -1,80 +1,12 @@
import { OverlayAnchor } from "@/components/overlays/OverlayAnchor";
import { Overlay, OverlayDisplay } from "@/components/overlays/OverlayDisplay";
import { OverlayPage } from "@/components/overlays/OverlayPage";
import { OverlayRouter } from "@/components/overlays/OverlayRouter";
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { useState } from "react";
// simple empty view, perfect for putting in tests
import { Button } from "@/components/Button";
// mostly empty view, add whatever you need
export default function TestView() {
const router = useOverlayRouter("test");
const [val, setVal] = useState(false);
return (
<OverlayDisplay>
<div className="h-[400px] w-[800px] flex justify-center items-center">
<button
type="button"
onClick={() => {
router.open();
}}
>
Open
</button>
<OverlayAnchor
id={router.id}
className="h-20 w-20 hover:w-24 mt-[50rem] bg-white"
/>
<Overlay id={router.id}>
<OverlayRouter id={router.id}>
<OverlayPage id={router.id} path="/" width={400} height={400}>
<div className="bg-blue-900 p-4">
<p>HOME</p>
<button
type="button"
onClick={() => {
router.navigate("/two");
}}
>
open page two
</button>
<button
type="button"
onClick={() => {
router.navigate("/one");
}}
>
open page one
</button>
</div>
</OverlayPage>
<OverlayPage id={router.id} path="/one" width={300} height={300}>
<div className="bg-blue-900 p-4">
<p>ONE</p>
<button
type="button"
onClick={() => {
router.navigate("/");
}}
>
back home
</button>
</div>
</OverlayPage>
<OverlayPage id={router.id} path="/two" width={200} height={200}>
<div className="bg-blue-900 p-4">
<p>TWO</p>
<button
type="button"
onClick={() => {
router.navigate("/");
}}
>
back home
</button>
</div>
</OverlayPage>
</OverlayRouter>
</Overlay>
</div>
</OverlayDisplay>
);
if (val) throw new Error("I crashed");
return <Button onClick={() => setVal(true)}>Crash me!</Button>;
}

View File

@ -0,0 +1,44 @@
import { Component } from "react";
import { ErrorPart } from "@/pages/parts/errors/ErrorPart";
interface ErrorBoundaryState {
error?: {
error: any;
errorInfo: any;
};
}
export class ErrorBoundary extends Component<
Record<string, unknown>,
ErrorBoundaryState
> {
constructor(props: { children: any }) {
super(props);
this.state = {
error: undefined,
};
}
componentDidCatch(error: any, errorInfo: any) {
console.error("Render error caught", error, errorInfo);
this.setState((s) => ({
...s,
error: {
error,
errorInfo,
},
}));
}
render() {
if (!this.state.error) return this.props.children as any;
return (
<ErrorPart
error={this.state.error.error}
errorInfo={this.state.error.errorInfo}
/>
);
}
}

View File

@ -1,23 +1,5 @@
import { useTranslation } from "react-i18next";
import { IconPatch } from "@/components/buttons/IconPatch";
import { Icons } from "@/components/Icon";
import { ArrowLink } from "@/components/text/ArrowLink";
import { Title } from "@/components/text/Title";
import { ErrorWrapperPart } from "@/pages/parts/errors/ErrorWrapperPart";
import { NotFoundPart } from "@/pages/parts/errors/ErrorWrapperPart";
export function NotFoundPage() {
const { t } = useTranslation();
return (
<ErrorWrapperPart>
<IconPatch
icon={Icons.EYE_SLASH}
className="mb-6 text-xl text-bink-600"
/>
<Title>{t("notFound.page.title")}</Title>
<p className="mb-12 mt-5 max-w-sm">{t("notFound.page.description")}</p>
<ArrowLink to="/" linkText={t("notFound.backArrow")} />
</ErrorWrapperPart>
);
return <NotFoundPart />;
}

View File

@ -1,6 +1,16 @@
import { FooterView } from "@/components/layout/Footer";
import { Navigation } from "@/components/layout/Navigation";
export function BlurEllipsis() {
return (
<>
{/* Blur elipsis */}
<div className="absolute top-0 -right-48 rotate-[32deg] w-[50rem] h-[15rem] rounded-[70rem] bg-background-accentA blur-[100px] pointer-events-none opacity-25" />
<div className="absolute top-0 right-48 rotate-[32deg] w-[50rem] h-[15rem] rounded-[70rem] bg-background-accentB blur-[100px] pointer-events-none opacity-25" />
</>
);
}
export function SubPageLayout(props: { children: React.ReactNode }) {
return (
<div
@ -10,10 +20,7 @@ export function SubPageLayout(props: { children: React.ReactNode }) {
"linear-gradient(to bottom, var(--tw-gradient-from), var(--tw-gradient-to) 800px)",
}}
>
{/* Blur elipsis */}
<div className="absolute top-0 -right-48 rotate-[32deg] w-[50rem] h-[15rem] rounded-[70rem] bg-background-accentA blur-[100px] pointer-events-none opacity-25" />
<div className="absolute top-0 right-48 rotate-[32deg] w-[50rem] h-[15rem] rounded-[70rem] bg-background-accentB blur-[100px] pointer-events-none opacity-25" />
<BlurEllipsis />
{/* Main page */}
<FooterView>
<Navigation noLightbar />

View File

@ -0,0 +1,33 @@
import { ButtonPlain } from "@/components/Button";
import { Icons } from "@/components/Icon";
import { IconPill } from "@/components/layout/IconPill";
import { Title } from "@/components/text/Title";
import { Paragraph } from "@/components/utils/Text";
import { ErrorContainer, ErrorLayout } from "@/pages/layouts/ErrorLayout";
export function ErrorPart(props: { error: any; errorInfo: any }) {
const data = JSON.stringify({
error: props.error,
errorInfo: props.errorInfo,
});
return (
<div className="relative flex flex-1 flex-col">
<div className="flex h-full flex-1 flex-col items-center justify-center p-5 text-center">
<ErrorLayout>
<ErrorContainer>
<IconPill icon={Icons.EYE_SLASH}>It broke</IconPill>
<Title>Failed to load meta data</Title>
<Paragraph>{data}</Paragraph>
<ButtonPlain
theme="purple"
className="mt-6 md:px-12 p-2.5"
onClick={() => window.location.reload()}
>
Reload the page
</ButtonPlain>
</ErrorContainer>
</ErrorLayout>
</div>
</div>
);
}

View File

@ -1,10 +1,15 @@
import { ReactNode } from "react";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/Button";
import { Icons } from "@/components/Icon";
import { IconPill } from "@/components/layout/IconPill";
import { Navigation } from "@/components/layout/Navigation";
import { Title } from "@/components/text/Title";
import { Paragraph } from "@/components/utils/Text";
import { ErrorContainer, ErrorLayout } from "@/pages/layouts/ErrorLayout";
export function ErrorWrapperPart(props: { children?: ReactNode }) {
export function NotFoundPart() {
const { t } = useTranslation();
return (
@ -14,7 +19,28 @@ export function ErrorWrapperPart(props: { children?: ReactNode }) {
</Helmet>
<Navigation />
<div className="flex h-full flex-1 flex-col items-center justify-center p-5 text-center">
{props.children}
<ErrorLayout>
<ErrorContainer>
<IconPill icon={Icons.EYE_SLASH}>
{t("notFound.genericTitle")}
</IconPill>
<Title>Failed to load meta data</Title>
<Paragraph>
Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost
bestest, but alas, no wucky videos to be spotted anywhere (´ω`)
Please don&apos;t be angwy, wittle movie-web ish twying so hard.
Can you find it in your heart to forgive? UwU 💖
</Paragraph>
<Button
href="/"
theme="purple"
padding="md:px-12 p-2.5"
className="mt-6"
>
Go home
</Button>
</ErrorContainer>
</ErrorLayout>
</div>
</div>
);

View File

@ -0,0 +1,26 @@
import { BrandPill } from "@/components/layout/BrandPill";
import { Loading } from "@/components/layout/Loading";
import { BlurEllipsis } from "@/pages/layouts/SubPageLayout";
export function MigrationPart() {
return (
<div className="flex flex-col justify-center items-center h-screen text-center font-medium">
{/* Overlaid elements */}
<BlurEllipsis />
<div className="right-[calc(2rem+env(safe-area-inset-right))] top-6 absolute">
<BrandPill />
</div>
{/* Content */}
<Loading />
<p className="max-w-[19rem] mt-3 mb-12 text-type-secondary">
Please hold, we are migrating your data. This shouldn&apos;t take long.
Also, fuck you.
</p>
<div className="w-[8rem] h-1 rounded-full bg-progress-background bg-opacity-25 mb-2">
<div className="w-1/4 h-full bg-progress-filled rounded-full" />
</div>
<p>25%</p>
</div>
);
}

View File

@ -26,23 +26,23 @@ module.exports = {
"ash-400": "#3D394D",
"ash-300": "#2C293A",
"ash-200": "#2B2836",
"ash-100": "#1E1C26",
"ash-100": "#1E1C26"
},
/* fonts */
fontFamily: {
"open-sans": "'Open Sans'",
"open-sans": "'Open Sans'"
},
/* animations */
keyframes: {
"loading-pin": {
"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: [
require("tailwind-scrollbar"),
@ -52,31 +52,31 @@ module.exports = {
colors: {
// Branding
pill: {
background: "#1C1C36",
background: "#1C1C36"
},
// meta data for the theme itself
global: {
accentA: "#505DBD",
accentB: "#3440A1",
accentB: "#3440A1"
},
// light bar
lightBar: {
light: "#2A2A71",
light: "#2A2A71"
},
// Buttons
buttons: {
toggle: "#8D44D6",
toggleDisabled: "#202836",
toggleDisabled: "#202836"
},
// only used for body colors/textures
background: {
main: "#0A0A10",
accentA: "#6E3B80",
accentB: "#1F1F50",
accentB: "#1F1F50"
},
// typography
@ -85,7 +85,7 @@ module.exports = {
text: "#73739D",
dimmed: "#926CAD",
divider: "#262632",
secondary: "#64647B",
secondary: "#64647B"
},
// search bar
@ -94,7 +94,7 @@ module.exports = {
focused: "#24243C",
placeholder: "#4A4A71",
icon: "#545476",
text: "#FFFFFF",
text: "#FFFFFF"
},
// media cards
@ -106,7 +106,7 @@ module.exports = {
barColor: "#4B4B63",
barFillColor: "#BA7FD6",
badge: "#151522",
badgeText: "#5F5F7A",
badgeText: "#5F5F7A"
},
// Error page
@ -115,14 +115,20 @@ module.exports = {
border: "#252534",
type: {
secondary: "#62627D",
},
secondary: "#62627D"
}
},
// About page
about: {
circle: "#262632",
circleText: "#9A9AC3",
circleText: "#9A9AC3"
},
progress: {
background: "#8787A8",
preloaded: "#8787A8",
filled: "#A75FC9"
},
// video player
@ -134,17 +140,11 @@ module.exports = {
error: "#E44F4F",
success: "#40B44B",
loading: "#B759D8",
noresult: "#64647B",
},
progress: {
background: "#8787A8",
preloaded: "#8787A8",
watched: "#A75FC9",
noresult: "#64647B"
},
audio: {
set: "#A75FC9",
set: "#A75FC9"
},
buttons: {
@ -157,7 +157,7 @@ module.exports = {
purple: "#6b298a",
purpleHover: "#7f35a1",
cancel: "#252533",
cancelHover: "#3C3C4A",
cancelHover: "#3C3C4A"
},
context: {
@ -177,19 +177,19 @@ module.exports = {
buttons: {
list: "#161C26",
active: "#0D1317",
active: "#0D1317"
},
type: {
main: "#617A8A",
secondary: "#374A56",
accent: "#A570FA",
},
},
},
},
},
},
}),
],
accent: "#A570FA"
}
}
}
}
}
}
})
]
};