Chapter 2: Build Core Todo
Categories
Categories
Now that CRUD is working, let's add structure. Categories let users organize their todos into groups like Work, Personal, and Shopping -- with colored tags so you can tell them apart at a glance.
Update the data model
Start by extending our Todo type:
"Update the Todo interface in src/types/todo.ts to include a category field. Create a Category type that is a union of 'Work' | 'Personal' | 'Shopping' | 'Health' | 'Other'. Also export a CATEGORIES constant array with these values and their display colors."
export type Category = 'Work' | 'Personal' | 'Shopping' | 'Health' | 'Other';
export interface Todo {
id: string;
title: string;
completed: boolean;
createdAt: string;
category: Category;
}
export const CATEGORIES: { value: Category; label: string; color: string }[] = [
{ value: 'Work', label: 'Work', color: 'bg-blue-100 text-blue-800' },
{ value: 'Personal', label: 'Personal', color: 'bg-purple-100 text-purple-800' },
{ value: 'Shopping', label: 'Shopping', color: 'bg-green-100 text-green-800' },
{ value: 'Health', label: 'Health', color: 'bg-red-100 text-red-800' },
{ value: 'Other', label: 'Other', color: 'bg-gray-100 text-gray-800' },
];Using a union type ('Work' | 'Personal' | ...) instead of a plain string gives us type safety. TypeScript will catch typos like 'Wrk' at compile time, not at runtime.
Update the AddTodo form
"Update the AddTodo component to include a category dropdown next to the input. Default to 'Personal'. The onAdd callback should now accept both title and category. Style the select to match the input field."
interface AddTodoProps {
onAdd: (title: string, category: Category) => void;
}
export default function AddTodo({ onAdd }: AddTodoProps) {
const [title, setTitle] = useState('');
const [category, setCategory] = useState<Category>('Personal');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
const trimmed = title.trim();
if (!trimmed) return;
onAdd(trimmed, category);
setTitle('');
};
return (
<form onSubmit={handleSubmit} className="flex gap-2">
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="What needs to be done?"
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg
focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<select
value={category}
onChange={(e) => setCategory(e.target.value as Category)}
className="px-3 py-2 border border-gray-300 rounded-lg
focus:outline-none focus:ring-2 focus:ring-blue-500
bg-white"
>
{CATEGORIES.map((cat) => (
<option key={cat.value} value={cat.value}>
{cat.label}
</option>
))}
</select>
<button
type="submit"
className="px-6 py-2 bg-blue-500 text-white rounded-lg
hover:bg-blue-600 transition-colors"
disabled={!title.trim()}
>
Add
</button>
</form>
);
}Show categories in the list
"Update the TodoList component to display a small colored badge showing each todo's category next to the title."
Claude Code will add a category badge:
<span
className={`text-xs px-2 py-0.5 rounded-full font-medium ${
CATEGORIES.find((c) => c.value === todo.category)?.color
}`}
>
{todo.category}
</span>Update page.tsx
"Update the addTodo function in page.tsx to accept a category parameter and include it in the new todo object. Also update the sample todos to have categories."
const addTodo = (title: string, category: Category) => {
const newTodo: Todo = {
id: generateId(),
title,
completed: false,
createdAt: new Date().toISOString(),
category,
};
setTodos([newTodo, ...todos]);
};When you extend a data model, Claude Code can trace the impact across your codebase. Ask "what other files need to be updated for this type change?" and it will find every place that creates or uses a Todo object.
Test it
- Select a category from the dropdown and add a todo
- The todo should appear with a colored category badge
- Try different categories -- each should have a distinct color
- Existing sample todos should now show their categories too
The app is starting to feel feature-rich. Next, we'll add the ability to filter by category so users can focus on what matters.