Weekend Build Part 3

Frontend: Core UI

Sunday morning. The API is done and tested. Now we build the React frontend — auth flow, state management, dashboard layout, and project views — all wired to the backend.


Before You Start: Prerequisites

Assumptions for This Part

The Component Map

Before generating any components, let's lay out what we're building this session. Milestones 6, 7, and 8 from our plan.

App
+-- AuthProvider → manages token, redirects
+-- Router
— +-- /loginLoginPage
— +-- /registerRegisterPage
— +-- /DashboardLayout (protected)
— +-- Sidebar → project list + create
— +-- ProjectView
— +-- FilterBar → Part 4
— +-- KanbanBoard
— +-- Column — 4
— — +-- TaskCard — n
— +-- TaskModal → create/edit

Stores:
+-- useAuthStore → user, token, login(), logout()
+-- useProjectStore → projects, tasks, CRUD actions

M6Frontend Auth Flow

State Management First

We build the Zustand stores before any UI components. This is the foundation everything renders from. Notice how we share the types and API endpoints as context.

You

Build the Zustand auth store for Taskflow.

Types: [paste shared types.ts]
API base URL: http://localhost:3001

The store needs:
- State: user: User | null, token: string | null, isLoading: boolean
- Actions: login(email, password), register(email, password, name), logout(), initialize()
- initialize checks localStorage for existing token and validates it
- All API calls use fetch with proper error handling
- Store token in localStorage on login/register, remove on logout
- Return typed error messages from the API's { error, code } format

Use TypeScript strictly. No any.

import { create } from 'zustand';
import type { User } from '@taskflow/shared';

const API = 'http://localhost:3001';

interface AuthState {
  user: User | null;
  token: string | null;
  isLoading: boolean;
  login: (email: string, password: string) => Promise<void>;
  register: (email: string, password: string, name: string) => Promise<void>;
  logout: () => void;
  initialize: () => Promise<void>;
}

export const useAuthStore = create<AuthState>((set, get) => ({
  user: null,
  token: null,
  isLoading: true,

  login: async (email, password) => {
    const res = await fetch(`${API}/auth/login`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });

    if (!res.ok) {
      const err = await res.json();
      throw new Error(err.error || 'Login failed');
    }

    const { token, user } = await res.json();
    localStorage.setItem('token', token);
    set({ token, user });
  },

  register: async (email, password, name) => {
    const res = await fetch(`${API}/auth/register`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password, name }),
    });

    if (!res.ok) {
      const err = await res.json();
      throw new Error(err.error || 'Registration failed');
    }

    const { token, user } = await res.json();
    localStorage.setItem('token', token);
    set({ token, user });
  },

  logout: () => {
    localStorage.removeItem('token');
    set({ user: null, token: null });
  },

  initialize: async () => {
    const token = localStorage.getItem('token');
    if (!token) { set({ isLoading: false }); return; }

    try {
      const res = await fetch(`${API}/auth/me`, {
        headers: { Authorization: `Bearer ${token}` },
      });
      if (res.ok) {
        const { user } = await res.json();
        set({ token, user, isLoading: false });
      } else {
        localStorage.removeItem('token');
        set({ isLoading: false });
      }
    } catch {
      set({ isLoading: false });
    }
  },
}));

Pro Tip: The /auth/me Endpoint

The initialize function needs a GET /auth/me endpoint to validate stored tokens. This wasn't in our original API plan — the critique step missed it. When you discover gaps like this during implementation, add the endpoint to the backend (it's a 5-line route behind authMiddleware) and keep moving. Don't let missing details stop your flow.

Login and Register Pages

You

Build the LoginPage and RegisterPage components for Taskflow.

Here's the auth store: [paste useAuthStore.ts]

Requirements:
- Tailwind CSS for all styling, clean minimal design
- Form with email + password (+ name for register)
- Show error messages from the API
- Loading state on submit button
- Link to switch between login/register
- On success, redirect to / using react-router's useNavigate
- Center the form on the page with a card layout

AI generates both pages. The key things to verify:

Protected Routes

import { Navigate } from 'react-router-dom';
import { useAuthStore } from '../stores/authStore';

export function ProtectedRoute({ children }: { children: React.ReactNode }) {
  const { user, isLoading } = useAuthStore();

  if (isLoading) {
    return (
      <div className="h-screen flex items-center justify-center">
        <div className="animate-spin h-8 w-8 border-2 border-blue-500
                        border-t-transparent rounded-full" />
      </div>
    );
  }

  if (!user) return <Navigate to="/login" replace />;

  return <>{children}</>;
}

Simple, clear, no over-engineering. Loading spinner while checking token, redirect if not authenticated, render children if authenticated.

M6 Complete — Login, register, token persistence, protected routes

M7Dashboard & Project List

The Project Store

Same pattern as auth — build the store first, then the components that consume it.

You

Build the Zustand project store for Taskflow.

Here's the auth store for reference: [paste useAuthStore.ts]
Types: [paste types.ts]

The store needs:
- State: projects: Project[], activeProjectId: string | null, tasks: Task[]
- Actions: fetchProjects(), createProject(name), archiveProject(id), setActiveProject(id), fetchTasks(projectId), createTask(input), updateTask(id, input), deleteTask(id), moveTask(id, status, position)
- All API calls include the JWT token from useAuthStore
- setActiveProject should also trigger fetchTasks
- Follow the exact same patterns as useAuthStore

The critical pattern in this store: every API call reads the token from the auth store and includes it in the Authorization header. Here's the helper that makes this clean:

// Helper: authenticated fetch
async function authFetch(path: string, options: RequestInit = {}) {
  const token = useAuthStore.getState().token;
  const res = await fetch(`${API}${path}`, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
      ...options.headers,
    },
  });

  if (res.status === 401) {
    useAuthStore.getState().logout();
    throw new Error('Session expired');
  }

  if (!res.ok) {
    const err = await res.json();
    throw new Error(err.error || 'Request failed');
  }

  return res.json();
}

This helper handles auth header injection and automatic logout on expired tokens. Every action in the project store uses authFetch instead of raw fetch.

Dashboard Layout

You

Build the DashboardLayout, Sidebar, and ProjectView components.

Here's the project store: [paste useProjectStore.ts]
Auth store: [paste useAuthStore.ts]

Layout:
- Fixed sidebar (240px) on the left, main content fills the rest
- Sidebar shows: app name at top, project list, "New Project" button at bottom, user name + logout at very bottom
- Clicking a project sets it as active and loads its tasks
- Active project is highlighted in sidebar
- ProjectView renders the KanbanBoard for the active project (placeholder for now)
- If no project is selected, show an empty state: "Select a project or create one"
- Tailwind for everything. Dark theme: bg-gray-900 for sidebar, bg-gray-950 for main.

Context Is Doing the Heavy Lifting

Notice how every prompt pastes existing code. AI sees the store, the types, and the patterns — so it generates components that integrate correctly on the first pass. Without this context, you'd spend 30 minutes fixing import paths, state shapes, and API call patterns. The 2 minutes of pasting saves 30 minutes of fixing.

At this point, you should have a working flow: login → see dashboard → create project → see it appear in sidebar → select it → see empty project view. Test this in the browser before moving on.

M7 Complete — Dashboard layout, sidebar, project list, create & select

M8Kanban Board

The Kanban Board

This is the centerpiece of the app. Four columns, task cards in each, and a modal for creating and editing tasks. We build it with progressive complexity — columns and cards first, drag-and-drop in Part 4.

You

Build the KanbanBoard, Column, and TaskCard components for Taskflow.

Project store: [paste useProjectStore.ts]
Types: [paste types.ts]

KanbanBoard:
- Renders 4 columns: To Do, In Progress, Review, Done
- Filters tasks from the store by status into each column
- Sorts tasks within each column by position
- Shows task count per column in the header

Column:
- Header with status name and count badge
- "+" button to create a task in this column
- Renders TaskCard for each task
- Color-coded header accent (blue for todo, amber for in progress, violet for review, green for done)

TaskCard:
- Shows title, priority badge (color-coded), due date if set, assignee if set
- Click opens TaskModal (placeholder onClick for now)
- Compact card design, subtle shadow

Tailwind for everything. Dark theme consistent with sidebar.

const COLUMNS: { status: TaskStatus; label: string; accent: string }[] = [
  { status: 'todo', label: 'To Do', accent: 'bg-blue-500' },
  { status: 'in_progress', label: 'In Progress', accent: 'bg-amber-500' },
  { status: 'review', label: 'Review', accent: 'bg-violet-500' },
  { status: 'done', label: 'Done', accent: 'bg-green-500' },
];

export function KanbanBoard() {
  const tasks = useProjectStore((s) => s.tasks);

  return (
    <div className="grid grid-cols-4 gap-4 p-6 h-full overflow-x-auto">
      {COLUMNS.map((col) => {
        const columnTasks = tasks
          .filter((t) => t.status === col.status)
          .sort((a, b) => a.position - b.position);

        return (
          <Column
            key={col.status}
            status={col.status}
            label={col.label}
            accent={col.accent}
            tasks={columnTasks}
          />
        );
      })}
    </div>
  );
}

Clean separation: KanbanBoard filters and distributes tasks, Column renders the list, TaskCard renders individual items. Each component has one job.

Task Modal

You

Build the TaskModal component — a dialog for creating and editing tasks.

Props: mode: 'create' | 'edit', task?: Task (for edit mode), defaultStatus: TaskStatus (for create from column "+" button), onClose: () => void

Fields: title (required), description (textarea), priority (select), due date (date input), assignee (text input)

On submit: call createTask or updateTask from the store. Close modal on success. Show errors on failure.

Use a dialog overlay with Tailwind. Trap focus. Close on Escape key and backdrop click.

The modal ties together the full flow: click "+" in a column → modal opens with that column's status pre-selected → fill in details → submit → task appears in the column. Test this end-to-end before moving on.

Test the Full Flow

Before declaring M8 complete, run through the entire user journey in the browser: register → login → create project → create 3 tasks → verify they appear in the correct columns → edit a task → delete a task. If any step breaks, fix it now. It's much harder to debug integration issues after adding more features.

M8 Complete — Kanban board with columns, task cards, create/edit modal

Sunday Morning Checkpoint

At this point, Taskflow is a working application. Users can register, log in, create projects, create tasks, view them on a kanban board, and edit them. The architecture is clean — stores handle data, components handle rendering, the API handles persistence.

What's left for this afternoon: drag-and-drop, filtering, responsive design, and polish. Then this evening: testing, security review, and deployment.

The AI-First Difference

In a traditional weekend build, you'd still be writing CSS or debugging API integration. With AI-first methodology, the core app is working by Sunday morning. The afternoon is for making it great — not for making it work.


Part 3 Complete — Core Frontend Done

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 production release.
Previous Part Part 2 — Backend: API & Database
Next Part Part 4 — Polish & Features