diff --git a/src/client/App.tsx b/src/client/App.tsx index b601d9d..ddc6ceb 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -8,6 +8,7 @@ import { Timeline } from "./components/Timeline.tsx"; import { Welcome } from "./components/Welcome.tsx"; import { Colophon } from "./components/Colophon.tsx"; import { Legend } from "./components/Legend.tsx"; +import { Toast } from "./components/Toast.tsx"; import { KEYS } from "./state/storage.ts"; import { useMarkSet } from "./state/useMarkSet.ts"; import { usePrefs } from "./state/usePrefs.ts"; @@ -52,11 +53,20 @@ export function App() { const [state, setState] = useState({ status: "loading" }); const [view, setView] = useState("soon"); const [openId, setOpenId] = useState(null); + // The event most recently ignored, so it can be put back without hunting for + // a row that just disappeared. + const [lastIgnored, setLastIgnored] = useState<{ id: string; title: string } | null>(null); const now = useNow(); const online = useOnline(); const { prefs, update, toggleGame } = usePrefs(); const completed = useMarkSet(KEYS.completions); const ignored = useMarkSet(KEYS.ignored); + + const toggleIgnored = (id: string, title: string) => { + const wasIgnored = ignored.marks[id] !== undefined; + ignored.toggle(id); + setLastIgnored(wasIgnored ? null : { id, title }); + }; const completions = completed.marks; const toggle = completed.toggle; @@ -212,7 +222,9 @@ export function App() { key={row.event.id} row={row} completed={completions[row.event.id] !== undefined} + ignored={ignored.marks[row.event.id] !== undefined} onToggle={toggle} + onRestore={(id) => ignored.toggle(id)} onOpen={setOpenId} /> ))} @@ -226,7 +238,9 @@ export function App() { key={row.event.id} row={row} completed={completions[row.event.id] !== undefined} + ignored={ignored.marks[row.event.id] !== undefined} onToggle={toggle} + onRestore={(id) => ignored.toggle(id)} onOpen={setOpenId} /> ))} @@ -273,12 +287,24 @@ export function App() { + {lastIgnored !== null && ( + { + ignored.toggle(lastIgnored.id); + setLastIgnored(null); + }} + onDismiss={() => setLastIgnored(null)} + /> + )} + {openRow !== null && ( toggleIgnored(id, openRow.event.title)} onToggle={toggle} onClose={() => setOpenId(null)} /> diff --git a/src/client/components/EventRow.tsx b/src/client/components/EventRow.tsx index ec62969..854ee62 100644 --- a/src/client/components/EventRow.tsx +++ b/src/client/components/EventRow.tsx @@ -15,11 +15,21 @@ export interface RowEvent { interface EventRowProps { row: RowEvent; completed: boolean; + /** Only ever true when the reader has chosen to reveal ignored events. */ + ignored?: boolean | undefined; onToggle: (id: string) => void; + onRestore?: ((id: string) => void) | undefined; onOpen: (id: string) => void; } -export function EventRow({ row, completed, onToggle, onOpen }: EventRowProps) { +export function EventRow({ + row, + completed, + ignored = false, + onToggle, + onRestore, + onOpen, +}: EventRowProps) { const { event, clock } = row; const game = gameMeta(event.game); const heat = URGENCY_COLOR[clock.urgency]; @@ -58,8 +68,13 @@ export function EventRow({ row, completed, onToggle, onOpen }: EventRowProps) {
- - {game.short} + + {game.short} + {ignored && ( + + ignored + + )}
- {/* Sits above the row target so ticking done never opens the sheet. */} + {/* Sits above the row target so ticking done never opens the sheet. + On a revealed ignored row this becomes the undo, which is the most + direct place to put it. */} + {ignored && onRestore !== undefined ? ( + + ) : ( + )}
); diff --git a/src/client/components/Toast.tsx b/src/client/components/Toast.tsx new file mode 100644 index 0000000..99dbd06 --- /dev/null +++ b/src/client/components/Toast.tsx @@ -0,0 +1,62 @@ +import { useEffect } from "react"; + +/** + * A short-lived confirmation with an undo. + * + * Ignoring an event makes it vanish, which is exactly what was asked for and + * also the moment a mistake is least recoverable — the row you would click to + * undo is the row that just disappeared. The undo lives here instead. + */ +export function Toast({ + message, + actionLabel, + onAction, + onDismiss, + ms = 7000, +}: { + message: string; + actionLabel: string; + onAction: () => void; + onDismiss: () => void; + ms?: number; +}) { + useEffect(() => { + const id = setTimeout(onDismiss, ms); + return () => clearTimeout(id); + }, [onDismiss, ms, message]); + + return ( +
+
+

{message}

+ + +
+
+ ); +}