Weekend Build Part 3

Frontend: Core UI

Sunday morning in the tutorial plan. After verifying the API locally, build the React frontend — auth flow, state management, dashboard layout, and project views — and test the integration.

Last reviewed: May 26 2026

About this series: The Weekend Build is an illustrated tutorial, not a published reference implementation. Code, UI states, timing claims, and checkpoint results below are examples to implement and verify locally. No companion repository or live demo is currently published for this series.

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 Target Checkpoint — Verify login, register, token persistence, and 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. Providing the store, types, and patterns gives AI a better chance of generating compatible components. It can reduce avoidable import and state-shape fixes, but the result still needs compilation and browser testing.

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 Target Checkpoint — Verify dashboard, project creation, and selection flow

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 Target Checkpoint — Verify kanban board and task create/edit flow

Sunday Morning Checkpoint

Target checkpoint: your local Taskflow implementation supports registration, login, project creation, task creation, kanban viewing, and editing. Verify the full journey in the browser before treating the frontend as functional.

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

AI assistance can help draft stores and components quickly, but integration work varies by project. Move on to polish only after the core journey works in your implementation.


Part 3 Target Checkpoint — Core Frontend

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