import { gameMeta } from "../../shared/games.ts"; import { freshness, type SourceHealth } from "../../shared/feed.ts"; import { formatAbsolute, formatRemaining } from "../../shared/time.ts"; export const REPO_URL = "https://github.com/StereotypicalCat/gacha-event-tracker"; /** * Where a reader takes a problem or an idea. * * `template=` names the file in `.github/ISSUE_TEMPLATE/`, so renaming one of * those files breaks these links — GitHub falls back to the template chooser * rather than erroring, which is a soft landing but not the intended one. */ export const BUG_URL = `${REPO_URL}/issues/new?template=bug_report.yml`; export const FEATURE_URL = `${REPO_URL}/issues/new?template=feature_request.yml`; /** * The bug form, with the footer's own freshness line already filled in. * * A wrong end date and a stale calendar look identical to a reader, and eight of * the sources cannot be fetched from CI at all — so "how old is this page's data" * is the first thing anyone triaging a date report has to establish, and the one * thing they cannot recover after the fact. Asking the reader to copy it works; * carrying it for them works more often. * * `refreshed` must stay the `id` of the matching field in `bug_report.yml`. * GitHub silently drops a parameter that names no field, so a drift here costs * the prefill with no error anywhere — `test/issue-templates.test.tsx` pins it. */ export function bugReportUrl(refreshed: string | null): string { if (refreshed === null) return BUG_URL; return `${BUG_URL}&refreshed=${encodeURIComponent(refreshed)}`; } /** Who built this, and where to find them. */ export const AUTHOR = { name: "Lucas Winther", site: "https://lucaswinther.info", github: "https://github.com/StereotypicalCat", } as const; const LINK = "text-muted underline decoration-hairline underline-offset-2 transition-colors duration-150 hover:text-ink hover:decoration-near"; /** * Forks whose ideas ended up here. * * Ideas and design, not code — nothing in this list carries a licence * obligation, which is why it sits here as thanks rather than in NOTICE with * the terms. Handles rather than names: a handle is verifiable and stable, and * whoever is behind a fork does not always publish a name to credit. * * Unlike the source and studio credits above, this one cannot be derived from * the feed — nothing in the data knows a fork exists. Hardcoded on purpose; * adding another is one line. */ export const IDEA_CREDITS = [ { handle: "phuduong85", url: "https://github.com/phuduong85/gacha-event-tracker", }, ] as const; /** * How many lagging games to name before summarising the rest. * * Naming them is the point — a reader can act on a name — but past a handful the * list stops being read, and every extra entry repeating the same age crowds out * the sentence that matters. */ const STALE_NAMES = 4; function GitHubMark() { return ( ); } /** Display name and homepage for a source host. */ const SITES: Record = { "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. * * Everything here is derived from the feed rather than written down, so adding * a source or a game credits the right people automatically. A hardcoded list * 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({ sources, now, }: { sources: SourceHealth[]; now: number; }) { const games = [...new Set(sources.map((s) => s.game))].map(gameMeta); const studios = [...new Set(games.map((g) => g.studio))]; const { refreshedAt, stale } = freshness(sources, now); // Built once so the sentence a reader reads and the value the bug form is // prefilled with cannot drift apart. const ago = refreshedAt === null ? null : formatRemaining(now - Date.parse(refreshedAt)); const refreshedLine = refreshedAt === null ? null : `${formatAbsolute(refreshedAt, true)} — ${ago} ago`; 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 ( ); }