diff --git a/.gitignore b/.gitignore index 93acad9..c523f8d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,7 @@ node_modules /functions/\[\[path\]\].js /functions/\[\[path\]\].js.map /public/build -.env \ No newline at end of file +.env + +.DS_Store +**/.DS_Store diff --git a/.node-version b/.node-version index 5b0ad74..86d4688 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -16.13.0 \ No newline at end of file +20.2.0 diff --git a/app/components/about.css.ts b/app/components/about.css.ts new file mode 100644 index 0000000..30a6df7 --- /dev/null +++ b/app/components/about.css.ts @@ -0,0 +1,78 @@ +import { style } from "@vanilla-extract/css"; + +export const header = style({ + fontSize: "2rem", + padding: "1rem", +}); + +export const outer = style({ + display: "flex", + alignItems: "center", + justifyContent: "center", + minHeight: "calc(100vh - 300px)", + textAlign: "center", + flexDirection: "column", +}); + +export const link = style({ + color: "#9e9e9e", + textDecoration: "none", + transition: "color 0.2s ease-in-out", + ":hover": { + color: "#d53875", + }, +}); + +export const itemContainer = style({ + display: "flex", + flexWrap: "wrap", + justifyContent: "center", + alignItems: "center", + flexDirection: "column", +}); + +export const item = style({ + listStyle: "none", + display: "flex", + padding: "1.5rem 1rem", + fontSize: "1.5rem", + backgroundColor: "#222", + margin: "1rem", + borderRadius: "0.4rem", + justifyContent: "space-between", +}); + +export const itemLink = style({ + display: "block", + marginRight: "0.75rem", + paddingRight: "0.75rem", + borderRight: "1px solid #666", + color: "#d53875", + textDecoration: "none", + transition: "color 0.2s ease-in-out", + + ":hover": { + color: "#efefef", + }, +}); + +export const itemGithubLink = style({ + display: "flex", + alignItems: "center", + marginLeft: "0.75rem", + paddingLeft: "0.75rem", + borderLeft: "1px solid #666", + color: "#ddd", + textDecoration: "none", + transition: "color 0.2s ease-in-out", + fontSize: "1rem", + + ":hover": { + color: "#efefef", + }, +}); + +export const love = style({ + fontSize: "0.9rem", + color: "#aaa", +}); diff --git a/app/components/alert-timer.css.ts b/app/components/alert-timer.css.ts new file mode 100644 index 0000000..7803a2e --- /dev/null +++ b/app/components/alert-timer.css.ts @@ -0,0 +1,23 @@ +import { keyframes, style } from "@vanilla-extract/css"; + +const alertDotBlink = keyframes({ + from: { + backgroundColor: "#ff2d2d", + }, + to: { + backgroundColor: "#662929", + }, +}); + +export const alertDot = style({ + display: "inline-block", + height: "0.5rem", + width: "0.5rem", + borderRadius: "50%", + background: "#ff2d2d", + animation: `${alertDotBlink} var(--speed) ease-in-out infinite alternate`, +}); + +export const timer = style({ + fontSize: "0.8rem", +}); diff --git a/app/components/alert-timer.tsx b/app/components/alert-timer.tsx new file mode 100644 index 0000000..ac035e1 --- /dev/null +++ b/app/components/alert-timer.tsx @@ -0,0 +1,66 @@ +import { useEffect, useState } from "react"; +import type { MetagameWorld } from "~/utils/metagame"; +import { humanTimeAgo } from "~/utils/strings"; +import * as styles from "./alert-timer.css"; + +const endTime = (alert: Required["alert"]) => { + let alertDurationMins = 90; + + switch (alert.alert_type) { + case "air": + alertDurationMins = 30; + break; + + case "sudden_death": + case "max": + alertDurationMins = 15; + break; + + default: + break; + } + + return new Date(alert.start_time).getTime() + alertDurationMins * 60 * 1000; +}; + +const timeLeftString = (alert: MetagameWorld["zones"][0]["alert"]) => { + if (alert) { + const time = endTime(alert) - Date.now(); + if (time < 2000) { + return <>JUST ENDED; + } + + const speed = time < 1000 * 60 * 15 ? "1s" : "4s"; + + return ( + <> + {humanTimeAgo(time, true).toUpperCase()} LEFT{" "} +
+ + ); + } else { + return <>; + } +}; + +export const AlertTimer = ({ + alert, +}: { + alert: MetagameWorld["zones"][0]["alert"]; +}) => { + const [timeLeft, setTimeLeft] = useState(timeLeftString(alert)); + + useEffect(() => { + if (alert) { + const interval = setInterval(() => { + setTimeLeft(timeLeftString(alert)); + }, 1000); + return () => clearInterval(interval); + } + }, [alert]); + + return
{timeLeft}
; +}; diff --git a/app/components/faction-bar.css.ts b/app/components/faction-bar.css.ts new file mode 100644 index 0000000..ef2609c --- /dev/null +++ b/app/components/faction-bar.css.ts @@ -0,0 +1,42 @@ +import type { ComplexStyleRule } from "@vanilla-extract/css"; +import { style } from "@vanilla-extract/css"; + +export const bar = style({ + display: "flex", + alignItems: "center", + justifyContent: "center", + flexDirection: "row", + overflow: "hidden", + borderRadius: "0.4rem", + border: "2px solid #4d4d4d", +}); + +export const tinyBar = style({ + display: "flex", + alignItems: "center", + justifyContent: "center", + flexDirection: "row", + overflow: "hidden", + fontSize: 5, +}); + +const shared: ComplexStyleRule = { + textAlign: "center", + boxShadow: "inset 0 0 0.5rem rgb(0 0 0 / 10%)", +}; + +export const left = style({ + ...shared, + backgroundColor: "#991cba", +}); +export const center = style({ + ...shared, + backgroundColor: "#1564cc", + borderLeft: "1px solid #4d4d4d", + borderRight: "2px solid #4d4d4d", + boxShadow: "inset 0 0 0.5rem rgb(180 180 180 / 10%)", +}); +export const right = style({ + ...shared, + backgroundColor: "#d30101", +}); diff --git a/app/components/faction-bar.tsx b/app/components/faction-bar.tsx new file mode 100644 index 0000000..727c65c --- /dev/null +++ b/app/components/faction-bar.tsx @@ -0,0 +1,33 @@ +import { useMemo } from "react"; +import type { Population } from "~/utils/saerro"; +import * as styles from "./faction-bar.css"; + +export const FactionBar = ({ + population: { vs, nc, tr }, + tiny, +}: { + population: Population; + tiny?: boolean; +}) => { + 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 ( +
+
+ {tiny ? <>  : `${vsPercent}%`} +
+
+ {tiny ? <>  : `${ncPercent}%`} +
+
+ {tiny ? <>  : `${trPercent}%`} +
+
+ ); +}; diff --git a/app/components/faction-pie.css.ts b/app/components/faction-pie.css.ts new file mode 100644 index 0000000..4dc2a88 --- /dev/null +++ b/app/components/faction-pie.css.ts @@ -0,0 +1,19 @@ +import { style } from "@vanilla-extract/css"; + +export const pieRoot = style({ + width: "1em", + height: "1em", + borderRadius: "50%", + position: "relative", + + "::after": { + content: "''", + position: "absolute", + top: "var(--inner-margin)", + left: "var(--inner-margin)", + right: "var(--inner-margin)", + bottom: "var(--inner-margin)", + borderRadius: "50%", + background: "var(--inner-bg)", + }, +}); diff --git a/app/components/faction-pie.tsx b/app/components/faction-pie.tsx new file mode 100644 index 0000000..2d9ed32 --- /dev/null +++ b/app/components/faction-pie.tsx @@ -0,0 +1,40 @@ +import type { Population } from "~/utils/saerro"; +import { pieRoot } from "./faction-pie.css"; + +export const FactionPie = ({ + population, + innerMargin, + innerBackground, + size, +}: { + population: Population; + innerMargin?: number; + innerBackground?: string; + size?: string; +}) => { + const { nc, tr, vs } = population; + const total = nc + tr + vs; + + const trPct = (tr / total) * 100; + const vsPct = (vs / total) * 100; + + return ( +
+   +
+ ); +}; diff --git a/app/components/footer.css.ts b/app/components/footer.css.ts new file mode 100644 index 0000000..f652d2e --- /dev/null +++ b/app/components/footer.css.ts @@ -0,0 +1,76 @@ +import { style } from "@vanilla-extract/css"; +import footer from "~/images/footer.jpg"; + +export const root = style({ + height: 300, + position: "relative", + display: "flex", + alignItems: "center", + justifyContent: "center", + flexDirection: "column", + lineHeight: 1, +}); +export const background = style({ + backgroundImage: `url(${footer})`, + backgroundSize: "cover", + backgroundPosition: "bottom", + maskImage: "linear-gradient(to bottom, transparent 25%, black)", + WebkitMaskImage: "linear-gradient(to bottom, transparent 25%, black)", + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0, +}); + +export const logo = style({ + fontSize: "3rem", + fontWeight: "bold", + fontFamily: "Unbounded, monospace", + zIndex: 1, + textShadow: "0 0 2em black", +}); +export const logoX = style({ + color: "#c8a714", + position: "relative", + top: "0.8rem", + left: "0.075rem", +}); +export const logoLive = style({ + color: "#d8d8d8", +}); +export const logoDot = style({ + display: "inline-block", + width: "0.75em", + height: "0.75em", + borderRadius: "50%", + backgroundColor: "#d8d8d8", + position: "relative", + left: "0.05rem", + top: "0.075em", + boxShadow: "0 0 2em black", + backgroundImage: `conic-gradient( + #d30101 0deg 45deg, + #991cba 75deg 165deg, + #1564cc 195deg 285deg, + #d30101 315deg 360deg + )`, +}); +export const lowerLogo = style({ + textAlign: "right", + display: "flex", + justifyContent: "space-between", + fontSize: "0.8rem", + fontWeight: "bold", + fontFamily: "Helvetica, Arial, sans-serif", + padding: "0 0.2rem", + color: "#aaa", +}); +export const link = style({ + color: "#aaa", + textDecoration: "none", + transition: "color 0.2s ease-in-out", + ":hover": { + color: "gold", + }, +}); diff --git a/app/components/footer.tsx b/app/components/footer.tsx new file mode 100644 index 0000000..978f384 --- /dev/null +++ b/app/components/footer.tsx @@ -0,0 +1,29 @@ +import { Link } from "@remix-run/react"; +import * as styles from "./footer.css"; + +export const Footer = ({ isMainPage }: { isMainPage?: boolean }) => ( + +); diff --git a/app/components/index-world-container.css.ts b/app/components/index-world-container.css.ts new file mode 100644 index 0000000..d1487b1 --- /dev/null +++ b/app/components/index-world-container.css.ts @@ -0,0 +1,8 @@ +import { style } from "@vanilla-extract/css"; + +export const container = style({ + display: "flex", + flexBasis: "100%", + flexWrap: "wrap", + justifyContent: "center", +}); diff --git a/app/components/index-world-container.tsx b/app/components/index-world-container.tsx new file mode 100644 index 0000000..44ae6f0 --- /dev/null +++ b/app/components/index-world-container.tsx @@ -0,0 +1,24 @@ +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"; + +export const WorldContainer = ({ + metagame, + population, +}: { + metagame: MetagameWorld[]; + population: PopulationWorld[]; +}) => ( +
+ {metagame.map((world) => ( + p.id === world.id) as PopulationWorld + } + /> + ))} +
+); diff --git a/app/components/index-world.css.ts b/app/components/index-world.css.ts new file mode 100644 index 0000000..9f8f2e1 --- /dev/null +++ b/app/components/index-world.css.ts @@ -0,0 +1,212 @@ +import { keyframes, style } from "@vanilla-extract/css"; + +export const container = style({ + background: "#333", + flexBasis: "30%", + margin: "0.5rem", + + "@media": { + // under 600px + "screen and (max-width: 800px)": { + flexBasis: "100%", + }, + // between 600px and 1000px + "screen and (min-width: 800px) and (max-width: 1300px)": { + flexBasis: "45%", + }, + }, +}); + +export const header = style({ + display: "flex", + alignItems: "center", + color: "inherit", + textDecoration: "none", + transition: "background-color 0.2s ease-in-out", + backgroundColor: "#222", + ":hover": { + backgroundColor: "#383838", + }, +}); +export const headerName = style({ + padding: "0.5rem", + fontSize: "1.5rem", +}); +export const headerDetailsLink = style({ + fontVariant: "small-caps", + fontSize: "0.8rem", + color: "#aaa", + paddingRight: "0.5rem", +}); +export const headerMarkers = style({ + fontSize: "0.8rem", + flex: 1, + fontWeight: "bold", + color: "#aaa", +}); + +export const circle = style({ + display: "inline-block", + width: "0.4rem", + height: "0.4rem", + borderRadius: "50%", + marginLeft: "0.2rem", +}); + +export const details = style({ + padding: "0.5rem", +}); + +export const population = style({ + display: "flex", + alignItems: "center", + justifyContent: "space-evenly", +}); + +export const popFaction = style({ + display: "flex", + flexDirection: "row", + alignItems: "center", + justifyContent: "center", +}); + +export const popImage = style({ + height: "1.5rem", + marginRight: "0.5rem", +}); + +export const totalPop = style({ + fontWeight: "bold", + fontSize: "1.2rem", +}); + +export const continent = style({ + display: "flex", + flexDirection: "row", + alignItems: "center", + justifyContent: "space-evenly", + padding: "1rem", + background: "#222", + margin: "0.5rem", + borderRadius: "0.4rem", +}); + +export const contBars = style({ + flex: 1, + paddingLeft: "0.5rem", +}); + +export const contBarTitle = style({ + fontWeight: "bold", + fontSize: "0.7rem", + padding: "0.15rem", + lineHeight: 0.8, + display: "flex", + justifyContent: "space-between", +}); + +export const barSeparator = style({ + height: "0.5rem", + width: "100%", +}); + +export const contCircle = style({ + height: "2rem", + width: "2rem", + borderRadius: "50%", + background: "linear-gradient(45deg, var(--upper-color), var(--lower-color))", + boxShadow: "0 0 0.5rem 0.1rem rgb(var(--lower-color) / 15%)", +}); + +export const contName = style({ + display: "flex", + alignItems: "center", + justifyContent: "center", + fontWeight: "bold", + flexDirection: "column", + minWidth: "4rem", + paddingTop: "0.5rem", +}); + +export const jaegerConts = style({ + display: "flex", + flexDirection: "row", + alignItems: "center", + padding: "1rem", + justifyContent: "space-evenly", + backgroundColor: "#222", + borderRadius: "0.4rem", + margin: "0.5rem", +}); + +const alertFade = keyframes({ + from: { + borderColor: "#ff2d2d", + }, + to: { + borderColor: "#222", + }, +}); + +export const alertCont = style({ + border: "2px solid #ff2d2d", + animation: `${alertFade} 1s ease-in-out 4 alternate`, +}); + +const alertDotBlink = keyframes({ + from: { + backgroundColor: "#ff2d2d", + }, + to: { + backgroundColor: "#662929", + }, +}); + +export const alertDot = style({ + display: "inline-block", + height: "0.5rem", + width: "0.5rem", + borderRadius: "50%", + background: "#ff2d2d", + animation: `${alertDotBlink} var(--speed) ease-in-out infinite alternate`, +}); + +export const nextCont = style({ + display: "flex", + flexDirection: "row", + alignItems: "center", + padding: "0.5rem", + justifyContent: "center", + backgroundColor: "#222", + borderRadius: "0.4rem", + margin: "0.5rem", + color: "#aaa", +}); + +export const nextContText = style({ + fontWeight: "bold", + textTransform: "uppercase", + marginRight: "0.5rem", +}); + +export const oopsies = style({ + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + fontSize: "1.5rem", + height: "10rem", +}); + +const oopsiesSpinAnim = keyframes({ + from: { + transform: "rotate(0deg)", + }, + to: { + transform: "rotate(360deg)", + }, +}); + +export const oopsiesSpin = style({ + animation: `${oopsiesSpinAnim} 2s linear infinite`, +}); diff --git a/app/components/index-world.tsx b/app/components/index-world.tsx new file mode 100644 index 0000000..31a61f3 --- /dev/null +++ b/app/components/index-world.tsx @@ -0,0 +1,194 @@ +import { Link } from "@remix-run/react"; +import { + humanTimeAgo, + 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 { 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 { AlertTimer } from "./alert-timer"; + +export type IndexWorldProps = { + metagame: MetagameWorld; + population: PopulationWorld; +}; + +export const IndexWorld = ({ metagame, population }: IndexWorldProps) => { + const worldId = metagame.id; + const { platform, location, name } = worlds[String(worldId || "default")]; + + if (metagame.zones.length === 0) { + return ; + } + + const nextZone = metagame.zones.sort( + (a, b) => + new Date(a.locked_since ?? Date.now()).getTime() - + new Date(b.locked_since ?? Date.now()).getTime() + )[0]; + const nextZoneStrings = zones[nextZone.id]; + + return ( +
+ +
{name}
+
+ [{location}] [{platform}]{" "} +
+
DETAILS โ‡จ
+ +
+
+
+ {population.factions.vs + + population.factions.nc + + population.factions.tr} +
+
+ VS{" "} + {population.factions.vs} +
+
+ NC{" "} + {population.factions.nc} +
+
+ TR{" "} + {population.factions.tr} +
+
+ +
+
+ {metagame.zones + .filter((zone) => !zone.locked) + .sort((a, b) => { + return a.alert && !b.alert ? -1 : b.alert && !a.alert ? 1 : 0; + }) + .map((zone) => { + return worldId !== 19 ? ( + + ) : ( + + ); + })} + {worldId !== 19 && ( +
+
Next continent »
{" "} +
+
+
{nextZoneStrings.name}
+
+
+ )} +
+
+ ); +}; + +const JaegerContinent = ({ zone }: { zone: MetagameWorld["zones"][0] }) => { + const { + name, + colors: [upper, lower], + } = zones[zone.id]; + return ( +
+
+
{name}
+
+ ); +}; + +const Continent = ({ zone }: { zone: MetagameWorld["zones"][0] }) => { + const { + name, + colors: [upper, lower], + } = zones[zone.id]; + + return ( +
+
+
+
{name}
+
+
+
+
TERRITORY CONTROL
+ +
+ {zone.alert && ( + <> +
+
+
+
+ {snakeCaseToTitleCase(zone.alert.alert_type).toUpperCase()}{" "} + ALERT PROGRESS +
{" "} +
+ {" "} +
+
+ +
+ + )} +
+
+ ); +}; + +const BrokenWorld = ({ worldId }: { worldId: number }) => { + const { platform, location, name } = worlds[String(worldId || "default")]; + + return ( +
+ +
{name}
+
+ [{location}] [{platform}]{" "} +
+
DETAILS โ‡จ
+ +
+
+ Daybreak made an oopsie. +
+
๐Ÿ™‚
+
+
+
+ ); +}; diff --git a/app/components/index.css.ts b/app/components/index.css.ts new file mode 100644 index 0000000..6932004 --- /dev/null +++ b/app/components/index.css.ts @@ -0,0 +1,8 @@ +import { style } from "@vanilla-extract/css"; + +export const outer = style({ + display: "flex", + alignItems: "center", + justifyContent: "center", + minHeight: "100vh", +}); diff --git a/app/components/world.css.ts b/app/components/world.css.ts new file mode 100644 index 0000000..edd9208 --- /dev/null +++ b/app/components/world.css.ts @@ -0,0 +1,92 @@ +import { style } from "@vanilla-extract/css"; + +export const headerFont = style({ + fontFamily: "Unbounded, Impact, monospace", + fontWeight: "bold", +}); + +export const header = style({ + backgroundColor: "#222", + padding: "2em", + display: "flex", + flexWrap: "wrap", + fontFamily: "Unbounded, Impact, monospace", +}); + +export const headerName = style({ + fontSize: "4rem", + display: "flex", + alignItems: "center", + flexBasis: "100%", +}); + +export const headerSub = style({ + fontSize: "1rem", + color: "#ccc", + marginLeft: "1em", +}); + +export const outer = style({ + display: "flex", + justifyContent: "center", + minHeight: "100vh", + flexDirection: "column", + maxWidth: "1920px", +}); + +export const population = style({ + display: "flex", + flexWrap: "wrap", + width: "100%", + flexDirection: "column", + justifyContent: "space-evenly", +}); +export const populationHead = style({ + display: "flex", + alignItems: "center", + justifyContent: "space-evenly", + flexBasis: "100%", +}); + +export const popNumbers = style({ + display: "flex", + justifyContent: "space-evenly", +}); + +export const popItem = style({ + display: "flex", + alignItems: "center", + justifyContent: "center", +}); + +export const totalPop = style({ + fontSize: "2rem", + display: "block", + width: "4em", +}); + +export const headerConts = style({ + display: "flex", + alignItems: "center", + justifyContent: "space-evenly", + flexBasis: "100%", +}); + +export const contChart = style({ + fontSize: "4rem", +}); + +export const cont = style({ + display: "flex", + alignItems: "center", + justifyContent: "center", + flexDirection: "column", +}); + +export const contSub = style({ + width: "10rem", + fontSize: "0.8rem", + textAlign: "center", + color: "#ccc", + paddingTop: "0.5rem", +}); diff --git a/app/images/footer.jpg b/app/images/footer.jpg new file mode 100644 index 0000000..27fa63b Binary files /dev/null and b/app/images/footer.jpg differ diff --git a/app/images/nc-100.png b/app/images/nc-100.png new file mode 100644 index 0000000..5128e75 Binary files /dev/null and b/app/images/nc-100.png differ diff --git a/app/images/nc.png b/app/images/nc.png new file mode 100644 index 0000000..e1f57a1 Binary files /dev/null and b/app/images/nc.png differ diff --git a/app/images/tr-100.png b/app/images/tr-100.png new file mode 100644 index 0000000..1cf05e3 Binary files /dev/null and b/app/images/tr-100.png differ diff --git a/app/images/tr.png b/app/images/tr.png new file mode 100644 index 0000000..d2c97f7 Binary files /dev/null and b/app/images/tr.png differ diff --git a/app/images/vs-100.png b/app/images/vs-100.png new file mode 100644 index 0000000..2b15f5a Binary files /dev/null and b/app/images/vs-100.png differ diff --git a/app/images/vs.png b/app/images/vs.png new file mode 100644 index 0000000..748753b Binary files /dev/null and b/app/images/vs.png differ diff --git a/app/reset.css b/app/reset.css new file mode 100644 index 0000000..8bc8914 --- /dev/null +++ b/app/reset.css @@ -0,0 +1,47 @@ +*,*::before,*::after { + box-sizing: border-box +} + +body,h1,h2,h3,h4,p,figure,blockquote,dl,dd { + margin: 0 +} + +ul[role="list"],ol[role="list"] { + list-style: none +} + +html:focus-within { + scroll-behavior: smooth +} + +body { + min-height: 100vh; + text-rendering: optimizeSpeed; + line-height: 1.5 +} + +a:not([class]) { + text-decoration-skip-ink: auto +} + +img,picture { + max-width: 100%; + display: block +} + +input,button,textarea,select { + font: inherit +} + +@media(prefers-reduced-motion:reduce) { + html:focus-within { + scroll-behavior: auto + } + + *,*::before,*::after { + animation-duration: .01ms !important; + animation-iteration-count: 1 !important; + transition-duration: .01ms !important; + scroll-behavior: auto !important + } +} diff --git a/app/root.css.ts b/app/root.css.ts new file mode 100644 index 0000000..8d68faa --- /dev/null +++ b/app/root.css.ts @@ -0,0 +1,8 @@ +import { style } from "@vanilla-extract/css"; + +export const root = style({ + fontFamily: "Arial, Helvetica, sans-serif", + background: "#101010", + color: "#efefef", + lineHeight: 1.6, +}); diff --git a/app/root.tsx b/app/root.tsx index 0294fbd..95c8baf 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -8,8 +8,25 @@ import { Scripts, ScrollRestoration, } from "@remix-run/react"; +import * as styles from "./root.css"; +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", + }, + { + rel: "stylesheet", + href: "https://fonts.googleapis.com/css2?family=Unbounded:wght@700&display=swap", + }, + ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []), ]; @@ -22,7 +39,7 @@ export default function App() { - + diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 643c7aa..24db3ce 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -1,38 +1,38 @@ -import type { V2_MetaFunction } from "@remix-run/cloudflare"; +import { json, type V2_MetaFunction } from "@remix-run/cloudflare"; +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"; + +export const loader = async () => { + const [metagame, population] = await Promise.all([ + fetchMetagameWorlds(), + fetchPopulationWorlds(), + ]); + + return json({ metagame: metagame.sort((a, b) => a.id - b.id), population }); +}; export const meta: V2_MetaFunction = () => { - return [{ title: "New Remix App" }]; + return [ + { title: "PS2.LIVE" }, + { + name: "description", + content: "PlanetSide 2 Live Population Stats", + }, + ]; }; export default function Index() { + const data = useLoaderData(); return ( -
-

Welcome to Remix

- +
+
+ +
+
); } diff --git a/app/routes/about.tsx b/app/routes/about.tsx new file mode 100644 index 0000000..1b0f3d7 --- /dev/null +++ b/app/routes/about.tsx @@ -0,0 +1,109 @@ +import { Footer } from "~/components/footer"; +import { + header, + item, + itemContainer, + itemGithubLink, + itemLink, + link, + love, + outer, +} from "~/components/about.css"; + +export default function About() { + return ( +
+
+
+

+ PS2.LIVE is a network of services that report on the ongoing + war on Auraxis. +

+

+ hat tips:{" "} + + fisu + + ,{" "} + + honu & varunda + + ,{" "} + + Voidwell & Lampjaw + + ,{" "} + + Sanctuary & Falcon + + ,{" "} + + PS2Alerts team + + ,{" "} + + PS2devs Discord + + , Daybreak Census Team ๐Ÿ’– +

+
+
+
    + {[ + { + name: "Saerro", + url: "https://saerro.ps2.live", + github: "https://github.com/genudine/saerro", + description: + "Population GraphQL API focussing on deep granularity.", + }, + { + name: "Metagame API", + url: "https://metagame.ps2.live", + github: "https://github.com/genudine/metagame", + description: "World states, contininent locks, alerts, etc.", + }, + { + name: "Population API", + url: "https://agg.ps2.live/population", + github: "https://github.com/genudine/agg-population", + description: "Population as seen by many services, averaged.", + }, + { + name: "Census Playground", + url: "https://try.ps2.live", + github: "https://github.com/genudine/try.ps2.live", + description: "Explore and share the Census API.", + }, + { + name: "ps2.live", + url: "https://ps2.live", + github: "https://github.com/genudine/ps2.live", + description: "This website. It's pretty cool.", + }, + { + name: "Medkit", + url: "https://github.com/genudine/medkit2", + github: "https://github.com/genudine/medkit2", + description: + "PS2 Discord bot for population/continents in channel names.", + }, + ].map(({ name, url, github, description }, i) => ( +
  • + + {name} + +
    {description}
    + + github + +
  • + ))} +
+
+

Built with ๐Ÿ’– by Doll

+
+
+
+ ); +} diff --git a/app/routes/debug.components.tsx b/app/routes/debug.components.tsx new file mode 100644 index 0000000..5d7b186 --- /dev/null +++ b/app/routes/debug.components.tsx @@ -0,0 +1,72 @@ +import { useState } from "react"; +import { FactionBar } from "~/components/faction-bar"; +import { FactionPie } from "~/components/faction-pie"; +import type { Population } from "~/utils/saerro"; + +export default function DebugComponents() { + const [population, setPopulation] = useState({ + nc: 33, + tr: 33, + vs: 33, + }); + + const [innerMargin, setInnerMargin] = useState(10); + const [innerColor, setInnerColor] = useState("black"); + return ( +
+

Debug Components

+

Faction Viz

+
+ NC{" "} + + setPopulation((p) => ({ ...p, nc: Number(e.target.value) })) + } + />{" "} + || TR{" "} + + setPopulation((p) => ({ ...p, tr: Number(e.target.value) })) + } + />{" "} + || VS{" "} + + setPopulation((p) => ({ ...p, vs: Number(e.target.value) })) + } + /> +
+
+

Horizontal Stacked Bar Chart

+ +

Pie Chart

+
+ + +
+ Inner margin{" "} + setInnerMargin(Number(e.target.value))} + /> + Inner color{" "} + setInnerColor(e.target.value)} + /> +
+
+ ); +} diff --git a/app/routes/worlds.$id.tsx b/app/routes/worlds.$id.tsx index 291ba26..6476747 100644 --- a/app/routes/worlds.$id.tsx +++ b/app/routes/worlds.$id.tsx @@ -1,46 +1,192 @@ -import type { LoaderArgs } from "@remix-run/cloudflare"; +import type { LoaderArgs, V2_MetaFunction } from "@remix-run/cloudflare"; import { json } from "@remix-run/cloudflare"; 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 { - WorldResponse, - Zone, allClasses, allVehicles, + totalPopulation, worldQuery, } from "~/utils/saerro"; -import { pascalCaseToTitleCase, toTitleCase } from "~/utils/strings"; +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"; -export const loader = async ({ params }: LoaderArgs) => { - return json(await worldQuery(params.id as string)); +type LoaderData = { + saerro: WorldResponse; + metagame: MetagameWorld; + id: string; }; -export default function Index() { - const { world } = useLoaderData(); +export async function loader({ params }: LoaderArgs) { + const [saerro, metagame] = await Promise.all([ + worldQuery(params.id as string), + fetchSingleMetagameWorld(params.id as string), + ]); + return json({ saerro, metagame, id: params.id } as LoaderData); +} + +export const meta: V2_MetaFunction = ({ data }) => { + const { saerro, id } = data as LoaderData; + const date = new Date(); + const worldInfo = worlds[String(id || "default")]; + const datetimeHumanFriendly = date.toLocaleString(worldInfo.locale, { + timeZone: worldInfo.timeZone, + dateStyle: "medium", + timeStyle: "short", + }); + return [ + { + title: `${ + worldInfo.name || "Unknown world" + } | PlanetSide 2 Live Population Stats`, + }, + { + name: "description", + content: `${worldInfo.name} currently has ${totalPopulation( + saerro.world.population + )} players online as of ${datetimeHumanFriendly} ${ + worldInfo.name + } time. VS: ${saerro.world.population.vs}, NC: ${ + saerro.world.population.nc + }, TR: ${ + saerro.world.population.tr + } -- See more detailed stats on ps2.live.`, + }, + ]; +}; + +export default function World() { + const { + saerro: { world }, + id, + metagame, + } = useLoaderData(); + + const worldInfo = worlds[String(id || "default")]; + const nextZoneID = + metagame.zones.length !== 0 + ? metagame.zones.sort( + (a, b) => + new Date(a.locked_since ?? Date.now()).getTime() - + new Date(b.locked_since ?? Date.now()).getTime() + )[0].id + : 0; return ( -
-

{world.name}

-

Total Population

-

- {world.population.total} players ({world.population.vs} VS,{" "} - {world.population.nc} NC, {world.population.tr} TR) -

-
-

Continents

- {world.zones.all.map((zone) => ( - - ))} + <> +
+
+
+
+
{worldInfo.name.toUpperCase()}
+
+ [{worldInfo.location}] [{worldInfo.platform}] +
+
+
+
+
+ {totalPopulation(world.population).toLocaleString()} +
+ PLAYERS +
+
+
+
+ VS{" "} + {world.population.vs} +
+
+ NC{" "} + {world.population.nc} +
+
+ TR{" "} + {world.population.tr} +
+
+ +
+
+
+
CONTINENT CONTROL
+ {metagame.zones.sort(contPrioritySort).map((zone, idx) => { + const zoneInfo = zones[String(zone.id)]; + return ( +
+
{zoneInfo.name.toUpperCase()}
+
+ +
+
+ {zone.alert ? ( + + ) : zone.locked ? ( + nextZoneID == zone.id ? ( + <>NEXT UP ยป + ) : ( + <>LOCKED + ) + ) : ( + <>UNLOCKED + )} +
+
+ ); + })} +
+
+
+

Continents

+ {world.zones.all.map((zone) => ( + + ))} +
+
-
+