Before You Start: Prerequisites
- Complete Part 2: Backend endpoints, auth flow, and database are running.
- Local services: API available on
http://localhost:3001and client onhttp://localhost:5173. - Frontend stack: React + TypeScript + Tailwind scaffolding already in place.
- Shared types:
/shared/types.tsis importable from client code. - UI pace: Focused on functional core UI first; deeper polish comes in later parts.
Assumptions for This Part
- State management uses Zustand for speed rather than a heavier architecture.
- You are comfortable iterating components in short loops: generate, review, test, refine.
- Accessibility and responsive behavior are considered now, then expanded in polish/deployment stages.
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.
+-- AuthProvider → manages token, redirects
+-- Router
— +-- /login → LoginPage
— +-- /register → RegisterPage
— +-- / → 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
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.
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
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:
- Error handling — try/catch around store actions, error message displayed to user
- Loading state — Button disabled during submission, prevents double-submit
- Redirect —
navigate('/')after successful login/register - No hardcoded styles — Pure Tailwind classes, no inline CSS objects
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.
The Project Store
Same pattern as auth — build the store first, then the components that consume it.
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
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.
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.
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.
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
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.
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.
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.
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
- Zustand auth store with login, register, token persistence, and auto-initialization
- Login and register pages with error handling, loading states, and redirects
- Protected route component with loading spinner and automatic redirect
- Zustand project store with all CRUD actions and authenticated API calls
- authFetch helper for automatic token injection and session expiry handling
- Dashboard layout with fixed sidebar, project list, create, and active project highlighting
- Kanban board with four columns, task cards, priority badges, and task counts
- Task modal for creating and editing tasks with validation and error display