diff --git a/src/client/App.tsx b/src/client/App.tsx index 1a9f517..708b58e 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -35,7 +35,7 @@ import { clockFor, formatRemaining } from "../shared/time.ts"; import { dailySummary, isDaily, resolveDaily } from "../shared/daily.ts"; import { goalStatus, readGoal, type GoalStatus } from "../shared/goal.ts"; import { taskStatus, type TaskShape } from "../shared/task.ts"; -import { activeRoutines, routineToAsk } from "../shared/routines.ts"; +import { activeRoutines, mineFrom, routineToAsk, type Routine } from "../shared/routines.ts"; import { strandedOccurrences } from "../shared/recurrence.ts"; import { orderGames } from "./state/gameOrder.ts"; import { GameMetaProvider, type MetaResolver } from "./state/gameMeta.tsx"; @@ -203,6 +203,21 @@ export function App() { const routines = activeRoutines(prefs.routineGames, copiedFrom); const stripTasks = [...tasks.tasks, ...routines]; + /** + * Copy a built-in into the reader's own tasks, ticks and numbers included. + * Additive: the built-in's own log stays under its id, so deleting the copy + * brings the original back as it was. A second press while a copy exists + * does nothing rather than making two. + */ + const makeItMine = (routine: Routine) => { + if (tasks.tasks.some((t) => t.from === routine.id)) return; + const id = tasks.addTask(mineFrom(routine)); + const log = daily.logs[routine.id]; + if (log !== undefined) daily.merge({ [id]: log }); + const numbers = amounts.amounts[routine.id]; + if (numbers !== undefined) amounts.merge({ [id]: numbers }); + }; + const taskStatusOf = (task: TaskShape): GoalStatus => taskStatus({ task, @@ -727,6 +742,8 @@ export function App() { onAddTask: tasks.addTask, onEditTask: tasks.editTask, onRemoveTask: tasks.removeTask, + routines, + onMakeItMine: makeItMine, now, // The index lists rules; the sheet opens rows. A repeating rule's // own id is never a row — the lists hold its occurrences — so it is diff --git a/src/client/components/Controls.tsx b/src/client/components/Controls.tsx index 8605bd8..4d8a530 100644 --- a/src/client/components/Controls.tsx +++ b/src/client/components/Controls.tsx @@ -7,6 +7,7 @@ import { moveGame } from "../state/gameOrder.ts"; import type { ThemeChoice } from "../state/theme.ts"; import type { Prefs } from "../state/usePrefs.ts"; import { YourOwn } from "./YourOwn.tsx"; +import { asOfLabel, routinesFor } from "../../shared/routines.ts"; const REGIONS: Array<{ id: Region; label: string }> = ( ["america", "europe", "asia"] as const @@ -91,6 +92,7 @@ export function Controls({ /** Everything the reader entered themselves, and the ways to change it. */ own: React.ComponentProps; }) { + const gameMeta = useGameMeta(); const shown = games.filter((g) => !prefs.hiddenGames.includes(g)).length; const ownGames = Object.keys(own.games).length; const ownEvents = Object.keys(own.events).length; @@ -246,6 +248,36 @@ export function Controls({ hint="Commissions, resin, daily training and the rest. Nobody publishes these, so the app keeps a fixed list per game. Off, the strip shows only events with a checklist. Your ticks and streaks are kept either way." /> + {/* Beside the chores rather than on each game's row, which already + carries a handle, two arrows and a switch. Opt-in per game: these + are someone else's figures, each dated by its source. */} + {games + .filter((game) => routinesFor(game).length > 0) + .map((game) => { + const on = prefs.routineGames?.includes(game) ?? false; + const list = routinesFor(game); + return ( + + onUpdate({ + routineGames: checked + ? [...(prefs.routineGames ?? []), game] + : (prefs.routineGames ?? []).filter((g) => g !== game), + // Answering here answers the strip's question too. + routinesAsked: [ + ...(prefs.routinesAsked ?? []).filter((g) => g !== game), + game, + ], + }) + } + label={`Built-in weekly tasks for ${gameMeta(game).name}`} + hint={`${list.map((r) => r.title).join(", ")} — from a source as of ${asOfLabel(list[0]!.asOf)}. Off hides them; your numbers are kept.`} + /> + ); + })} + {ignoredCount > 0 && ( void) | undefined; onEditTask?: ((id: string, draft: TaskDraft) => void) | undefined; onRemoveTask?: ((id: string) => void) | undefined; + /** Built-in weekly tasks the reader has on, and how to copy one into their own. */ + routines?: readonly Routine[] | undefined; + onMakeItMine?: ((routine: Routine) => void) | undefined; }) { const gameMeta = useGameMeta(); const [adding, setAdding] = useState<"game" | "event" | "task" | null>(null); @@ -313,6 +319,44 @@ export function YourOwn({ )} + {/* Built-ins are not editable in place — they are someone else's figures, + dated by their source — so the way to change one is to make it yours: + a copy that replaces it while it exists, numbers and ticks included. */} + {routines.length > 0 && onMakeItMine !== undefined && ( + <> +

Built-in weekly tasks

+
    + {routines.map((routine) => ( +
  • +
    + {gameMeta(routine.game).short} + {routine.title} + + as of {asOfLabel(routine.asOf)} + + +
    +

    + {taskCaption(routine)} + {routine.note !== undefined && ` · ${routine.note}`} +

    +
  • + ))} +
+ + )} + {adding === "task" && onAddTask !== undefined && (