Remix migration

This commit is contained in:
regalijan 2023-10-19 16:49:09 -04:00
parent 5d2774fb2e
commit 04dcbb4181
Signed by: regalijan
GPG Key ID: 5D4196DA269EF520
33 changed files with 16545 additions and 1813 deletions

5
.gitignore vendored
View File

@ -24,3 +24,8 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# Remix files
.cache
functions/\[\[path\]\].js
public/build

View File

@ -1 +1 @@
v16.19.0 v18.14.2

19
app/context.tsx Normal file
View File

@ -0,0 +1,19 @@
import { createContext } from "react";
export interface ServerStyleContextData {
key: string;
ids: Array<string>;
css: string;
}
export const ServerStyleContext = createContext<
ServerStyleContextData[] | null
>(null);
export interface ClientStyleContextData {
reset: () => void;
}
export const ClientStyleContext = createContext<ClientStyleContextData | null>(
null
);

View File

@ -0,0 +1,7 @@
import createCache from "@emotion/cache";
export const defaultCache = createEmotionCache();
export default function createEmotionCache() {
return createCache.default({ key: "cha" });
}

39
app/entry.client.tsx Normal file
View File

@ -0,0 +1,39 @@
import { CacheProvider } from "@emotion/react";
import { ClientStyleContext } from "./context.js";
import createEmotionCache, { defaultCache } from "./createEmotionCache.js";
import { hydrateRoot } from "react-dom/client";
import { Integrations } from "@sentry/tracing";
import { RemixBrowser } from "@remix-run/react";
import * as Sentry from "@sentry/react";
import { type ReactNode, StrictMode, useState } from "react";
Sentry.init({
dsn:
document.querySelector("meta[name='dsn']")?.getAttribute("content") ??
undefined,
integrations: [new Integrations.BrowserTracing()],
tracesSampleRate: 0.1,
});
function ClientCacheProvider({ children }: { children: ReactNode }) {
const [cache, setCache] = useState(defaultCache);
function reset() {
setCache(createEmotionCache());
}
return (
<ClientStyleContext.Provider value={{ reset }}>
<CacheProvider value={cache}>{children}</CacheProvider>
</ClientStyleContext.Provider>
);
}
hydrateRoot(
document,
<StrictMode>
<ClientCacheProvider>
<RemixBrowser />
</ClientCacheProvider>
</StrictMode>
);

41
app/entry.server.tsx Normal file
View File

@ -0,0 +1,41 @@
import { CacheProvider } from "@emotion/react";
import createEmotionCache from "./createEmotionCache.js";
import { createEmotionServer } from "../emotion-server.js";
import { type EntryContext } from "@remix-run/cloudflare";
import { RemixServer } from "@remix-run/react";
import { renderToString } from "react-dom/server";
import { ServerStyleContext } from "./context.js";
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
const cache = createEmotionCache();
const { extractCriticalToChunks } = createEmotionServer(cache);
const html = renderToString(
<ServerStyleContext.Provider value={null}>
<CacheProvider value={cache}>
<RemixServer context={remixContext} url={request.url} />
</CacheProvider>
</ServerStyleContext.Provider>
);
const chunks = extractCriticalToChunks(html);
const markup = renderToString(
<ServerStyleContext.Provider value={chunks.styles}>
<CacheProvider value={cache}>
<RemixServer context={remixContext} url={request.url} />
</CacheProvider>
</ServerStyleContext.Provider>
);
responseHeaders.set("content-type", "text/html;charset=utf-8");
return new Response("<!DOCTYPE html>" + markup, {
headers: responseHeaders,
status: responseStatusCode,
});
}

177
app/root.tsx Normal file
View File

@ -0,0 +1,177 @@
import {
ChakraProvider,
Container,
cookieStorageManagerSSR,
Heading,
Link,
Text,
} from "@chakra-ui/react";
import { ClientStyleContext, ServerStyleContext } from "./context.js";
import fontStyle from "@fontsource/plus-jakarta-sans/index.css";
import Forbidden from "../components/Forbidden.js";
import globalStyles from "../index.css";
import { HelmetProvider } from "react-helmet-async";
import {
Links,
LiveReload,
Outlet,
Scripts,
useCatch,
useLoaderData,
} from "@remix-run/react";
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";
export function CatchBoundary() {
const { status } = useCatch();
switch (status) {
case 303:
return "";
case 401:
return getMarkup({ hide: true }, <Login />);
case 403:
return getMarkup({ hide: true }, <Forbidden />);
case 404:
return getMarkup(
{ 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>
);
default:
return getMarkup(
{ hide: true },
<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>
);
}
}
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 }> {
let data: { [k: string]: string } = {};
if (context.data.current_user) data = { ...context.data.current_user };
if (context.env.DSN) data.dsn = context.env.DSN;
if (context.data.theme) data.theme = context.data.theme;
return data;
}
function getMarkup(
loaderData: { [k: string]: any },
child: ReactNode
): JSX.Element {
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 helmetContext: { [k: string]: any } = {};
const body = (
<StrictMode>
<ChakraProvider
colorModeManager={cookieStorageManagerSSR(
typeof document === "undefined" ? "" : document.cookie
)}
theme={theme}
>
<HelmetProvider>
<div className="App">
<Navigation {...loaderData} />
{children}
<Scripts />
<LiveReload />
</div>
</HelmetProvider>
</ChakraProvider>
</StrictMode>
);
const { helmet } = helmetContext;
return (
<html lang="en-US">
<head>
<Links />
{serverStyleData?.map(({ key, ids, css }) => (
<style
key={key}
data-emotion={`${key} ${ids.join(" ")}`}
dangerouslySetInnerHTML={{ __html: css }}
/>
))}
<meta charSet="UTF-8" />
{loaderData.dsn ? (
<meta name="dsn" content={loaderData.dsn} />
) : null}
<meta name="theme-color" content="#00a8f8" />
{helmet.meta?.toString()}
{helmet.title?.toString() ?? <title>Car Crushers</title>}
</head>
<body>{body}</body>
</html>
);
}
);
return <Document>{child}</Document>;
}
export default function () {
const loaderData = useLoaderData<typeof loader>();
return getMarkup(loaderData, <Outlet />);
}

View File

@ -1,6 +1,6 @@
import { Box, Container, Text } from "@chakra-ui/react"; import { Box, Container, Text } from "@chakra-ui/react";
export function Page() { export default function () {
return ( return (
<> <>
<Box alignContent="left"> <Box alignContent="left">
@ -14,5 +14,3 @@ export function Page() {
</> </>
); );
} }
export const title = "Home - Car Crushers";

View File

@ -21,13 +21,39 @@ import {
useDisclosure, useDisclosure,
useToast, useToast,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { useLoaderData } from "@remix-run/react";
import { useState } from "react"; import { useState } from "react";
import Login from "../components/Login"; import Success from "../../components/Success.js";
import Success from "../components/Success";
export function Page(pageProps: { [p: string]: any }) { export async function loader({ context }: { context: RequestContext }) {
if (!pageProps.logged_in) return <Login />; if (!context.data.current_user)
throw new Response(null, {
status: 401,
});
const { current_user: currentUser } = context.data;
const dataKV = context.env.DATA;
const disabled = await dataKV.get("appeal_disabled");
return {
can_appeal:
!Boolean(disabled) &&
!Boolean(await dataKV.get(`blockedappeal_${currentUser.id}`)) &&
!Boolean(
(
await dataKV.list({
prefix: `appeal_${currentUser.id}`,
})
).keys.find((appeal) => (appeal.metadata as { [k: string]: any }).open)
),
can_toggle:
currentUser.permissions & (1 << 0) || currentUser.permissions & (1 << 11),
disabled: Boolean(disabled),
};
}
export default function () {
const pageProps = useLoaderData<typeof loader>();
const { isOpen, onClose, onOpen } = useDisclosure(); const { isOpen, onClose, onOpen } = useDisclosure();
const [showSuccess, setShowSuccess] = useState(false); const [showSuccess, setShowSuccess] = useState(false);
const toast = useToast(); const toast = useToast();
@ -195,7 +221,7 @@ export function Page(pageProps: { [p: string]: any }) {
<br /> <br />
<br /> <br />
<Button <Button
disabled={pageProps.disabled || pageProps.already_submitted} disabled={pageProps.can_appeal}
onClick={async () => await submit()} onClick={async () => await submit()}
> >
Submit Submit

View File

@ -8,13 +8,68 @@ import {
VStack, VStack,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { lazy, useState } from "react"; import { lazy, useState } from "react";
const AppealCard = lazy(() => import("../components/AppealCard")); import AppealCard from "../../components/AppealCard.js";
import Login from "../components/Login"; import Login from "../../components/Login.js";
import { useLoaderData } from "@remix-run/react";
export function Page(pageProps: { [p: string]: any }) { export async function loader({ context }: { context: RequestContext }) {
if (!pageProps.logged_in) const { current_user: currentUser } = context.data;
return <Login />;
if (!currentUser)
throw new Response(null, {
status: 401,
});
const newItemPermissions = {
game_ban: [1 << 5],
inactivity: [1 << 2, 1 << 9, 1 << 10],
infraction: [1 << 0, 1 << 2, 1 << 6, 1 << 7],
};
const newItemNames: { [k: string]: string } = {
game_ban: "Game Ban",
inactivity: "Inactivity Notice",
infraction: "Infraction",
};
const typePermissions = {
appeal: [1 << 0, 1 << 1],
gma: [1 << 5],
report: [1 << 5],
};
const typeNames: { [k: string]: string } = {
appeal: "Discord Appeals",
gma: "Game Appeals",
report: "Game Reports",
};
const allowedNewItems = [];
const allowedTypes = [];
for (const [item, ints] of Object.entries(newItemPermissions)) {
if (ints.find((i) => currentUser.permissions & i))
allowedNewItems.push({ name: newItemNames[item], value: item });
}
for (const [type, ints] of Object.entries(typePermissions)) {
if (ints.find((i) => currentUser.permissions & i))
allowedTypes.push({ name: typeNames[type], value: type });
}
if (!allowedTypes.length)
throw new Response(null, {
status: 403,
});
return {
entry_types: allowedTypes,
item_types: allowedNewItems,
};
}
export default function () {
const pageProps = useLoaderData<typeof loader>();
const isDesktop = useBreakpointValue({ base: false, lg: true }); const isDesktop = useBreakpointValue({ base: false, lg: true });
const entryTypes = []; const entryTypes = [];
const [entries, setEntries] = useState([] as JSX.Element[]); const [entries, setEntries] = useState([] as JSX.Element[]);
@ -83,4 +138,3 @@ export function Page(pageProps: { [p: string]: any }) {
</Container> </Container>
); );
} }
export const title = "Mod Queue - Car Crushers";

View File

@ -1,6 +1,6 @@
import { Container, Heading, Link, Text } from "@chakra-ui/react"; import { Container, Heading, Link, Text } from "@chakra-ui/react";
export function Page() { export default function () {
return ( return (
<Container maxW="container.lg" pb="8vh" pt="4vh" textAlign="start"> <Container maxW="container.lg" pb="8vh" pt="4vh" textAlign="start">
<Heading>Privacy Policy</Heading> <Heading>Privacy Policy</Heading>
@ -19,7 +19,7 @@ export function Page() {
authorize requests. A list of information available can be found at{" "} authorize requests. A list of information available can be found at{" "}
<Link <Link
color="#646cff" color="#646cff"
href="https://discord.com/developers/docs/resources/use" href="https://discord.com/developers/docs/resources/user"
target="_blank" target="_blank"
> >
Discord's Developer Portal Discord's Developer Portal
@ -198,7 +198,9 @@ export function Page() {
<Text>You have the right to:</Text> <Text>You have the right to:</Text>
<br /> <br />
<ul> <ul>
<li>Access your personal information</li> <li>
Access your personal information (or export it in a portable format)
</li>
<li>Delete your personal information</li> <li>Delete your personal information</li>
<li> <li>
Request restrictions on the processing of your personal information Request restrictions on the processing of your personal information
@ -234,5 +236,3 @@ export function Page() {
</Container> </Container>
); );
} }
export const title = "Privacy - Car Crushers";

View File

@ -13,12 +13,21 @@ import {
useToast, useToast,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Login from "../components/Login"; import { useLoaderData } from "@remix-run/react";
import Success from "../components/Success"; import Success from "../../components/Success.js";
export function Page(pageProps: { [p: string]: any }) { export async function loader({
if (!pageProps.logged_in) return <Login />; context,
}: {
context: RequestContext;
}): Promise<{ logged_in: boolean; site_key: string }> {
return {
logged_in: Boolean(context.data.current_user),
site_key: context.env.TURNSTILE_SITEKEY,
};
}
export default function () {
const [fileProgress, setFileProgress] = useState(0); const [fileProgress, setFileProgress] = useState(0);
const [showSuccess, setShowSuccess] = useState(false); const [showSuccess, setShowSuccess] = useState(false);
const [supportsRequestStreams, setSupportsRequestStreams] = useState(false); const [supportsRequestStreams, setSupportsRequestStreams] = useState(false);
@ -165,19 +174,22 @@ export function Page(pageProps: { [p: string]: any }) {
sessionStorage.setItem("REPORT_SUCCESS", "1"); sessionStorage.setItem("REPORT_SUCCESS", "1");
} }
const { logged_in, site_key } = useLoaderData<typeof loader>();
return showSuccess ? ( return showSuccess ? (
<Success <Success
heading="Report Submitted" heading="Report Submitted"
message="We will review it as soon as possible." message="We will review it as soon as possible."
/> />
) : ( ) : (
<>
<Container maxW="container.md" pt="4vh" textAlign="start"> <Container maxW="container.md" pt="4vh" textAlign="start">
<Heading mb="4vh">Report an Exploiter</Heading> <Heading mb="4vh">Report an Exploiter</Heading>
<br /> <br />
<FormControl isRequired> <FormControl isRequired>
<FormLabel> <FormLabel>
Username(s) - To specify more than one, provide a comma-delimited list Username(s) - To specify more than one, provide a comma-delimited
(User1, User2, User3...) list (User1, User2, User3...)
</FormLabel> </FormLabel>
<Input id="usernames" placeholder="builderman" /> <Input id="usernames" placeholder="builderman" />
</FormControl> </FormControl>
@ -225,6 +237,18 @@ export function Page(pageProps: { [p: string]: any }) {
) : null} ) : null}
</CircularProgress> </CircularProgress>
</HStack> </HStack>
<div
className="cf-turnstile"
data-sitekey={useLoaderData<typeof loader>()}
></div>
</Container> </Container>
{logged_in ? null : (
<script
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
async
defer
></script>
)}
</>
); );
} }

View File

@ -12,7 +12,7 @@ import {
VStack, VStack,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
export function Page() { export default function () {
return ( return (
<Container <Container
borderRadius="12px" borderRadius="12px"
@ -134,5 +134,3 @@ export function Page() {
</Container> </Container>
); );
} }
export const title = "Support - Car Crushers";

View File

@ -9,9 +9,9 @@ import {
Stack, Stack,
Text, Text,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import team from "../data/team.json"; import team from "../../data/team.json";
export function Page() { export default function () {
return ( return (
<Container maxW="container.xl" pt="4vh"> <Container maxW="container.xl" pt="4vh">
<Heading textAlign="start">Our Team</Heading> <Heading textAlign="start">Our Team</Heading>
@ -52,5 +52,3 @@ export function Page() {
</Container> </Container>
); );
} }
export const title = "Team - Car Crushers";

View File

@ -1,6 +1,6 @@
import { Container, Heading, Link, Text } from "@chakra-ui/react"; import { Container, Heading, Link, Text } from "@chakra-ui/react";
export function Page() { export default function () {
return ( return (
<Container maxW="container.lg" pb="8vh" pt="4vh" textAlign="start"> <Container maxW="container.lg" pb="8vh" pt="4vh" textAlign="start">
<Heading>Terms and Conditions</Heading> <Heading>Terms and Conditions</Heading>
@ -118,5 +118,3 @@ export function Page() {
</Container> </Container>
); );
} }
export const title = "Terms - Car Crushers";

View File

@ -1,5 +1,5 @@
import { Component, type ReactNode } from "react"; import { Component, type ReactNode } from "react";
import Navigation from "./Navigation"; import Navigation from "./Navigation.js";
import { Code, Container, Heading, Text } from "@chakra-ui/react"; import { Code, Container, Heading, Text } from "@chakra-ui/react";
interface ErrorState { interface ErrorState {

View File

@ -32,6 +32,7 @@ export default function (props: {
avatar?: string; avatar?: string;
discriminator?: string; discriminator?: string;
email?: string; email?: string;
hide?: boolean;
id?: string; id?: string;
permissions?: number; permissions?: number;
username?: string; username?: string;
@ -105,7 +106,7 @@ export default function (props: {
</Center> </Center>
<Spacer /> <Spacer />
<Spacer /> <Spacer />
{props.id ? ( {props.hide ? null : props.id ? (
<HStack spacing="3"> <HStack spacing="3">
<Avatar <Avatar
display={props.id ? "flex" : "none"} display={props.id ? "flex" : "none"}

79
emotion-server.js Normal file
View File

@ -0,0 +1,79 @@
// @emotion/server is known to not work correctly in the Cloudflare Workers environment
// See https://github.com/emotion-js/emotion/issues/2446#issuecomment-1372440174
function createExtractCriticalToChunks(cache) {
return function (html) {
const RGX = new RegExp(`${cache.key}-([a-zA-Z0-9-_]+)`, "gm");
const o = { html, styles: [] };
let match;
const ids = {};
while ((match = RGX.exec(html)) !== null) {
if (ids[match[1]] === undefined) {
ids[match[1]] = true;
}
}
const regularCssIds = [];
let regularCss = "";
Object.keys(cache.inserted).forEach((id) => {
if (
(ids[id] !== undefined ||
cache.registered[`${cache.key}-${id}`] === undefined) &&
cache.inserted[id] !== true
) {
if (cache.registered[`${cache.key}-${id}`]) {
regularCssIds.push(id);
regularCss += cache.inserted[id];
} else {
o.styles.push({
key: `${cache.key}-global`,
ids: [id],
css: cache.inserted[id],
});
}
}
});
o.styles.push({ key: cache.key, ids: regularCssIds, css: regularCss });
return o;
};
}
function generateStyleTag(cssKey, ids, styles, nonceString) {
return `<style data-emotion="${cssKey} ${ids}"${nonceString}>${styles}</style>`;
}
function createConstructStyleTagsFromChunks(cache, nonceString) {
return function (criticalData) {
let styleTagsString = "";
criticalData.styles.forEach((item) => {
styleTagsString += generateStyleTag(
item.key,
item.ids.join(" "),
item.css,
nonceString
);
});
return styleTagsString;
};
}
export function createEmotionServer(cache) {
if (cache.compat !== true) {
cache.compat = true;
}
const nonceString =
cache.nonce !== undefined ? ` nonce="${cache.nonce}"` : "";
return {
extractCriticalToChunks: createExtractCriticalToChunks(cache),
constructStyleTagsFromChunks: createConstructStyleTagsFromChunks(
cache,
nonceString
),
};
}

View File

@ -1,5 +1,3 @@
import { renderPage } from "vite-plugin-ssr";
async function constructHTML(context: RequestContext) { async function constructHTML(context: RequestContext) {
const { pathname } = new URL(context.request.url); const { pathname } = new URL(context.request.url);
@ -12,21 +10,7 @@ async function constructHTML(context: RequestContext) {
) )
return await context.env.ASSETS.fetch(context.request); return await context.env.ASSETS.fetch(context.request);
const { httpResponse, status } = await renderPage({ return await context.next();
current_user: context.data.current_user,
kv: context.env.DATA,
status: 200,
urlOriginal: context.request.url,
});
return new Response(httpResponse?.getReadableWebStream(), {
headers: {
"content-type": httpResponse?.contentType ?? "text/html;charset=utf-8",
},
status: [200, 404, 500].includes(status)
? httpResponse?.statusCode
: status,
});
} }
async function generateTokenHash(token: string) { async function generateTokenHash(token: string) {

27
index.d.ts vendored
View File

@ -1,8 +1,8 @@
/// <reference types="vite/client" /> import type { EmotionCache } from "@emotion/utils";
import { type PageContextBuiltIn } from "vite-plugin-ssr";
declare global { declare global {
module "*.css";
interface Env { interface Env {
ASSETS: Fetcher; ASSETS: Fetcher;
DATA: KVNamespace; DATA: KVNamespace;
@ -10,15 +10,20 @@ declare global {
} }
type RequestContext = EventContext<Env, string, { [k: string]: any }>; type RequestContext = EventContext<Env, string, { [k: string]: any }>;
interface PageContext extends PageContextBuiltIn {
current_user?: { [k: string]: any }; interface EmotionCriticalToChunks {
kv: KVNamespace; html: string;
pageProps: { styles: { key: string; ids: string[]; css: string }[];
[k: string]: any;
};
requireAuth?: boolean;
status: number;
} }
interface EmotionServer {
constructStyleTagsFromChunks: (
criticalData: EmotionCriticalToChunks
) => string;
extractCriticalToChunks: (html: string) => EmotionCriticalToChunks;
}
export function createEmotionServer(cache: EmotionCache): EmotionServer;
} }
export {}; export {};

17298
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,21 +13,25 @@
"@emotion/react": "^11.10.6", "@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6", "@emotion/styled": "^11.10.6",
"@fontsource/plus-jakarta-sans": "^4.5.11", "@fontsource/plus-jakarta-sans": "^4.5.11",
"@sentry/react": "^7.38.0", "@remix-run/cloudflare": "^1.14.0",
"@sentry/tracing": "^7.38.0", "@remix-run/cloudflare-pages": "^1.14.0",
"framer-motion": "^9.0.4", "@remix-run/react": "^1.14.0",
"@sentry/react": "^7.40.0",
"@sentry/tracing": "^7.40.0",
"framer-motion": "^10.0.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0",
"react-helmet-async": "^1.3.0"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.20230215.0", "@cloudflare/workers-types": "^4.20230215.0",
"@remix-run/dev": "^1.14.0",
"@types/node": "^18.14.6",
"@types/react": "^18.0.28", "@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^3.1.0", "@vitejs/plugin-react": "^3.1.0",
"esbuild": "^0.17.8", "esbuild": "^0.17.8",
"prettier": "^2.8.4", "prettier": "^2.8.4",
"typescript": "^4.9.5", "typescript": "^4.9.5"
"vite": "^4.1.2",
"vite-plugin-ssr": "^0.4.84"
} }
} }

View File

@ -1,43 +0,0 @@
export async function onBeforeRender(pageContext: PageContext) {
if (!pageContext.current_user)
return {
pageContext: {
pageProps: {
logged_in: false,
},
status: 401,
},
};
const blockedAppeal = await pageContext.kv?.get(
`blockedappeal_${pageContext.current_user.id}`
);
const disabledStatus = await pageContext.kv?.get("appeal_disabled");
const openAppeals = await pageContext.kv?.list({
prefix: `appeal_${pageContext.current_user.id}`,
});
return {
pageContext: {
pageProps: {
can_appeal:
!Boolean(disabledStatus) &&
!Boolean(blockedAppeal) &&
!Boolean(
openAppeals.keys.find(
(appeal) => (appeal.metadata as { [k: string]: any }).open
)
),
can_toggle:
pageContext.current_user?.permissions & (1 << 0) ||
pageContext.current_user?.permissions & (1 << 11),
disabled: Boolean(disabledStatus),
logged_in: true,
},
status: pageContext.current_user ? 200 : 401,
},
};
}
export const description = "Appeal your Discord ban here.";
export const title = "Appeals - Car Crushers";

View File

@ -1,72 +0,0 @@
export async function onBeforeRender(pageContext: PageContext) {
const { current_user: currentUser } = pageContext;
if (!currentUser)
return {
pageContext: {
pageProps: {
logged_in: false,
},
status: 401,
},
};
const newItemPermissions = {
game_ban: [1 << 5],
inactivity: [1 << 2, 1 << 9, 1 << 10],
infraction: [1 << 0, 1 << 2, 1 << 6, 1 << 7]
};
const newItemNames: { [k: string]: string } = {
game_ban: "Game Ban",
inactivity: "Inactivity Notice",
infraction: "Infraction",
};
const typePermissions = {
appeal: [1 << 0, 1 << 1],
gma: [1 << 5],
report: [1 << 5],
};
const typeNames: { [k: string]: string } = {
appeal: "Discord Appeals",
gma: "Game Appeals",
report: "Game Reports",
};
const allowedNewItems = [];
const allowedTypes = [];
for (const [item, ints] of Object.entries(newItemPermissions)) {
if (ints.find((i) => currentUser.permissions & i))
allowedNewItems.push({ name: newItemNames[item], value: item })
}
for (const [type, ints] of Object.entries(typePermissions)) {
if (ints.find((i) => currentUser.permissions & i))
allowedTypes.push({ name: typeNames[type], value: type });
}
if (!allowedTypes.length)
return {
pageContext: {
pageProps: {
entry_types: [],
item_types: [],
logged_in: true,
},
status: 403,
},
};
return {
pageContext: {
pageProps: {
entry_types: allowedTypes,
item_types: allowedNewItems,
logged_in: true,
},
},
};
}

View File

@ -1,13 +0,0 @@
export async function onBeforeRender(pageContext: PageContext) {
return {
pageContext: {
pageProps: {
logged_in: Boolean(pageContext.current_user),
},
status: pageContext.current_user ? 200 : 401,
},
};
}
export const description = "Found a cheater in Car Crushers 2? Report them here.";
export const title = "Report - Car Crushers";

10
remix.config.js Normal file
View File

@ -0,0 +1,10 @@
export default {
future: {
v2_routeConvention: true,
},
server: "./server.ts",
serverBuildTarget: "cloudflare-pages",
serverMinify: true,
serverModuleFormat: "esm",
serverPlatform: "neutral",
};

View File

@ -1,41 +0,0 @@
import { StrictMode } from "react";
import { createRoot, hydrateRoot } from "react-dom/client";
import { ChakraProvider } from "@chakra-ui/react";
import "../index.css";
import "@fontsource/plus-jakarta-sans";
import theme from "../theme";
import * as Sentry from "@sentry/react";
import { Integrations } from "@sentry/tracing";
import Fallback from "../components/Fallback";
import Navigation from "../components/Navigation";
Sentry.init({
dsn: import.meta.env.VITE_DSN,
integrations: [new Integrations.BrowserTracing()],
tracesSampleRate: import.meta.env.VITE_SAMPLE_RATE
? parseFloat(import.meta.env.VITE_SAMPLE_RATE)
: 0.1,
});
export async function render(pageContext: PageContext) {
const { Page, pageProps } = pageContext;
const root = document.getElementById("root") as HTMLElement;
const reactRoot = (
<StrictMode>
<ChakraProvider theme={theme}>
<div className="App">
<Fallback>
<Navigation {...pageContext.current_user} />
<Page {...pageProps} />
</Fallback>
</div>
</ChakraProvider>
</StrictMode>
);
if (root.innerHTML === "") {
createRoot(root).render(reactRoot);
} else {
hydrateRoot(root, reactRoot);
}
}

View File

@ -1,61 +0,0 @@
import ReactDOMServer from "react-dom/server";
import { StrictMode } from "react";
import { dangerouslySkipEscape, escapeInject } from "vite-plugin-ssr";
import theme from "../theme";
import { ChakraProvider } from "@chakra-ui/react";
import Fallback from "../components/Fallback";
import Navigation from "../components/Navigation";
import Login from "../components/Login";
import Forbidden from "../components/Forbidden";
export const passToClient = ["current_user", "pageProps"];
export async function render(
pageContext: PageContext & { pageProps: { [k: string]: any } }
) {
const { exports, Page, pageProps, status } = pageContext;
const reactHTML = Page
? ReactDOMServer.renderToString(
<StrictMode>
<ChakraProvider theme={theme}>
<div className="App">
<Fallback>
<Navigation {...pageContext.current_user} />
{status === 200 ? (
Page ? (
<Page {...pageProps} />
) : (
""
)
) : (
{ 401: <Login />, 403: <Forbidden /> }[status]
)}
</Fallback>
</div>
</ChakraProvider>
</StrictMode>
)
: "";
return escapeInject`<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<meta name="theme-color" content="#00a8f8" />
<link rel="icon" href="/favicon.ico" />
<link rel="apple-touch-icon" type="image/png" href="/files/logo192.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="${
(exports.description as string) ?? "Car Crushers Website"
}" />
<meta property="og:description" content="${
(exports.description as string | null) ?? "Car Crushers Website"
}" />
<title>${(exports.title as string | null) ?? "Car Crushers"}</title>
</head>
<body>
<div id="root">${dangerouslySkipEscape(reactHTML)}</div>
</body>
</html>`;
}

View File

@ -1,33 +0,0 @@
import { Container, Heading, Link, Text } from "@chakra-ui/react";
import { PageContextBuiltIn } from "vite-plugin-ssr";
export function Page(pageProps: PageContextBuiltIn) {
if (pageProps.is404)
return (
<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>
);
return (
<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>
);
}

12
server.ts Normal file
View File

@ -0,0 +1,12 @@
import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";
import * as build from "@remix-run/dev/server-build.js";
const handleRequest = createPagesFunctionHandler({
build,
mode: process.env.NODE_ENV,
getLoadContext: (context) => context,
});
export function onRequest(context: RequestContext) {
return handleRequest(context);
}

View File

@ -1,21 +1,22 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ES2020",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"], "lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": false, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": true, "strict": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Node", "moduleResolution": "Node16",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"types": ["@cloudflare/workers-types"] "types": ["@cloudflare/workers-types", "@types/node"],
"baseUrl": "."
}, },
"references": [{ "path": "./tsconfig.node.json" }] "include": ["**/*.ts", "**/*.tsx"]
} }

View File

@ -1,9 +0,0 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -1,8 +0,0 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import ssr from "vite-plugin-ssr/plugin";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), ssr({ prerender: true })],
});