feat(custom): model reader-authored games and events
The data layer for PRD F13, with no UI and no behaviour change yet.
src/shared/custom.ts defines the two key spaces, their schemas, and the
projection into what the views read. Two properties are the point of it:
- A reader's event id is random, not derived from their title. They can type a
scraped event's exact name and date, which under ${game}:${slug}:${date} is a
byte-identical key — one completion mark and one streak silently shared by two
events. Randomness also means renaming their own event never moves its id.
- A reader's event carries no sourceUrl, so a hand-entered date can never be
attributed to a source, and claims no region split, because they entered one
instant and inventing three would fabricate two of them.
The rest is widening what was GameId-shaped into a lane that may be one of
theirs: clockFor takes the boundary fields structurally so their events run on
the identical countdown rather than a second one, day keys fall back to the
regional default for a lane with no server map, and gameMeta becomes a context
resolver so metaFor stays pure and total — a lane can outlive its game when an
import carries an event whose game did not come with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4029885833
commit
3702ee7a4c
@@ -1,5 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { gameMeta } from "../../shared/games.ts";
|
||||
import { useGameMeta } from "../state/gameMeta.tsx";
|
||||
import { formatAbsolute, formatRemaining } from "../../shared/time.ts";
|
||||
import type { RowEvent } from "./EventRow.tsx";
|
||||
import { pressure, pressureReason, type Effort } from "../../shared/effort.ts";
|
||||
@@ -52,6 +52,7 @@ export function EventDetail({
|
||||
onNote: (id: string, n: string) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const gameMeta = useGameMeta();
|
||||
const { event, clock } = row;
|
||||
const game = gameMeta(event.game);
|
||||
const heat = URGENCY_COLOR[clock.urgency];
|
||||
@@ -217,14 +218,19 @@ export function EventDetail({
|
||||
>
|
||||
{completed ? "Mark not done" : "Mark done"}
|
||||
</button>
|
||||
<a
|
||||
href={event.sourceUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="rounded-lg border border-hairline px-4 py-2.5 text-sm text-muted transition-colors hover:text-ink"
|
||||
>
|
||||
Source
|
||||
</a>
|
||||
{/* Nothing to link to when the reader typed this themselves, and a
|
||||
dead "Source" button would imply somebody else vouched for the
|
||||
date. Provenance is stated above instead. */}
|
||||
{event.sourceUrl !== null && (
|
||||
<a
|
||||
href={event.sourceUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="rounded-lg border border-hairline px-4 py-2.5 text-sm text-muted transition-colors hover:text-ink"
|
||||
>
|
||||
Source
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Ignoring is not completing. "Done" keeps an event visible and
|
||||
|
||||
Reference in New Issue
Block a user