feat: credit the sources, disclaim affiliation, link the repo

Game8's editors compile the calendars this depends on, and the studios make
the games — both belong on the same screen as the data rather than one
navigation step away.

States plainly that this is an unofficial fan-made tool with no connection
to Game8, HoYoverse, Kuro Games, Hypergryph or Hotta Studio, and that the
source page is the authority when the two disagree.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-15 00:52:41 +02:00
co-authored by Claude Opus 5
parent 817ad41dcd
commit 1a04e9472b
2 changed files with 132 additions and 13 deletions
+41 -13
View File
@@ -6,6 +6,7 @@ import { EventRow, type RowEvent } from "./components/EventRow.tsx";
import { NextUp } from "./components/NextUp.tsx";
import { Timeline } from "./components/Timeline.tsx";
import { Welcome } from "./components/Welcome.tsx";
import { Colophon } from "./components/Colophon.tsx";
import { useCompletions } from "./state/useCompletions.ts";
import { usePrefs } from "./state/usePrefs.ts";
import { clockFor, DAY, endingSoonestFirst, formatRemaining } from "../shared/time.ts";
@@ -13,6 +14,28 @@ import type { GameId } from "../shared/schema.ts";
type View = "soon" | "calendar";
/**
* Connection state. Offline is not an error here — the service worker serves
* the last feed it saw and countdowns run off the local clock — but it does
* change what the reader can trust, so it is surfaced rather than hidden.
*/
function useOnline(): boolean {
const [online, setOnline] = useState(() =>
typeof navigator === "undefined" ? true : navigator.onLine,
);
useEffect(() => {
const up = () => setOnline(true);
const down = () => setOnline(false);
window.addEventListener("online", up);
window.addEventListener("offline", down);
return () => {
window.removeEventListener("online", up);
window.removeEventListener("offline", down);
};
}, []);
return online;
}
/** Ticks once a second so countdowns stay honest without re-fetching. */
function useNow(intervalMs = 1000): number {
const [now, setNow] = useState(() => Date.now());
@@ -28,6 +51,7 @@ export function App() {
const [view, setView] = useState<View>("soon");
const [openId, setOpenId] = useState<string | null>(null);
const now = useNow();
const online = useOnline();
const { prefs, update, toggleGame } = usePrefs();
const { completions, toggle, merge } = useCompletions();
@@ -122,8 +146,14 @@ export function App() {
<p className="font-display text-[0.9375rem] font-bold tracking-[0.02em]">
EVENT<span className="text-near">CLOCK</span>
</p>
<p className="mt-0.5 text-xs text-faint">
<p className="mt-0.5 flex items-center gap-1.5 text-xs text-faint">
{live.length} live · {upcoming.length} upcoming
{!online && (
<span className="inline-flex items-center gap-1 text-soon">
<span aria-hidden className="size-1.5 rounded-full bg-soon" />
offline
</span>
)}
</p>
</div>
@@ -219,19 +249,17 @@ export function App() {
onImport={(file) => void importProgress(file, merge)}
/>
<footer className="px-4 pb-10 pt-2 text-xs leading-relaxed text-faint">
<p>
Dates come from community wikis and are shown in your local time. Every
event links to its source — check there before the last hours.
{!online && (
<p className="border-t border-hairline px-4 py-3 text-xs leading-relaxed text-soon">
You're offline. These are the events last downloaded
{" "}
{formatRemaining(now - Date.parse(state.feed.generatedAt))} ago, and
countdowns are still running. Anything rescheduled since then won't
show until you reconnect.
</p>
{staleSources.length > 0 && (
<p className="mt-2 text-soon">
{staleSources.length} source
{staleSources.length > 1 ? "s have" : " has"} not refreshed in over two
days. Some end dates may have moved.
</p>
)}
</footer>
)}
<Colophon sources={state.feed.sources} staleCount={staleSources.length} />
{openRow !== null && (
<EventDetail
+91
View File
@@ -0,0 +1,91 @@
import { gameMeta } from "../../shared/games.ts";
import type { SourceHealth } from "../../shared/feed.ts";
export const REPO_URL = "https://github.com/StereotypicalCat/gacha-event-tracker";
/**
* Credit, disclaimer, and where the code lives.
*
* This sits at the foot of the page rather than behind an "About" link: the
* people whose work this depends on should be visible on the same screen as
* the data, not one navigation step away from it.
*/
export function Colophon({
sources,
staleCount,
}: {
sources: SourceHealth[];
staleCount: number;
}) {
const games = [...new Set(sources.map((s) => s.game))];
return (
<footer className="border-t border-hairline px-4 pb-12 pt-6 text-xs leading-relaxed text-faint">
<p>
Dates are shown in your local time. Every event links to the page it came
from check there before the last hours.
</p>
{staleCount > 0 && (
<p className="mt-2 text-soon">
{staleCount} source{staleCount > 1 ? "s have" : " has"} not refreshed in
over two days. Some end dates may have moved.
</p>
)}
<div className="mt-5">
<p className="eyebrow">With thanks to</p>
<p className="mt-1.5">
<a
href="https://game8.co"
target="_blank"
rel="noreferrer noopener"
className="text-muted underline decoration-hairline underline-offset-2 transition-colors duration-150 hover:text-ink hover:decoration-near"
>
Game8
</a>
{", whose editors compile and maintain the event calendars this reads from. The schedules are their work; this page only rearranges them."}
</p>
<p className="mt-2">
And to HoYoverse, Kuro Games, Hypergryph and Hotta Studio, who make the
games worth keeping track of {" "}
{games.map((g, i) => (
<span key={g}>
{i > 0 && ", "}
<span style={{ color: gameMeta(g).hue }}>{gameMeta(g).name}</span>
</span>
))}
{"."}
</p>
</div>
<div className="mt-5 border-t border-hairline pt-4">
<p>
<strong className="font-semibold text-muted">Not affiliated</strong>{" "}
with Game8, HoYoverse, Kuro Games, Hypergryph, Hotta Studio, or any
other publisher or source named here. This is an unofficial fan-made
tool, not endorsed by or connected to any of them. All game names,
event names and trademarks belong to their respective owners.
</p>
<p className="mt-2">
Event dates can be wrong or go out of date. Treat the source page as
the authority, not this one.
</p>
</div>
<p className="mt-5">
<a
href={REPO_URL}
target="_blank"
rel="noreferrer noopener"
className="inline-flex items-center gap-1.5 text-muted transition-colors duration-150 hover:text-ink"
>
<svg viewBox="0 0 16 16" className="size-3.5" fill="currentColor" aria-hidden>
<path d="M8 0a8 8 0 0 0-2.53 15.59c.4.07.55-.17.55-.38l-.01-1.49c-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82a7.4 7.4 0 0 1 4 0c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48l-.01 2.19c0 .21.15.46.55.38A8 8 0 0 0 8 0Z" />
</svg>
Source code on GitHub
</a>
</p>
</footer>
);
}