Endfield was previously written off as undatable. That was wrong: a first pass only inspected its Duration rows (all "Permanently Available") and its year-less version grid, and missed an "Event | Schedule & Summary" table whose cells read "Period: 08/09/26 - 08/30/26 During the event...". Two real events, with a year. Adds MM/DD/YY range parsing for that shape. Month-first ordering is not assumed lightly — Endfield's own version grid reads 01/22, 04/17, 07/16 for versions 1.0, 1.2 and 1.4, chronological only if the month comes first. Also stops presenting unlock conditions as descriptions. Several templates put "Reach Union Level 8" where a blurb would go; filling the summary slot with text that never says what the event is is worse than leaving it empty. Wuthering Waves correctly drops to zero summaries as a result. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
57 lines
1.7 KiB
TypeScript
57 lines
1.7 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
import { readJson, writeJson } from "./storage.ts";
|
|
|
|
export interface Mark {
|
|
/** When the reader made this mark. */
|
|
at: string;
|
|
}
|
|
export type Marks = Record<string, Mark>;
|
|
|
|
/**
|
|
* A set of per-event marks, keyed by event ID and persisted locally.
|
|
*
|
|
* Completions ("I finished this") and ignores ("stop showing me this") are the
|
|
* same shape and want the same guarantees, so they share one implementation.
|
|
* They stay separate stores because they mean different things: an ignored
|
|
* event is hidden, a completed one is dimmed but still counted.
|
|
*/
|
|
export function useMarkSet(storageKey: string) {
|
|
const [marks, setMarks] = useState<Marks>(() =>
|
|
readJson<Marks>(storageKey, {}),
|
|
);
|
|
|
|
useEffect(() => {
|
|
writeJson(storageKey, marks);
|
|
}, [storageKey, marks]);
|
|
|
|
const toggle = useCallback((id: string) => {
|
|
setMarks((prev) => {
|
|
if (prev[id] !== undefined) {
|
|
const { [id]: _removed, ...rest } = prev;
|
|
return rest;
|
|
}
|
|
return { ...prev, [id]: { at: new Date().toISOString() } };
|
|
});
|
|
}, []);
|
|
|
|
const clear = useCallback(() => setMarks({}), []);
|
|
|
|
/**
|
|
* Union merge, keeping the earlier mark. Never removes: nothing else holds a
|
|
* copy of these, so a silent deletion would be unrecoverable.
|
|
*/
|
|
const merge = useCallback((incoming: Marks) => {
|
|
setMarks((prev) => {
|
|
const next = { ...prev };
|
|
for (const [id, value] of Object.entries(incoming)) {
|
|
const existing = next[id];
|
|
next[id] =
|
|
existing === undefined || value.at < existing.at ? value : existing;
|
|
}
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
return { marks, toggle, merge, clear };
|
|
}
|