Weekend Build Part 4

Polish & Features

Sunday afternoon in the tutorial plan. After verifying the core flow, add usability features — drag-and-drop kanban, filtering, responsive design, and toast notifications.

Last reviewed: May 26 2026

About this series: The Weekend Build is an illustrated tutorial, not a published reference implementation. Feature results, time estimates, and review findings below are example checkpoints to implement and verify locally. No companion repository or live demo is currently published for this series.

M9Drag-and-Drop & Filtering

Drag-and-Drop Kanban

This is the feature that turns the prototype into an interactive kanban board. This tutorial uses @dnd-kit, which supports React drag-and-drop patterns and accessibility configuration; verify keyboard and pointer interactions in your implementation.

You

Add drag-and-drop to the Taskflow kanban board using @dnd-kit/core and @dnd-kit/sortable.

Here are the existing components: [paste KanbanBoard.tsx, Column.tsx, TaskCard.tsx]
Here's the project store: [paste useProjectStore.ts, specifically the moveTask action]

Requirements:
- Drag tasks between columns (change status)
- Drag tasks within a column (reorder by position)
- Calculate position using float midpoint: between tasks at 1.0 and 2.0, insert at 1.5. End of column = max + 1.
- Optimistic update: move the card immediately in the UI, then call the API. If API fails, revert.
- Visual feedback: dragging card gets slight opacity + shadow, drop zone highlighted
- Keep the existing component structure, just wrap with DnD providers

The prompt is specific about the position calculation and optimistic updates — these are the two areas where AI needs the most guidance. Here's the core logic:

function calculatePosition(
  tasks: Task[],
  overIndex: number
): number {
  // Dropping at the end of column
  if (overIndex >= tasks.length) {
    const last = tasks[tasks.length - 1];
    return last ? last.position + 1 : 1;
  }

  // Dropping at the beginning
  if (overIndex === 0) {
    return tasks[0].position / 2;
  }

  // Dropping between two tasks
  const before = tasks[overIndex - 1];
  const after = tasks[overIndex];
  return (before.position + after.position) / 2;
}

And the optimistic update pattern in the drag end handler:

function handleDragEnd(event: DragEndEvent) {
  const { active, over } = event;
  if (!over) return;

  const taskId = active.id as string;
  const newStatus = over.data.current?.status as TaskStatus;
  const overIndex = over.data.current?.index as number;

  // Get tasks in the target column
  const columnTasks = tasks
    .filter((t) => t.status === newStatus && t.id !== taskId)
    .sort((a, b) => a.position - b.position);

  const newPosition = calculatePosition(columnTasks, overIndex);

  // Optimistic update: move immediately in local state
  useProjectStore.getState().optimisticMoveTask(taskId, newStatus, newPosition);

  // Then sync with API (revert on failure)
  useProjectStore.getState().moveTask(taskId, newStatus, newPosition)
    .catch(() => {
      // Revert: re-fetch tasks from server
      useProjectStore.getState().fetchTasks(activeProjectId!);
    });
}

Pro Tip: Optimistic Updates

The optimistic update is what makes the UI feel instant. The card moves immediately when you drop it — no waiting for the API. If the API call fails, we revert by re-fetching from the server. This pattern requires adding an optimisticMoveTask action to the Zustand store that updates local state without an API call. Ask AI to add it — it's a 10-line function.


Filtering & Search

You

Build a FilterBar component for the Taskflow kanban board.

Current components: [paste KanbanBoard.tsx]
Types: [paste types.ts]

The FilterBar sits above the kanban columns and provides:
- Text search (filters tasks by title, case-insensitive)
- Priority filter (toggle buttons: All, Low, Medium, High, Urgent)
- Assignee filter (text input, filters by assignee field)
- Active filter count badge

Implementation:
- Store filter state locally in FilterBar with useState (not Zustand — these are ephemeral UI state)
- Pass a filterFn callback to KanbanBoard that filters the task array
- Debounce the text search input (300ms)
- "Clear all" button when any filter is active

Tailwind. Compact single row. Dark theme.

// FilterBar passes this to KanbanBoard
const filterTasks = useCallback(
  (tasks: Task[]) => {
    return tasks.filter((task) => {
      // Text search
      if (searchQuery && !task.title.toLowerCase().includes(searchQuery.toLowerCase())) {
        return false;
      }

      // Priority filter
      if (priorityFilter && task.priority !== priorityFilter) {
        return false;
      }

      // Assignee filter
      if (assigneeFilter && task.assignee?.toLowerCase() !== assigneeFilter.toLowerCase()) {
        return false;
      }

      return true;
    });
  },
  [searchQuery, priorityFilter, assigneeFilter]
);

// KanbanBoard applies it before distributing to columns
const filteredTasks = filterFn ? filterFn(tasks) : tasks;
Local vs. Global State

Filters are kept in local component state, not Zustand. Why? Because they're ephemeral — they reset when you switch projects, they're not shared across components, and they don't need persistence. Not everything belongs in a global store. This distinction matters for keeping your architecture clean.

M9 Target Checkpoint — Verify drag-and-drop, filtering, and search

Toast Notifications

Every successful action and every error needs feedback. Without toasts, the user creates a task and has no confirmation it worked. Without error toasts, API failures are silent.

You

Build a minimal toast notification system for Taskflow.

Requirements:
- useToast hook that returns toast.success(message) and toast.error(message)
- Toasts appear in bottom-right corner, stack vertically
- Auto-dismiss after 3 seconds (success) or 5 seconds (error)
- Slide-in animation from the right
- Green accent for success, red for error
- Small Zustand store for toast state
- ToastContainer component added once in App.tsx

Keep it minimal — no library, just Zustand + Tailwind + a bit of CSS animation. Under 80 lines total.

import { create } from 'zustand';

interface Toast {
  id: string;
  message: string;
  type: 'success' | 'error';
}

interface ToastState {
  toasts: Toast[];
  success: (message: string) => void;
  error: (message: string) => void;
  dismiss: (id: string) => void;
}

export const useToast = create<ToastState>((set) => ({
  toasts: [],

  success: (message) => {
    const id = crypto.randomUUID();
    set((s) => ({ toasts: [...s.toasts, { id, message, type: 'success' }] }));
    setTimeout(() => set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })), 3000);
  },

  error: (message) => {
    const id = crypto.randomUUID();
    set((s) => ({ toasts: [...s.toasts, { id, message, type: 'error' }] }));
    setTimeout(() => set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })), 5000);
  },

  dismiss: (id) => set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })),
}));

Then add toast.success('Task created') after successful store actions and toast.error(err.message) in relevant catch blocks. Review each action and test both success and error paths.


Responsive Design

You

Make the Taskflow dashboard responsive.

Current layout: [paste DashboardLayout.tsx, Sidebar.tsx, KanbanBoard.tsx]

Breakpoints:
- Desktop (=1024px): sidebar + kanban as-is
- Tablet (768—1023px): collapsible sidebar (hamburger icon), kanban columns 2—2 grid
- Mobile (<768px): sidebar as full-screen overlay, kanban as single scrollable column with status headers

Implementation:
- Add a sidebarOpen state to DashboardLayout
- Sidebar gets a backdrop overlay on mobile
- KanbanBoard switches layout via Tailwind responsive classes
- TaskCard stays the same across all breakpoints
- FilterBar stacks vertically on mobile

The responsive prompt is detailed because a minimal breakpoint change may not cover real interaction needs. Specifying the layout per breakpoint gives you concrete behavior to check on narrow and wide screens.

// KanbanBoard responsive layout
<div className={`
  grid gap-4 p-4 h-full
  grid-cols-1 overflow-y-auto
  md:grid-cols-2 md:overflow-y-auto
  lg:grid-cols-4 lg:overflow-x-auto lg:overflow-y-hidden
`}>
  {COLUMNS.map((col) => (
    <Column key={col.status} {...col} tasks={columnTasks} />
  ))}
</div>

Pro Tip: Test on Real Devices

Open http://your-local-ip:5173 on your phone while developing. A real touch screen can expose issues that an emulator does not, especially with the sidebar overlay and kanban scrolling. Check the core interactions before calling the layout ready.


Quick Wins

With the core features implemented, consider these focused polish tasks. Choose only what you have time to implement, review, and test:

You

Add due date highlighting to TaskCard. Here's the current component: [paste TaskCard.tsx]

Rules: if the due date has passed, show it in red (text-red-400). If it's today, show amber (text-amber-400). If it's in the future, show muted (text-gray-500). If no due date, don't render the date element at all.

Each quick win still requires implementation review and interaction testing. Apply the changes that improve your prototype without crowding out verification.


The Refactor Pause

Once the build has accumulated several features, pause before testing and deployment for the refactor step from the manual.

You

Review all the frontend code we've built for Taskflow.

[paste all component files, stores, and utilities]

A senior developer reviewing this codebase would improve:
1. Functions that have grown too long
2. Duplicated logic that should be extracted
3. Components with too many responsibilities
4. Inconsistent patterns between similar components
5. Missing TypeScript types (any accidental any types?)

Identify the top 5 issues. Fix confirmed high-value issues and list anything deferred before we assess demo readiness.

AI

Top 5 issues:

1. Duplicated API error handling — Three components have identical try/catch/toast patterns. Extract a withToast wrapper function.

2. KanbanBoard is 180 lines — The drag handler logic should be extracted into a useDragAndDrop custom hook.

3. Date formatting repeated — TaskCard and TaskModal both format dates differently. Extract a formatDate utility.

4. Magic strings — Column statuses and priorities are hardcoded in multiple places. Use the COLUMNS config array everywhere.

5. Missing types on event handlers — Two onChange handlers are typed as any. Fixed: React.ChangeEvent<HTMLInputElement>.

"Fix top 5, leave the rest" is a scope constraint. Fix the most important confirmed problems, record anything deferred, and do not deploy with unresolved high-risk findings.


Part 4 Target Checkpoint — Feature And Polish Pass

Jump to Any Part

01
Part 1 — Planning & Architecture System design, milestones, and shared types.
02
Part 2 — Backend: API & Database Express routes, auth, and SQLite persistence.
03
Part 3 — Frontend: Core UI Auth flow, dashboard shell, and kanban core.
04
Part 4 — Polish & Features Drag-and-drop, filtering, and responsive polish.
05
Part 5 — Testing, Review & Deployment Quality gates, hardening, and demo deployment verification.
Previous Part Part 3 — Frontend: Core UI
Final Part Part 5 — Testing, Review & Deployment