diff --git a/app/components/faction-bar.css.ts b/app/components/faction-bar.css.ts index 925e43e..0be8c07 100644 --- a/app/components/faction-bar.css.ts +++ b/app/components/faction-bar.css.ts @@ -11,6 +11,15 @@ export const bar = style({ border: "2px solid #2d2d2d", }); +export const tinyBar = style({ + display: "flex", + alignItems: "center", + justifyContent: "center", + flexDirection: "row", + overflow: "hidden", + fontSize: 5, +}); + const shared: ComplexStyleRule = { textAlign: "center", }; diff --git a/app/components/faction-bar.tsx b/app/components/faction-bar.tsx index 8924b74..727c65c 100644 --- a/app/components/faction-bar.tsx +++ b/app/components/faction-bar.tsx @@ -1,15 +1,16 @@ import { useMemo } from "react"; import type { Population } from "~/utils/saerro"; -import { totalPopulation } 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 = totalPopulation({ vs, nc, tr, total: 0 }); + const total = nc + vs + tr; return { vsPercent: Math.round((vs / total) * 100) || 0, ncPercent: Math.round((nc / total) * 100) || 0, @@ -17,15 +18,15 @@ export const FactionBar = ({ }; }, [vs, nc, tr]); return ( -
+
- {vsPercent}% + {tiny ? <>  : `${vsPercent}%`}
- {ncPercent}% + {tiny ? <>  : `${ncPercent}%`}
- {trPercent}% + {tiny ? <>  : `${trPercent}%`}
); diff --git a/app/components/index-world-container.tsx b/app/components/index-world-container.tsx index f9501ff..44ae6f0 100644 --- a/app/components/index-world-container.tsx +++ b/app/components/index-world-container.tsx @@ -1,21 +1,23 @@ -import { useMemo } from "react"; -import type { Health, World } from "~/utils/saerro"; 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 = ({ - worlds, - health, + metagame, + population, }: { - worlds: World[]; - health: Health; + metagame: MetagameWorld[]; + population: PopulationWorld[]; }) => (
- {worlds.map((world) => ( + {metagame.map((world) => ( world.name.toLowerCase() === w.name)} + metagame={world} + population={ + population.find((p) => p.id === world.id) as PopulationWorld + } /> ))}
diff --git a/app/components/index-world.css.ts b/app/components/index-world.css.ts index 0f504a4..0e6044c 100644 --- a/app/components/index-world.css.ts +++ b/app/components/index-world.css.ts @@ -1,4 +1,4 @@ -import { style } from "@vanilla-extract/css"; +import { keyframes, style } from "@vanilla-extract/css"; export const container = style({ background: "#333", @@ -7,11 +7,11 @@ export const container = style({ "@media": { // under 600px - "screen and (max-width: 600px)": { + "screen and (max-width: 800px)": { flexBasis: "100%", }, // between 600px and 1000px - "screen and (min-width: 600px) and (max-width: 1000px)": { + "screen and (min-width: 800px) and (max-width: 1300px)": { flexBasis: "45%", }, }, @@ -79,3 +79,68 @@ 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.2rem 0.5rem", +}); + +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`, +}); diff --git a/app/components/index-world.tsx b/app/components/index-world.tsx index a1371f7..b00d112 100644 --- a/app/components/index-world.tsx +++ b/app/components/index-world.tsx @@ -1,60 +1,126 @@ import { Link } from "@remix-run/react"; -import { Health, totalPopulation, World } from "~/utils/saerro"; -import { humanTimeAgo } from "~/utils/strings"; -import { worlds } from "~/utils/worlds"; +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"; export type IndexWorldProps = { - world: World; - health?: Health["worlds"][number]; + metagame: MetagameWorld; + population: PopulationWorld; }; -export const IndexWorld = ({ world, health }: IndexWorldProps) => { - const { platform, location } = worlds[String(world.id || "default")]; - - const timeSinceLastEvent = humanTimeAgo( - new Date().getTime() - new Date(health?.lastEvent || 0).getTime() - ); +export const IndexWorld = ({ metagame, population }: IndexWorldProps) => { + const worldId = metagame.id; + const { platform, location, name } = worlds[String(worldId || "default")]; return (
- -
{world.name}
+ +
{name}
[{location}] [{platform}]{" "} -
DETAILS ⇨
- {totalPopulation(world.population)} + {population.factions.vs + + population.factions.nc + + population.factions.tr}
VS{" "} - {world.population.vs} + {population.factions.vs}
NC{" "} - {world.population.nc} + {population.factions.nc}
TR{" "} - {world.population.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) => { + let { + name, + colors: [upper, lower], + } = zones[zone.id]; + return worldId !== 19 ? ( +
+
+
+
{name}
+
+
+
+
TERRITORY CONTROL
+ +
+ {zone.alert && ( +
+
+ {snakeCaseToTitleCase( + zone.alert.alert_type + ).toUpperCase()}{" "} + ALERT PROGRESS | STARTED{" "} + {humanTimeAgo( + Date.now() - + new Date(zone.alert.start_time).getTime(), + true + ).toUpperCase()}{" "} + AGO +
+ +
+ )} +
+
+ ) : ( +
+
+
{name}
+
+ ); + })}
); 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/routes/_index.tsx b/app/routes/_index.tsx index 79b71b4..f60e7b5 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -2,10 +2,18 @@ import { json, type V2_MetaFunction } from "@remix-run/cloudflare"; import { useLoaderData } from "@remix-run/react"; import { IndexWorld } from "~/components/index-world"; import { WorldContainer } from "~/components/index-world-container"; +import { outer } from "~/components/index.css"; +import { fetchMetagameWorlds } from "~/utils/metagame"; +import { fetchPopulationWorlds } from "~/utils/population"; import { indexQuery } from "~/utils/saerro"; export const loader = async () => { - return json(await indexQuery()); + const [metagame, population] = await Promise.all([ + fetchMetagameWorlds(), + fetchPopulationWorlds(), + ]); + + return json({ metagame, population }); }; export const meta: V2_MetaFunction = () => { @@ -21,10 +29,8 @@ export const meta: V2_MetaFunction = () => { export default function Index() { const data = useLoaderData(); return ( -
-

PS2.LIVE

-

Worlds

- +
+
); } diff --git a/app/routes/worlds.$id.tsx b/app/routes/worlds.$id.tsx index 11b0c97..8877705 100644 --- a/app/routes/worlds.$id.tsx +++ b/app/routes/worlds.$id.tsx @@ -4,8 +4,7 @@ import { useLoaderData } from "@remix-run/react"; import type { Zone } from "~/utils/saerro"; import { totalPopulation } from "~/utils/saerro"; import { allClasses, allVehicles, worldQuery } from "~/utils/saerro"; -import { pascalCaseToTitleCase, toTitleCase } from "~/utils/strings"; -import { worlds } from "~/utils/worlds"; +import { pascalCaseToTitleCase, toTitleCase, worlds } from "~/utils/strings"; export const loader = async ({ params }: LoaderArgs) => { return json(await worldQuery(params.id as string)); diff --git a/app/utils/classes.ts b/app/utils/classes.ts new file mode 100644 index 0000000..46c5047 --- /dev/null +++ b/app/utils/classes.ts @@ -0,0 +1 @@ +export const c = (...args: any[]) => args.filter((x) => !!x).join(" "); diff --git a/app/utils/metagame.ts b/app/utils/metagame.ts new file mode 100644 index 0000000..f20acd8 --- /dev/null +++ b/app/utils/metagame.ts @@ -0,0 +1,31 @@ +export type MetagameWorld = { + id: number; + zones: { + id: number; + locked: boolean; + alert?: { + id: number; + zone: number; + start_time: string; + end_time: string; + ps2alerts: string; + alert_type: string; + percentages: { + nc: number; + tr: number; + vs: number; + }; + }; + territory: { + nc: number; + tr: number; + vs: number; + }; + }[]; +}; + +export const fetchMetagameWorlds = async (): Promise => { + const response = await fetch("https://metagame.ps2.live/all"); + const data: MetagameWorld[] = await response.json(); + return data; +}; diff --git a/app/utils/population.ts b/app/utils/population.ts new file mode 100644 index 0000000..1497429 --- /dev/null +++ b/app/utils/population.ts @@ -0,0 +1,15 @@ +export type PopulationWorld = { + id: number; + average: number; + factions: { + nc: number; + tr: number; + vs: number; + }; +}; + +export const fetchPopulationWorlds = async (): Promise => { + const response = await fetch("https://agg.ps2.live/population/all"); + const data: PopulationWorld[] = await response.json(); + return data.map(({ id, average, factions }) => ({ id, average, factions })); +}; diff --git a/app/utils/saerro.ts b/app/utils/saerro.ts index b8da249..b0773f4 100644 --- a/app/utils/saerro.ts +++ b/app/utils/saerro.ts @@ -12,7 +12,7 @@ export const saerroFetch = async (query: string): Promise => { }; export type Population = { - total: number; + total?: number; nc: number; tr: number; vs: number; @@ -22,10 +22,10 @@ export type Zone = { id: string; name: string; population: Population; - vehicles?: Record<(typeof allVehicles)[number], Population> & { + vehicles?: Record & { total: number; }; - classes?: Record<(typeof allClasses)[number], Population>; + classes?: Record; }; export type World = { diff --git a/app/utils/strings.ts b/app/utils/strings.ts index 94b54d5..6198263 100644 --- a/app/utils/strings.ts +++ b/app/utils/strings.ts @@ -8,14 +8,18 @@ export const pascalCaseToTitleCase = (str: string) => { return toTitleCase(str.replace(/([A-Z])/g, " $1")); }; -export const humanTimeAgo = (ms: number) => { +export const snakeCaseToTitleCase = (str: string) => { + return toTitleCase(str.replace(/_/g, " ")); +}; + +export const humanTimeAgo = (ms: number, full?: boolean) => { const millis = Math.floor(ms % 1000); const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { - return `${hours}h`; + return full ? `${hours}h ${minutes % 60}m` : `${hours}h`; } if (minutes > 0) { @@ -28,3 +32,106 @@ export const humanTimeAgo = (ms: number) => { return `${millis}ms`; }; + +export const worlds: Record< + string, + { + timeZone: string; + locale: string; + location: string; + platform: string; + name: string; + } +> = { + "1": { + name: "Connery", + timeZone: "America/Los_Angeles", + locale: "en-US", + location: "US-W", + platform: "PC", + }, + "10": { + name: "Miller", + timeZone: "UTC", + locale: "en-GB", + location: "EU", + platform: "PC", + }, + "13": { + name: "Cobalt", + timeZone: "UTC", + locale: "en-GB", + location: "EU", + platform: "PC", + }, + "17": { + name: "Emerald", + timeZone: "America/New_York", + locale: "en-US", + location: "US-E", + platform: "PC", + }, + "19": { + name: "Jaeger", + timeZone: "America/New_York", + locale: "en-US", + location: "US-E", + platform: "PC", + }, + "40": { + name: "SolTech", + timeZone: "Asia/Tokyo", + locale: "en-GB", + location: "JP", + platform: "PC", + }, + "1000": { + name: "Genudine", + timeZone: "America/New_York", + locale: "en-US", + location: "US-E", + platform: "PS4", + }, + "2000": { + name: "Ceres", + timeZone: "UTC", + locale: "en-GB", + location: "EU", + platform: "PS4", + }, + default: { + name: "Unknown", + timeZone: "UTC", + locale: "en-US", + location: "???", + platform: "???", + }, +}; + +export const zones: Record = + { + "2": { + name: "Indar", + colors: ["#edb96b", "#964c2f"], + }, + "4": { + name: "Hossin", + colors: ["#47570d", "#7b9c05"], + }, + "6": { + name: "Amerish", + colors: ["#87a12a", "#5f634f"], + }, + "8": { + name: "Esamir", + colors: ["#d5f3f5", "#a1c7e6"], + }, + "344": { + name: "Oshur", + colors: ["#00c2bf", "#174185"], + }, + default: { + name: "Unknown", + colors: ["#000000", "#000000"], + }, + }; diff --git a/app/utils/worlds.ts b/app/utils/worlds.ts deleted file mode 100644 index ed6dcd9..0000000 --- a/app/utils/worlds.ts +++ /dev/null @@ -1,59 +0,0 @@ -export const worlds: Record< - string, - { timeZone: string; locale: string; location: string; platform: string } -> = { - "1": { - timeZone: "America/Los_Angeles", - locale: "en-US", - location: "US-W", - platform: "PC", - }, - "10": { - timeZone: "UTC", - locale: "en-GB", - location: "EU", - platform: "PC", - }, - "13": { - timeZone: "UTC", - locale: "en-GB", - location: "EU", - platform: "PC", - }, - "17": { - timeZone: "America/New_York", - locale: "en-US", - location: "US-E", - platform: "PC", - }, - "19": { - timeZone: "America/New_York", - locale: "en-US", - location: "US-E", - platform: "PC", - }, - "40": { - timeZone: "Asia/Tokyo", - locale: "en-GB", - location: "JP", - platform: "PC", - }, - "1000": { - timeZone: "America/New_York", - locale: "en-US", - location: "US-E", - platform: "PS4", - }, - "2000": { - timeZone: "UTC", - locale: "en-GB", - location: "EU", - platform: "PS4", - }, - default: { - timeZone: "UTC", - locale: "en-US", - location: "???", - platform: "???", - }, -};