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:
co-authored by
Claude Opus 5
parent
817ad41dcd
commit
1a04e9472b
+41
-13
@@ -6,6 +6,7 @@ import { EventRow, type RowEvent } from "./components/EventRow.tsx";
|
|||||||
import { NextUp } from "./components/NextUp.tsx";
|
import { NextUp } from "./components/NextUp.tsx";
|
||||||
import { Timeline } from "./components/Timeline.tsx";
|
import { Timeline } from "./components/Timeline.tsx";
|
||||||
import { Welcome } from "./components/Welcome.tsx";
|
import { Welcome } from "./components/Welcome.tsx";
|
||||||
|
import { Colophon } from "./components/Colophon.tsx";
|
||||||
import { useCompletions } from "./state/useCompletions.ts";
|
import { useCompletions } from "./state/useCompletions.ts";
|
||||||
import { usePrefs } from "./state/usePrefs.ts";
|
import { usePrefs } from "./state/usePrefs.ts";
|
||||||
import { clockFor, DAY, endingSoonestFirst, formatRemaining } from "../shared/time.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";
|
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. */
|
/** Ticks once a second so countdowns stay honest without re-fetching. */
|
||||||
function useNow(intervalMs = 1000): number {
|
function useNow(intervalMs = 1000): number {
|
||||||
const [now, setNow] = useState(() => Date.now());
|
const [now, setNow] = useState(() => Date.now());
|
||||||
@@ -28,6 +51,7 @@ export function App() {
|
|||||||
const [view, setView] = useState<View>("soon");
|
const [view, setView] = useState<View>("soon");
|
||||||
const [openId, setOpenId] = useState<string | null>(null);
|
const [openId, setOpenId] = useState<string | null>(null);
|
||||||
const now = useNow();
|
const now = useNow();
|
||||||
|
const online = useOnline();
|
||||||
const { prefs, update, toggleGame } = usePrefs();
|
const { prefs, update, toggleGame } = usePrefs();
|
||||||
const { completions, toggle, merge } = useCompletions();
|
const { completions, toggle, merge } = useCompletions();
|
||||||
|
|
||||||
@@ -122,8 +146,14 @@ export function App() {
|
|||||||
<p className="font-display text-[0.9375rem] font-bold tracking-[0.02em]">
|
<p className="font-display text-[0.9375rem] font-bold tracking-[0.02em]">
|
||||||
EVENT<span className="text-near">CLOCK</span>
|
EVENT<span className="text-near">CLOCK</span>
|
||||||
</p>
|
</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
|
{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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -219,19 +249,17 @@ export function App() {
|
|||||||
onImport={(file) => void importProgress(file, merge)}
|
onImport={(file) => void importProgress(file, merge)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<footer className="px-4 pb-10 pt-2 text-xs leading-relaxed text-faint">
|
{!online && (
|
||||||
<p>
|
<p className="border-t border-hairline px-4 py-3 text-xs leading-relaxed text-soon">
|
||||||
Dates come from community wikis and are shown in your local time. Every
|
You're offline. These are the events last downloaded
|
||||||
event links to its source — check there before the last hours.
|
{" "}
|
||||||
|
{formatRemaining(now - Date.parse(state.feed.generatedAt))} ago, and
|
||||||
|
countdowns are still running. Anything rescheduled since then won't
|
||||||
|
show until you reconnect.
|
||||||
</p>
|
</p>
|
||||||
{staleSources.length > 0 && (
|
)}
|
||||||
<p className="mt-2 text-soon">
|
|
||||||
{staleSources.length} source
|
<Colophon sources={state.feed.sources} staleCount={staleSources.length} />
|
||||||
{staleSources.length > 1 ? "s have" : " has"} not refreshed in over two
|
|
||||||
days. Some end dates may have moved.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
{openRow !== null && (
|
{openRow !== null && (
|
||||||
<EventDetail
|
<EventDetail
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user