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)}
+
+
+

{" "}
+ {world.population.vs}
+
+
+

{" "}
+ {world.population.nc}
+
+
+

{" "}
+ {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": {