feat(card): checklist with progress and owner editing

This commit is contained in:
Dave 2026-09-16 10:59:45 +02:00
parent 1f1f9854ff
commit dd9cbe6c24

View file

@ -0,0 +1,139 @@
"use client";
import { useState, useTransition } from "react";
import { Plus, X } from "lucide-react";
import { addSubtask, deleteSubtask, toggleSubtask } from "@/app/actions/subtasks";
import { cn } from "@/lib/cn";
import type { SubtaskRecord } from "@/lib/pb/types";
export function SubtaskList({
cardId,
subtasks,
editable,
clientSlug,
projectSlug,
}: {
cardId: string;
subtasks: SubtaskRecord[];
editable: boolean;
clientSlug: string;
projectSlug: string;
}) {
const [pending, startTransition] = useTransition();
const [draft, setDraft] = useState("");
const done = subtasks.filter((s) => s.done).length;
const percent = subtasks.length
? Math.round((done / subtasks.length) * 100)
: 0;
function submit(event: React.FormEvent) {
event.preventDefault();
const title = draft.trim();
if (!title) return;
setDraft("");
startTransition(() =>
addSubtask({ cardId, title, clientSlug, projectSlug }).then(() => undefined)
);
}
return (
<section className="space-y-3">
<div className="flex items-center gap-3">
<h2 className="text-xs font-semibold uppercase tracking-wide text-text-secondary">
Checklist
</h2>
{subtasks.length > 0 && (
<>
<span className="tabular text-xs text-text-muted">
{done}/{subtasks.length}
</span>
<div
className="h-1.5 flex-1 overflow-hidden rounded-full bg-surface-raised"
role="progressbar"
aria-valuenow={percent}
aria-valuemin={0}
aria-valuemax={100}
aria-label="Checklist progress"
>
<div
className="h-full rounded-full bg-brand transition-[width]"
style={{ width: `${percent}%` }}
/>
</div>
</>
)}
</div>
{subtasks.length === 0 && !editable && (
<p className="text-sm text-text-muted">No checklist items yet.</p>
)}
<ul className="space-y-1">
{subtasks.map((subtask) => (
<li key={subtask.id} className="group flex items-center gap-2.5">
<input
type="checkbox"
checked={subtask.done}
disabled={!editable || pending}
onChange={(event) =>
startTransition(() =>
toggleSubtask({
subtaskId: subtask.id,
done: event.target.checked,
clientSlug,
projectSlug,
}).then(() => undefined)
)
}
className="h-4 w-4 shrink-0 rounded border-border-strong accent-[var(--brand)]"
/>
<span
className={cn(
"flex-1 text-sm",
subtask.done
? "text-text-muted line-through"
: "text-text-primary"
)}
>
{subtask.title}
</span>
{editable && (
<button
type="button"
aria-label={`Delete ${subtask.title}`}
disabled={pending}
onClick={() =>
startTransition(() =>
deleteSubtask({
subtaskId: subtask.id,
clientSlug,
projectSlug,
}).then(() => undefined)
)
}
className="opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100"
>
<X size={14} strokeWidth={2} className="text-text-muted" />
</button>
)}
</li>
))}
</ul>
{editable && (
<form onSubmit={submit} className="flex items-center gap-2">
<Plus size={14} strokeWidth={2} className="text-text-muted" />
<input
value={draft}
onChange={(event) => setDraft(event.target.value)}
placeholder="Add an item"
disabled={pending}
className="flex-1 bg-transparent text-sm text-text-primary outline-none placeholder:text-text-muted"
/>
</form>
)}
</section>
);
}