# Project Board

> A project workspace with breadcrumbs, a board / activity view switch and cards you can drag between columns.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/templates/project-board
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/project-board.json
```

Adds the page at `app/project-board/page.tsx` together with the components below. Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Components

- [App Sidebar](https://chromotion.dev/r/app-sidebar.md): `@/components/app-sidebar`
- [Breadcrumb](https://chromotion.dev/r/breadcrumb.md): `@/components/breadcrumb`
- [Animated Tabs](https://chromotion.dev/r/animated-tabs.md): `@/components/animated-tabs`
- [Kanban Board](https://chromotion.dev/r/kanban-board.md): `@/components/kanban-board`
- [Activity Timeline](https://chromotion.dev/r/activity-timeline.md): `@/components/activity-timeline`

## Dependencies

```bash
npm install lucide-react
```

## Source

```tsx title="app/project-board/page.tsx"
"use client"

import * as React from "react"
import {
  Calendar,
  CheckCircle2,
  FolderKanban,
  GitMerge,
  Hexagon,
  House,
  Inbox,
  MessageSquare,
  Plus,
  Settings,
  Users,
} from "lucide-react"

import { ActivityTimeline, type TimelineItem } from "@/components/activity-timeline"
import { AnimatedTabs } from "@/components/animated-tabs"
import { AppSidebar } from "@/components/app-sidebar"
import { Breadcrumb } from "@/components/breadcrumb"
import { KanbanBoard, type KanbanCard } from "@/components/kanban-board"

const columns = [
  { id: "backlog", title: "Backlog", color: "#a1a1aa" },
  { id: "progress", title: "In progress", color: "#f97316" },
  { id: "review", title: "In review", color: "#a855f7" },
  { id: "done", title: "Done", color: "#22c55e" },
]

const initialCards: KanbanCard[] = [
  { id: "c1", columnId: "backlog", title: "Audit color tokens for contrast", tags: ["Design"], assignee: "AL" },
  { id: "c2", columnId: "backlog", title: "Add RTL support to the tabs", tags: ["i18n"] },
  { id: "c3", columnId: "backlog", title: "Write migration guide for v2", tags: ["Docs"], assignee: "GH" },
  { id: "c4", columnId: "progress", title: "Command palette fuzzy ranking", tags: ["Feature"], assignee: "AT" },
  { id: "c5", columnId: "progress", title: "Drawer drag-to-close on touch", tags: ["Mobile"], assignee: "MH" },
  { id: "c6", columnId: "review", title: "Data table column sorting", tags: ["Feature"], assignee: "LT" },
  { id: "c7", columnId: "done", title: "Ship the date picker", tags: ["Release"], assignee: "AL" },
  { id: "c8", columnId: "done", title: "Fix focus ring in Safari", tags: ["Bug"] },
]

const members = ["AL", "GH", "AT", "MH", "LT"]

const views = [
  { id: "board", label: "Board" },
  { id: "activity", label: "Activity" },
]

const history: TimelineItem[] = [
  { icon: <GitMerge />, color: "#a855f7", title: "Linus moved “Data table column sorting” to In review", time: "20m ago" },
  { icon: <MessageSquare />, color: "#3b82f6", title: "Grace commented on “Write migration guide for v2”", time: "2h ago" },
  { icon: <CheckCircle2 />, color: "#22c55e", title: "Ada completed “Ship the date picker”", time: "Yesterday" },
  { icon: <Plus />, title: "Alan created “Command palette fuzzy ranking”", time: "Nov 4" },
]

export default function ProjectBoard() {
  const [view, setView] = React.useState("board")
  // The board owns its cards; this copy keeps the header counts and the activity feed in sync.
  const [cards, setCards] = React.useState(initialCards)
  const [moves, setMoves] = React.useState<TimelineItem[]>([])

  const done = cards.filter((card) => card.columnId === "done").length
  const progress = Math.round((done / Math.max(cards.length, 1)) * 100)

  const changeCards = (next: KanbanCard[]) => {
    const moved = next.find((card) => cards.find((before) => before.id === card.id)?.columnId !== card.columnId)
    const column = moved && columns.find((item) => item.id === moved.columnId)
    if (moved && column && cards.some((before) => before.id === moved.id)) {
      setMoves((current) => [
        { icon: <FolderKanban />, color: column.color, title: `You moved “${moved.title}” to ${column.title}`, time: "Just now" },
        ...current,
      ])
    }
    setCards(next)
  }

  return (
    <AppSidebar
      collapseBelow={768}
      brand={
        <>
          <Hexagon className="size-5 shrink-0 fill-primary text-primary" /> Hexa
        </>
      }
      brandIcon={<Hexagon className="size-5 shrink-0 fill-primary text-primary" />}
      groups={[
        {
          items: [
            { label: "Home", href: "#", icon: <House /> },
            { label: "Inbox", href: "#", icon: <Inbox />, badge: 3 },
            { label: "Calendar", href: "#", icon: <Calendar /> },
          ],
        },
        {
          label: "Projects",
          items: [
            { label: "Design system", href: "#", icon: <FolderKanban />, active: true },
            { label: "Marketing site", href: "#", icon: <FolderKanban /> },
            { label: "Mobile app", href: "#", icon: <FolderKanban /> },
          ],
        },
        {
          label: "Team",
          items: [
            { label: "Members", href: "#", icon: <Users /> },
            { label: "Settings", href: "#", icon: <Settings /> },
          ],
        },
      ]}
      user={{ name: "Ada Lovelace", email: "ada@hexa.dev", initials: "AL" }}
      accentColor="var(--ring)"
    >
      <div className="flex flex-col gap-5 border-b bg-background px-6 pt-4 pb-0">
        <Breadcrumb
          items={[
            { label: "Hexa", href: "#", icon: <House className="size-3.5" /> },
            { label: "Projects", href: "#" },
            { label: "Design system" },
          ]}
        />
        <div className="flex flex-wrap items-end justify-between gap-4">
          <div className="min-w-0">
            <h1 className="text-xl font-semibold tracking-tight">Design system</h1>
            <p className="mt-1 text-sm text-muted-foreground">
              {done} of {cards.length} tasks done · due Nov 28
            </p>
          </div>
          <div className="flex items-center gap-4">
            <div className="flex -space-x-2">
              {members.map((initials) => (
                <span
                  key={initials}
                  className="flex size-8 items-center justify-center rounded-full border-2 border-background bg-muted text-[11px] font-medium"
                >
                  {initials}
                </span>
              ))}
            </div>
            <div className="flex w-32 flex-col gap-1.5">
              <div className="flex justify-between text-xs">
                <span className="text-muted-foreground">Progress</span>
                <span className="font-medium tabular-nums">{progress}%</span>
              </div>
              <div className="h-1.5 overflow-hidden rounded-full bg-muted">
                <div className="h-full rounded-full bg-primary transition-[width] duration-500" style={{ width: `${progress}%` }} />
              </div>
            </div>
          </div>
        </div>
        <AnimatedTabs tabs={views} value={view} onChange={setView} variant="underline" />
      </div>

      <div className="p-6">
        {/* Hidden rather than unmounted, so the board keeps its cards while you look at the feed */}
        <div hidden={view !== "board"}>
          <KanbanBoard columns={columns} defaultCards={initialCards} onCardsChange={changeCards} columnWidth={272} />
        </div>
        {view === "activity" && (
          <div className="max-w-xl rounded-2xl border bg-card p-6 shadow-sm">
            <ActivityTimeline items={[...moves, ...history]} />
          </div>
        )}
      </div>
    </AppSidebar>
  )
}
```
