246 lines
8.2 KiB
TypeScript
Executable File
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'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'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>
|
|
);
|
|
}
|