Step 1: Scaffold the project
Time to build. Open a terminal, navigate to ~/dev/web-dev/p2, and launch Claude Code. Your CLAUDE.md is already in the project root from Unit 2. Claude reads it automatically at session start, so it already knows the client, the tech stack, and the ticket breakdown.
Paste the T1 setup prompt:
Read CLAUDE.md. Set up the project: initialize a Vite + React + TypeScript project, install Tailwind CSS, and configure the development environment. Use the product data from materials/product-data.json and the photos from materials/photos/. The project structure should match the architecture in planning/architecture.md.
Claude will scaffold the project, install dependencies, configure Tailwind, and create the initial file structure. Wait for it to finish. You should see src/, public/, package.json, vite.config.ts, tsconfig.json, and tailwind.config.js in your project directory.
Start the dev server to confirm everything works:
npm run dev
The terminal should show Vite's output with a local URL, typically localhost:5173. Open that URL in Chrome. You should see the default Vite + React starter page. If you see an error instead, read the terminal output before asking Claude to fix it. The error message tells you what went wrong.
Step 2: Understand React's component model
Before you direct Claude to build components, you need to understand the contract those components follow. React is built on JavaScript, and the bugs React produces are often JavaScript bugs wearing a component costume.
A React component is a function that returns what should appear on screen. That function receives data through props (short for properties) and can track data that changes through state. When props or state change, React re-renders the component. That's the rendering contract.
The contract sounds simple, but it has consequences that trip up AI-generated code. Three specific ones matter right now:
Missing keys in lists. When React renders a list of items, each item needs a unique key prop so React can track which items changed, moved, or were removed. AI generates lists without keys or uses array indices as keys, which causes subtle DOM reconciliation bugs when items are reordered or filtered. You won't see an error. The wrong item will just update silently.
Stale closures. A closure is a JavaScript function that "remembers" variables from its parent scope. In React, event callbacks can capture old values of state or props and keep using the stale version instead of the current one. This is a JavaScript closure problem, not a React-specific one, but it surfaces constantly in components. AI generates callbacks that look right but reference yesterday's data.
Incorrect useEffect dependencies. useEffect runs code in response to changes. If you tell it to watch the wrong variables (or none at all), it either runs every render (performance problem) or never re-runs when it should (stale data). AI defaults to patterns from its training data without analyzing what this specific effect should watch.
You don't need to memorize these. You need to know they exist so you can spot them in Claude's output. When a filter "doesn't update" or a list "shows the wrong item," these are the likely causes.
Step 3: Build the product card component
Start with the smallest visible piece: a single product card. This is task-sizing in action. "Build the whole page" produces worse results than focused, component-level requests. Start with one card.
Direct Claude to build the product card component:
Build the ProductCard component for T2. It should display one product from materials/product-data.json: the photo, product name, leather type, and price range in TND. Use TypeScript for the props interface — every prop should have a specific type. Use Tailwind CSS for styling. The image must have descriptive alt text specific to the product, not generic placeholder text. Save as src/components/ProductCard.tsx.
Notice the sequence: you're asking Claude to build this component after the project scaffold is in place, and before any other component. That order matters. The product card defines the data shape that other components will consume. If you built the filter first, you'd be guessing at what the card needs. Building the card first establishes the props interface that everything else inherits.
Check the dev server after Claude finishes. You should see a product card rendering in the browser. If it shows a blank page or an error, read the browser console (F12, Console tab) before asking Claude to fix it.
Step 4: Review the generated code
Open src/components/ProductCard.tsx in VS Code. You're looking at two things: the TypeScript interface and the component structure.
The TypeScript interface sits near the top of the file. It defines what data the component expects:
interface ProductCardProps {
id: string;
name: string;
category: string;
leatherType: string;
priceRange: string;
mainPhoto: string;
altText: string;
onClick: () => void;
}
Every property has a specific type. That specificity is the point. If Claude had written data: any instead, the code would still run. But TypeScript type errors are information about AI's misunderstanding of the data shape. When Claude uses any, it's bypassing the safety net that would catch a runtime null reference hours later. If you see any in the props interface, that's a signal to redirect: "Replace any with specific types for each property."
Now look at the JSX. A few things to check:
- Does the image use a meaningful
altattribute? "Product image" is not meaningful. "Medina Tote in vegetable-tanned goatskin" is. Yasmine's buyers include collectors who use screen readers, and generic alt text tells them nothing about the piece. - Does Claude render any content using
innerHTMLordangerouslySetInnerHTML? These patterns inject raw HTML into the page, and AI reaches for them when rendering user-provided content. For product data you control, they're unnecessary and they open a cross-site scripting risk. If you see either one, redirect Claude to use JSX directly. - Does the component use semantic elements? A card might use
articleorfigure. The image might sit inside afigurewith afigcaption. These choices affect both accessibility and maintainability.
Also consider the component's overall weight. JavaScript has a unique performance cost. It must be downloaded, parsed, compiled, and executed, blocking the main thread. A product card that imports five utility libraries for simple formatting adds bundle weight that users pay for on every page load. Check what Claude imported at the top of the file. If there are imports beyond React and local files, ask yourself whether the card genuinely needs them.
Step 5: Build the category filter
The filter is the second component, and it introduces state. The product card receives data through props. The category filter owns data that changes: which category is currently selected.
Direct Claude to build it:
Build the CategoryFilter component for T3. Categories are: All, Bags, Wallets, Belts, Custom. The filter should track which category is currently active. When the user clicks a category, only matching products appear in the grid. The filter buttons must be keyboard-accessible — a user should be able to tab to each button and press Enter or Space to select it. Use Tailwind CSS. Save as src/components/CategoryFilter.tsx.
Keyboard accessibility is not a bonus feature. Yasmine's customers include people who navigate with keyboards, and an interactive filter that only responds to mouse clicks excludes them. The filter buttons need to be real button elements (not styled div or span tags), because native buttons get keyboard behavior for free. AI generates components that work for sighted mouse users but fail keyboard-only navigation. Check this immediately: after Claude builds the filter, tab through the buttons in the browser. If tabbing skips them, they're not real buttons.
Where state lives matters. The selected category needs to be accessible to both the filter component (to highlight the active button) and the product grid (to show only matching products). If Claude puts the state inside the filter component, the grid can't access it. State should live in a shared parent component that passes the selected category down to both the filter and the grid as props.
This is a design decision with maintenance consequences. A single component that handles both filtering and display seems simpler now, but it becomes harder to change later. If Yasmine asks for a search bar next month, the filter logic needs to compose with search logic. Separate components that communicate through props make that composition possible.
Step 6: Check the composition
The two components should work together. The filter controls what the grid displays. Open the browser and test the full flow.
Click each category button: All, Bags, Wallets, Belts, Custom. When you click "Bags," only bag products should appear. When you click "All," every product returns. If clicking a category shows the wrong products or shows nothing, the filtering logic has a bug. Read the component code before asking Claude to fix it. The bug is usually in how the selected category flows from the filter to the grid.
Now test keyboard navigation. Press Tab until the first filter button has focus (you should see a visible focus ring). Press Enter or Space. The filter should activate, and only matching products should appear. Tab to the next button, activate it, and check again. If focus rings are invisible or if keyboard activation doesn't work, the components need fixing.
Check that each product in the grid has a unique key. Open the browser console (F12). If React warns about missing or duplicate keys, you'll see a yellow warning message. AI frequently generates lists that use array indices as keys (key={index}) instead of unique identifiers (key={product.id}). Index keys cause silent rendering bugs when the list is filtered or reordered, because React loses track of which item is which.
Check: Run the dev server (npm run dev). Click each category filter -- only matching products appear. Tab through the filter buttons -- each is keyboard-selectable. Open a product card's TypeScript file -- props have specific types, not any.