Compare commits

...
Sign in to create a new pull request.

28 commits
reset ... main

Author SHA1 Message Date
noe
a5bffa763e
Update about.tsx 2024-04-17 08:00:12 -04:00
b037d56838 max and sudden_death look the same 2023-08-03 23:45:46 -04:00
43c22e6492 fix air alert time 2023-07-08 01:39:53 -04:00
3b8d8f15b0 fix empty metagame for world page too 2023-07-01 19:00:29 -04:00
52f03ad30f fix empty metagame 2023-07-01 18:51:31 -04:00
c4d2abdbc6 add back ceres next cont 2023-06-29 15:33:13 -04:00
990013af2b first pass of new server page 2023-06-18 01:00:34 -04:00
23a1f7708b ceres doesn't report correct continent 2023-06-16 11:49:55 -04:00
59295ac746 fix ps2alerts link 2023-06-11 09:10:51 -04:00
783a0fc8a1 adjust subpixel alignment on bar 2023-06-11 09:10:16 -04:00
4fbc738fdf fix favicon 2023-06-10 23:34:42 -04:00
251931c838 update base, add /about, footer 2023-06-10 14:41:33 -04:00
434f29b967 sync missing updates 2023-06-10 11:00:43 -04:00
2314da4763 Improve alert timer 2023-06-10 10:58:30 -04:00
8b5ceaf599 sort by world id 2023-06-09 16:48:24 -04:00
ca9be2d846 show next continent 2023-06-09 16:44:05 -04:00
9b439d0a19 swap to ultra-fast APIs on homepage, finish continents 2023-06-09 12:55:24 -04:00
644e25f673 update node-version 2023-05-29 19:36:32 -04:00
7aaec57e6f make responsive 2023-05-22 21:21:12 -04:00
73dec35378 faction bar to use generic round 2023-05-22 21:13:21 -04:00
be757ddaec remove .DS_Store 2023-05-22 21:05:25 -04:00
88015a98cd initial index 2023-05-22 21:04:29 -04:00
62cc828d6a attempt cooler description 2023-05-22 15:30:55 -04:00
59bdad5113 incorrect meta style 2023-05-22 15:19:17 -04:00
d31c88115a add meta description for world page 2023-05-22 15:06:21 -04:00
ae45b29e93 handle incorrect world in meta 2023-05-22 11:32:51 -04:00
0dcc2450a2 add homepage 2023-05-21 16:02:58 -04:00
2712285a35 change saerroFetch to GET to allow caching 2023-05-21 13:40:51 -04:00
43 changed files with 3026 additions and 1568 deletions

3
.gitignore vendored
View file

@ -5,3 +5,6 @@ node_modules
/functions/\[\[path\]\].js.map
/public/build
.env
.DS_Store
**/.DS_Store

View file

@ -1 +1 @@
16.13.0
20.2.0

View file

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

View file

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

View file

@ -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<MetagameWorld["zones"][0]>["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{" "}
<div
className={styles.alertDot}
style={{ "--speed": speed } as any}
></div>
</>
);
} 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 <div className={styles.timer}>{timeLeft}</div>;
};

View file

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

View file

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

View file

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

View file

@ -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 (
<div
className={pieRoot}
style={
{
fontSize: size || "1em",
backgroundImage: `conic-gradient(
#d30101 0% ${trPct}%,
#991cba ${trPct}% ${trPct + vsPct}%,
#1564cc ${trPct + vsPct}% 100%
)`,
"--inner-margin": innerMargin ? `${innerMargin}px` : "0",
"--inner-bg": innerBackground || "none",
} as any
}
>
&nbsp;
</div>
);
};

View file

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

29
app/components/footer.tsx Normal file
View file

@ -0,0 +1,29 @@
import { Link } from "@remix-run/react";
import * as styles from "./footer.css";
export const Footer = ({ isMainPage }: { isMainPage?: boolean }) => (
<footer>
<div className={styles.root}>
<div className={styles.background}></div>
<div className={styles.logo}>
PS2
<div className={styles.logoDot}></div>
<span className={styles.logoLive}>LIVE</span>
<div className={styles.lowerLogo}>
<div>
{isMainPage ? (
<Link className={styles.link} to="/about">
more stuff »
</Link>
) : (
<Link className={styles.link} to="/">
less stuff »
</Link>
)}
</div>
<div>&copy; {new Date().getFullYear()}</div>
</div>
</div>
</div>
</footer>
);

View file

@ -0,0 +1,8 @@
import { style } from "@vanilla-extract/css";
export const container = style({
display: "flex",
flexBasis: "100%",
flexWrap: "wrap",
justifyContent: "center",
});

View file

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

View file

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

View file

@ -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 <BrokenWorld worldId={worldId} />;
}
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 (
<div className={styles.container}>
<Link to={`/worlds/${worldId}`} className={styles.header}>
<div className={styles.headerName}>{name}</div>
<div className={styles.headerMarkers}>
[{location}] [{platform}]{" "}
</div>
<div className={styles.headerDetailsLink}>DETAILS </div>
</Link>
<div className={styles.details}>
<div className={styles.population}>
<div className={styles.totalPop}>
{population.factions.vs +
population.factions.nc +
population.factions.tr}
</div>
<div className={styles.popFaction}>
<img className={styles.popImage} src={vsLogo} alt="VS" />{" "}
{population.factions.vs}
</div>
<div className={styles.popFaction}>
<img className={styles.popImage} src={ncLogo} alt="NC" />{" "}
{population.factions.nc}
</div>
<div className={styles.popFaction}>
<img className={styles.popImage} src={trLogo} alt="TR" />{" "}
{population.factions.tr}
</div>
</div>
<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) => {
return worldId !== 19 ? (
<Continent key={zone.id} zone={zone} />
) : (
<JaegerContinent key={zone.id} zone={zone} />
);
})}
{worldId !== 19 && (
<div className={styles.nextCont}>
<div className={styles.nextContText}>Next continent &raquo;</div>{" "}
<div className={styles.contName}>
<div
className={styles.contCircle}
style={
{
"--upper-color": nextZoneStrings.colors[0],
"--lower-color": nextZoneStrings.colors[1],
} as any
}
></div>
<div>{nextZoneStrings.name}</div>
</div>
</div>
)}
</div>
</div>
);
};
const JaegerContinent = ({ zone }: { zone: MetagameWorld["zones"][0] }) => {
const {
name,
colors: [upper, lower],
} = zones[zone.id];
return (
<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>
);
};
const Continent = ({ zone }: { zone: MetagameWorld["zones"][0] }) => {
const {
name,
colors: [upper, lower],
} = zones[zone.id];
return (
<div key={zone.id} className={c(styles.continent)}>
<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 className={styles.barSeparator}></div>
<div>
<div className={styles.contBarTitle}>
<div>
{snakeCaseToTitleCase(zone.alert.alert_type).toUpperCase()}{" "}
ALERT PROGRESS
</div>{" "}
<div>
<AlertTimer alert={zone.alert} />{" "}
</div>
</div>
<FactionBar population={zone.alert.percentages} />
</div>
</>
)}
</div>
</div>
);
};
const BrokenWorld = ({ worldId }: { worldId: number }) => {
const { platform, location, name } = worlds[String(worldId || "default")];
return (
<div className={styles.container}>
<Link to={`/worlds/${worldId}`} className={styles.header}>
<div className={styles.headerName}>{name}</div>
<div className={styles.headerMarkers}>
[{location}] [{platform}]{" "}
</div>
<div className={styles.headerDetailsLink}>DETAILS </div>
</Link>
<div className={styles.details}>
<div className={styles.oopsies}>
Daybreak made an oopsie.
<br />
<div className={styles.oopsiesSpin}>🙂</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

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

BIN
app/images/footer.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

BIN
app/images/nc-100.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
app/images/nc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 KiB

BIN
app/images/tr-100.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
app/images/tr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

BIN
app/images/vs-100.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
app/images/vs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

47
app/reset.css Normal file
View file

@ -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
}
}

8
app/root.css.ts Normal file
View file

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

View file

@ -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() {
<Meta />
<Links />
</head>
<body>
<body className={styles.root}>
<Outlet />
<ScrollRestoration />
<Scripts />

View file

@ -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<typeof loader>();
return (
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
<h1>Welcome to Remix</h1>
<ul>
<li>
<a
target="_blank"
href="https://remix.run/tutorials/blog"
rel="noreferrer"
>
15m Quickstart Blog Tutorial
</a>
</li>
<li>
<a
target="_blank"
href="https://remix.run/tutorials/jokes"
rel="noreferrer"
>
Deep Dive Jokes App Tutorial
</a>
</li>
<li>
<a target="_blank" href="https://remix.run/docs" rel="noreferrer">
Remix Docs
</a>
</li>
</ul>
<div>
<div className={outer}>
<WorldContainer metagame={data.metagame} population={data.population} />
</div>
<Footer isMainPage />
</div>
);
}

109
app/routes/about.tsx Normal file
View file

@ -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 (
<div>
<div className={outer}>
<div>
<p className={header}>
<b>PS2.LIVE</b> is a network of services that report on the ongoing
war on Auraxis.
</p>
<p style={{ fontStyle: "italic" }}>
hat tips:{" "}
<a className={link} href="https://ps2.fisu.pw/">
fisu
</a>
,{" "}
<a className={link} href="https://wt.honu.pw/">
honu &amp; varunda
</a>
,{" "}
<a className={link} href="https://voidwell.com/">
Voidwell &amp; Lampjaw
</a>
,{" "}
<a className={link} href="https://census.lithafalcon.cc/">
Sanctuary &amp; Falcon
</a>
,{" "}
<a className={link} href="https://ps2alerts.com/">
PS2Alerts team
</a>
,{" "}
<a className={link} href="https://discord.gg/yVzGEg3RKV">
PS2devs Discord
</a>
, Daybreak Census Team 💖
</p>
</div>
<div>
<ul className={itemContainer}>
{[
{
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) => (
<li className={item} key={i}>
<a href={url} className={itemLink}>
{name}
</a>
<div>{description} </div>
<a href={github} className={itemGithubLink}>
github
</a>
</li>
))}
</ul>
</div>
<p className={love}>Built with 💖 by Doll</p>
</div>
<Footer />
</div>
);
}

View file

@ -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<Population>({
nc: 33,
tr: 33,
vs: 33,
});
const [innerMargin, setInnerMargin] = useState<number>(10);
const [innerColor, setInnerColor] = useState<string>("black");
return (
<div>
<h1>Debug Components</h1>
<h2>Faction Viz</h2>
<div>
NC{" "}
<input
type="number"
value={population.nc}
onChange={(e) =>
setPopulation((p) => ({ ...p, nc: Number(e.target.value) }))
}
/>{" "}
|| TR{" "}
<input
type="number"
value={population.tr}
onChange={(e) =>
setPopulation((p) => ({ ...p, tr: Number(e.target.value) }))
}
/>{" "}
|| VS{" "}
<input
type="number"
value={population.vs}
onChange={(e) =>
setPopulation((p) => ({ ...p, vs: Number(e.target.value) }))
}
/>
</div>
<div>
<h3>Horizontal Stacked Bar Chart</h3>
<FactionBar population={population} />
<h3>Pie Chart</h3>
<div style={{ fontSize: "5rem" }}>
<FactionPie population={population} />
<FactionPie
population={population}
innerBackground={innerColor}
innerMargin={innerMargin}
/>
</div>
Inner margin{" "}
<input
type="number"
value={innerMargin}
onChange={(e) => setInnerMargin(Number(e.target.value))}
/>
Inner color{" "}
<input
type="color"
value={innerColor}
onChange={(e) => setInnerColor(e.target.value)}
/>
</div>
</div>
);
}

View file

@ -1,30 +1,172 @@
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<WorldResponse>();
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<typeof loader> = ({ 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<typeof loader>();
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 (
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.6" }}>
<h1>{world.name}</h1>
<h2>Total Population</h2>
<p>
{world.population.total} players ({world.population.vs} VS,{" "}
{world.population.nc} NC, {world.population.tr} TR)
</p>
<>
<div className={styles.outer}>
<div>
<div className={styles.header}>
<div className={c(styles.headerName, styles.headerFont)}>
<div>{worldInfo.name.toUpperCase()}</div>
<div className={styles.headerSub}>
[{worldInfo.location}] [{worldInfo.platform}]
</div>
</div>
<div className={styles.populationHead}>
<div className={styles.headerFont}>
<div className={styles.totalPop}>
{totalPopulation(world.population).toLocaleString()}
</div>
PLAYERS
</div>
<div className={styles.population}>
<div className={styles.popNumbers}>
<div
className={styles.popItem}
style={{ flex: world.population.vs + 1 }}
>
<img className={popImage} src={vsLogo} alt="VS" />{" "}
{world.population.vs}
</div>
<div
className={styles.popItem}
style={{ flex: world.population.nc + 1 }}
>
<img className={popImage} src={ncLogo} alt="NC" />{" "}
{world.population.nc}
</div>
<div
className={styles.popItem}
style={{ flex: world.population.tr + 1 }}
>
<img className={popImage} src={trLogo} alt="TR" />{" "}
{world.population.tr}
</div>
</div>
<FactionBar population={world.population} />
</div>
</div>
<div className={styles.headerConts}>
<div className={styles.headerSub}>CONTINENT CONTROL</div>
{metagame.zones.sort(contPrioritySort).map((zone, idx) => {
const zoneInfo = zones[String(zone.id)];
return (
<div key={idx} className={styles.cont}>
<div style={{ flex: 0 }}>{zoneInfo.name.toUpperCase()}</div>
<div style={{ flex: 1 }}>
<FactionPie
size="4rem"
population={zone.alert?.percentages ?? zone.territory}
innerBackground={`linear-gradient(45deg, ${zoneInfo.colors[0]}, ${zoneInfo.colors[1]})`}
innerMargin={10}
/>
</div>
<div className={styles.contSub}>
{zone.alert ? (
<AlertTimer alert={zone.alert} />
) : zone.locked ? (
nextZoneID == zone.id ? (
<>NEXT UP »</>
) : (
<>LOCKED</>
)
) : (
<>UNLOCKED</>
)}
</div>
</div>
);
})}
</div>
</div>
<div>
<h2>Continents</h2>
{world.zones.all.map((zone) => (
@ -32,15 +174,19 @@ export default function Index() {
))}
</div>
</div>
</div>
<Footer isMainPage />
</>
);
}
const ZoneInfo = ({ zone }: { zone: Zone }) => {
const zoneInfo = zones[String(zone.id)];
return (
<section>
<h3>{zone.name}</h3>
<h3>{zoneInfo.name}</h3>
<p>
{zone.population.total} players ({zone.population.vs} VS,{" "}
{totalPopulation(zone.population)} players ({zone.population.vs} VS,{" "}
{zone.population.nc} NC, {zone.population.tr} TR)
</p>
<p>
@ -55,13 +201,14 @@ const ZoneInfo = ({ zone }: { zone: Zone }) => {
</ul>
</p>
<p>
{zone.vehicles?.total} vehicles...
{totalPopulation(zone.vehicles as any)} vehicles...
<ul>
{allVehicles.map((vehicle, idx) => (
<li key={idx}>
<b>{toTitleCase(vehicle)}</b>: {zone.vehicles?.[vehicle].total}{" "}
total, {zone.vehicles?.[vehicle].vs} VS,{" "}
{zone.vehicles?.[vehicle].nc} NC, {zone.vehicles?.[vehicle].tr} TR
<b>{toTitleCase(vehicle)}</b>:{" "}
{totalPopulation(zone.vehicles?.[vehicle] as any)} total,{" "}
{zone.vehicles?.[vehicle].vs} VS, {zone.vehicles?.[vehicle].nc}{" "}
NC, {zone.vehicles?.[vehicle].tr} TR
</li>
))}
</ul>

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

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

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

@ -0,0 +1,40 @@
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;
};
locked_since?: string;
}[];
};
export const fetchMetagameWorlds = async (): Promise<MetagameWorld[]> => {
const response = await fetch("https://metagame.ps2.live/all");
const data: MetagameWorld[] = await response.json();
return data;
};
export const fetchSingleMetagameWorld = async (
id: string | number
): Promise<MetagameWorld> => {
const response = await fetch(`https://metagame.ps2.live/${id}`);
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

@ -1,17 +1,18 @@
export const saerroFetch = async <T>(query: string): Promise<T> => {
const response = await fetch("https://saerro.ps2.live/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
const response = await fetch(
`https://saerro.ps2.live/graphql?query=${query}`,
{
cf: {
cacheTtl: 60,
},
body: JSON.stringify({ query }),
});
}
);
const json: { data: T } = await response.json();
return json.data;
};
export type Population = {
total: number;
total?: number;
nc: number;
tr: number;
vs: number;
@ -21,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 = {
@ -36,63 +37,6 @@ export type World = {
};
};
export type Health = {
ingestReachable: string;
ingest: string;
database: string;
worlds: {
name: string;
status: string;
}[];
};
export type IndexResponse = {
health: Health;
allWorlds: World[];
};
export const indexQuery = async (): Promise<IndexResponse> => {
const query = `query {
health {
ingestReachable
ingest
database
worlds {
name
status
}
}
allWorlds {
id
name
population {
total
nc
tr
vs
}
zones {
all {
id
name
population {
total
nc
tr
vs
}
}
}
}
}`;
const indexData: IndexResponse = await saerroFetch(query);
indexData.allWorlds.sort((a, b) => a.id - b.id);
return indexData;
};
export type WorldResponse = {
world: World;
};
@ -128,12 +72,10 @@ export const allClasses = [
];
export const worldQuery = async (worldID: string): Promise<WorldResponse> => {
const query = `query {
const query = `{
world(by: {id: ${Number(worldID)}}) {
id
name
population {
total
nc
tr
vs
@ -141,7 +83,6 @@ export const worldQuery = async (worldID: string): Promise<WorldResponse> => {
zones {
all {
id
name
classes {
${allClasses.map((cls) => `${cls} { total nc tr vs }`).join(" ")}
}
@ -152,7 +93,6 @@ export const worldQuery = async (worldID: string): Promise<WorldResponse> => {
.join(" ")}
}
population {
total
nc
tr
vs
@ -162,9 +102,10 @@ export const worldQuery = async (worldID: string): Promise<WorldResponse> => {
}
}`;
console.log(query);
const worldData: WorldResponse = await saerroFetch(query);
return worldData;
};
export const totalPopulation = ({ nc, vs, tr }: Population): number =>
nc + vs + tr;

43
app/utils/sorting.ts Normal file
View file

@ -0,0 +1,43 @@
import type { MetagameWorld } from "./metagame";
export const contPrioritySort = (
a: MetagameWorld["zones"][number],
b: MetagameWorld["zones"][number]
) => {
// Sort priority:
// 1. oldest alert
// 2. unlocked by id
// 3. oldest locked since
if (a.locked && !b.locked) {
return 1;
} else if (!a.locked && b.locked) {
return -1;
}
if (a.alert && b.alert) {
return Date.parse(a.alert.start_time) - Date.parse(b.alert.start_time);
}
if (a.alert) {
return -1;
}
if (b.alert) {
return 1;
}
if (a.locked_since && b.locked_since) {
return Date.parse(a.locked_since) - Date.parse(b.locked_since);
}
if (a.locked_since) {
return -1;
}
if (b.locked_since) {
return 1;
}
return 0;
};

View file

@ -7,3 +7,131 @@ export const toTitleCase = (str: string) => {
export const pascalCaseToTitleCase = (str: string) => {
return toTitleCase(str.replace(/([A-Z])/g, " $1"));
};
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 full ? `${hours}h ${minutes % 60}m ${seconds % 60}s` : `${hours}h`;
}
if (minutes > 0) {
return full ? `${minutes}m ${seconds % 60}s` : `${minutes}m`;
}
if (seconds > 0) {
return `${seconds}s`;
}
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: ["#111111", "#cccccc"],
},
};

2773
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,36 +1,38 @@
{
"name": "ps2.live",
"private": true,
"sideEffects": false,
"scripts": {
"build": "remix build",
"dev:remix": "remix watch",
"dev:wrangler": "cross-env NODE_ENV=development npm run wrangler",
"dev": "npm-run-all build --parallel \"dev:*\"",
"start": "cross-env NODE_ENV=production npm run wrangler",
"dev:wrangler": "npm run wrangler",
"dev": "cross-env NODE_ENV=development npm-run-all build --parallel \"dev:*\"",
"start": "cross-env NODE_ENV=production npm run wrangler --live-reload",
"typecheck": "tsc",
"wrangler": "wrangler pages dev ./public",
"pages:deploy": "npm run build && wrangler pages publish ./public"
},
"dependencies": {
"@remix-run/cloudflare": "^1.16.0",
"@remix-run/cloudflare-pages": "^1.16.0",
"@remix-run/css-bundle": "^1.16.0",
"@remix-run/react": "^1.16.0",
"@remix-run/cloudflare": "^1.17.0",
"@remix-run/cloudflare-pages": "^1.17.0",
"@remix-run/css-bundle": "^1.17.0",
"@remix-run/react": "^1.17.0",
"cross-env": "^7.0.3",
"isbot": "^3.6.8",
"isbot": "^3.6.10",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@cloudflare/workers-types": "^3.19.0",
"@remix-run/dev": "^1.16.0",
"@remix-run/eslint-config": "^1.16.0",
"@types/react": "^18.0.35",
"@types/react-dom": "^18.0.11",
"eslint": "^8.38.0",
"@cloudflare/workers-types": "3.x",
"@remix-run/dev": "^1.17.0",
"@remix-run/eslint-config": "^1.17.0",
"@types/react": "^18.2.10",
"@types/react-dom": "^18.2.4",
"@vanilla-extract/css": "^1.11.1",
"eslint": "^8.42.0",
"npm-run-all": "^4.1.5",
"typescript": "^5.0.4",
"wrangler": "^2.15.1"
"typescript": "^5.1.3",
"wrangler": "^3.1.0"
},
"engines": {
"node": ">=16.13"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -15,6 +15,7 @@ module.exports = {
// publicPath: "/build/",
future: {
v2_errorBoundary: true,
v2_headers: true,
v2_meta: true,
v2_normalizeFormMethod: true,
v2_routeConvention: true,

View file

@ -1,2 +1 @@
compatibility_date = "2022-04-05"
compatibility_flags = ["streams_enable_constructors"]
compatibility_date = "2023-06-10"