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 (
);
}