sync
1
.envrc
Normal file
|
@ -0,0 +1 @@
|
|||
use flake;
|
1
.gitignore
vendored
|
@ -8,3 +8,4 @@ node_modules
|
|||
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
.direnv
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import type { MetagameWorld } from "~/utils/metagame";
|
||||
import { humanTimeAgo } from "~/utils/strings";
|
||||
import type { MetagameWorld } from "../utils/metagame";
|
||||
import { humanTimeAgo } from "../utils/strings";
|
||||
import * as styles from "./alert-timer.css";
|
||||
|
||||
const endTime = (alert: Required<MetagameWorld["zones"][0]>["alert"]) => {
|
||||
|
@ -51,7 +51,7 @@ export const AlertTimer = ({
|
|||
}: {
|
||||
alert: MetagameWorld["zones"][0]["alert"];
|
||||
}) => {
|
||||
const [timeLeft, setTimeLeft] = useState(timeLeftString(alert));
|
||||
const [timeLeft, setTimeLeft] = useState(<>s</>);
|
||||
|
||||
useEffect(() => {
|
||||
if (alert) {
|
||||
|
|
15
app/components/faction-bar-sxs.css.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { style } from "@vanilla-extract/css";
|
||||
import { background100, background200 } from "../utils/theme";
|
||||
|
||||
export const bar = style({
|
||||
backgroundColor: background200,
|
||||
width: "0.3em",
|
||||
height: "1em",
|
||||
border: `1px solid ${background100}`,
|
||||
// margin: 2
|
||||
});
|
||||
|
||||
export const container = style({
|
||||
display: "flex",
|
||||
});
|
||||
|
41
app/components/faction-bar-sxs.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { useMemo } from "react";
|
||||
import { Population } from "../utils/saerro";
|
||||
import * as styles from "./faction-bar-sxs.css";
|
||||
import { background200, ncFaction, trFaction, vsFaction } from "../utils/theme";
|
||||
|
||||
export const FactionBarSxS = ({
|
||||
population: { nc, vs, tr },
|
||||
}: {
|
||||
population: Population;
|
||||
}) => {
|
||||
const { vsPercent, ncPercent, trPercent } = useMemo(() => {
|
||||
const total = nc + vs + tr;
|
||||
return {
|
||||
vsPercent: Math.round((vs / total) * 100) || 0,
|
||||
ncPercent: Math.round((nc / total) * 100) || 0,
|
||||
trPercent: Math.round((tr / total) * 100) || 0,
|
||||
};
|
||||
}, [vs, nc, tr]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Bar percent={vsPercent} color={vsFaction} />
|
||||
<Bar percent={ncPercent} color={ncFaction} />
|
||||
<Bar percent={trPercent} color={trFaction} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Bar = (props: { percent: number; color: string }) => (
|
||||
<div
|
||||
className={styles.bar}
|
||||
style={{
|
||||
backgroundImage: `linear-gradient( to top,
|
||||
${props.color} 0% ${props.percent}% ,
|
||||
${background200} ${props.percent + 0.1}% 100%
|
||||
)`,
|
||||
}}
|
||||
>
|
||||
|
||||
</div>
|
||||
);
|
|
@ -1,5 +1,6 @@
|
|||
import type { ComplexStyleRule } from "@vanilla-extract/css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
import { edge, ncFaction, trFaction, vsFaction } from "../utils/theme";
|
||||
|
||||
export const bar = style({
|
||||
display: "flex",
|
||||
|
@ -8,7 +9,7 @@ export const bar = style({
|
|||
flexDirection: "row",
|
||||
overflow: "hidden",
|
||||
borderRadius: "0.4rem",
|
||||
border: "2px solid #4d4d4d",
|
||||
border: `2px solid ${edge}`,
|
||||
});
|
||||
|
||||
export const tinyBar = style({
|
||||
|
@ -27,16 +28,16 @@ const shared: ComplexStyleRule = {
|
|||
|
||||
export const left = style({
|
||||
...shared,
|
||||
backgroundColor: "#991cba",
|
||||
backgroundColor: vsFaction,
|
||||
});
|
||||
export const center = style({
|
||||
...shared,
|
||||
backgroundColor: "#1564cc",
|
||||
borderLeft: "1px solid #4d4d4d",
|
||||
borderRight: "2px solid #4d4d4d",
|
||||
backgroundColor: ncFaction,
|
||||
borderLeft: `1px solid ${edge}`,
|
||||
borderRight: `2px solid ${edge}`,
|
||||
boxShadow: "inset 0 0 0.5rem rgb(180 180 180 / 10%)",
|
||||
});
|
||||
export const right = style({
|
||||
...shared,
|
||||
backgroundColor: "#d30101",
|
||||
backgroundColor: trFaction,
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useMemo } from "react";
|
||||
import type { Population } from "~/utils/saerro";
|
||||
import type { Population } from "../utils/saerro";
|
||||
import * as styles from "./faction-bar.css";
|
||||
|
||||
export const FactionBar = ({
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { Population } from "~/utils/saerro";
|
||||
import { background200, ncFaction, trFaction, vsFaction } from "../utils/theme";
|
||||
import type { Population } from "../utils/saerro";
|
||||
import { pieRoot } from "./faction-pie.css";
|
||||
|
||||
export const FactionPie = ({
|
||||
|
@ -24,10 +25,11 @@ export const FactionPie = ({
|
|||
style={
|
||||
{
|
||||
fontSize: size || "1em",
|
||||
backgroundColor: background200,
|
||||
backgroundImage: `conic-gradient(
|
||||
#d30101 0% ${trPct}%,
|
||||
#991cba ${trPct}% ${trPct + vsPct}%,
|
||||
#1564cc ${trPct + vsPct}% 100%
|
||||
${trFaction} 0% ${trPct}%,
|
||||
${vsFaction} ${trPct}% ${trPct + vsPct}%,
|
||||
${ncFaction} ${trPct + vsPct}% 100%
|
||||
)`,
|
||||
"--inner-margin": innerMargin ? `${innerMargin}px` : "0",
|
||||
"--inner-bg": innerBackground || "none",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { style } from "@vanilla-extract/css";
|
||||
import footer from "~/images/footer.jpg";
|
||||
import footer from "../images/footer.jpg";
|
||||
|
||||
export const root = style({
|
||||
height: 300,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { IndexWorld } from "./index-world";
|
||||
import * as styles from "./index-world-container.css";
|
||||
import type { MetagameWorld } from "~/utils/metagame";
|
||||
import type { PopulationWorld } from "~/utils/population";
|
||||
import type { MetagameWorld } from "../utils/metagame";
|
||||
import type { PopulationWorld } from "../utils/population";
|
||||
|
||||
export const WorldContainer = ({
|
||||
metagame,
|
||||
|
|
|
@ -1,19 +1,13 @@
|
|||
import { Link } from "@remix-run/react";
|
||||
import {
|
||||
humanTimeAgo,
|
||||
snakeCaseToTitleCase,
|
||||
worlds,
|
||||
zones,
|
||||
} from "~/utils/strings";
|
||||
import { snakeCaseToTitleCase, worlds, zones } from "../utils/strings";
|
||||
import * as styles from "./index-world.css";
|
||||
import vsLogo from "~/images/vs-100.png";
|
||||
import ncLogo from "~/images/nc-100.png";
|
||||
import trLogo from "~/images/tr-100.png";
|
||||
import vsLogo from "../images/vs-100.png";
|
||||
import ncLogo from "../images/nc-100.png";
|
||||
import trLogo from "../images/tr-100.png";
|
||||
import { FactionBar } from "./faction-bar";
|
||||
import type { MetagameWorld } from "~/utils/metagame";
|
||||
import type { PopulationWorld } from "~/utils/population";
|
||||
import { c } from "~/utils/classes";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { MetagameWorld } from "../utils/metagame";
|
||||
import type { PopulationWorld } from "../utils/population";
|
||||
import { c } from "../utils/classes";
|
||||
import { AlertTimer } from "./alert-timer";
|
||||
|
||||
export type IndexWorldProps = {
|
||||
|
|
58
app/components/world-zone-container.css.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
export const zoneContainer = style({
|
||||
margin: "0.5em 1em",
|
||||
backgroundColor: "#222",
|
||||
padding: "1em",
|
||||
boxSizing: "border-box",
|
||||
});
|
||||
|
||||
export const zoneHeader = style({
|
||||
display: "flex",
|
||||
});
|
||||
|
||||
export const chartTileTotal = style({
|
||||
textAlign: "center",
|
||||
lineHeight: 1,
|
||||
minWidth: "2em",
|
||||
maxWidth: "2em",
|
||||
fontSize: "3rem",
|
||||
overflowY: "hidden",
|
||||
});
|
||||
|
||||
export const chartTilePopLine = style({
|
||||
display: "flex",
|
||||
fontSize: "1.5rem",
|
||||
fontWeight: "bold",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
lineHeight: 1.1,
|
||||
margin: 0,
|
||||
});
|
||||
|
||||
export const chartTilePopImage = style({
|
||||
width: "1em",
|
||||
// height: "1em",
|
||||
marginRight: 4,
|
||||
});
|
||||
|
||||
export const chartTile = style({
|
||||
fontSize: "4rem",
|
||||
flex: "0 3 33%",
|
||||
display: "flex",
|
||||
marginTop: "0.2em",
|
||||
});
|
||||
|
||||
export const classesContainer = style({
|
||||
display: "flex",
|
||||
flex: "1 3 100%",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "space-evenly",
|
||||
});
|
||||
|
||||
export const chartTileChart = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
});
|
98
app/components/world-zone-container.tsx
Normal file
|
@ -0,0 +1,98 @@
|
|||
import {
|
||||
allClasses,
|
||||
Population,
|
||||
totalPopulation,
|
||||
World,
|
||||
Zone,
|
||||
} from "../utils/saerro";
|
||||
import { headerFont } from "./world.css";
|
||||
import {
|
||||
chartTile,
|
||||
chartTileChart,
|
||||
chartTilePopImage,
|
||||
chartTilePopLine,
|
||||
chartTileTotal,
|
||||
classesContainer,
|
||||
zoneContainer,
|
||||
zoneHeader,
|
||||
} from "./world-zone-container.css";
|
||||
import { classIconMap } from "../utils/class-icons";
|
||||
import { FactionBarSxS } from "./faction-bar-sxs";
|
||||
import { c } from "../utils/classes";
|
||||
import vsLogo from "../images/vs-100.png";
|
||||
import ncLogo from "../images/nc-100.png";
|
||||
import trLogo from "../images/tr-100.png";
|
||||
|
||||
export type WZCProps = {
|
||||
world: World;
|
||||
zone: Zone;
|
||||
};
|
||||
|
||||
export const WorldZoneContainer = (props: WZCProps) => {
|
||||
return (
|
||||
<section className={zoneContainer} title={props.zone.name}>
|
||||
<div className={zoneHeader}>
|
||||
<h3 className={headerFont}>{props.zone.name.toUpperCase()}</h3>
|
||||
{/* TODO: metagame */}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className={headerFont}>CLASSES</h4>
|
||||
<div style={{ display: "flex" }}>
|
||||
<Classes classes={props.zone.classes} />
|
||||
<div style={{ display: "flex", flex: "1 3 100%", flexWrap: "wrap" }}>
|
||||
|
||||
</div>
|
||||
<div style={{ display: "flex", flex: "1 3 100%", flexWrap: "wrap" }}>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const Classes = (props: { classes: Zone["classes"] }) => {
|
||||
if (props.classes === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classesContainer}>
|
||||
{allClasses.map((name) => (
|
||||
<ChartTile
|
||||
key={name}
|
||||
name={name}
|
||||
pop={(props.classes as Record<string, Population>)[name]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ChartTile = (props: { name: string; pop: Population }) => (
|
||||
<div className={chartTile}>
|
||||
<div className={chartTileChart}>
|
||||
<FactionBarSxS population={props.pop} />
|
||||
<img src={(classIconMap as any)[props.name]} />
|
||||
</div>
|
||||
<div>
|
||||
<div className={c(headerFont, chartTileTotal)}>
|
||||
{totalPopulation(props.pop)}
|
||||
</div>
|
||||
<div>
|
||||
<div className={chartTilePopLine}>
|
||||
<img className={chartTilePopImage} src={vsLogo} alt="VS" />{" "}
|
||||
{props.pop.vs}
|
||||
</div>
|
||||
<div className={chartTilePopLine}>
|
||||
<img className={chartTilePopImage} src={ncLogo} alt="NC" />{" "}
|
||||
{props.pop.nc}
|
||||
</div>
|
||||
<div className={chartTilePopLine}>
|
||||
<img className={chartTilePopImage} src={trLogo} alt="TR" />{" "}
|
||||
{props.pop.tr}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
|
@ -28,7 +28,6 @@ export const headerSub = style({
|
|||
|
||||
export const outer = style({
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
minHeight: "100vh",
|
||||
flexDirection: "column",
|
||||
maxWidth: "1920px",
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
/**
|
||||
* 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";
|
||||
|
||||
startTransition(() => {
|
||||
hydrateRoot(
|
||||
document,
|
||||
<StrictMode>
|
||||
<RemixBrowser />
|
||||
</StrictMode>
|
||||
);
|
||||
});
|
|
@ -1,38 +0,0 @@
|
|||
/**
|
||||
* 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";
|
||||
|
||||
export default async function handleRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext
|
||||
) {
|
||||
const body = await renderToReadableStream(
|
||||
<RemixServer context={remixContext} url={request.url} />,
|
||||
{
|
||||
signal: request.signal,
|
||||
onError(error: unknown) {
|
||||
console.error(error);
|
||||
responseStatusCode = 500;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (isbot(request.headers.get("user-agent"))) {
|
||||
await body.allReady;
|
||||
}
|
||||
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
return new Response(body, {
|
||||
headers: responseHeaders,
|
||||
status: responseStatusCode,
|
||||
});
|
||||
}
|
BIN
app/images/icon_engi.png
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
app/images/icon_heavy.png
Normal file
After Width: | Height: | Size: 844 B |
BIN
app/images/icon_infil.png
Normal file
After Width: | Height: | Size: 1,008 B |
BIN
app/images/icon_light.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
app/images/icon_max.png
Normal file
After Width: | Height: | Size: 902 B |
BIN
app/images/icon_medic.png
Normal file
After Width: | Height: | Size: 266 B |
BIN
app/images/vehicles/ant.png
Normal file
After Width: | Height: | Size: 231 B |
BIN
app/images/vehicles/chimera.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
app/images/vehicles/corsair.png
Normal file
After Width: | Height: | Size: 375 B |
BIN
app/images/vehicles/dervish.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
app/images/vehicles/flash.png
Normal file
After Width: | Height: | Size: 429 B |
BIN
app/images/vehicles/galaxy.png
Normal file
After Width: | Height: | Size: 466 B |
BIN
app/images/vehicles/harasser.png
Normal file
After Width: | Height: | Size: 505 B |
BIN
app/images/vehicles/javelin.png
Normal file
After Width: | Height: | Size: 361 B |
BIN
app/images/vehicles/liberator.png
Normal file
After Width: | Height: | Size: 505 B |
BIN
app/images/vehicles/lightning.png
Normal file
After Width: | Height: | Size: 505 B |
BIN
app/images/vehicles/magrider.png
Normal file
After Width: | Height: | Size: 585 B |
BIN
app/images/vehicles/mosquito.png
Normal file
After Width: | Height: | Size: 429 B |
BIN
app/images/vehicles/prowler.png
Normal file
After Width: | Height: | Size: 554 B |
BIN
app/images/vehicles/reaver.png
Normal file
After Width: | Height: | Size: 467 B |
BIN
app/images/vehicles/scythe.png
Normal file
After Width: | Height: | Size: 535 B |
BIN
app/images/vehicles/sunderer.png
Normal file
After Width: | Height: | Size: 511 B |
BIN
app/images/vehicles/valkyrie.png
Normal file
After Width: | Height: | Size: 555 B |
BIN
app/images/vehicles/vanguard.png
Normal file
After Width: | Height: | Size: 488 B |
13
app/root.tsx
|
@ -1,4 +1,4 @@
|
|||
import type { LinksFunction } from "@remix-run/cloudflare";
|
||||
import type { LinksFunction } from "@remix-run/node";
|
||||
import { cssBundleHref } from "@remix-run/css-bundle";
|
||||
import {
|
||||
Links,
|
||||
|
@ -14,17 +14,11 @@ import "./reset.css";
|
|||
export const links: LinksFunction = () => [
|
||||
{
|
||||
rel: "preconnect",
|
||||
href: "https://fonts.gstatic.com",
|
||||
crossOrigin: "anonymous",
|
||||
},
|
||||
{
|
||||
rel: "preconnect",
|
||||
href: "ttps://fonts.googleapis.com",
|
||||
crossOrigin: "anonymous",
|
||||
href: "https://fonts.bunny.net",
|
||||
},
|
||||
{
|
||||
rel: "stylesheet",
|
||||
href: "https://fonts.googleapis.com/css2?family=Unbounded:wght@700&display=swap",
|
||||
href: "https://fonts.bunny.net/css?family=unbounded:700",
|
||||
},
|
||||
|
||||
...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
|
||||
|
@ -43,7 +37,6 @@ export default function App() {
|
|||
<Outlet />
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
<LiveReload />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { json, type V2_MetaFunction } from "@remix-run/cloudflare";
|
||||
import { json, type MetaFunction } from "@remix-run/node";
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
import { Footer } from "~/components/footer";
|
||||
import { WorldContainer } from "~/components/index-world-container";
|
||||
import { outer } from "~/components/index.css";
|
||||
import { fetchMetagameWorlds } from "~/utils/metagame";
|
||||
import { fetchPopulationWorlds } from "~/utils/population";
|
||||
import { Footer } from "../components/footer";
|
||||
import { WorldContainer } from "../components/index-world-container";
|
||||
import { outer } from "../components/index.css";
|
||||
import { fetchMetagameWorlds } from "../utils/metagame";
|
||||
import { fetchPopulationWorlds } from "../utils/population";
|
||||
|
||||
export const loader = async () => {
|
||||
const [metagame, population] = await Promise.all([
|
||||
|
@ -15,7 +15,7 @@ export const loader = async () => {
|
|||
return json({ metagame: metagame.sort((a, b) => a.id - b.id), population });
|
||||
};
|
||||
|
||||
export const meta: V2_MetaFunction = () => {
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "PS2.LIVE" },
|
||||
{
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Footer } from "~/components/footer";
|
||||
import { Footer } from "../components/footer";
|
||||
import {
|
||||
header,
|
||||
item,
|
||||
|
@ -8,7 +8,7 @@ import {
|
|||
link,
|
||||
love,
|
||||
outer,
|
||||
} from "~/components/about.css";
|
||||
} from "../components/about.css";
|
||||
|
||||
export default function About() {
|
||||
return (
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { useState } from "react";
|
||||
import { FactionBar } from "~/components/faction-bar";
|
||||
import { FactionPie } from "~/components/faction-pie";
|
||||
import type { Population } from "~/utils/saerro";
|
||||
import { FactionBar } from "../components/faction-bar";
|
||||
import { FactionPie } from "../components/faction-pie";
|
||||
import type { Population } from "../utils/saerro";
|
||||
import { FactionBarSxS } from "../components/faction-bar-sxs";
|
||||
|
||||
export default function DebugComponents() {
|
||||
const [population, setPopulation] = useState<Population>({
|
||||
|
@ -66,6 +67,10 @@ export default function DebugComponents() {
|
|||
value={innerColor}
|
||||
onChange={(e) => setInnerColor(e.target.value)}
|
||||
/>
|
||||
<h3>Vertical Side-by-Side Bar Chart</h3>
|
||||
<div style={{ fontSize: "5rem" }}>
|
||||
<FactionBarSxS population={population}></FactionBarSxS>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,32 +1,33 @@
|
|||
import type { LoaderArgs, V2_MetaFunction } from "@remix-run/cloudflare";
|
||||
import { json } from "@remix-run/cloudflare";
|
||||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { json } from "@remix-run/node";
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
import { Footer } from "~/components/footer";
|
||||
import type { MetagameWorld } from "~/utils/metagame";
|
||||
import { fetchSingleMetagameWorld } from "~/utils/metagame";
|
||||
import type { WorldResponse, Zone } from "~/utils/saerro";
|
||||
import { Footer } from "../components/footer";
|
||||
import type { MetagameWorld } from "../utils/metagame";
|
||||
import { fetchSingleMetagameWorld } from "../utils/metagame";
|
||||
import type { WorldResponse, Zone } from "../utils/saerro";
|
||||
import {
|
||||
allClasses,
|
||||
allVehicles,
|
||||
totalPopulation,
|
||||
worldQuery,
|
||||
} from "~/utils/saerro";
|
||||
} from "../utils/saerro";
|
||||
import {
|
||||
pascalCaseToTitleCase,
|
||||
toTitleCase,
|
||||
worlds,
|
||||
zones,
|
||||
} from "~/utils/strings";
|
||||
import * as styles from "~/components/world.css";
|
||||
import { c } from "~/utils/classes";
|
||||
import { FactionBar } from "~/components/faction-bar";
|
||||
import { popImage } from "~/components/index-world.css";
|
||||
import vsLogo from "~/images/vs-100.png";
|
||||
import ncLogo from "~/images/nc-100.png";
|
||||
import trLogo from "~/images/tr-100.png";
|
||||
import { FactionPie } from "~/components/faction-pie";
|
||||
import { AlertTimer } from "~/components/alert-timer";
|
||||
import { contPrioritySort } from "~/utils/sorting";
|
||||
} from "../utils/strings";
|
||||
import * as styles from "../components/world.css";
|
||||
import { c } from "../utils/classes";
|
||||
import { FactionBar } from "../components/faction-bar";
|
||||
import { popImage } from "../components/index-world.css";
|
||||
import vsLogo from "../images/vs-100.png";
|
||||
import ncLogo from "../images/nc-100.png";
|
||||
import trLogo from "../images/tr-100.png";
|
||||
import { FactionPie } from "../components/faction-pie";
|
||||
import { AlertTimer } from "../components/alert-timer";
|
||||
import { contPrioritySort, zonePopulationSort } from "../utils/sorting";
|
||||
import { WorldZoneContainer } from "../components/world-zone-container";
|
||||
|
||||
type LoaderData = {
|
||||
saerro: WorldResponse;
|
||||
|
@ -34,7 +35,7 @@ type LoaderData = {
|
|||
id: string;
|
||||
};
|
||||
|
||||
export async function loader({ params }: LoaderArgs) {
|
||||
export async function loader({ params }) {
|
||||
const [saerro, metagame] = await Promise.all([
|
||||
worldQuery(params.id as string),
|
||||
fetchSingleMetagameWorld(params.id as string),
|
||||
|
@ -42,7 +43,7 @@ export async function loader({ params }: LoaderArgs) {
|
|||
return json({ saerro, metagame, id: params.id } as LoaderData);
|
||||
}
|
||||
|
||||
export const meta: V2_MetaFunction<typeof loader> = ({ data }) => {
|
||||
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
||||
const { saerro, id } = data as LoaderData;
|
||||
const date = new Date();
|
||||
const worldInfo = worlds[String(id || "default")];
|
||||
|
@ -168,10 +169,13 @@ export default function World() {
|
|||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Continents</h2>
|
||||
{world.zones.all.map((zone) => (
|
||||
<ZoneInfo zone={zone} key={zone.id} />
|
||||
))}
|
||||
<h2 className={styles.headerFont}>WARZONES</h2>
|
||||
<div>
|
||||
{world.zones.all.sort(zonePopulationSort).map((zone) => (
|
||||
<WorldZoneContainer key={zone.id} world={world} zone={zone} />
|
||||
// <ZoneInfo key={zone.id} zone={zone} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -183,8 +187,8 @@ export default function World() {
|
|||
const ZoneInfo = ({ zone }: { zone: Zone }) => {
|
||||
const zoneInfo = zones[String(zone.id)];
|
||||
return (
|
||||
<section>
|
||||
<h3>{zoneInfo.name}</h3>
|
||||
<section className={styles.zone}>
|
||||
<h3 className={styles.headerFont}>{zoneInfo.name.toUpperCase()}</h3>
|
||||
<p>
|
||||
{totalPopulation(zone.population)} players ({zone.population.vs} VS,{" "}
|
||||
{zone.population.nc} NC, {zone.population.tr} TR)
|
||||
|
|
17
app/utils/class-icons.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import combatMedic from "../images/icon_medic.png";
|
||||
import engineer from "../images/icon_engi.png";
|
||||
import heavyAssault from "../images/icon_heavy.png";
|
||||
import infiltrator from "../images/icon_infil.png";
|
||||
import lightAssault from "../images/icon_light.png";
|
||||
import max from "../images/icon_max.png";
|
||||
|
||||
export { combatMedic, engineer, heavyAssault, infiltrator, lightAssault, max };
|
||||
|
||||
export const classIconMap = {
|
||||
combatMedic,
|
||||
engineer,
|
||||
heavyAssault,
|
||||
infiltrator,
|
||||
lightAssault,
|
||||
max,
|
||||
};
|
|
@ -1,11 +1,6 @@
|
|||
export const saerroFetch = async <T>(query: string): Promise<T> => {
|
||||
const response = await fetch(
|
||||
`https://saerro.ps2.live/graphql?query=${query}`,
|
||||
{
|
||||
cf: {
|
||||
cacheTtl: 60,
|
||||
},
|
||||
}
|
||||
`https://saerro.ps2.live/graphql?query=${query}`
|
||||
);
|
||||
const json: { data: T } = await response.json();
|
||||
return json.data;
|
||||
|
@ -22,10 +17,10 @@ export type Zone = {
|
|||
id: string;
|
||||
name: string;
|
||||
population: Population;
|
||||
vehicles?: Record<typeof allVehicles[number], Population> & {
|
||||
vehicles?: Record<(typeof allVehicles)[number], Population> & {
|
||||
total: number;
|
||||
};
|
||||
classes?: Record<typeof allClasses[number], Population>;
|
||||
classes?: Record<(typeof allClasses)[number], Population>;
|
||||
};
|
||||
|
||||
export type World = {
|
||||
|
@ -83,6 +78,7 @@ export const worldQuery = async (worldID: string): Promise<WorldResponse> => {
|
|||
zones {
|
||||
all {
|
||||
id
|
||||
name
|
||||
classes {
|
||||
${allClasses.map((cls) => `${cls} { total nc tr vs }`).join(" ")}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { MetagameWorld } from "./metagame";
|
||||
import { totalPopulation, type Zone } from "./saerro";
|
||||
|
||||
export const contPrioritySort = (
|
||||
a: MetagameWorld["zones"][number],
|
||||
|
@ -41,3 +42,24 @@ export const contPrioritySort = (
|
|||
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const zonePopulationSort = (a: Zone, b: Zone): number => {
|
||||
const total = ({ nc, vs, tr }: { nc: number; vs: number; tr: number }) =>
|
||||
nc + vs + tr;
|
||||
const ap = total(a.population);
|
||||
const bp = total(b.population);
|
||||
|
||||
if (ap < bp) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (ap > bp) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ap === bp) {
|
||||
return a.id < b.id ? 1 : -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
|
8
app/utils/theme.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export const trFaction = "#d30101";
|
||||
export const ncFaction = "#1564cc";
|
||||
export const vsFaction = "#991cba";
|
||||
|
||||
export const background100 = "#222";
|
||||
export const background200 = "#444";
|
||||
|
||||
export const edge = "#4d4d4d";
|
60
app/utils/vehicle-icons.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import ant from "../images/vehicles/ant.png";
|
||||
import chimera from "../images/vehicles/chimera.png";
|
||||
import corsair from "../images/vehicles/corsair.png";
|
||||
import dervish from "../images/vehicles/dervish.png";
|
||||
import flash from "../images/vehicles/flash.png";
|
||||
import galaxy from "../images/vehicles/galaxy.png";
|
||||
import harasser from "../images/vehicles/harasser.png";
|
||||
import javelin from "../images/vehicles/javelin.png";
|
||||
import liberator from "../images/vehicles/liberator.png";
|
||||
import lightning from "../images/vehicles/lightning.png";
|
||||
import magrider from "../images/vehicles/magrider.png";
|
||||
import mosquito from "../images/vehicles/mosquito.png";
|
||||
import prowler from "../images/vehicles/prowler.png";
|
||||
import reaver from "../images/vehicles/reaver.png";
|
||||
import scythe from "../images/vehicles/scythe.png";
|
||||
import sunderer from "../images/vehicles/sunderer.png";
|
||||
import valkyrie from "../images/vehicles/valkyrie.png";
|
||||
import vanguard from "../images/vehicles/vanguard.png";
|
||||
|
||||
export {
|
||||
ant,
|
||||
chimera,
|
||||
corsair,
|
||||
dervish,
|
||||
flash,
|
||||
galaxy,
|
||||
harasser,
|
||||
javelin,
|
||||
liberator,
|
||||
lightning,
|
||||
magrider,
|
||||
mosquito,
|
||||
prowler,
|
||||
reaver,
|
||||
scythe,
|
||||
sunderer,
|
||||
valkyrie,
|
||||
vanguard,
|
||||
};
|
||||
|
||||
export const vehicleIconMap = {
|
||||
ant,
|
||||
chimera,
|
||||
corsair,
|
||||
dervish,
|
||||
flash,
|
||||
galaxy,
|
||||
harasser,
|
||||
javelin,
|
||||
liberator,
|
||||
lightning,
|
||||
magrider,
|
||||
mosquito,
|
||||
prowler,
|
||||
reaver,
|
||||
scythe,
|
||||
sunderer,
|
||||
valkyrie,
|
||||
vanguard,
|
||||
};
|
1904
build/index.js
Normal file
7
build/index.js.map
Normal file
1
build/metafile.css.json
Normal file
1
build/metafile.js.json
Normal file
1
build/metafile.server.json
Normal file
1
build/version.txt
Normal file
|
@ -0,0 +1 @@
|
|||
918ff520
|
BIN
bun.lockb
Executable file
58
flake.lock
generated
Normal file
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1717285511,
|
||||
"narHash": "sha256-iKzJcpdXih14qYVcZ9QC9XuZYnPc6T8YImb6dX166kw=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "2a55567fcf15b1b1c7ed712a2c6fadaec7412ea8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1718318537,
|
||||
"narHash": "sha256-4Zu0RYRcAY/VWuu6awwq4opuiD//ahpc2aFHg2CWqFY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e9ee548d90ff586a6471b4ae80ae9cfcbceb3420",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"lastModified": 1717284937,
|
||||
"narHash": "sha256-lIbdfCsf8LMFloheeE6N31+BMIeixqyQWbSr2vk79EQ=",
|
||||
"type": "tarball",
|
||||
"url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
"url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-parts": "flake-parts",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
23
flake.nix
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
description = "https://noe.sh";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||
};
|
||||
|
||||
outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } {
|
||||
systems = [ "x86_64-linux" "aarch64-linux" ];
|
||||
perSystem = { config, self', pkgs, lib, system, ... }: {
|
||||
devShells.default = import ./shell.nix { inherit pkgs; };
|
||||
|
||||
packages.default = pkgs.stdenvNoCC.mkDerivation {
|
||||
name = "noe.sh";
|
||||
src = ./.;
|
||||
installPhase = ''
|
||||
cp -r . $out/
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
42
hack/download-vehicle-icons.mjs
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { writeFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
// thanks varoombaa
|
||||
const vehicles = [
|
||||
["sunderer", 2, 264],
|
||||
["flash", 1, 262],
|
||||
["lightning", 3, 258],
|
||||
["magrider", 4, 259],
|
||||
["vanguard", 5, 265],
|
||||
["prowler", 6, 261],
|
||||
["scythe", 7, 266],
|
||||
["reaver", 8, 263],
|
||||
["mosquito", 9, 260],
|
||||
["liberator", 10, 257],
|
||||
["galaxy", 11, 256],
|
||||
["harasser", 12, 8852],
|
||||
["valkyrie", 14, 79711],
|
||||
["ant", 15, 84726],
|
||||
["glaive", 163, 264],
|
||||
["flash", 1001, 260],
|
||||
["liberator", 1010, 258],
|
||||
["galaxy", 1011, 256],
|
||||
["colossus", 2007, 92801],
|
||||
["bastion", 2019, 92392],
|
||||
["javelin", 2033, 92332],
|
||||
["dervish", 2136, 93607],
|
||||
["chimera", 2137, 93604],
|
||||
["corsair", 2142, 95012],
|
||||
];
|
||||
|
||||
for (let [name, , imageID] of vehicles) {
|
||||
const response = await fetch(
|
||||
`https://census.daybreakgames.com/files/ps2/images/static/${imageID}.png`
|
||||
);
|
||||
|
||||
const buf = await response.arrayBuffer();
|
||||
const outPath = `./app/images/vehicles/${name}.png`;
|
||||
writeFileSync(outPath, Buffer.from(buf));
|
||||
|
||||
console.log(`wrote ${name} (${imageID}) to ${outPath}`);
|
||||
}
|
12214
package-lock.json
generated
52
package.json
|
@ -2,39 +2,39 @@
|
|||
"name": "ps2.live",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "remix build",
|
||||
"dev:remix": "remix watch",
|
||||
"dev:wrangler": "npm run wrangler",
|
||||
"dev": "cross-env NODE_ENV=development npm-run-all build --parallel \"dev:*\"",
|
||||
"start": "cross-env NODE_ENV=production npm run wrangler --live-reload",
|
||||
"typecheck": "tsc",
|
||||
"wrangler": "wrangler pages dev ./public",
|
||||
"pages:deploy": "npm run build && wrangler pages publish ./public"
|
||||
"build": "remix vite:build",
|
||||
"dev": "node ./server.mjs",
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@remix-run/cloudflare": "^1.17.0",
|
||||
"@remix-run/cloudflare-pages": "^1.17.0",
|
||||
"@remix-run/css-bundle": "^1.17.0",
|
||||
"@remix-run/react": "^1.17.0",
|
||||
"@remix-run/css-bundle": "^2.9.2",
|
||||
"@remix-run/express": "^2.9.2",
|
||||
"@remix-run/node": "^2.9.2",
|
||||
"@remix-run/react": "^2.9.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"isbot": "^3.6.10",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"express": "^4.19.2",
|
||||
"isbot": "^4.4.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "3.x",
|
||||
"@remix-run/dev": "^1.17.0",
|
||||
"@remix-run/eslint-config": "^1.17.0",
|
||||
"@types/react": "^18.2.10",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"@vanilla-extract/css": "^1.11.1",
|
||||
"@remix-run/dev": "^2.9.2",
|
||||
"@remix-run/eslint-config": "^2.9.2",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vanilla-extract/css": "^1.15.3",
|
||||
"@vanilla-extract/vite-plugin": "^4.0.11",
|
||||
"eslint": "^8.42.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"typescript": "^5.1.3",
|
||||
"wrangler": "^3.1.0"
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.13"
|
||||
}
|
||||
"description": "- [Remix Docs](https://remix.run/docs)",
|
||||
"version": "1.0.0",
|
||||
"main": ".eslintrc.js",
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
/** @type {import('@remix-run/dev').AppConfig} */
|
||||
module.exports = {
|
||||
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",
|
||||
// publicPath: "/build/",
|
||||
future: {
|
||||
v2_errorBoundary: true,
|
||||
v2_headers: true,
|
||||
v2_meta: true,
|
||||
v2_normalizeFormMethod: true,
|
||||
v2_routeConvention: true,
|
||||
},
|
||||
};
|
2
remix.env.d.ts
vendored
|
@ -1,3 +1 @@
|
|||
/// <reference types="@remix-run/dev" />
|
||||
/// <reference types="@remix-run/cloudflare" />
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
|
|
26
server.mjs
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { createRequestHandler } from "@remix-run/express";
|
||||
import express from "express";
|
||||
|
||||
const viteDevServer =
|
||||
process.env.NODE_ENV === "production"
|
||||
? null
|
||||
: await import("vite").then((vite) =>
|
||||
vite.createServer({
|
||||
server: { middlewareMode: true },
|
||||
})
|
||||
);
|
||||
|
||||
const app = express();
|
||||
app.use(
|
||||
viteDevServer ? viteDevServer.middlewares : express.static("build/client")
|
||||
);
|
||||
|
||||
const build = viteDevServer
|
||||
? () => viteDevServer.ssrLoadModule("virtual:remix/server-build")
|
||||
: await import("./build/server/index.js");
|
||||
|
||||
app.all("*", createRequestHandler({ build }));
|
||||
|
||||
app.listen(3000, () => {
|
||||
console.log("App listening on http://localhost:3000");
|
||||
});
|
|
@ -1,8 +0,0 @@
|
|||
import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";
|
||||
import * as build from "@remix-run/dev/server-build";
|
||||
|
||||
export const onRequest = createPagesFunctionHandler({
|
||||
build,
|
||||
getLoadContext: (context) => context.env,
|
||||
mode: process.env.NODE_ENV,
|
||||
});
|
8
shell.nix
Normal file
|
@ -0,0 +1,8 @@
|
|||
{ pkgs ? import <nixpkgs> {} }: pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
nodePackages.serve
|
||||
nodePackages.prettier
|
||||
nodejs
|
||||
bun
|
||||
];
|
||||
}
|
|
@ -12,9 +12,6 @@
|
|||
"allowJs": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./app/*"]
|
||||
},
|
||||
|
||||
// Remix takes care of building everything in `remix build`.
|
||||
"noEmit": true
|
||||
|
|
7
vite.config.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { vitePlugin as remix } from "@remix-run/dev";
|
||||
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [remix(), vanillaExtractPlugin()],
|
||||
});
|
|
@ -1 +0,0 @@
|
|||
compatibility_date = "2023-06-10"
|