diff --git a/.gitignore b/.gitignore index e0a700b..c523f8d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ node_modules .env .DS_Store -*/.DS_Store \ No newline at end of file +**/.DS_Store diff --git a/app/components/faction-bar.css.ts b/app/components/faction-bar.css.ts new file mode 100644 index 0000000..925e43e --- /dev/null +++ b/app/components/faction-bar.css.ts @@ -0,0 +1,30 @@ +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 #2d2d2d", +}); + +const shared: ComplexStyleRule = { + textAlign: "center", +}; + +export const left = style({ + ...shared, + backgroundColor: "#991cba", +}); +export const center = style({ + ...shared, + backgroundColor: "#1564cc", + borderBottom: "1px solid #2d2d2d", +}); +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..917a15b --- /dev/null +++ b/app/components/faction-bar.tsx @@ -0,0 +1,32 @@ +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 }, +}: { + population: Population; +}) => { + const { vsPercent, ncPercent, trPercent } = useMemo(() => { + const total = totalPopulation({ vs, nc, tr, total: 0 }); + return { + vsPercent: Math.floor((vs / total) * 100) || 0, + ncPercent: Math.floor((nc / total) * 100) || 0, + trPercent: Math.floor((tr / total) * 100) || 0, + }; + }, [vs, nc, tr]); + return ( +
+
+ {vsPercent}% +
+
+ {ncPercent}% +
+
+ {trPercent}% +
+
+ ); +}; 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..f9501ff --- /dev/null +++ b/app/components/index-world-container.tsx @@ -0,0 +1,22 @@ +import { useMemo } from "react"; +import type { Health, World } from "~/utils/saerro"; +import { IndexWorld } from "./index-world"; +import * as styles from "./index-world-container.css"; + +export const WorldContainer = ({ + worlds, + health, +}: { + worlds: World[]; + health: Health; +}) => ( +
+ {worlds.map((world) => ( + world.name.toLowerCase() === w.name)} + /> + ))} +
+); diff --git a/app/components/index-world.css.ts b/app/components/index-world.css.ts new file mode 100644 index 0000000..5f2b720 --- /dev/null +++ b/app/components/index-world.css.ts @@ -0,0 +1,70 @@ +import { style } from "@vanilla-extract/css"; + +export const container = style({ + background: "#333", + flexBasis: "30%", + margin: "0.5rem", +}); + +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", +}); diff --git a/app/components/index-world.tsx b/app/components/index-world.tsx index 2d6d6bf..a1371f7 100644 --- a/app/components/index-world.tsx +++ b/app/components/index-world.tsx @@ -1,18 +1,61 @@ -import { World } from "~/utils/saerro"; +import { Link } from "@remix-run/react"; +import { Health, totalPopulation, World } from "~/utils/saerro"; +import { humanTimeAgo } from "~/utils/strings"; +import { worlds } from "~/utils/worlds"; +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"; export type IndexWorldProps = { world: World; + health?: Health["worlds"][number]; }; -export const IndexWorld = ({ world }: IndexWorldProps) => { +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() + ); + return ( -
-

- {world.name} (total: {world.population.total}) -

-

VS: {world.population.vs}

-

NC: {world.population.nc}

-

TR: {world.population.tr}

+
+ +
{world.name}
+
+ [{location}] [{platform}]{" "} +
+
+
DETAILS ⇨
+ +
+
+
+ {totalPopulation(world.population)} +
+
+ VS{" "} + {world.population.vs} +
+
+ NC{" "} + {world.population.nc} +
+
+ TR{" "} + {world.population.tr} +
+
+ +
); }; 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/root.css.ts b/app/root.css.ts new file mode 100644 index 0000000..e1d36a1 --- /dev/null +++ b/app/root.css.ts @@ -0,0 +1,7 @@ +import { style } from "@vanilla-extract/css"; + +export const root = style({ + fontFamily: "Arial, Helvetica, sans-serif", + background: "#101010", + color: "#efefef", +}); diff --git a/app/root.tsx b/app/root.tsx index 0294fbd..70fd3a7 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -8,8 +8,13 @@ import { Scripts, ScrollRestoration, } from "@remix-run/react"; +import * as styles from "./root.css"; export const links: LinksFunction = () => [ + { + rel: "stylesheet", + href: "https://unpkg.com/modern-css-reset@1.4.0/dist/reset.min.css", + }, ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []), ]; @@ -22,7 +27,7 @@ export default function App() { - + diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index e5b4062..79b71b4 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -1,7 +1,7 @@ import { json, type V2_MetaFunction } from "@remix-run/cloudflare"; import { useLoaderData } from "@remix-run/react"; import { IndexWorld } from "~/components/index-world"; -import type { IndexResponse } from "~/utils/saerro"; +import { WorldContainer } from "~/components/index-world-container"; import { indexQuery } from "~/utils/saerro"; export const loader = async () => { @@ -9,18 +9,22 @@ export const loader = async () => { }; export const meta: V2_MetaFunction = () => { - return [{ title: "PS2.LIVE" }]; + return [ + { title: "PS2.LIVE" }, + { + name: "description", + content: "PlanetSide 2 Live Population Stats", + }, + ]; }; export default function Index() { const data = useLoaderData(); return ( -
+

PS2.LIVE

Worlds

- {data.allWorlds.map((world) => ( - - ))} +
); } diff --git a/app/routes/worlds.$id.tsx b/app/routes/worlds.$id.tsx index a2d8691..11b0c97 100644 --- a/app/routes/worlds.$id.tsx +++ b/app/routes/worlds.$id.tsx @@ -1,9 +1,11 @@ import type { LoaderArgs, V2_MetaFunction } from "@remix-run/cloudflare"; import { json } from "@remix-run/cloudflare"; import { useLoaderData } from "@remix-run/react"; -import type { WorldResponse, Zone } from "~/utils/saerro"; +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"; export const loader = async ({ params }: LoaderArgs) => { return json(await worldQuery(params.id as string)); @@ -12,50 +14,34 @@ export const loader = async ({ params }: LoaderArgs) => { export const meta: V2_MetaFunction = ({ data }) => { const date = new Date(); const id = data?.world.id; - const timeZone = - id === 1 - ? "America/Los_Angeles" - : id === 17 || id === 19 || id === 1000 - ? "America/New_York" - : id === 40 - ? "Asia/Tokyo" - : "UTC"; - const datetimeHumanFriendly = date.toLocaleString("en-GB", { - timeZone, - hour12: true, + const worldInfo = worlds[String(id || "default")]; + const datetimeHumanFriendly = date.toLocaleString(worldInfo.locale, { + timeZone: worldInfo.timeZone, dateStyle: "medium", timeStyle: "short", }); return [ - { title: `${data?.world.name || "Unknown world"} | PS2.LIVE` }, { - name: "description", - content: `${data?.world.name} currently has ${ - data?.world.population.total - } players online as of ${datetimeHumanFriendly} local server time (). VS: ${data?.world.population.vs}, NC: ${ - data?.world.population.nc - }, TR: ${ - data?.world.population.tr - } -- See more detailed stats on ps2.live.`, + title: `${ + data?.world.name || "Unknown world" + } | PlanetSide 2 Live Population Stats`, }, { - name: "timestamp", - content: date.toISOString(), + name: "description", + content: `${data?.world.name} currently has ${data?.world.population.total} players online as of ${datetimeHumanFriendly} ${data?.world.name} time. VS: ${data?.world.population.vs}, NC: ${data?.world.population.nc}, TR: ${data?.world.population.tr} -- See more detailed stats on ps2.live.`, }, ]; }; export default function World() { - const { world } = useLoaderData(); + const { world } = useLoaderData(); return (

{world.name}

Total Population

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

@@ -73,7 +59,7 @@ const ZoneInfo = ({ zone }: { zone: Zone }) => {

{zone.name}

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

@@ -88,13 +74,14 @@ const ZoneInfo = ({ zone }: { zone: Zone }) => {

- {zone.vehicles?.total} vehicles... + {totalPopulation(zone.vehicles as any)} vehicles...

    {allVehicles.map((vehicle, idx) => (
  • - {toTitleCase(vehicle)}: {zone.vehicles?.[vehicle].total}{" "} - total, {zone.vehicles?.[vehicle].vs} VS,{" "} - {zone.vehicles?.[vehicle].nc} NC, {zone.vehicles?.[vehicle].tr} TR + {toTitleCase(vehicle)}:{" "} + {totalPopulation(zone.vehicles?.[vehicle] as any)} total,{" "} + {zone.vehicles?.[vehicle].vs} VS, {zone.vehicles?.[vehicle].nc}{" "} + NC, {zone.vehicles?.[vehicle].tr} TR
  • ))}
diff --git a/app/utils/saerro.ts b/app/utils/saerro.ts index c29b7f9..b8da249 100644 --- a/app/utils/saerro.ts +++ b/app/utils/saerro.ts @@ -44,6 +44,7 @@ export type Health = { worlds: { name: string; status: string; + lastEvent: string; }[]; }; @@ -55,19 +56,16 @@ export type IndexResponse = { export const indexQuery = async (): Promise => { const query = `{ health { - ingestReachable - ingest - database worlds { name status + lastEvent } } allWorlds { id name population { - total nc tr vs @@ -77,7 +75,6 @@ export const indexQuery = async (): Promise => { id name population { - total nc tr vs @@ -91,8 +88,6 @@ export const indexQuery = async (): Promise => { indexData.allWorlds.sort((a, b) => a.id - b.id); - console.log(indexData); - return indexData; }; @@ -169,3 +164,6 @@ export const worldQuery = async (worldID: string): Promise => { return worldData; }; + +export const totalPopulation = ({ nc, vs, tr }: Population): number => + nc + vs + tr; diff --git a/app/utils/strings.ts b/app/utils/strings.ts index e3fc68b..94b54d5 100644 --- a/app/utils/strings.ts +++ b/app/utils/strings.ts @@ -7,3 +7,24 @@ export const toTitleCase = (str: string) => { export const pascalCaseToTitleCase = (str: string) => { return toTitleCase(str.replace(/([A-Z])/g, " $1")); }; + +export const humanTimeAgo = (ms: number) => { + 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`; + } + + if (minutes > 0) { + return `${minutes}m`; + } + + if (seconds > 0) { + return `${seconds}s`; + } + + return `${millis}ms`; +}; diff --git a/app/utils/worlds.ts b/app/utils/worlds.ts new file mode 100644 index 0000000..ed6dcd9 --- /dev/null +++ b/app/utils/worlds.ts @@ -0,0 +1,59 @@ +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: "???", + }, +}; diff --git a/package.json b/package.json index a0384c1..c43018b 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,5 @@ { + "name": "ps2.live", "private": true, "sideEffects": false, "scripts": {