swap to ultra-fast APIs on homepage, finish continents

This commit is contained in:
41666 2023-06-09 12:55:24 -04:00
parent 644e25f673
commit 9b439d0a19
14 changed files with 365 additions and 114 deletions

View file

@ -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",
};

View file

@ -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 (
<div className={styles.bar}>
<div className={tiny ? styles.tinyBar : styles.bar}>
<div className={styles.left} style={{ flexGrow: vs + 1 }}>
{vsPercent}%
{tiny ? <>&nbsp;</> : `${vsPercent}%`}
</div>
<div className={styles.center} style={{ flexGrow: nc + 1 }}>
{ncPercent}%
{tiny ? <>&nbsp;</> : `${ncPercent}%`}
</div>
<div className={styles.right} style={{ flexGrow: tr + 1 }}>
{trPercent}%
{tiny ? <>&nbsp;</> : `${trPercent}%`}
</div>
</div>
);

View file

@ -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[];
}) => (
<div className={styles.container}>
{worlds.map((world) => (
{metagame.map((world) => (
<IndexWorld
key={world.id}
world={world}
health={health.worlds.find((w) => world.name.toLowerCase() === w.name)}
metagame={world}
population={
population.find((p) => p.id === world.id) as PopulationWorld
}
/>
))}
</div>

View file

@ -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`,
});

View file

@ -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 (
<div className={styles.container}>
<Link to={`/worlds/${world.id}`} className={styles.header}>
<div className={styles.headerName}>{world.name}</div>
<Link to={`/worlds/${worldId}`} className={styles.header}>
<div className={styles.headerName}>{name}</div>
<div className={styles.headerMarkers}>
[{location}] [{platform}]{" "}
<div
className={styles.circle}
style={{
backgroundColor: health?.status === "UP" ? "limegreen" : "red",
}}
title={`Status: ${health?.status} || Last event: ${timeSinceLastEvent}`}
></div>
</div>
<div className={styles.headerDetailsLink}>DETAILS </div>
</Link>
<div className={styles.details}>
<div className={styles.population}>
<div className={styles.totalPop}>
{totalPopulation(world.population)}
{population.factions.vs +
population.factions.nc +
population.factions.tr}
</div>
<div className={styles.popFaction}>
<img className={styles.popImage} src={vsLogo} alt="VS" />{" "}
{world.population.vs}
{population.factions.vs}
</div>
<div className={styles.popFaction}>
<img className={styles.popImage} src={ncLogo} alt="NC" />{" "}
{world.population.nc}
{population.factions.nc}
</div>
<div className={styles.popFaction}>
<img className={styles.popImage} src={trLogo} alt="TR" />{" "}
{world.population.tr}
{population.factions.tr}
</div>
</div>
<FactionBar population={world.population} />
<FactionBar population={population.factions} />
</div>
<div className={c(worldId === 19 && styles.jaegerConts)}>
{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 ? (
<div
key={zone.id}
className={c(styles.continent, zone.alert && styles.alertCont)}
>
<div className={styles.contName}>
<div
className={styles.contCircle}
style={
{
"--upper-color": upper,
"--lower-color": lower,
} as any
}
></div>
<div>{name}</div>
</div>
<div className={styles.contBars}>
<div>
<div className={styles.contBarTitle}>TERRITORY CONTROL</div>
<FactionBar population={zone.territory} />
</div>
{zone.alert && (
<div>
<div className={styles.contBarTitle}>
{snakeCaseToTitleCase(
zone.alert.alert_type
).toUpperCase()}{" "}
ALERT PROGRESS | STARTED{" "}
{humanTimeAgo(
Date.now() -
new Date(zone.alert.start_time).getTime(),
true
).toUpperCase()}{" "}
AGO
</div>
<FactionBar population={zone.alert.percentages} />
</div>
)}
</div>
</div>
) : (
<div key={zone.id} className={styles.contName}>
<div
className={styles.contCircle}
style={
{
"--upper-color": upper,
"--lower-color": lower,
} as any
}
></div>
<div>{name}</div>
</div>
);
})}
</div>
</div>
);

View file

@ -0,0 +1,8 @@
import { style } from "@vanilla-extract/css";
export const outer = style({
display: "flex",
alignItems: "center",
justifyContent: "center",
minHeight: "100vh",
});

View file

@ -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<typeof loader>();
return (
<div>
<h1>PS2.LIVE</h1>
<h2>Worlds</h2>
<WorldContainer worlds={data.allWorlds} health={data.health} />
<div className={outer}>
<WorldContainer metagame={data.metagame} population={data.population} />
</div>
);
}

View file

@ -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));

1
app/utils/classes.ts Normal file
View file

@ -0,0 +1 @@
export const c = (...args: any[]) => args.filter((x) => !!x).join(" ");

31
app/utils/metagame.ts Normal file
View file

@ -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<MetagameWorld[]> => {
const response = await fetch("https://metagame.ps2.live/all");
const data: MetagameWorld[] = await response.json();
return data;
};

15
app/utils/population.ts Normal file
View file

@ -0,0 +1,15 @@
export type PopulationWorld = {
id: number;
average: number;
factions: {
nc: number;
tr: number;
vs: number;
};
};
export const fetchPopulationWorlds = async (): Promise<PopulationWorld[]> => {
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 }));
};

View file

@ -12,7 +12,7 @@ export const saerroFetch = async <T>(query: string): Promise<T> => {
};
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<typeof allVehicles[number], Population> & {
total: number;
};
classes?: Record<(typeof allClasses)[number], Population>;
classes?: Record<typeof allClasses[number], Population>;
};
export type World = {

View file

@ -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<string, { name: string; colors: [string, string] }> =
{
"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"],
},
};

View file

@ -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: "???",
},
};