300 lines
8.7 KiB
TypeScript
300 lines
8.7 KiB
TypeScript
import {
|
|
ChakraProvider,
|
|
Container,
|
|
cookieStorageManagerSSR,
|
|
Flex,
|
|
Heading,
|
|
Link,
|
|
Spacer,
|
|
Text,
|
|
} from "@chakra-ui/react";
|
|
import { ClientStyleContext, ServerStyleContext } from "./context.js";
|
|
import fontStyle from "@fontsource-variable/plus-jakarta-sans/index.css";
|
|
import Forbidden from "../components/Forbidden.js";
|
|
import globalStyles from "../index.css";
|
|
import {
|
|
isRouteErrorResponse,
|
|
Links,
|
|
Meta,
|
|
Outlet,
|
|
Scripts,
|
|
useLoaderData,
|
|
useRouteError,
|
|
useRouteLoaderData,
|
|
} from "@remix-run/react";
|
|
import { type ErrorResponse } from "@remix-run/router";
|
|
import { LinksFunction } from "@remix-run/cloudflare";
|
|
import Login from "../components/Login.js";
|
|
import Navigation from "../components/Navigation.js";
|
|
import { type ReactNode, StrictMode, useContext, useEffect } from "react";
|
|
import theme from "../theme.js";
|
|
import { withEmotionCache } from "@emotion/react";
|
|
import {
|
|
captureRemixErrorBoundaryError,
|
|
setUser,
|
|
withSentry,
|
|
} from "@sentry/remix";
|
|
|
|
export function ErrorBoundary() {
|
|
const error = useRouteError() as ErrorResponse;
|
|
|
|
if (!isRouteErrorResponse(error))
|
|
return (
|
|
<DocumentWrapper loaderData={{ hide: true }}>
|
|
<Container maxW="container.lg" pt="8vh" textAlign="left">
|
|
<Heading size="4xl">???</Heading>
|
|
<br />
|
|
<Text fontSize="xl">Something bad happened!</Text>
|
|
<br />
|
|
<br />
|
|
<br />
|
|
<Text>Details: {error}</Text>
|
|
<br />
|
|
<br />
|
|
<Link color="#646cff" onClick={() => location.reload()}>
|
|
Refresh
|
|
</Link>
|
|
</Container>
|
|
</DocumentWrapper>
|
|
);
|
|
|
|
const { status } = error;
|
|
const loaderData = useRouteLoaderData<typeof loader>("root") || {};
|
|
|
|
switch (status) {
|
|
case 303:
|
|
return "";
|
|
|
|
case 401:
|
|
return (
|
|
<DocumentWrapper loaderData={loaderData}>
|
|
<Login />
|
|
</DocumentWrapper>
|
|
);
|
|
|
|
case 403:
|
|
return (
|
|
<DocumentWrapper loaderData={loaderData}>
|
|
<Forbidden />
|
|
</DocumentWrapper>
|
|
);
|
|
|
|
case 404:
|
|
return (
|
|
<DocumentWrapper loaderData={{ ...loaderData, hide: true }}>
|
|
<Container maxW="container.lg" pt="8vh" textAlign="left">
|
|
<Heading size="4xl">404</Heading>
|
|
<br />
|
|
<Text fontSize="xl">There is nothing to find here.</Text>
|
|
<br />
|
|
<br />
|
|
<br />
|
|
<Link color="#646cff" onClick={() => history.go(-1)}>
|
|
Go back
|
|
</Link>
|
|
</Container>
|
|
</DocumentWrapper>
|
|
);
|
|
|
|
case 503:
|
|
return (
|
|
<DocumentWrapper loaderData={{ hide: true }}>
|
|
<Container
|
|
left="50%"
|
|
maxW="container.md"
|
|
pos="absolute"
|
|
top="50%"
|
|
transform="translate(-50%, -50%)"
|
|
>
|
|
<Flex>
|
|
<Spacer />
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="128"
|
|
height="128"
|
|
fill="currentColor"
|
|
viewBox="0 0 16 16"
|
|
>
|
|
<path d="M12.496 8a4.5 4.5 0 0 1-1.703 3.526L9.497 8.5l2.959-1.11q.04.3.04.61" />
|
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-1 0a7 7 0 1 0-13.202 3.249l1.988-1.657a4.5 4.5 0 0 1 7.537-4.623L7.497 6.5l1 2.5 1.333 3.11c-.56.251-1.18.39-1.833.39a4.5 4.5 0 0 1-1.592-.29L4.747 14.2A7 7 0 0 0 15 8m-8.295.139a.25.25 0 0 0-.288-.376l-1.5.5.159.474.808-.27-.595.894a.25.25 0 0 0 .287.376l.808-.27-.595.894a.25.25 0 0 0 .287.376l1.5-.5-.159-.474-.808.27.596-.894a.25.25 0 0 0-.288-.376l-.808.27z" />
|
|
</svg>
|
|
<Spacer />
|
|
</Flex>
|
|
<br />
|
|
<Heading textAlign="center">
|
|
The engineers are breaking stuff again
|
|
</Heading>
|
|
<br />
|
|
<Text textAlign="center">
|
|
Someday they will finish, come back later.
|
|
</Text>
|
|
</Container>
|
|
</DocumentWrapper>
|
|
);
|
|
|
|
default:
|
|
captureRemixErrorBoundaryError(useRouteError());
|
|
return (
|
|
<DocumentWrapper loaderData={loaderData}>
|
|
<Container maxW="container.lg" pt="8vh" textAlign="left">
|
|
<Heading size="4xl">500</Heading>
|
|
<br />
|
|
<Text fontSize="xl">S̶̡͈̠̗̠͖͙̭o̶̶͕͚̥͍̪̤m̸̨͏͈͔̖͚̖̰̱͞e҉̵͖͚͇̀t̕͟͠͏͎̺̯̲̱̣̤̠̟͙̠̙̫̬ḩ̸̭͓̬͎̙̀į̞̮͉͖̰̥̹͚̫̙̪̗̜̳̕ͅn҉͔̯̪̗̝̝͖̲͇͍͎̲̲̤̖̫͈̪͡g̴̰̻̙̝͉̭͇̖̰̝̙͕̼͙͘͜ ̵̶̫̥̳̲̘̻̗͈͕̭̲͇̘̜̺̟̥̖̥b̴̙̭̹͕̞͠r̞͎̠̩͈̖̰̞̯̯͢͢͠ͅo̝̯̗̹̳͍̰͉͕̘̰̠̺̥̰͔̕ͅk̵̸̻̠͕̺̦̦͖̲̺̦̞̝̞͞͡e̶͏̤̼̼͔̘̰̰̭͈̀͞͡</Text>
|
|
<br />
|
|
<br />
|
|
<br />
|
|
<Link color="#646cff" onClick={() => location.reload()}>
|
|
Reload
|
|
</Link>
|
|
</Container>
|
|
</DocumentWrapper>
|
|
);
|
|
}
|
|
}
|
|
|
|
export const links: LinksFunction = () => {
|
|
return [
|
|
{ href: "/favicon.ico", rel: "icon" },
|
|
{ href: "/files/logo192.png", rel: "apple-touch-icon", type: "image/png" },
|
|
{ href: fontStyle, rel: "stylesheet" },
|
|
{ href: globalStyles, rel: "stylesheet" },
|
|
];
|
|
};
|
|
|
|
export async function loader({
|
|
context,
|
|
}: {
|
|
context: RequestContext;
|
|
}): Promise<{ [k: string]: any }> {
|
|
if (context.data.mx)
|
|
throw new Response(null, {
|
|
status: 503,
|
|
});
|
|
|
|
let data: { [k: string]: string } = {};
|
|
|
|
if (context.env.COMMIT_SHA) data.commit_sha = context.env.COMMIT_SHA;
|
|
if (context.data.current_user) data = { ...context.data.current_user };
|
|
if (context.env.REMIX_DSN) data.dsn = context.env.REMIX_DSN;
|
|
if (context.data.nonce) data.nonce = context.data.nonce;
|
|
if (context.data.theme) data.theme = context.data.theme;
|
|
|
|
return data;
|
|
}
|
|
|
|
export function meta() {
|
|
return [{ title: "Car Crushers" }];
|
|
}
|
|
|
|
function DocumentWrapper(props: {
|
|
loaderData: { [k: string]: any };
|
|
children: ReactNode;
|
|
}) {
|
|
const { children: child, loaderData } = props;
|
|
const Document = withEmotionCache(
|
|
({ children }: { children: ReactNode }, emotionCache) => {
|
|
const serverStyleData = useContext(ServerStyleContext);
|
|
const clientStyleData = useContext(ClientStyleContext);
|
|
|
|
useEffect(() => {
|
|
emotionCache.sheet.container = document.head;
|
|
const tags = emotionCache.sheet.tags;
|
|
|
|
emotionCache.sheet.flush();
|
|
tags.forEach((tag) => {
|
|
(emotionCache.sheet as any)._insertTag(tag);
|
|
});
|
|
|
|
clientStyleData?.reset();
|
|
}, []);
|
|
|
|
const body = (
|
|
<StrictMode>
|
|
<ChakraProvider
|
|
colorModeManager={cookieStorageManagerSSR(
|
|
typeof document === "undefined"
|
|
? `chakra-ui-color-mode=${loaderData.theme}`
|
|
: document.cookie,
|
|
)}
|
|
theme={theme}
|
|
>
|
|
<div className="App">
|
|
<Navigation {...loaderData} />
|
|
{children}
|
|
<Scripts />
|
|
</div>
|
|
</ChakraProvider>
|
|
</StrictMode>
|
|
);
|
|
|
|
return (
|
|
<html data-theme={loaderData.theme} lang="en-US">
|
|
<head>
|
|
<Links />
|
|
<style>
|
|
{`
|
|
:root {
|
|
color-scheme: ${loaderData.theme};
|
|
}
|
|
`}
|
|
</style>
|
|
{serverStyleData?.map(({ key, ids, css }) => (
|
|
<style
|
|
key={key}
|
|
data-emotion={`${key} ${ids.join(" ")}`}
|
|
dangerouslySetInnerHTML={{ __html: css }}
|
|
/>
|
|
))}
|
|
<meta charSet="UTF-8" />
|
|
{loaderData.commit_sha ? (
|
|
<meta name="commit_sha" content={loaderData.commit_sha} />
|
|
) : null}
|
|
{loaderData.dsn ? (
|
|
<meta name="dsn" content={loaderData.dsn} />
|
|
) : null}
|
|
<meta name="theme-color" content="#00a8f8" />
|
|
<meta
|
|
name="viewport"
|
|
content="width=device-width, initial-scale=1.0"
|
|
/>
|
|
<Meta />
|
|
</head>
|
|
<body
|
|
{...(loaderData.theme && {
|
|
className: `chakra-ui-${loaderData.theme}`,
|
|
})}
|
|
>
|
|
{body}
|
|
</body>
|
|
</html>
|
|
);
|
|
},
|
|
);
|
|
|
|
return <Document>{child}</Document>;
|
|
}
|
|
|
|
function App() {
|
|
const loaderData = useLoaderData<typeof loader>();
|
|
|
|
if (
|
|
loaderData.id &&
|
|
[0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].find(
|
|
(p) => loaderData.permissions & (1 << p),
|
|
)
|
|
)
|
|
setUser({
|
|
email: loaderData.email,
|
|
id: loaderData.id,
|
|
username: loaderData.username,
|
|
});
|
|
|
|
return (
|
|
<DocumentWrapper loaderData={loaderData}>
|
|
<Outlet />
|
|
</DocumentWrapper>
|
|
);
|
|
}
|
|
|
|
export default withSentry(App);
|