Marketing
Write the Diwali offer email
Productivity
Three columns of task cards with tags, assignees, due dates and comment counts.
3 columns · counts · tags · avatars · due date · comments · checklist progress · add card
import { CalendarDays, CheckSquare, MessageSquare, Plus } from "lucide-react";
import { cn } from "@/lib/utils";
export interface Task {
id: string;
title: string;
tags: string[];
assignees: string[];
due: string;
overdue?: boolean;
comments?: number;
checklist?: [done: number, total: number];
}
export interface Column { id: string; title: string; tasks: Task[] }
const sample: Column[] = [
{ id: "todo", title: "To do", tasks: [
{ id: "t1", title: "Write the Diwali offer email", tags: ["Marketing"], assignees: ["Ritika Shah"], due: "17 Oct", comments: 2 },
{ id: "t2", title: "Shoot photos for the new menu", tags: ["Content"], assignees: ["Anil Kumar", "Maya Nair"], due: "21 Oct" },
{ id: "t3", title: "Update GST details on invoices", tags: ["Finance", "Urgent"], assignees: ["Kabir Rao"], due: "Today", overdue: true, comments: 1 },
] },
{ id: "doing", title: "In progress", tasks: [
{ id: "t4", title: "Menu page redesign", tags: ["Design"], assignees: ["Arjun Mehta", "Priya Nair"], due: "19 Oct", comments: 6, checklist: [3, 5] },
{ id: "t5", title: "Payment reminder emails", tags: ["Engineering"], assignees: ["Sana Khan"], due: "23 Oct", comments: 3 },
] },
{ id: "review", title: "Review", tasks: [
{ id: "t6", title: "Pricing page copy", tags: ["Marketing"], assignees: ["Ritika Shah", "Asha Raman"], due: "16 Oct", comments: 4 },
{ id: "t7", title: "Booking form validation", tags: ["Engineering"], assignees: ["Sana Khan"], due: "18 Oct", comments: 1, checklist: [4, 4] },
] },
];
const initials = (n: string) => n.split(" ").map((w) => w[0]).join("").slice(0, 2);
function TaskCard({ task }: { task: Task }) {
return (
<article className="grid gap-2.5 rounded border border-border bg-bg px-3 py-2.5 text-[13.5px] shadow-sm" tabIndex={0}>
<div className="flex flex-wrap gap-1.5">
{task.tags.map((t) => (
<span key={t} className={cn("rounded-full px-[7px] py-px text-[11.5px] font-medium",
t === "Urgent" ? "bg-danger-soft text-danger-fg" : "bg-surface-2 text-fg-muted")}>{t}</span>
))}
</div>
<p className="font-medium leading-snug">{task.title}</p>
<div className="flex items-center justify-between gap-2 text-xs text-fg-subtle">
<div className="flex items-center gap-2.5 [&_svg]:size-[13px]">
<span className={cn("inline-flex items-center gap-1", task.overdue && "text-danger-fg")}><CalendarDays aria-hidden />{task.due}</span>
{!!task.comments && <span className="inline-flex items-center gap-1"><MessageSquare aria-label="Comments" />{task.comments}</span>}
{task.checklist && <span className="inline-flex items-center gap-1 tabular-nums"><CheckSquare aria-label="Checklist" />{task.checklist[0]}/{task.checklist[1]}</span>}
</div>
<div className="flex [&>*:not(:first-child)]:-ml-1.5">
{task.assignees.map((a) => (
<span key={a} title={a} className="grid size-[22px] place-items-center rounded-full border-[1.5px] border-bg bg-primary-soft text-[10px] font-medium text-primary-soft-fg">{initials(a)}</span>
))}
</div>
</div>
</article>
);
}
export function KanbanBoard({ columns = sample, onAdd }: { columns?: Column[]; onAdd?: (columnId: string) => void }) {
return (
<div className="w-full overflow-x-auto">
<div className="grid grid-cols-[repeat(3,minmax(220px,1fr))] items-start gap-3">
{columns.map((col) => (
<section key={col.id} aria-label={col.title} className="grid gap-2 rounded-lg bg-surface p-2">
<header className="flex items-center gap-2 px-1 pb-0.5 pt-1 text-[13px] font-medium">
{col.title}
<span className="rounded-full bg-surface-2 px-[7px] text-xs text-fg-muted">{col.tasks.length}</span>
<button onClick={() => onAdd?.(col.id)} aria-label={"Add card to " + col.title}
className="ml-auto grid size-7 place-items-center rounded text-fg-muted hover:bg-surface-2">
<Plus className="size-4" />
</button>
</header>
{col.tasks.map((t) => <TaskCard key={t.id} task={t} />)}
</section>
))}
</div>
</div>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.