A lightweight saved list (favorites/wishlist) feature using localStorage and jQuery. No backend required. Data persists across browser sessions indefinitely.
Requirements
- jQuery (any recent version)
- Font Awesome 6 (for heart icons)
- Bootstrap 5 (optional, for layout classes used in examples)
Installation
Include saved-list.js in your layout, after jQuery and before your closing </body> tag.
<script src="/layout_bootstrap/js/saved-list.js"></script>How It Works
- Product IDs are stored in
localStorageunder the key_tat_saved_product_idsas a JSON array of strings. - When the saved list changes, a jQuery event
tat:savedlist:changedis triggered onwindow. Any part of the page can listen to this event. - On page load, all save buttons on the page are scanned and their heart icon state is restored from
localStorage. - The navbar badge and icon are also synced on load and on every change.
localStorage Key
All data is stored under a prefixed key to avoid conflicts with other scripts on the same domain.
JavaScript API
The SavedList object is available globally as window.SavedList.
Example Usage
// Get all saved IDs (e.g. to send to your server)
const ids = SavedList.getAll();
// ["42", "87", "103"]
// Check if a product is saved
if (SavedList.has("42")) {
console.log("Product 42 is saved");
}
// Manually toggle a product
const added = SavedList.toggle("42");
console.log(added ? "Added" : "Removed");
// Clear all saved items
SavedList.clear();Events
Listening to changes
$(window).on("tat:savedlist:changed", function(e, ids) {
console.log("Saved list updated:", ids);
});Part 1: Save Button (Product Card)
Add a save button inside each product card. The button must have:
- Class
tat-sl-btn— used by the script to find all buttons on page load - Attribute
data-product-id— the unique ID of the product - An
<i>tag with classtat-sl-heart-{id}— used to update the icon state style="pointer-events: none;"on the<i>tag — prevents the icon from intercepting the click event
HTML Example
<button
type="button"
class="tat-sl-btn"
data-product-id="42"
aria-label="Save this product"
style="z-index: 10; position: absolute; top: 0; right: 0;"
>
<i class="fa-regular fa-heart tat-sl-heart-42" style="pointer-events: none;"></i>
</button>Liquid Template (loop)
{% for p in products %}
<div class="position-relative">
<!-- product image -->
<img src="{{ p.primary_media.original }}" alt="{{ p.title | default: '' }}">
<!-- save button -->
<button
type="button"
class="tat-sl-btn position-absolute top-0 end-0 border-0 bg-transparent"
data-product-id="{{ p.id }}"
aria-label="Save {{ p.title | default: 'product' }}"
style="z-index: 10;"
>
<i class="fa-regular fa-heart fs-3 text-white tat-sl-heart-{{ p.id }}" style="pointer-events: none;"></i>
</button>
<!-- overlay link — must have lower z-index than the button -->
<a href="{{ p.permalink }}" class="position-absolute top-0 start-0 w-100 h-100" style="z-index: 5;"></a>
</div>
{% endfor %}Important: If your card has a full-size overlay <a> tag, the save button must have a higher z-index than the overlay link. Use inline style="z-index" values to avoid conflicts with CSS utility classes that may or may not be defined in your project.
Icon States
To customise the icon classes, edit the tatUpdateHeartIcon function in saved-list.js.
Part 2: Navbar Button
Add this anywhere in your navbar. It shows a heart icon and a badge with the count of saved items. The badge is hidden when the count is zero.
Required IDs
HTML Example
<a href="/saved" title="Saved list">
<i class="fa-regular fa-heart" id="tat-sl-navbar-heart-icon"></i>
<span id="tat-sl-count-badge" class="d-none">0</span>
</a>Navbar Icon States
To customise icon classes or badge style, edit the tatSyncNavbar function in saved-list.js.
Part 3: Saved List Page
The saved list page is intentionally not included in saved-list.js. It lives only on the saved list page itself. It reads IDs from SavedList.getAll(), fetches product data from the API, and renders the cards.
API Endpoint
Replace [AGENCY TOKEN] in the script below with your actual token.
Required HTML elements on the page
HTML Example
<p>You have <span id="saved-list-count">0</span> saved resort(s).</p>
<p id="saved-empty-msg"></p>
<div class="row g-4" id="saved-list-container"></div>Page Script
Place this script at the bottom of the saved list page, after saved-list.js.
document.addEventListener('DOMContentLoaded', () => {
const ids = SavedList.getAll();
const container = document.getElementById('saved-list-container');
const emptyMsg = document.getElementById('saved-empty-msg');
document.getElementById('saved-list-count').innerText = ids.length;
if (!ids.length) {
emptyMsg.textContent = 'No favorite resorts at the moment.';
return;
}
if (emptyMsg) emptyMsg.remove();
const authHeader = 'Basic ' + btoa('agency:[AGENCY TOKEN]');
fetch(`https://api.gttwl.net/post?page_size=100&ids=${ids.join(',')}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': authHeader
}
})
.then(r => r.json())
.then(({ data: { entries: products } }) => {
container.innerHTML = products.map(p => {
const price = p?.custom_fields?.price_category;
const resortSize = p?.custom_fields?.resort_size;
const weddingSize = p?.custom_fields?.wedding_size;
return `
<div class="col-lg-6 col-xl-4 saved-card-item" data-aos="fade-up">
<div class="bg-white rounded-4 overflow-hidden position-relative shadow h-100">
<div class="position-relative">
<img src="${p?.primary_media?.large || ''}" class="w-100" alt="${p?.title || ''}">
</div>
<div class="px-3 pb-5 pt-3">
<h5 class="lh-sm fs-4 text-primary">${p?.title || ''}</h5>
<div class="d-flex justify-content-between position-absolute bottom-0 start-0 w-100 px-3 pb-3 fs-tiny gap-4">
<div class="d-flex flex-row">
${price && price.length > 1 ? `
<div class="d-flex align-items-center lh-sm me-3">
<i class="fa-solid fa-dollar-sign"></i>
<span class="ms-1">${price}</span>
</div>` : ''}
${resortSize && resortSize.length > 1 ? `
<div class="d-flex align-items-center lh-sm me-3">
<i class="fa-solid fa-bed"></i>
<span class="ms-1">${resortSize}</span>
</div>` : ''}
${weddingSize && weddingSize.length > 1 ? `
<div class="d-flex align-items-center lh-sm me-3" title="wedding group">
<i class="fa-solid fa-users"></i>
<span class="ms-1">2 - ${weddingSize}</span>
</div>` : ''}
<div class="d-flex align-items-center lh-sm" title="accessible">
<i class="fa-solid fa-wheelchair"></i>
</div>
</div>
<button class="btn btn-sm btn-outline-danger" style="z-index: 10;" onclick="removeSaved('${p.id}', this)">
<i class="fa-solid fa-trash" style="pointer-events: none;"></i>
</button>
</div>
</div>
<a href="${p?.permalink || '#'}" class="position-absolute top-0 start-0 w-100 h-100" style="z-index: 5;"></a>
</div>
</div>
`;
}).join('');
})
.catch(err => {
console.error('Fetch error:', err);
container.innerHTML = '<p class="text-danger">Failed to load saved items.</p>';
});
});
function removeSaved(id, btn) {
SavedList.toggle(id);
// Remove the card column wrapper
const card = btn.closest('.saved-card-item');
if (card) card.remove();
// Update count display
document.getElementById('saved-list-count').innerText = SavedList.count();
// Show empty message if no cards remain
if (!document.querySelectorAll('#saved-list-container .saved-card-item').length) {
document.getElementById('saved-list-container').innerHTML =
'<p class="text-muted">No saved items.</p>';
}
}Notes
- The
removeSavedfunction callsSavedList.toggle(id)which also triggerstat:savedlist:changed, so the navbar badge updates automatically. - The remove button uses
style="z-index: 10;"and the overlay link usesstyle="z-index: 5;"so the button stays clickable above the card overlay. - The
<i>inside the remove button hasstyle="pointer-events: none;"so clicks always land on the button element, not the icon. - The API returns up to 100 results per call (
page_size=100). If a user saves more than 100 items, only the first 100 will be returned.
Global Functions Reference
These functions are exposed on window and can be called from anywhere.
DOM IDs and Classes Reference
Troubleshooting
Heart button not clickable
- If your card has a full-size overlay
<a>tag, the button'sz-indexmust be higher than the overlay. Use inlinestyle="z-index: 10;"on the button andstyle="z-index: 5;"on the overlay link. - Make sure the
<i>icon hasstyle="pointer-events: none;"so clicks pass through to the button and not the icon.
Icon state not restored on page load
- Make sure
saved-list.jsis loaded after jQuery. - Make sure the button has class
tat-sl-btnand attributedata-product-id. - Make sure the icon has class
tat-sl-heart-{id}matching the same product ID value.
Navbar badge not showing
- Make sure
id="tat-sl-navbar-heart-icon"andid="tat-sl-count-badge"exist in the DOM when the page loads. - The badge must start with class
d-none. The script removes this class when count is greater than zero.
Saved data lost after browser close
- This should not happen with
localStorage. Check that the user is not browsing in private or incognito mode —localStoragedoes not persist after the session ends in private mode.