Files
adventure/apps/web/src/components/features/todays-adventure.tsx
Zaine 3b37368e7d
Some checks failed
CI / test (push) Has been cancelled
initial 2
2026-06-26 09:26:50 +01:00

246 lines
8.2 KiB
TypeScript
Executable File

"use client";
import type { DailyAdventureItemData, DailyTodoData, DayMode } from "@adventureos/shared";
import { DAY_MODES } from "@adventureos/shared";
import { useState } from "react";
import Link from "next/link";
import { useAdventureMutations } from "@/hooks/useAdventureMutations";
import { AdventureItemRow } from "@/components/features/adventure-item-row";
import { ChecklistItem } from "@/components/features/checklist-item";
interface TodaysAdventureProps {
date: string;
chapter: string;
journeyDay: number;
isRestDay: boolean;
isCustomized: boolean;
workHoursTarget: number | null;
dayMode: DayMode;
isBackfilled: boolean;
items: DailyAdventureItemData[];
todos: DailyTodoData[];
}
export function TodaysAdventure({
date,
chapter,
journeyDay,
isRestDay,
isCustomized,
workHoursTarget,
dayMode,
isBackfilled,
items,
todos,
}: TodaysAdventureProps) {
const [customize, setCustomize] = useState(false);
const [newTodo, setNewTodo] = useState("");
const [newItemLabel, setNewItemLabel] = useState("");
const [workTarget, setWorkTarget] = useState(
workHoursTarget ?? items.find((i) => i.type === "duration")?.config?.targetHours ?? 7.5
);
const { updateItem, addItem, removeItem, todoMut, restDay, updateWorkHours, updateDayMode } =
useAdventureMutations(date, items);
const checklists = items.filter((i) => i.type === "checklist");
const main = items.filter((i) => i.type !== "checklist");
return (
<div className="retro-window">
<div className="retro-titlebar">
<span>Today&apos;s Adventure</span>
<span className="text-xs font-normal opacity-90">
{chapter} · Day {journeyDay}
{isBackfilled && " · Logged later"}
</span>
</div>
<div className="p-4">
<div className="flex flex-wrap gap-2 mb-3 items-center">
<select
className="retro-window-inset text-xs p-1"
value={dayMode}
onChange={(e) => updateDayMode.mutate(e.target.value)}
>
{Object.entries(DAY_MODES).map(([key, label]) => (
<option key={key} value={key}>
{label}
</option>
))}
</select>
{dayMode === "low_energy" && (
<span className="text-xs italic text-[var(--xp-blue)]">Low-energy day lighter expectations</span>
)}
<button
className={`retro-btn text-xs ${customize ? "retro-btn-primary" : ""}`}
onClick={() => setCustomize(!customize)}
>
{customize ? "Done customizing" : "Customize Today"}
</button>
{customize && (
<Link href="/settings?section=templates" className="text-xs underline text-[var(--warm-grey)]">
Edit weekday template
</Link>
)}
</div>
{customize && (
<p className="text-xs italic mb-3 text-[var(--xp-blue)]">
Editing today only templates unchanged
{isCustomized && " · This day has custom overrides"}
</p>
)}
{isRestDay && (
<p className="text-sm italic mb-3 text-[var(--xp-blue)]">
Rest day your adventure pauses gently today.
</p>
)}
<div className="space-y-2">
{main.map((item) => (
<AdventureItemRow
key={item.id}
item={item}
disabled={isRestDay}
customize={customize}
workTarget={workHoursTarget ?? (item.config?.targetHours as number) ?? 7.5}
onUpdate={(value, state) => updateItem.mutate({ id: item.id, value, state })}
onMetaUpdate={(updates) => updateItem.mutate({ id: item.id, ...updates })}
onRemove={() => removeItem.mutate(item.id)}
/>
))}
</div>
{customize && (
<div className="mt-3 flex gap-2 items-center">
<input
className="retro-window-inset flex-1 p-1 text-sm"
placeholder="Add custom item..."
value={newItemLabel}
onChange={(e) => setNewItemLabel(e.target.value)}
/>
<button
className="retro-btn text-xs"
disabled={!newItemLabel.trim()}
onClick={() => {
addItem.mutate(newItemLabel.trim(), {
onSuccess: () => setNewItemLabel(""),
});
}}
>
Add
</button>
</div>
)}
{customize && items.some((i) => i.type === "duration" && i.label === "Work") && (
<div className="mt-3 flex gap-2 items-center">
<span className="text-sm">Work target (hours):</span>
<input
type="number"
step="0.5"
className="retro-window-inset w-20 p-1 text-sm"
value={workTarget}
onChange={(e) => setWorkTarget(Number(e.target.value))}
/>
<button
className="retro-btn text-xs"
onClick={() => updateWorkHours.mutate(workTarget)}
>
Save
</button>
</div>
)}
{checklists.length > 0 && (
<>
<hr className="my-4 border-[var(--warm-grey)] opacity-30" />
<p className="text-xs font-bold mb-2 text-[var(--warm-grey)]">Checklists</p>
<div className="flex flex-wrap gap-4">
{checklists.map((item) => (
<ChecklistItem
key={item.id}
label={item.label}
checks={(item.value?.checks as boolean[]) ?? []}
disabled={isRestDay}
onToggle={(checks) => updateItem.mutate({ id: item.id, value: { checks } })}
/>
))}
</div>
</>
)}
<hr className="my-4 border-[var(--warm-grey)] opacity-30" />
<p className="text-xs font-bold mb-2 text-[var(--warm-grey)]">Today&apos;s Todos</p>
<div className="space-y-1">
{todos.map((todo) => (
<label key={todo.id} className="flex items-center gap-2 py-0.5 cursor-pointer">
<input
type="checkbox"
checked={todo.done}
disabled={isRestDay}
onChange={() =>
todoMut.mutate({ action: "update", id: todo.id, done: !todo.done })
}
className="w-4 h-4"
/>
<span className={todo.done ? "line-through opacity-70 text-sm" : "text-sm"}>
{todo.label}
</span>
{customize && (
<button
className="text-xs text-[var(--muted-rose)] ml-auto"
onClick={() => todoMut.mutate({ action: "delete", id: todo.id })}
>
</button>
)}
</label>
))}
</div>
{!isRestDay && (
<div className="mt-2 flex gap-2">
<input
className="retro-window-inset flex-1 p-1 text-sm"
placeholder="Add a todo for today..."
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && newTodo.trim()) {
todoMut.mutate(
{ action: "create", label: newTodo.trim() },
{ onSuccess: () => setNewTodo("") }
);
}
}}
/>
<button
className="retro-btn text-xs"
disabled={!newTodo.trim()}
onClick={() =>
todoMut.mutate(
{ action: "create", label: newTodo.trim() },
{ onSuccess: () => setNewTodo("") }
)
}
>
Add
</button>
</div>
)}
<div className="mt-4 flex gap-2">
<button
className="retro-btn text-xs"
onClick={() => restDay.mutate()}
disabled={isRestDay}
>
Mark Rest Day
</button>
</div>
</div>
</div>
);
}