Files
minabot/src/components/HabitForm.tsx
syntaxbullet 833d5673eb Refactor HabitColorPicker to use HABIT_COLOR_CHOICES, update styles and tests
- Replaced HABIT_PALETTES with HABIT_COLOR_CHOICES in HabitColorPicker component and related tests.
- Adjusted the layout and styles of the color picker for improved usability.
- Updated tests to reflect changes in color selection logic.
- Modified WorkspacePanel and SettingRow components to allow optional eyebrow and description props.
- Enhanced accessibility tests for calendar navigation.
- Updated calendar model to include a 1-month view option.
- Refined styles across various components for consistency and improved responsiveness.
2026-09-08 15:42:47 +02:00

201 lines
6.7 KiB
TypeScript

import { Modal } from "./design-system/Modal";
import { useRef, useState, type FormEvent } from "react";
import { Button } from "./design-system/primitives";
import { Field } from "./design-system/Field";
import { ScheduleEditor } from "./design-system/EditingWorkbench";
import { HabitColorPicker } from "./design-system/HabitColorPicker";
import { habitInput, type Schedule } from "../habits/contracts";
import { habitRequest } from "../lib/dashboard";
/** Account behavior composed from the design system's existing form components. */
export function HabitForm({
date,
onCancel,
onCreated,
onExpired,
}: {
date: string;
onCancel: () => void;
onCreated: (name: string) => void;
onExpired: () => void;
}) {
const form = useRef<HTMLFormElement>(null);
const saving = useRef(false);
const [name, setName] = useState("");
const [method, setMethod] = useState<"manual" | "count" | "tasks">("manual");
const [target, setTarget] = useState(10);
const [unit, setUnit] = useState("pages");
const [tasks, setTasks] = useState([""]);
const [schedule, setSchedule] = useState<Schedule>({ type: "daily" });
const [color, setColor] = useState("#3e805d");
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
async function submit(event: FormEvent) {
event.preventDefault();
if (saving.current) return;
const parsed = habitInput.safeParse({
name,
method,
schedule,
color,
...(method === "count" ? { target, unit } : {}),
...(method === "tasks" ? { tasks: tasks.map((name) => ({ name })) } : {}),
});
if (!parsed.success) {
setError(parsed.error.issues.map((issue) => issue.message).join(" "));
return;
}
saving.current = true;
setBusy(true);
setError("");
try {
await habitRequest("/habits", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(parsed.data),
});
onCreated(parsed.data.name);
} catch (error) {
if (
error instanceof Error &&
error.message.includes("session has expired")
)
onExpired();
else
setError(
error instanceof Error
? error.message
: "Could not create your habit.",
);
} finally {
saving.current = false;
setBusy(false);
}
}
return (
<Modal id="new-habit" title="Create habit" onClose={onCancel} closeLabel="Close new habit" busy={busy} size="compact" initialFocus="input" returnFocus={() => document.getElementById("add-habit")}>
<form ref={form} className="ds-edit-content" onSubmit={submit}>
<fieldset className="ds-task-editor-fieldset ds-habit-form-grid" disabled={busy}>
<Field label="Habit name">
{(id) => (
<input
id={id}
required
maxLength={200}
placeholder="e.g. Read ten pages"
value={name}
onChange={(event) => setName(event.target.value)}
/>
)}
</Field>
<Field label="Tracking">
{(id) => (
<select
id={id}
value={method}
onChange={(event) =>
setMethod(event.target.value as typeof method)
}
>
<option value="manual">A simple check-in</option>
<option value="count">A count target</option>
<option value="tasks">A routine checklist</option>
</select>
)}
</Field>
<ScheduleEditor value={schedule} onChange={setSchedule} anchorDate={date} />
{method === "count" && (
<div className="ds-form-grid">
<Field label="Daily target">
{(id) => (
<input
id={id}
type="number"
required
min={1}
max={10000}
step={1}
value={Number.isNaN(target) ? "" : target}
onChange={(event) => setTarget(event.target.valueAsNumber)}
/>
)}
</Field>
<Field label="Unit">
{(id) => (
<input
id={id}
required
maxLength={80}
value={unit}
onChange={(event) => setUnit(event.target.value)}
/>
)}
</Field>
</div>
)}
{method === "tasks" && (
<div className="ds-form-section">
{tasks.map((task, index) => (
<Field key={index} label={`Task ${index + 1}`}>
{(id) => (
<div className="ds-input-row">
<input
id={id}
required
maxLength={200}
value={task}
onChange={(event) =>
setTasks((current) =>
current.map((value, i) =>
i === index ? event.target.value : value,
),
)
}
/>
<Button
variant="text"
aria-label={`Remove task ${index + 1}`}
disabled={tasks.length === 1}
onClick={() =>
setTasks((current) =>
current.filter((_, i) => i !== index),
)
}
>
Remove
</Button>
</div>
)}
</Field>
))}
<Button
variant="secondary"
disabled={tasks.length >= 100}
onClick={() => setTasks((current) => [...current, ""])}
>
Add task +
</Button>
</div>
)}
<HabitColorPicker value={color} onChange={setColor} mode="create" />
</fieldset>
{error && (
<p className="ds-form-feedback" role="alert">
{error}
</p>
)}
<div className="ds-form-actions">
<Button type="submit" disabled={busy}>
{busy ? "Creating…" : "Create habit"}
</Button>
<Button variant="text" disabled={busy} onClick={onCancel}>
Cancel
</Button>
</div>
</form>
</Modal>
);
}