The Tour Favorites List with localStorage
The favorites list lets visitors save tours they are interested in. The saved list survives page reloads — close the tab, come back, and the favorites are still there. This combines everything from Module 07: events, delegation, DOM manipulation, and localStorage.
What it does when complete
Section titled “What it does when complete”- Each tour card has an “Add to Favorites” button
- Clicking it adds the tour to a favorites list shown on the page
- Each favorite has a “Remove” button
- The list persists across page reloads via localStorage
Step 1 — HTML prerequisites
Section titled “Step 1 — HTML prerequisites”Add to Favorites button
Section titled “Add to Favorites button”Every tour card needs a button with class="btn-add-favorite". Add it to the JS-rendered cards by updating renderTourCard in main.js:
function renderTourCard(tour) { const status = tour.available ? 'Available' : 'Sold Out'; return ` <article class="tour-card" data-category="${tour.category}"> <div class="tour-card-image-wrap"> <img class="tour-card-image" src="${tour.img}" alt="${tour.alt}"> </div> <div class="tour-card-body"> <h3>${tour.name}</h3> <p class="tour-price">${formatPrice(tour.price)}</p> <p class="tour-status">${status}</p> <button class="btn-add-favorite">Add to Favorites</button> </div> </article> `;}Also add the button to every hardcoded tour card in index.html and tours.html — place it inside the card’s CTA or footer div, after the existing link:
<!-- index.html — inside .tour-card-cta --><div class="tour-card-cta"> <a href="tours.html" class="btn btn-primary">View Tour</a> <button class="btn-add-favorite">Add to Favorites</button></div>
<!-- tours.html — inside .tour-card-footer --><div class="tour-card-footer"> <div class="tour-price">$65 <span class="tour-price-label">per person</span></div> <a href="contact.html" class="btn btn-primary">Book this tour</a> <button class="btn-add-favorite">Add to Favorites</button></div>Favorites list section
Section titled “Favorites list section”Add a favorites section to index.html and tours.html — place it after the main tours section, before the footer:
<section class="favorites-section section-padding"> <div class="content-wrapper"> <h2>Your Saved Tours</h2> <ul class="favorites-list"></ul> </div></section>The <ul> starts empty — renderFavorites() populates it immediately on page load.
Step 2 — Add CSS
Section titled “Step 2 — Add CSS”Add the following to styles.css:
/* === Favorites === */
.btn-add-favorite { margin-top: var(--space-sm); padding: var(--space-xs) var(--space-md); background: none; border: 2px solid var(--color-primary); color: var(--color-primary); border-radius: var(--radius-sm); cursor: pointer; font-size: 0.875rem; font-weight: 600; transition: background-color 0.2s, color 0.2s;}
.btn-add-favorite:hover { background-color: var(--color-primary); color: var(--color-white);}
.favorites-list { list-style: none; padding: 0; margin: 0;}
.favorites-list li { display: flex; justify-content: space-between; align-items: center; padding: var(--space-sm) 0; border-bottom: 1px solid var(--color-border); font-weight: 600;}
.favorites-empty { color: var(--color-text-muted); font-style: italic; font-weight: 400;}
.btn-remove-favorite { padding: var(--space-xs) var(--space-sm); background: none; border: 1px solid var(--color-error); color: var(--color-error); border-radius: var(--radius-sm); cursor: pointer; font-size: 0.8rem; font-weight: 600; transition: background-color 0.2s, color 0.2s;}
.btn-remove-favorite:hover { background-color: var(--color-error); color: var(--color-white);}The data model
Section titled “The data model”The simplest structure: an array of tour name strings.
// In localStorage, stored as:// '["Cascade Ridge Hike","Summit Loop Trek"]'
// In JavaScript:let favorites = []; // will be populated from localStorage on loadWhen a tour is added, push its name. When removed, filter it out. Persist after every change.
Loading persisted data
Section titled “Loading persisted data”Read from localStorage when the page loads:
const FAVORITES_KEY = 'sto-favorites';
function loadFavorites() { const saved = localStorage.getItem(FAVORITES_KEY); return saved ? JSON.parse(saved) : [];}
let favorites = loadFavorites();Saving and rendering
Section titled “Saving and rendering”Save after every mutation, then re-render the list:
function saveFavorites() { localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites));}
const favoritesList = document.querySelector('.favorites-list');
function renderFavorites() { if (!favoritesList) return; favoritesList.innerHTML = '';
if (favorites.length === 0) { favoritesList.innerHTML = '<li class="favorites-empty">No favorites saved yet.</li>'; return; }
favorites.forEach(name => { const li = document.createElement('li'); li.textContent = name;
const removeBtn = document.createElement('button'); removeBtn.textContent = 'Remove'; removeBtn.dataset.tour = name; removeBtn.className = 'btn-remove-favorite';
li.appendChild(removeBtn); favoritesList.appendChild(li); });}favoritesList.innerHTML = '' clears the current list before re-rendering — simple and reliable.
Adding a favorite via delegation
Section titled “Adding a favorite via delegation”container is already defined earlier in main.js — it is the .tours-grid element used to render tour cards. Register the add-favorite listener on the same element using closest('.btn-add-favorite') as the guard:
if (container) { container.addEventListener('click', (event) => { const btn = event.target.closest('.btn-add-favorite'); if (!btn) return;
const card = btn.closest('.tour-card'); if (!card) return;
const name = card.querySelector('h3').textContent;
if (!favorites.includes(name)) { favorites.push(name); saveFavorites(); renderFavorites(); } });}includes prevents duplicates — if the tour is already in the array, do nothing.
Removing a favorite via delegation
Section titled “Removing a favorite via delegation”One click listener on the favorites list:
if (favoritesList) { favoritesList.addEventListener('click', (event) => { const btn = event.target.closest('.btn-remove-favorite'); if (!btn) return;
favorites = favorites.filter(f => f !== btn.dataset.tour); saveFavorites(); renderFavorites(); });}filter returns a new array with the matching item removed. Reassigning favorites replaces the old array.
Complete feature code
Section titled “Complete feature code”container is already declared in main.js from the tour card rendering section — do not redeclare it. The favorites code adds to the existing if (container) block and introduces its own new variables and functions.
const FAVORITES_KEY = 'sto-favorites';const favoritesList = document.querySelector('.favorites-list');
function loadFavorites() { const saved = localStorage.getItem(FAVORITES_KEY); return saved ? JSON.parse(saved) : [];}
function saveFavorites() { localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites));}
function renderFavorites() { if (!favoritesList) return; favoritesList.innerHTML = '';
if (favorites.length === 0) { favoritesList.innerHTML = '<li class="favorites-empty">No favorites saved yet.</li>'; return; }
favorites.forEach(name => { const li = document.createElement('li'); li.textContent = name;
const removeBtn = document.createElement('button'); removeBtn.textContent = 'Remove'; removeBtn.dataset.tour = name; removeBtn.className = 'btn-remove-favorite';
li.appendChild(removeBtn); favoritesList.appendChild(li); });}
let favorites = loadFavorites();renderFavorites();
if (container) { container.addEventListener('click', (event) => { const btn = event.target.closest('.btn-add-favorite'); if (!btn) return;
const card = btn.closest('.tour-card'); if (!card) return;
const name = card.querySelector('h3').textContent; if (!favorites.includes(name)) { favorites.push(name); saveFavorites(); renderFavorites(); } });}
if (favoritesList) { favoritesList.addEventListener('click', (event) => { const btn = event.target.closest('.btn-remove-favorite'); if (!btn) return;
favorites = favorites.filter(f => f !== btn.dataset.tour); saveFavorites(); renderFavorites(); });}Verify in DevTools
Section titled “Verify in DevTools”- Add a favorite — it appears in the list
- Reload the page — it is still there
- Open DevTools → Application (Chrome) or Storage (Firefox) → Local Storage → your site origin — the key and JSON value are visible
- Remove a favorite — it disappears from both the list and localStorage
- Add all three tours — then reload — all three persist
- Navigate to the tours page — the same favorites appear (requires local server)
- The data model is an array of strings — simple enough to serialize with
JSON.stringify, simple enough to search withincludes. loadFavorites()reads from localStorage with a fallback[]— always returns an array.renderFavorites()rebuilds the DOM from the array on every change — one source of truth.saveFavorites()is called after every mutation — add or remove always triggers a persist.containeris already in scope from the tour rendering section — the add-favorite listener registers on the same element, not a new selection.- Two delegated listeners: one on the tours container for adding, one on the favorites list for removing.
- localStorage is scoped to the origin — on a real server all pages share the same store. With
file://URLs, browsers treat each file as a separate origin, so cross-page persistence only works when served from localhost or a deployed host.