Skip to content

izaiasmorais/dnd

Repository files navigation

DnD

dnd.mp4

A kanban board implementation with drag and drop fully integrated with a real API and database. Built to show how to implement @hello-pangea/dnd with server state, optimistic UI, and high render performance in a production-grade setup.


Stack

Layer Technology
Framework Next.js 15 (App Router)
UI React 19
Styling Tailwind CSS v4
Drag & Drop @hello-pangea/dnd
Server State TanStack Query v5
Client State Jotai
Database SQLite (better-sqlite3)
API Next.js Route Handlers
Validation Zod

What this project covers

Real API integration

Drag and drop connected to actual HTTP endpoints — not mock data or local state. Every card move triggers a PATCH /api/issues/:id/position with the new position and status, persisted in a SQLite database.

GET    /api/issues
POST   /api/issues
GET    /api/issues/:id
PATCH  /api/issues/:id
DELETE /api/issues/:id
PATCH  /api/issues/:id/position

Optimistic updates

The board updates instantly on drop — before the API responds. TanStack Query handles the optimistic state: the UI reflects the new card position immediately, and rolls back automatically if the request fails.

1. User drops card
2. UI updates instantly (optimistic)
3. PATCH request fires in background
4. On success: server state is confirmed
5. On error: UI rolls back to previous state

Fractional indexing

Cards are sorted by a position field (float). Moving a card calculates a new position between its neighbors — no re-indexing of the full list needed.

Before: [pos=1] [pos=2] [pos=3] [pos=4]
Move last card between 1 and 2:
After:  [pos=1] [pos=1.5] [pos=2] [pos=3]

Only 1 database row is updated per drop, regardless of how many cards are in the column.

Render performance

@hello-pangea/dnd (a fork of react-beautiful-dnd) forces re-renders on all children of Droppable and Draggable during any drag. This project uses React.memo with a custom comparator to block those forced re-renders:

export const DragAndDropReRenderBreaker = memo(
  ({ children }: React.PropsWithChildren<{ memoizationId: string }>) => {
    return <>{children}</>;
  },
  (prev, next) => prev.memoizationId === next.memoizationId,
);

Applied at two levels — column and card — so dragging one card doesn't re-render any other card or column content.


Running locally

bun install
bun dev

The database is created and seeded automatically on first run.


Project structure

src/
├── app/
│   ├── layout.tsx                          # Root HTML layout, fonts, metadata
│   ├── page.tsx                            # Home route — renders KanbanApp
│   └── api/
│       └── issues/
│           ├── route.ts                    # GET /api/issues, POST /api/issues
│           └── [id]/
│               ├── route.ts               # GET, PATCH, DELETE /api/issues/:id
│               └── position/
│                   └── route.ts           # PATCH /api/issues/:id/position
│
├── db/
│   ├── index.ts                            # SQLite connection singleton (WAL mode)
│   ├── schema.ts                           # CREATE TABLE issues
│   ├── queries.ts                          # getAllIssues, getIssueById, createIssue,
│   │                                       # updateIssue, deleteIssue, updateIssuePosition
│   ├── seed.ts                             # Populates the database on first run
│   └── setup.ts                            # Runs schema + seed before any request
│
├── features/
│   └── issues/
│       ├── components/
│       │   ├── kanban-app.tsx              # Providers: QueryClient, Jotai, Toaster
│       │   ├── board/
│       │   │   ├── kanban-board.tsx        # Renders all columns side by side
│       │   │   └── kanban-board-drag-drop-context.tsx  # DragDropContext, onDragEnd handler,
│       │   │                                           # fractional position calc, API sync
│       │   ├── column/
│       │   │   ├── kanban-column.tsx       # Droppable wrapper + memo optimization
│       │   │   ├── kanban-column-header.tsx            # Title, status dot, card count
│       │   │   └── kanban-column-cards-container.tsx   # Scrollable droppable card list
│       │   └── card/
│       │       ├── kanban-card.tsx         # Title, tags, priority, date, assignee avatar
│       │       ├── kanban-card-draggable-container.tsx # Draggable wrapper + drop animation
│       │       └── kanban-card-priority-icon.tsx       # SVG bars (urgent/high/medium/low/none)
│       │
│       ├── hooks/
│       │   ├── use-issues.ts              # useQuery → GET /api/issues + Zod validation
│       │   └── use-update-issue-status.ts # useMutation → PATCH with optimistic update + toast
│       │
│       ├── schemas/
│       │   ├── issue.types.ts             # ColumnStatus, Priority, KanbanCard, KanbanTag
│       │   ├── issue.schemas.ts           # Zod schemas for issue and list validation
│       │   └── columns.ts                 # Column definitions (backlog → done)
│       │
│       └── data/
│           └── mock-data.ts               # 25 sample cards for initial seeding
│
├── shared/
│   ├── components/
│   │   ├── avatar.tsx                     # Initials avatar with custom background color
│   │   ├── tag-badge.tsx                  # Colored badge for issue tags
│   │   └── drag-and-drop-re-render-breaker.tsx  # React.memo wrapper blocking dnd re-renders
│   └── utils/
│       └── compute-new-position.ts        # Fractional indexing: calculates position between neighbors
│
└── index.css                              # Tailwind setup, scrollbar style, body defaults

About

A kanban board implementation with drag and drop fully integrated with a real API and database. Built to show how to implement `@hello-pangea/dnd` with server state, optimistic UI, and high render performance in a production-grade setup.

Topics

Resources

Stars

Watchers

Forks

Contributors