This commit is contained in:
41666 2023-05-20 23:39:58 -04:00
parent 9dd0c78850
commit 49fffc20a3
19 changed files with 2551 additions and 12482 deletions

View file

@ -1,29 +0,0 @@
import { Link } from "@remix-run/react";
import { styled } from "styletron-react";
export const CardBase = styled("div", {
display: "flex",
flexDirection: "column",
padding: "1rem",
backgroundColor: "#000",
border: "2px solid #fff",
borderRadius: "0.5rem",
margin: "1rem",
});
export const CardHeader = styled(Link, {
display: "flex",
flex: 1,
alignItems: "center",
justifyContent: "space-between",
backgroundColor: "#ccc",
borderRadius: "0.5rem",
fontSize: "1.25rem",
color: "#000",
textDecoration: "none",
padding: "0.5rem",
cursor: "pointer",
":hover": {
backgroundColor: "#fff",
},
});

View file

@ -1,55 +0,0 @@
import { styled } from "styletron-react";
const BarRoot = styled("div", {
display: "flex",
flexDirection: "row",
alignItems: "center",
overflow: "hidden",
borderRadius: "0.5rem",
border: "1px solid #888",
});
const Bar = styled(
"div",
({
color,
size,
borders = false,
}: {
color: string;
size: number;
borders?: boolean;
}) => ({
backgroundColor: color,
flex: size,
padding: "0 0.35rem",
textAlign: "center",
textShadow: "0 0 0.25rem #000",
})
);
export const FactionBar = ({
nc,
tr,
vs,
showNumbers = true,
}: {
showNumbers?: boolean;
nc: number;
tr: number;
vs: number;
}) => {
return (
<BarRoot>
<Bar size={nc} color="#22f" title="New Conglomerate">
{nc.toLocaleString()}
</Bar>
<Bar size={tr} color="#f11" title="Terran Republic">
{tr.toLocaleString()}
</Bar>
<Bar size={vs} color="#a0d" title="Vanu Sovreignty">
{vs.toLocaleString()}
</Bar>
</BarRoot>
);
};

View file

@ -1,22 +0,0 @@
import { HiChevronRight } from "react-icons/hi2";
import { World } from "~/utils/saerro";
import { CardBase, CardHeader } from "./Card";
import { FactionBar } from "./FactionBar";
export const WorldCard = ({ world }: { world: World }) => {
return (
<CardBase>
<CardHeader to={`/worlds/${world.id}`}>
<div>{world.name}</div>
<HiChevronRight />
</CardHeader>
<div>
<div>Population: {world.population.total.toLocaleString()}</div>
<div>
<FactionBar {...world.population} />
</div>
</div>
</CardBase>
);
};

View file

@ -1,22 +1,18 @@
/**
* By default, Remix will handle hydrating your app on the client for you.
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal`
* For more information, see https://remix.run/file-conventions/entry.client
*/
import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
function hydrate() {
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>
);
});
}
if (typeof requestIdleCallback === "function") {
requestIdleCallback(hydrate);
} else {
// Safari doesn't support requestIdleCallback
// https://caniuse.com/requestidlecallback
setTimeout(hydrate, 1);
}
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>
);
});

View file

@ -1,26 +1,38 @@
import type { EntryContext } from "@remix-run/node";
import { renderToString } from "react-dom/server";
/**
* By default, Remix will handle generating the HTTP Response for you.
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal`
* For more information, see https://remix.run/file-conventions/entry.server
*/
import type { EntryContext } from "@remix-run/cloudflare";
import { RemixServer } from "@remix-run/react";
import isbot from "isbot";
import { renderToReadableStream } from "react-dom/server";
import { collectStyles } from "./styletron";
export default function handleRequest(
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
let markup = renderToString(
<RemixServer context={remixContext} url={request.url} />
const body = await renderToReadableStream(
<RemixServer context={remixContext} url={request.url} />,
{
signal: request.signal,
onError(error: unknown) {
console.error(error);
responseStatusCode = 500;
},
}
);
// Add server-rendered styles
markup = markup.replace("__STYLES__", collectStyles());
if (isbot(request.headers.get("user-agent"))) {
await body.allReady;
}
responseHeaders.set("Content-Type", "text/html");
return new Response("<!DOCTYPE html>" + markup, {
status: responseStatusCode,
return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
}

View file

@ -1,11 +0,0 @@
import { styled } from "styletron-react";
export const Body = styled("body", {
backgroundImage:
"radial-gradient(ellipse at bottom right, #1b2735 0%,#090a0f 100%)",
backgroundSize: "cover",
backgroundAttachment: "fixed",
color: "#fff",
lineHeight: 1.6,
fontFamily: "'IBM Plex Mono', monospace",
});

View file

@ -1,5 +1,5 @@
import type { LinksFunction, MetaFunction } from "@remix-run/node";
import { Provider as StyletronProvider } from "styletron-react";
import type { LinksFunction } from "@remix-run/cloudflare";
import { cssBundleHref } from "@remix-run/css-bundle";
import {
Links,
LiveReload,
@ -9,38 +9,25 @@ import {
ScrollRestoration,
} from "@remix-run/react";
import { styletron } from "./styletron";
import { Body } from "./globalStyles";
export const meta: MetaFunction = () => ({
charset: "utf-8",
title: "New Remix App",
viewport: "width=device-width,initial-scale=1",
});
export const links: LinksFunction = () => [
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;700&display=swap",
},
...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
];
export default function App() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
{typeof document === "undefined" ? "__STYLES__" : null}
</head>
<StyletronProvider value={styletron}>
<Body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</Body>
</StyletronProvider>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}

38
app/routes/_index.tsx Normal file
View file

@ -0,0 +1,38 @@
import type { V2_MetaFunction } from "@remix-run/cloudflare";
export const meta: V2_MetaFunction = () => {
return [{ title: "New Remix App" }];
};
export default function Index() {
return (
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
<h1>Welcome to Remix</h1>
<ul>
<li>
<a
target="_blank"
href="https://remix.run/tutorials/blog"
rel="noreferrer"
>
15m Quickstart Blog Tutorial
</a>
</li>
<li>
<a
target="_blank"
href="https://remix.run/tutorials/jokes"
rel="noreferrer"
>
Deep Dive Jokes App Tutorial
</a>
</li>
<li>
<a target="_blank" href="https://remix.run/docs" rel="noreferrer">
Remix Docs
</a>
</li>
</ul>
</div>
);
}

View file

@ -1,112 +0,0 @@
import { CardBase, CardHeader } from "~/components/Card";
import { HiChevronRight } from "react-icons/hi2";
import type { IndexResponse, World } from "~/utils/saerro";
import { indexQuery } from "~/utils/saerro";
import { useLoaderData } from "@remix-run/react";
import { json } from "@remix-run/cloudflare";
import { FactionBar } from "~/components/FactionBar";
import { WorldCard } from "~/components/IndexWorldCard";
import { styled } from "styletron-react";
import React, { ReactNode } from "react";
export const loader = async () => {
return json(await indexQuery());
};
export default function Index() {
const { allWorlds } = useLoaderData<typeof loader>();
return (
<div>
<Header />
<div>
{allWorlds.map((world) => (
<WorldCard world={world} key={world.id} />
))}
</div>
</div>
);
}
const HeaderBase = styled("div", {
display: "flex",
flexDirection: "row",
borderColor: "#fff",
backgroundColor: "#fff",
color: "#fff",
borderLeftWidth: "0.5rem",
borderTopWidth: "0.5rem",
borderRightWidth: "0",
borderBottomWidth: "0.5rem",
borderStyle: "solid",
borderTopLeftRadius: "0.5rem",
position: "relative",
});
const Header = () => (
<HeaderBase>
<QuoteUnquoteLogo>
<b>PS2.LIVE</b>
<br />
Realtime tools for PlanetSide 2
</QuoteUnquoteLogo>
<TheList>
<ListLink href="https://saerro.ps2.live" color="gold">
Saerro Live Pop API
</ListLink>
<ListLink href="https://try.ps2.live" color="#3333ff">
Census API Playground
</ListLink>
<ListLink href="https://agg.ps2.live/population" color="limegreen">
Population API
</ListLink>
<ListLink href="https://agg.ps2.live/continents" color="#44aadd">
Continents API
</ListLink>
<ListLink href="https://discord.ps2.live" color="#aa77dd">
Discord Character Link
</ListLink>
<ListLink href="https://metagame.ps2.live" color="#ff3333">
Metagame Webhooks
</ListLink>
</TheList>
</HeaderBase>
);
const QuoteUnquoteLogo = styled("div", {
width: "8rem",
backgroundColor: "#fff",
color: "#000",
textAlign: "right",
padding: "0.5rem",
borderRight: "0.5rem solid #fff",
});
const TheList = styled("div", {
backgroundColor: "#000",
borderRadius: "0.5rem",
position: "relative",
left: "-0.5rem",
display: "flex",
flexWrap: "wrap",
padding: "0.5rem",
gap: "0.5rem",
flexDirection: "column",
maxHeight: "8rem",
alignContent: "flex-start",
justifyContent: "center",
flex: 1,
});
const ListLink = styled("a", ({ color }: { color: string }) => ({
display: "block",
padding: "0.5rem",
color,
textDecoration: "none",
borderRadius: "0.4rem",
transition: "all 0.2s ease-in-out",
":hover": {
backgroundColor: color,
color: "#000",
},
}));

71
app/routes/worlds.$id.tsx Normal file
View file

@ -0,0 +1,71 @@
import type { LoaderArgs } from "@remix-run/cloudflare";
import { json } from "@remix-run/cloudflare";
import { useLoaderData } from "@remix-run/react";
import {
WorldResponse,
Zone,
allClasses,
allVehicles,
worldQuery,
} from "~/utils/saerro";
import { pascalCaseToTitleCase, toTitleCase } from "~/utils/strings";
export const loader = async ({ params }: LoaderArgs) => {
return json(await worldQuery(params.id as string));
};
export default function Index() {
const { world } = useLoaderData<WorldResponse>();
return (
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.6" }}>
<h1>{world.name}</h1>
<h2>Total Population</h2>
<p>
{world.population.total} players ({world.population.vs} VS,{" "}
{world.population.nc} NC, {world.population.tr} TR)
</p>
<div>
<h2>Continents</h2>
{world.zones.all.map((zone) => (
<ZoneInfo zone={zone} key={zone.id} />
))}
</div>
</div>
);
}
const ZoneInfo = ({ zone }: { zone: Zone }) => {
return (
<section>
<h3>{zone.name}</h3>
<p>
{zone.population.total} players ({zone.population.vs} VS,{" "}
{zone.population.nc} NC, {zone.population.tr} TR)
</p>
<p>
<ul>
{allClasses.map((cls, idx) => (
<li key={idx}>
<b>{pascalCaseToTitleCase(cls)}</b>: {zone.classes?.[cls].total}{" "}
total, {zone.classes?.[cls].vs} VS, {zone.classes?.[cls].nc} NC,{" "}
{zone.classes?.[cls].tr} TR
</li>
))}
</ul>
</p>
<p>
{zone.vehicles?.total} vehicles...
<ul>
{allVehicles.map((vehicle, idx) => (
<li key={idx}>
<b>{toTitleCase(vehicle)}</b>: {zone.vehicles?.[vehicle].total}{" "}
total, {zone.vehicles?.[vehicle].vs} VS,{" "}
{zone.vehicles?.[vehicle].nc} NC, {zone.vehicles?.[vehicle].tr} TR
</li>
))}
</ul>
</p>
</section>
);
};

View file

@ -1,33 +0,0 @@
import { Client, Server } from "styletron-engine-atomic"; // or "styletron-engine-monolithic"
/**
* The Styletron engine to use on the current runtime.
*/
export const styletron =
typeof document === "undefined"
? new Server()
: new Client({
hydrate: getHydrateClass(),
});
export function isStyletronClient(engine: typeof styletron): engine is Client {
return styletron instanceof Client;
}
export function isStyletronServer(engine: typeof styletron): engine is Server {
return styletron instanceof Server;
}
export function collectStyles(): string {
if (!isStyletronServer(styletron)) {
throw new Error("Can only collect styles during server-side rendering.");
}
return styletron.getStylesheetsHtml();
}
function getHydrateClass(): HTMLCollectionOf<HTMLStyleElement> {
return document.getElementsByClassName(
"_styletron_hydrate_"
) as HTMLCollectionOf<HTMLStyleElement>;
}

View file

@ -21,6 +21,10 @@ export type Zone = {
id: string;
name: string;
population: Population;
vehicles?: Record<(typeof allVehicles)[number], Population> & {
total: number;
};
classes?: Record<(typeof allClasses)[number], Population>;
};
export type World = {
@ -88,3 +92,79 @@ export const indexQuery = async (): Promise<IndexResponse> => {
return indexData;
};
export type WorldResponse = {
world: World;
};
export const allVehicles = [
"flash",
"sunderer",
"lightning",
"scythe",
"vanguard",
"prowler",
"reaver",
"mosquito",
"galaxy",
"valkyrie",
"liberator",
"ant",
"harasser",
"dervish",
"chimera",
"javelin",
"corsair",
"magrider",
];
export const allClasses = [
"infiltrator",
"lightAssault",
"combatMedic",
"engineer",
"heavyAssault",
"max",
];
export const worldQuery = async (worldID: string): Promise<WorldResponse> => {
const query = `query {
world(by: {id: ${Number(worldID)}}) {
id
name
population {
total
nc
tr
vs
}
zones {
all {
id
name
classes {
${allClasses.map((cls) => `${cls} { total nc tr vs }`).join(" ")}
}
vehicles {
total
${allVehicles
.map((vehicle) => `${vehicle} { total nc tr vs }`)
.join(" ")}
}
population {
total
nc
tr
vs
}
}
}
}
}`;
console.log(query);
const worldData: WorldResponse = await saerroFetch(query);
return worldData;
};

9
app/utils/strings.ts Normal file
View file

@ -0,0 +1,9 @@
export const toTitleCase = (str: string) => {
return str.replace(/\w\S*/g, (txt) => {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
};
export const pascalCaseToTitleCase = (str: string) => {
return toTitleCase(str.replace(/([A-Z])/g, " $1"));
};

14405
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,34 +5,32 @@
"build": "remix build",
"dev:remix": "remix watch",
"dev:wrangler": "cross-env NODE_ENV=development npm run wrangler",
"dev": "remix build && run-p \"dev:*\"",
"dev": "npm-run-all build --parallel \"dev:*\"",
"start": "cross-env NODE_ENV=production npm run wrangler",
"typecheck": "tsc",
"wrangler": "wrangler pages dev ./public"
"wrangler": "wrangler pages dev ./public",
"pages:deploy": "npm run build && wrangler pages publish ./public"
},
"dependencies": {
"@remix-run/cloudflare": "^1.12.0",
"@remix-run/cloudflare-pages": "^1.12.0",
"@remix-run/react": "^1.12.0",
"@remix-run/cloudflare": "^1.16.0",
"@remix-run/cloudflare-pages": "^1.16.0",
"@remix-run/css-bundle": "^1.16.0",
"@remix-run/react": "^1.16.0",
"cross-env": "^7.0.3",
"isbot": "^3.6.8",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@cloudflare/workers-types": "^3.18.0",
"@remix-run/dev": "^1.12.0",
"@remix-run/eslint-config": "^1.12.0",
"@remix-run/node": "^1.12.0",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.8",
"eslint": "^8.27.0",
"@cloudflare/workers-types": "^3.19.0",
"@remix-run/dev": "^1.16.0",
"@remix-run/eslint-config": "^1.16.0",
"@types/react": "^18.0.35",
"@types/react-dom": "^18.0.11",
"eslint": "^8.38.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.8.3",
"react-icons": "^4.7.1",
"styletron-engine-atomic": "^1.5.0",
"styletron-react": "^6.1.0",
"typescript": "^4.8.4",
"wrangler": "^2.2.1"
"typescript": "^5.0.4",
"wrangler": "^2.15.1"
},
"engines": {
"node": ">=16.13"

View file

@ -1,2 +1,2 @@
/build/*
Cache-Control: public, max-age=31536000, s-maxage=31536000
Cache-Control: public, max-age=31536000, immutable

View file

@ -1,11 +1,22 @@
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
serverBuildTarget: "cloudflare-pages",
server: "./server.js",
devServerBroadcastDelay: 1000,
ignoredRouteFiles: ["**/.*"],
server: "./server.ts",
serverBuildPath: "functions/[[path]].js",
serverConditions: ["worker"],
serverDependenciesToBundle: "all",
serverMainFields: ["browser", "module", "main"],
serverMinify: true,
serverModuleFormat: "esm",
serverPlatform: "neutral",
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// serverBuildPath: "functions/[[path]].js",
// publicPath: "/build/",
future: {
v2_errorBoundary: true,
v2_meta: true,
v2_normalizeFormMethod: true,
v2_routeConvention: true,
},
};

View file

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

2
wrangler.toml Normal file
View file

@ -0,0 +1,2 @@
compatibility_date = "2022-04-05"
compatibility_flags = ["streams_enable_constructors"]