feat(card): checklist with progress and owner editing
This commit is contained in:
parent
1f1f9854ff
commit
dd9cbe6c24
1 changed files with 139 additions and 0 deletions
139
src/components/card/subtask-list.tsx
Normal file
139
src/components/card/subtask-list.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue