Capstone project
Capstone: a course catalogue
Lesson 20 of 20 · 30 min · Module 6: Timers and the capstone
Bring the whole course together in one realistic page: a catalogue that loads courses from a server, lets people search and filter them, remembers their favourites, and stays accessible and safe throughout.
Start this lesson →Free. No installation: you write and run the code in your browser, and it is checked automatically.
By the end, you’ll be able to…
- Plan a small application around its state
- Combine fetch, templates, events and storage in one page
- Keep a real interface accessible and safe
The idea
Plan the state first: the loaded courses, the search text, the chosen level and the favourite ids. Every event changes that state and calls one render(), which clones the card template for each visible course and updates the counts. Loading and error states, a debounced search, filter buttons with aria-pressed and favourites in localStorage are all techniques you have already practised.
const state = {
courses: [], // from GET /api/catalogue
query: '', // the search text
level: 'all', // 'all', 'Beginner' or 'Intermediate'
favourites: new Set(), // course ids, saved as a JSON array
};
01State first, then features
Write the state object before any listener. Search, filters and favourites then become one-line changes to that object followed by render(), and they combine without extra code: the visible courses are simply the ones that pass every condition.
02Derive, don’t store
The visible list and the counts are never stored; render() works them out from the state each time. Storing them separately would mean keeping two copies in sync, which is where bugs come from.
const visible = state.courses.filter(course =>
(state.level === 'all' || course.level === state.level) &&
course.title.toLowerCase().includes(query));
03The details make it professional
aria-pressed tells screen readers which filter is on, the role="status" line announces results, labels name every favourite button, textContent keeps server data harmless, and try/catch keeps damaged storage from breaking the page.
Your project brief
• Load: show
Loading courses… in #status and fetch /api/catalogue. If that fails, show Could not load the catalogue. and the #retry button, which tries again.• Render: clone
#course-card for every visible course, with data-id on the <li>, the title, level and number of lessons in .title, .level and .lessons, and on .favourite both aria-pressed and an aria-label such as Favourite HTML basics. Show Showing 2 of 6 courses in #status, and show #empty only when nothing matches.• Search: match titles, ignoring case and surrounding spaces, 250 ms after typing stops.
• Levels: a
.filter button chooses the level in its data-level, and only the chosen button has aria-pressed="true".• Favourites:
.favourite adds or removes the course, saved under ct-favourites as a JSON array of the numeric ids, such as [2,5], and restored on load. Damaged data means no favourites.What the checks look for
- A loading message shows while the catalogue loads
- Every course is drawn from the template
- Errors offer a working Try again button
- Search waits for a pause, then filters
- Search ignores case and surrounding spaces
- Level buttons filter and show which one is chosen
- Search and level work together
- Nothing found shows the empty message
- Favourites are toggled, named and saved
- Saved favourites come back
- Damaged favourites are ignored
- Course data is shown as text
Take this with you
One state object, one render function, and small listeners that change state.
Go a little further
Add a Favourites only filter, and sort the courses by title or by number of lessons.