fix: derive the colophon's credits from the feed

It still thanked only Game8 and hardcoded the studio list, so wiki.gg went
uncredited and unmentioned in the disclaimer after being added as a source.

Sources and studios are now read from the feed and the game registry, so
adding either credits the right people automatically. A hardcoded credit goes
stale the moment someone adds a source, and out of date is the one thing a
credit must not be.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-15 01:43:47 +02:00
co-authored by Claude Opus 5
parent 6fad016f66
commit 25c32dbe62
2 changed files with 65 additions and 28 deletions
+50 -15
View File
@@ -3,12 +3,28 @@ import type { SourceHealth } from "../../shared/feed.ts";
export const REPO_URL = "https://github.com/StereotypicalCat/gacha-event-tracker"; export const REPO_URL = "https://github.com/StereotypicalCat/gacha-event-tracker";
/** Display name and homepage for a source host. */
const SITES: Record<string, { name: string; url: string }> = {
"game8.co": { name: "Game8", url: "https://game8.co" },
"endfield.wiki.gg": { name: "wiki.gg", url: "https://wiki.gg" },
};
function siteFor(url: string): { name: string; url: string } {
try {
const host = new URL(url).host;
return SITES[host] ?? { name: host, url: `https://${host}` };
} catch {
return { name: url, url };
}
}
/** /**
* Credit, disclaimer, and where the code lives. * Credit, disclaimer, and where the code lives.
* *
* This sits at the foot of the page rather than behind an "About" link: the * Everything here is derived from the feed rather than written down, so adding
* people whose work this depends on should be visible on the same screen as * a source or a game credits the right people automatically. A hardcoded list
* the data, not one navigation step away from it. * silently goes stale the moment someone adds the seventh source, and the one
* thing a credit must not be is out of date.
*/ */
export function Colophon({ export function Colophon({
sources, sources,
@@ -17,7 +33,15 @@ export function Colophon({
sources: SourceHealth[]; sources: SourceHealth[];
staleCount: number; staleCount: number;
}) { }) {
const games = [...new Set(sources.map((s) => s.game))]; const games = [...new Set(sources.map((s) => s.game))].map(gameMeta);
const studios = [...new Set(games.map((g) => g.studio))];
const sites = [...new Map(sources.map((s) => {
const site = siteFor(s.url);
return [site.name, site] as const;
})).values()];
const named = [...sites.map((s) => s.name), ...studios];
return ( return (
<footer className="border-t border-hairline px-4 pb-12 pt-6 text-xs leading-relaxed text-faint"> <footer className="border-t border-hairline px-4 pb-12 pt-6 text-xs leading-relaxed text-faint">
@@ -36,23 +60,34 @@ export function Colophon({
<div className="mt-5"> <div className="mt-5">
<p className="eyebrow">With thanks to</p> <p className="eyebrow">With thanks to</p>
<p className="mt-1.5"> <p className="mt-1.5">
{sites.map((site, i) => (
<span key={site.name}>
{i > 0 && (i === sites.length - 1 ? " and " : ", ")}
<a <a
href="https://game8.co" href={site.url}
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
className="text-muted underline decoration-hairline underline-offset-2 transition-colors duration-150 hover:text-ink hover:decoration-near" className="text-muted underline decoration-hairline underline-offset-2 transition-colors duration-150 hover:text-ink hover:decoration-near"
> >
Game8 {site.name}
</a> </a>
</span>
))}
{", whose editors compile and maintain the event calendars this reads from. The schedules are their work; this page only rearranges them."} {", whose editors compile and maintain the event calendars this reads from. The schedules are their work; this page only rearranges them."}
</p> </p>
<p className="mt-2"> <p className="mt-2">
And to HoYoverse, Kuro Games, Hypergryph and Hotta Studio, who make the And to{" "}
games worth keeping track of {" "} {studios.map((studio, i) => (
{games.map((g, i) => ( <span key={studio}>
<span key={g}> {i > 0 && (i === studios.length - 1 ? " and " : ", ")}
{studio}
</span>
))}
, who make the games worth keeping track of {" "}
{games.map((game, i) => (
<span key={game.id}>
{i > 0 && ", "} {i > 0 && ", "}
<span style={{ color: gameMeta(g).hue }}>{gameMeta(g).name}</span> <span style={{ color: game.hue }}>{game.name}</span>
</span> </span>
))} ))}
{"."} {"."}
@@ -62,10 +97,10 @@ export function Colophon({
<div className="mt-5 border-t border-hairline pt-4"> <div className="mt-5 border-t border-hairline pt-4">
<p> <p>
<strong className="font-semibold text-muted">Not affiliated</strong>{" "} <strong className="font-semibold text-muted">Not affiliated</strong>{" "}
with Game8, HoYoverse, Kuro Games, Hypergryph, Hotta Studio, or any with {named.join(", ")}, or any other publisher or source named here.
other publisher or source named here. This is an unofficial fan-made This is an unofficial fan-made tool, not endorsed by or connected to
tool, not endorsed by or connected to any of them. All game names, any of them. All game names, event names and trademarks belong to their
event names and trademarks belong to their respective owners. respective owners.
</p> </p>
<p className="mt-2"> <p className="mt-2">
Event dates can be wrong or go out of date. Treat the source page as Event dates can be wrong or go out of date. Treat the source page as
+9 -7
View File
@@ -11,16 +11,18 @@ export interface GameMeta {
* "whose event is this?" and "how long have I got?" at the same time. * "whose event is this?" and "how long have I got?" at the same time.
*/ */
hue: string; hue: string;
/** Who makes it. Credited in the colophon, derived rather than hardcoded. */
studio: string;
} }
export const GAMES: Record<GameId, GameMeta> = { export const GAMES: Record<GameId, GameMeta> = {
genshin: { id: "genshin", name: "Genshin Impact", short: "Genshin", hue: "#4EA8DE" }, genshin: { id: "genshin", name: "Genshin Impact", short: "Genshin", hue: "#4EA8DE" , studio: "HoYoverse" },
hsr: { id: "hsr", name: "Honkai: Star Rail", short: "Star Rail", hue: "#7B8CFF" }, hsr: { id: "hsr", name: "Honkai: Star Rail", short: "Star Rail", hue: "#7B8CFF" , studio: "HoYoverse" },
zzz: { id: "zzz", name: "Zenless Zone Zero", short: "ZZZ", hue: "#F2A03D" }, zzz: { id: "zzz", name: "Zenless Zone Zero", short: "ZZZ", hue: "#F2A03D" , studio: "HoYoverse" },
wuwa: { id: "wuwa", name: "Wuthering Waves", short: "Wuwa", hue: "#3DD6A0" }, wuwa: { id: "wuwa", name: "Wuthering Waves", short: "Wuwa", hue: "#3DD6A0" , studio: "Kuro Games" },
arknights: { id: "arknights", name: "Arknights", short: "Arknights", hue: "#9AA3B8" }, arknights: { id: "arknights", name: "Arknights", short: "Arknights", hue: "#9AA3B8" , studio: "Hypergryph" },
endfield: { id: "endfield", name: "Arknights: Endfield", short: "Endfield", hue: "#E8635A" }, endfield: { id: "endfield", name: "Arknights: Endfield", short: "Endfield", hue: "#E8635A" , studio: "Hypergryph" },
nte: { id: "nte", name: "Neverness to Everness", short: "NTE", hue: "#C77DFF" }, nte: { id: "nte", name: "Neverness to Everness", short: "NTE", hue: "#C77DFF" , studio: "Hotta Studio" },
}; };
export const GAME_LIST: GameMeta[] = Object.values(GAMES); export const GAME_LIST: GameMeta[] = Object.values(GAMES);