7.
Completing the Static Catalog
Written by Eli Ganim
In the previous chapter, the Learning Tracker got its face: a responsive card grid, color-coded badges, styled buttons with honest focus states — all sitting on semantic, accessible markup. Between the data model, the component set and the styling, you’ve now touched every layer of a static React feature.
This chapter finishes the job. Not with a pile of new concepts, but with the professional pass that separates “works on my machine” from “ready to hand to a teammate.” You’ll reorganize the src folder so every file has an obvious home, extract the last two components the catalog deserves, add course categories, sweep out the remains of the Vite template and run a final quality audit.
Consolidation chapters are where knowledge settles. Everything you do here uses skills from Chapters 2 through 6 — so if any step feels foggy, that’s a signal worth following back to its chapter. By the end, Part I is complete: a static catalog you could genuinely ship, structured so that Part II can build on it without a demolition phase.
Surveying the Target
Professionals start a refactor by fixing the destination, so here’s yours — the completed static catalog:
Squint, and it looks like your current app, with one visible addition: a small category label above each course title. The bigger changes are the ones a screenshot can’t show. Here’s the component tree you’re heading for:
Two new components appear. CourseList takes over the job App has been doing inline — rendering the card grid — and also owns the decision of what to show when the list is empty, delegating that to a small EmptyState. App shrinks to what it should be: the page’s composition root.
And here’s where every file will live:
Note: Every file in this structure is a module — it declares what it depends on with imports, and what it offers with exports. Folders add no behavior; they’re purely for humans, so the bar for creating one is “does this make things easier to find?” Some codebases add index.ts files that re-export a folder’s contents to shorten import paths. That’s a real pattern you’ll meet in the wild, but it’s extra plumbing this project doesn’t need — so it stays out, and every import names its file directly.
Resist the urge to go further — a hooks folder with nothing in it, a utils folder “for later.” Structure should trail the code, not lead it. Three folders is what this app needs today.
Carving Out the Folders
Time to move house, in small steps that keep the app compiling. First, the types. Create a folder src/types, and in it a file course.ts. Move both type declarations — CourseLevel and Course — out of src/courses.ts and into the new file, so it contains exactly:
export type CourseLevel =
| 'Beginner'
| 'Intermediate'
| 'Advanced'
export type Course = {
id: string
title: string
description: string
level: CourseLevel
durationHours?: number
isFavorite: boolean
}
The shared contracts now have a single, findable source — that’s the consolidation the whole chapter is named for. Three files still expect these types in the old place. Fix them one by one. In src/courses.ts, add an import at the top:
import type { Course } from './types/course.ts'
The data file now borrows its own shape from the types module — a one-way dependency, data on types, never the reverse. Point the existing type imports in src/LevelBadge.tsx and src/CourseCard.tsx at './types/course.ts' as well. Save everything — the app compiles and looks unchanged.
Next, the data. Create src/data, and move courses.ts into it. The file’s own import now genuinely needs to climb out of data before descending into types — that’s what .. does, one folder up:
import type { Course } from '../types/course.ts'
App.tsx consumes the courses, so update its two imports:
import { courses } from './data/courses.ts'
import type { CourseLevel } from './types/course.ts'
Save — compiling, unchanged. Last and largest: Create src/components and move all five component files into it — Button.tsx, Card.tsx, CourseCard.tsx, LevelBadge.tsx and PageHeader.tsx. Then fix the paths that broke. Components importing components — like CourseCard importing Card — moved together, so their ./ imports survive. What breaks is everything that crosses a folder boundary. In src/components/PageHeader.tsx, the logo now lives one level up:
import logo from '../assets/logo.svg'
The type imports in src/components/LevelBadge.tsx and src/components/CourseCard.tsx need the same climb — change their paths to '../types/course.ts'. And in src/App.tsx, the component imports gain a folder:
import PageHeader from './components/PageHeader.tsx'
import CourseCard from './components/CourseCard.tsx'
Save everything, and build and run: The page hasn’t changed a pixel, which is the entire point — you’ve moved seven files and rewired their connections without the running app noticing. If anything is red, the error names the file and the path it can’t find; that’s your map.
Note: This project’s state right now — reorganized, nothing else — is saved as the checkpoint folder in this chapter’s materials. If your imports got tangled, compare or restart from there.
Finishing the Component Set
App still hand-rolls the card grid and the empty-state message. That’s list-presentation logic, and it deserves a component of its own. Start with the smaller piece. Create src/components/EmptyState.tsx:
type EmptyStateProps = {
message: string
}
function EmptyState({ message }: EmptyStateProps) {
return <p className="empty-note">{message}</p>
}
export default EmptyState
Almost embarrassingly simple — one prop in, one paragraph out. That’s fine: This component exists to give “nothing to show” a name and a home, so that when the app later needs a friendlier empty state — an illustration, a call-to-action button — there’s exactly one place to build it.
Now the list itself. Create src/components/CourseList.tsx:
import CourseCard from './CourseCard.tsx'
import EmptyState from './EmptyState.tsx'
import type { Course } from '../types/course.ts'
type CourseListProps = {
courses: Course[]
}
function CourseList({ courses }: CourseListProps) {
if (courses.length === 0) {
return (
<EmptyState message="No courses match this level yet." />
)
}
return (
<section className="catalog" aria-label="Course catalog">
{courses.map((course) => (
<CourseCard key={course.id} course={course} />
))}
</section>
)
}
export default CourseList
One new move here: two return statements. When the list is empty, the function returns the empty state and never reaches the grid; otherwise it skips straight past. This early return is the natural upgrade from Chapter 5’s && once the two outcomes are whole trees rather than a line — each return reads as its own complete story. The section, its label and the key logic all moved here from App intact — with one deliberate behavior change: When nothing matches, the component now renders only the message. Chapter 6’s version kept an empty grid section on the page next to the note; this version swaps in the empty state entirely, which is exactly the kind of decision a list component should own.
Time for App to enjoy its retirement from list management. In src/App.tsx, swap the CourseCard import for the new component:
import CourseList from './components/CourseList.tsx'
Then replace everything between </p> — the featured banner’s closing tag — and </main> with one line, so the JSX inside main ends with:
<CourseList courses={visibleCourses} />
App now reads as a table of contents again: a header, a banner, a list, with the filtering logic above feeding it. Save, and build and run: The page is unchanged in all three states — the grid and the empty message just come from CourseList now. You’ll verify that properly in a moment.
Adding Course Categories
The catalog’s last missing feature: Courses belong to subject areas, and the card should say so. By now this is a drill you know — extend the type, satisfy the data, render the field, style it.
In src/types/course.ts, add a category union above Course:
export type CourseCategory = 'Frontend' | 'Languages' | 'Design'
Three subject areas, same closed-union idea as the levels — and deliberately no more machinery than that. When every type is three lines, resist upgrading them into elaborate hierarchies; small flat types are the ones that survive contact with change. Now add the field to the Course shape, below description:
category: CourseCategory
TypeScript immediately demands a category for all four courses — the contract doing your project management. In src/data/courses.ts, add one line to each course, below its title:
category: 'Frontend',
React Basics and CSS Layout are 'Frontend', Modern TypeScript is 'Languages' and Web Accessibility is 'Design'. Next, render it. In src/components/CourseCard.tsx, add an eyebrow line at the top of the card, above the h2:
<p className="category">{course.category}</p>
An “eyebrow” is that small label above a heading — publishers’ jargon worth having. Finally, style it at the bottom of src/App.css:
.category {
margin: 0;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #6a7381;
}
Small, uppercase, quiet gray — informative without shouting over the title. Save and check the browser:
Four cards, four eyebrows — and because the field is union-typed, there’s no card the catalog could ever render without one.
Testing the Three States
A feature pass touched the list pipeline, so re-run the manual test from Chapter 5 — full, filtered, empty. The full state is on your screen. For filtered, set levelFilter in src/App.tsx to 'Beginner' and save:
The pipeline holds: filter feeds CourseList, CourseList renders the grid, and the two beginner cards arrive with every feature intact. For empty, set it to 'Advanced':
The message now renders through EmptyState via CourseList’s early return — same pixels as Chapter 5, better address. Set levelFilter back to 'All'.
Sweeping Out the Template
Every generated project carries scaffolding that shipped for the sake of the demo page you deleted in Chapter 1. Time to clear it — but never delete blind. The habit to build: Search for references first. Press Cmd-Shift-F (Ctrl-Shift-F on Windows/Linux) to search the whole project for each file’s name; if nothing imports or links it, it can go. Four files fail that test:
- src/assets/react.svg and src/assets/vite.svg — the spinning logos from the template’s demo page. Nothing has referenced them since your Chapter 2 rewrite. Delete both.
- src/assets/hero.png — the template demo’s hero image. Same story. Delete it.
- public/icons.svg — an icon set the template’s demo linked to. Your search finds no takers. Delete it.
Note the survivor: public/favicon.svg is referenced from index.html — it’s the icon in your browser tab. It stays. And while you’re in index.html, one last piece of polish. The title element still holds the project’s package name; make it human:
<title>Learning Tracker</title>
Save and check the browser: Expect no change in the page itself — everything you deleted was already unused. The one visible difference sits above the page: The browser tab now reads “Learning Tracker”, the app introducing itself properly.
Running the Final Audit
Close Part I the way you’d close a real feature — with the quality pass from Chapter 6, compressed into a minute.
- Keyboard: Tab through the page — every button reachable, focus ring visible throughout.
-
Landmarks: banner
header,mainand the named “Course catalog” region — all still in place after the reorganization; moving files changed no markup. -
Headings: one
h1, then anh2per card. The category eyebrow is a paragraph, not a heading, so the outline stays clean. - States: full, filtered and empty — all three verified above.
- Build: no TypeScript errors, no console warnings.
All green. Build and run one last time and put your result next to the target from the start of the chapter:
A perfect match. That audit wasn’t a formality — it’s the difference between believing the catalog is done and knowing it.
Challenge: Launch the Career Track
The catalog’s owners return with expansion plans: a career-skills category. Ship it end to end:
- Extend
CourseCategorywith'Career'. - Add a course to the data — Tech Interview Prep, “Practice the questions that get you hired.”, category
'Career', level'Intermediate', 5 hours, not a favorite, id'tech-interviews'. - Verify the three states again, and confirm the new card’s eyebrow, badge and meter all render correctly.
Two hints:
- A union member is one more
|arm; once it exists, TypeScript will accept — and spell-check — the new category everywhere. - If the new card looks wrong, read the card top to bottom against its object: Each line of JSX maps to one property.
You’ll find a complete solution in the challenge folder of this chapter’s materials.
Key Points
- Organize by role: Components render, data holds content, types define shape — and structure should trail the code, never outrun it.
- Every file is a module; imports are its declared dependencies, and
../climbs one folder before descending. - Consolidate shared types into one source — like types/course.ts — instead of scattering or duplicating shapes.
- Skip re-export index files until import paths actually hurt; every layer of plumbing must earn its place.
- An early return cleanly splits a component that renders two entirely different trees — the grown-up sibling of
&&. - Give empty states a named component; “nothing to show” is a feature, and features deserve a home.
- Extending a domain type makes TypeScript enumerate every place the data must catch up — the compiler as project manager.
- Never delete a file without searching for references first, and clean template leftovers from generated projects.
- Close features with an audit pass: keyboard, landmarks, headings, data states and a clean build.
Where to Go From Here?
Take a second to appreciate the distance. Seven chapters ago, this was npm create vite and a blinking cursor. Now it’s a typed, componentized, accessible, organized catalog — static by design, and complete on those terms. That’s Part I of this book, and Part I of every React app: Get the rendering story right before anything moves.
Part II makes it move. The “Add to favorites” buttons have been printing console messages long enough — in the next chapter, clicks start changing what’s on screen. That means events and, at long last, the concept Chapter 2 could only wave at: state, the memory that lets your app change while it runs. The static era ends here.