feat(settings): switch built-in weekly tasks per game, and make one your own

The switch sits beside the daily chores and answers the strip's question
too. Built-ins are listed with a link to their dated source. Make it mine
copies one into the reader's tasks, numbers and ticks included, and hides
the original while the copy exists. It's additive, so deleting the copy
brings the original back as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-13 14:10:58 +02:00
1 parent 228ce9132d
commit 624c725cbf
8 files changed
+161 -3

No files matched your search

+32
View File
@@ -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<typeof YourOwn>;
}) {
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 (
<Check
key={game}
checked={on}
onChange={(checked) =>
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 && (
<Check
checked={prefs.showIgnored}
+44
View File
@@ -7,6 +7,7 @@ import { cadenceLabel, EventForm, GameForm } from "./CustomForms.tsx";
import { taskCaption, type Task } from "../../shared/task.ts";
import type { TaskDraft } from "../state/useTasks.ts";
import { TaskForm } from "./TaskForm.tsx";
import { asOfLabel, type Routine } from "../../shared/routines.ts";
/**
* The reader's own games and events — a group of the settings panel (PRD F13).
@@ -68,6 +69,8 @@ export function YourOwn({
onAddTask,
onEditTask,
onRemoveTask,
routines = [],
onMakeItMine,
}: {
games: CustomGames;
events: CustomEvents;
@@ -89,6 +92,9 @@ export function YourOwn({
onAddTask?: ((draft: TaskDraft) => 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 && (
<>
<p className="mt-4 text-xs text-faint">Built-in weekly tasks</p>
<ul className="mt-1.5 flex flex-col gap-1">
{routines.map((routine) => (
<li key={routine.id}>
<div className="flex items-baseline gap-2">
<span className="shrink-0 text-xs text-faint">{gameMeta(routine.game).short}</span>
<span className="min-w-0 flex-1 truncate text-sm text-muted">{routine.title}</span>
<a
href={routine.source}
target="_blank"
rel="noreferrer noopener"
className="shrink-0 text-xs text-faint underline decoration-hairline underline-offset-2 transition-colors hover:text-muted"
>
as of {asOfLabel(routine.asOf)}
</a>
<button
type="button"
onClick={() => onMakeItMine(routine)}
className="shrink-0 text-xs text-faint transition-colors hover:text-ink"
>
Make it mine
</button>
</div>
<p className="text-xs text-faint">
{taskCaption(routine)}
{routine.note !== undefined && ` · ${routine.note}`}
</p>
</li>
))}
</ul>
</>
)}
{adding === "task" && onAddTask !== undefined && (
<TaskForm
lanes={lanes}