Life Style Products

<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
<title>Fieldstock — General Goods Catalogue, Vol. 04</title>
<link rel=”preconnect” href=”https://fonts.googleapis.com“>
<link rel=”preconnect” href=”https://fonts.gstatic.com” crossorigin>
<link href=”https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap” rel=”stylesheet”>
<style>
/* ———- tokens ———- */
:root{
–bg:#E2DCCB;
–paper:#FAF7F0;
–ink:#1E2A22;
–moss:#56684A;
–ochre:#C68A2E;
–rust:#A9512F;
–line:#C7BFA9;
–shadow:0 6px 18px rgba(30,42,34,0.12);
–radius:6px;
–container:1280px;
}
@media (prefers-reduced-motion: reduce){
*{ animation-duration:0.01ms !important; transition-duration:0.01ms !important; }
}
*{ box-sizing:border-box; }
html,body{ margin:0; padding:0; }
body{
background:var(–bg);
background-image:repeating-linear-gradient(135deg, rgba(30,42,34,0.025) 0 1px, transparent 1px 7px);
color:var(–ink);
font-family:’Inter’, sans-serif;
-webkit-font-smoothing:antialiased;
}
img,svg{ display:block; max-width:100%; }
button, input, select{ font-family:inherit; }
a{ color:inherit; }
*:focus-visible{ outline:2px solid var(–ochre); outline-offset:2px; border-radius:2px; }
.wrap{ max-width:var(–container); margin:0 auto; padding:0 28px; }
/* ———- header ———- */
.site-header{
display:flex; align-items:center; justify-content:space-between; gap:20px;
flex-wrap:wrap;
padding:22px 28px;
max-width:var(–container); margin:0 auto;
}
.brand{ display:flex; flex-direction:column; line-height:1; }
.brand-mark{
font-family:’Bricolage Grotesque’, sans-serif; font-weight:800;
font-size:26px; letter-spacing:0.01em;
}
.brand-sub{
font-family:’Space Mono’, monospace; font-size:11px; letter-spacing:.08em;
text-transform:uppercase; color:var(–moss); margin-top:4px;
}
.header-controls{ display:flex; align-items:center; gap:14px; flex:1; justify-content:flex-end; min-width:240px; }
#searchInput{
width:100%; max-width:280px;
border:1px solid var(–line); background:var(–paper);
padding:9px 13px; border-radius:20px; font-size:13.5px; color:var(–ink);
}
#searchInput::placeholder{ color:#8A8270; }
.bag-btn{
position:relative; display:flex; align-items:center; justify-content:center;
width:40px; height:40px; border-radius:50%; border:1px solid var(–ink);
background:var(–paper); cursor:pointer; flex:none;
}
.bag-icon{ width:18px; height:18px; stroke:var(–ink); fill:none; stroke-width:1.6; stroke-linecap:round; stroke-linejoin:round; }
.bag-count{
position:absolute; top:-6px; right:-6px; background:var(–rust); color:var(–paper);
font-family:’Space Mono’,monospace; font-size:10.5px; min-width:18px; height:18px;
border-radius:50%; display:flex; align-items:center; justify-content:center; padding:0 2px;
}
/* ———- hero ———- */
.hero{
position:relative;
max-width:var(–container); margin:10px auto 0; padding:36px 28px 30px;
}
.hero-eyebrow{
font-family:’Space Mono’, monospace; font-size:12px; letter-spacing:.1em; text-transform:uppercase;
color:var(–moss); margin:0 0 10px;
}
.hero-title{
font-family:’Bricolage Grotesque’, sans-serif; font-weight:700;
font-size:clamp(38px, 6vw, 72px); line-height:1.02; margin:0 0 18px; max-width:13ch;
}
.hero-copy{
font-size:16px; line-height:1.6; color:var(–ink); opacity:.85; max-width:520px; margin:0 0 26px;
}
.hero-stamp{
position:absolute; top:30px; right:28px;
width:96px; height:96px; border-radius:50%;
border:1.5px dashed var(–ink); opacity:.55;
display:flex; flex-direction:column; align-items:center; justify-content:center;
font-family:’Space Mono’, monospace; font-size:11px; letter-spacing:.08em;
transform:rotate(-8deg);
}
.hero-legend{
display:flex; flex-wrap:wrap; gap:6px 16px;
font-family:’Space Mono’, monospace; font-size:11.5px; letter-spacing:.02em;
color:var(–moss); border-top:1px solid var(–line); padding-top:16px;
}
/* ———- toolbar ———- */
.toolbar{
position:sticky; top:0; z-index:20;
background:rgba(226,220,203,0.92); backdrop-filter:blur(6px);
border-top:1px solid var(–line); border-bottom:1px solid var(–line);
padding:14px 0;
}
.toolbar-inner{
max-width:var(–container); margin:0 auto; padding:0 28px;
display:flex; align-items:center; gap:18px; justify-content:space-between; flex-wrap:wrap;
}
.pills{
display:flex; gap:8px; overflow-x:auto; padding-bottom:2px; flex:1; min-width:0;
scrollbar-width:none;
}
.pills::-webkit-scrollbar{ display:none; }
.pill{
flex:none; font-family:’Space Mono’, monospace; font-size:12px; letter-spacing:.02em;
white-space:nowrap; padding:7px 14px; border-radius:20px; border:1px solid var(–line);
background:var(–paper); color:var(–ink); cursor:pointer; transition:background .15s, color .15s, border-color .15s;
}
.pill:hover{ border-color:var(–ink); }
.pill.active{ background:var(–ink); color:var(–paper); border-color:var(–ink); }
.toolbar-controls{ display:flex; align-items:center; gap:14px; flex:none; }
.showing{ font-family:’Space Mono’, monospace; font-size:11.5px; color:var(–moss); white-space:nowrap; }
#sortSelect{
border:1px solid var(–line); background:var(–paper); border-radius:20px;
padding:7px 12px; font-size:12.5px; color:var(–ink); cursor:pointer;
}
/* ———- catalogue grid ———- */
.catalogue{ max-width:var(–container); margin:0 auto; padding:32px 28px 10px; }
.grid{
display:grid; grid-template-columns:repeat(auto-fill, minmax(218px, 1fr)); gap:20px;
}
.card{
background:var(–paper); border:1px solid var(–line); border-radius:var(–radius);
display:flex; flex-direction:column; overflow:hidden;
transition:transform .18s ease, box-shadow .18s ease;
}
.card:hover{ transform:translateY(-3px); box-shadow:var(–shadow); }
.swatch{
position:relative; aspect-ratio:1/1; display:flex; align-items:center; justify-content:center;
overflow:hidden;
}
.swatch svg{ width:52px; height:52px; stroke:var(–ink); opacity:.5; fill:none; stroke-width:1.4; stroke-linecap:round; stroke-linejoin:round; }
.tag{
position:absolute; top:10px; right:-6px;
background:var(–paper); border:1px solid var(–line);
padding:6px 11px 5px; transform:rotate(-4deg);
box-shadow:0 3px 6px rgba(0,0,0,.18);
display:flex; flex-direction:column; align-items:center;
font-family:’Space Mono’, monospace; font-size:11px; line-height:1.35;
}
.tag-code{ color:var(–moss); letter-spacing:.03em; }
.tag-rule{ width:100%; border-top:1px dashed var(–line); margin:3px 0; }
.tag-price{ font-weight:700; color:var(–ink); font-size:13px; }
.card-body{ padding:14px 14px 16px; display:flex; flex-direction:column; gap:6px; flex:1; }
.eyebrow{
font-family:’Space Mono’, monospace; font-size:10.5px; letter-spacing:.08em;
text-transform:uppercase; color:var(–moss); margin:0;
}
.card-name{
font-family:’Bricolage Grotesque’, sans-serif; font-weight:600; font-size:16.5px;
line-height:1.25; margin:0; color:var(–ink);
}
.card-meta{
margin-top:auto; display:flex; align-items:center; justify-content:space-between;
gap:8px; padding-top:8px;
}
.rating{ font-size:12.5px; color:var(–ink); opacity:.8; }
.reviews{ color:var(–moss); opacity:.9; }
.add-btn{
font-family:’Space Mono’, monospace; font-size:11.5px; border:1px solid var(–ink);
background:transparent; color:var(–ink); padding:6px 11px; border-radius:20px;
cursor:pointer; transition:background .15s, color .15s, border-color .15s; flex:none;
}
.add-btn:hover{ background:var(–ink); color:var(–paper); }
.add-btn.added{ background:var(–moss); border-color:var(–moss); color:var(–paper); }
/* ———- load more / footer ———- */
.load-more-wrap{ display:flex; flex-direction:column; align-items:center; gap:10px; padding:38px 0 10px; }
.load-more{
font-family:’Space Mono’, monospace; font-size:13px; letter-spacing:.02em;
border:1px solid var(–ink); background:var(–paper); color:var(–ink);
padding:11px 26px; border-radius:24px; cursor:pointer; transition:background .15s, color .15s;
}
.load-more:hover{ background:var(–ink); color:var(–paper); }
.load-more[hidden]{ display:none; }
.end-note{ font-family:’Space Mono’, monospace; font-size:12px; color:var(–moss); }
.site-footer{ border-top:1px solid var(–line); margin-top:30px; padding:36px 28px 28px; }
.footer-grid{
max-width:var(–container); margin:0 auto; display:grid;
grid-template-columns:2fr 1fr; gap:36px;
}
.footer-mark{ font-family:’Bricolage Grotesque’, sans-serif; font-weight:700; font-size:18px; margin:0 0 8px; }
.footer-label{ font-family:’Space Mono’, monospace; font-size:11px; letter-spacing:.08em; text-transform:uppercase; color:var(–moss); margin:0 0 8px; }
.footer-copy{ font-size:13.5px; line-height:1.6; opacity:.8; max-width:42ch; margin:0; }
.footer-fine{
max-width:var(–container); margin:26px auto 0; padding-top:18px; border-top:1px solid var(–line);
font-family:’Space Mono’, monospace; font-size:11px; color:var(–moss);
}
/* ———- toast ———- */
.toast{
position:fixed; left:50%; bottom:26px; transform:translate(-50%, 16px);
background:var(–ink); color:var(–paper); font-size:13px;
padding:11px 20px; border-radius:24px; box-shadow:var(–shadow);
opacity:0; pointer-events:none; transition:opacity .2s ease, transform .2s ease;
z-index:50; white-space:nowrap;
}
.toast.show{ opacity:1; transform:translate(-50%, 0); }
/* ———- responsive ———- */
@media (max-width:760px){
.hero-stamp{ display:none; }
.footer-grid{ grid-template-columns:1fr; gap:20px; }
.header-controls{ justify-content:space-between; }
#searchInput{ max-width:none; }
}
@media (max-width:480px){
.grid{ grid-template-columns:repeat(auto-fill, minmax(150px,1fr)); gap:14px; }
.hero-title{ font-size:34px; }
}
</style>
</head>
<body>
<header class=”site-header”>
<div class=”brand”>
<span class=”brand-mark”>FIELDSTOCK</span>
<span class=”brand-sub”>General Goods Co.</span>
</div>
<div class=”header-controls”>
<input type=”search” id=”searchInput” placeholder=”Search the catalogue…” aria-label=”Search products”>
<button class=”bag-btn” id=”bagBtn” aria-label=”View bag”>
<svg class=”bag-icon” viewBox=”0 0 24 24″><path d=”M6 8h12l-1 12H7L6 8Z”/><path d=”M9 8a3 3 0 0 1 6 0″/></svg>
<span class=”bag-count” id=”bagCount”>0</span>
</button>
</div>
</header>
<section class=”hero”>
<p class=”hero-eyebrow”>Volume 04 — Summer Issue</p>
<h1 class=”hero-title”>200 Goods<br>for Daily Life</h1>
<p class=”hero-copy”>A working catalogue of objects we keep coming back to — for the kitchen, the trail, the desk, and the quiet hours in between. Every item below is numbered, priced, and ready to add to your bag.</p>
<div class=”hero-stamp” aria-hidden=”true”><span>EST.</span><span>2024</span></div>
<div class=”hero-legend” id=”heroLegend”></div>
</section>
<div class=”toolbar”>
<div class=”toolbar-inner”>
<div class=”pills” id=”pills”></div>
<div class=”toolbar-controls”>
<span class=”showing” id=”showingText”></span>
<select id=”sortSelect” aria-label=”Sort products”>
<option value=”featured”>Featured</option>
<option value=”price-asc”>Price: Low to High</option>
<option value=”price-desc”>Price: High to Low</option>
<option value=”rating-desc”>Rating: High to Low</option>
</select>
</div>
</div>
</div>
<main class=”catalogue”>
<div class=”grid” id=”grid”></div>
<div class=”load-more-wrap”>
<button class=”load-more” id=”loadMoreBtn”>Load 24 More</button>
<p class=”end-note” id=”endNote” hidden>— end of catalogue —</p>
</div>
</main>
<footer class=”site-footer”>
<div class=”footer-grid”>
<div>
<p class=”footer-mark”>FIELDSTOCK</p>
<p class=”footer-copy”>A general goods catalogue, printed in spirit and browsed in pixels. Items 001 through 200 reflect this edition’s full run — once it’s added to your bag, it stays on the shelf for next time.</p>
</div>
<div>
<p class=”footer-label”>Numbering</p>
<p class=”footer-copy”>Each tag’s catalogue number is fixed to its item for this edition, regardless of how you sort or search.</p>
</div>
</div>
<p class=”footer-fine”>Fieldstock General Goods Co. — demonstration catalogue, Vol. 04.</p>
</footer>
<div class=”toast” id=”toast” role=”status” aria-live=”polite”></div>
<script>
/* ———- data ———- */
const CATS = [
{ key:’home’, name:’Home & Living’, icon:’lamp’, priceRange:[28,168],
swatches:[‘#D9CDB8′,’#C7B79A’,’#E3D9C5′],
nouns:[‘Table Lamp’,’Wool Throw’,’Ceramic Vase’,’Floor Mirror’,’Wall Clock’,’Storage Basket’,’Linen Curtain’,’Candle Holder’,’Bookend Set’,’Area Rug’],
adjectives:[‘Hand-Thrown’,’Brushed Brass’,’Minimal’,’Woven’,’Matte’,’Reclaimed Oak’,’Sculpted’,’Soft-Touch’] },
{ key:’kitchen’, name:’Kitchen’, icon:’mug’, priceRange:[14,128],
swatches:[‘#C97B4A’,’#E0B084′,’#B5562B’],
nouns:[‘Coffee Mug’,’Cutting Board’,’Tea Kettle’,’Mixing Bowl’,’Knife Set’,’Pour-Over Carafe’,’Spice Jar Set’,’Dish Towel’,’Cast Iron Pan’,’Serving Tray’],
adjectives:[‘Stoneware’,’Acacia Wood’,’Enamel’,’Hand-Glazed’,’Double-Wall’,’Recycled Glass’,’Slow-Drip’,’Heritage’] },
{ key:’wellness’, name:’Wellness’, icon:’leaf’, priceRange:[16,96],
swatches:[‘#9CAE8C’,’#C9D6B8′,’#7E9070′],
nouns:[‘Yoga Mat’,’Diffuser’,’Meditation Cushion’,’Bath Salts’,’Massage Roller’,’Sleep Mask’,’Herbal Tea Tin’,’Weighted Blanket’,’Foam Roller’,’Aromatherapy Set’],
adjectives:[‘Organic’,’Calming’,’Restorative’,’Plant-Based’,’Slow’,’Mineral-Rich’,’Natural’,’Grounding’] },
{ key:’outdoor’, name:’Outdoor’, icon:’tent’, priceRange:[22,188],
swatches:[‘#6B7A4F’,’#8C5A3C’,’#4F5D3A’],
nouns:[‘Camp Chair’,’Trail Backpack’,’Insulated Bottle’,’Hammock’,’Lantern’,’Picnic Blanket’,’Pocket Knife’,’Hiking Socks’,’Cooler Bag’,’Trail Map Tin’],
adjectives:[‘All-Weather’,’Packable’,’Rugged’,’Field-Tested’,’Lightweight’,’Canvas’,’Expedition’,’Trailhead’] },
{ key:’desk’, name:’Desk & Stationery’, icon:’pencil’, priceRange:[9,86],
swatches:[‘#B7AE9B’,’#8E8470′,’#D6CDB8′],
nouns:[‘Notebook’,’Fountain Pen’,’Desk Organizer’,’Letterpress Cards’,’Pencil Tin’,’Desk Mat’,’Bookmark Set’,’Wax Seal Kit’,’Wall Calendar’,’Paper Weight’],
adjectives:[‘Letterpress’,’Refillable’,’Brass’,’Cotton-Rag’,’Hand-Bound’,’Recycled’,’Archival’,’Pocket’] },
{ key:’bath’, name:’Bath & Body’, icon:’drop’, priceRange:[8,64],
swatches:[‘#CDB7C9′,’#A9C2C9′,’#D8C4A8’],
nouns:[‘Bar Soap’,’Body Oil’,’Clay Mask’,’Bath Brush’,’Lip Balm’,’Hand Cream’,’Salt Scrub’,’Shower Steamers’,’Body Brush’,’Cotton Robe’],
adjectives:[‘Cold-Pressed’,’Botanical’,’Unscented’,’Mineral’,’Small-Batch’,’Nourishing’,’Sun-Dried’,’Everyday’] },
{ key:’fashion’, name:’Fashion’, icon:’shirt’, priceRange:[18,228],
swatches:[‘#5B6B66′,’#8A7A6B’,’#3F4A45′],
nouns:[‘Wool Scarf’,’Canvas Tote’,’Leather Belt’,’Knit Beanie’,’Linen Shirt’,’Field Jacket’,’Crew Socks’,’Selvedge Denim’,’Pocket Square’,’Suede Loafer’],
adjectives:[‘Heavyweight’,’Heritage’,’Vegetable-Tanned’,’Brushed’,’Garment-Dyed’,’Classic’,’Unlined’,’Workwear’] },
{ key:’tech’, name:’Tech Accessories’, icon:’plug’, priceRange:[16,148],
swatches:[‘#3D4044′,’#5C6166′,’#23262A’],
nouns:[‘Charging Dock’,’Cable Organizer’,’Laptop Sleeve’,’Wireless Speaker’,’Phone Stand’,’Desk Hub’,’Travel Adapter’,’Earbud Case’,’Tablet Stand’,’Power Bank’],
adjectives:[‘Anodized’,’Minimal’,’Felt-Lined’,’Modular’,’Compact’,’Matte-Finish’,’Foldable’,’All-in-One’] }
];
const ICONS = {
lamp:'<path d=”M8 3h8l3 7H5l3-7Z”/><line x1=”12″ y1=”10″ x2=”12″ y2=”19″/><line x1=”8″ y1=”21″ x2=”16″ y2=”21″/>’,
mug:'<path d=”M5 8h11v8a4 4 0 0 1-4 4H9a4 4 0 0 1-4-4V8Z”/><path d=”M16 10h2a2 2 0 0 1 0 4h-2″/>’,
leaf:'<path d=”M5 19c0-7 4-13 14-14-1 10-7 14-14 14Z”/><path d=”M5 19c2-4 5-7 9-9″/>’,
tent:'<path d=”M3 20 12 5l9 15Z”/><path d=”M12 5v15″/>’,
pencil:'<path d=”M4 20l1-4L16 5l3 3L8 19l-4 1Z”/><path d=”M14 7l3 3″/>’,
drop:'<path d=”M12 3c4 5 7 8.5 7 12a7 7 0 0 1-14 0c0-3.5 3-7 7-12Z”/>’,
shirt:'<path d=”M8 4 4 7l2 3 2-1v11h8V9l2 1 2-3-4-3-2 2h-4L8 4Z”/>’,
plug:'<path d=”M9 2v6M15 2v6M7 8h10v4a5 5 0 0 1-10 0V8Z”/><path d=”M12 17v5″/>’
};
function getIcon(key){
return ‘<svg viewBox=”0 0 24 24″>’ + (ICONS[key] || ”) + ‘</svg>’;
}
/* seeded PRNG so the catalogue is stable across reloads */
function seededRandom(seed){
let t = seed += 0x6D2B79F5;
t = Math.imul(t ^ t >>> 15, t | 1);
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
return ((t ^ t >>> 14) >>> 0) / 4294967296;
}
function rand(seed, min, max){ return min + seededRandom(seed) * (max – min); }
function pick(seed, arr){ return arr[Math.floor(seededRandom(seed) * arr.length)]; }
const PRODUCTS = [];
(function generate(){
let id = 1;
CATS.forEach((cat) => {
for (let i = 0; i < 25; i++){
const seed = id * 97 + cat.key.length * 13 + i;
const adj = pick(seed, cat.adjectives);
const noun = pick(seed + 1, cat.nouns);
const price = Math.round(rand(seed + 2, cat.priceRange[0], cat.priceRange[1]));
const rating = rand(seed + 3, 3.8, 5.0).toFixed(1);
const reviews = Math.round(rand(seed + 4, 8, 540));
const swatch = pick(seed + 5, cat.swatches);
PRODUCTS.push({
id, code:String(id).padStart(3,’0′), catKey:cat.key, category:cat.name,
icon:cat.icon, name:`${adj} ${noun}`, price, rating, reviews, swatch
});
id++;
}
});
})();
/* ———- state ———- */
const state = { filter:’all’, search:”, sort:’featured’, visible:24 };
let bagCount = 0;
/* ———- render: hero legend + pills (static counts) ———- */
function renderLegendAndPills(){
const legend = document.getElementById(‘heroLegend’);
legend.innerHTML = CATS.map(c => `<span>${c.name.toUpperCase()} — ${25}</span>`).join(‘<span aria-hidden=”true”>·</span>’);
const pills = document.getElementById(‘pills’);
const allBtn = `<button class=”pill” data-cat=”all”>All (200)</button>`;
const catBtns = CATS.map(c => `<button class=”pill” data-cat=”${c.key}”>${c.name} (25)</button>`).join(”);
pills.innerHTML = allBtn + catBtns;
updatePillActive();
pills.addEventListener(‘click’, (e) => {
const btn = e.target.closest(‘.pill’);
if (!btn) return;
state.filter = btn.dataset.cat;
state.visible = 24;
updatePillActive();
renderGrid();
});
}
function updatePillActive(){
document.querySelectorAll(‘.pill’).forEach(p => {
p.classList.toggle(‘active’, p.dataset.cat === state.filter);
});
}
/* ———- filtering / sorting ———- */
function getFiltered(){
let list = PRODUCTS;
if (state.filter !== ‘all’) list = list.filter(p => p.catKey === state.filter);
if (state.search.trim()){
const q = state.search.trim().toLowerCase();
list = list.filter(p => p.name.toLowerCase().includes(q) || p.category.toLowerCase().includes(q));
}
const sorted = list.slice();
if (state.sort === ‘price-asc’) sorted.sort((a,b) => a.price – b.price);
else if (state.sort === ‘price-desc’) sorted.sort((a,b) => b.price – a.price);
else if (state.sort === ‘rating-desc’) sorted.sort((a,b) => b.rating – a.rating);
return sorted;
}
/* ———- card markup ———- */
function cardHTML(p){
return `
<article class=”card”>
<div class=”swatch” style=”background:${p.swatch}”>
${getIcon(p.icon)}
<div class=”tag”>
<span class=”tag-code”>No. ${p.code}</span>
<span class=”tag-rule”></span>
<span class=”tag-price”>$${p.price}</span>
</div>
</div>
<div class=”card-body”>
<p class=”eyebrow”>${p.category}</p>
<h3 class=”card-name”>${p.name}</h3>
<div class=”card-meta”>
<span class=”rating”>★ ${p.rating} <span class=”reviews”>(${p.reviews})</span></span>
<button class=”add-btn” data-id=”${p.id}”>+ Add</button>
</div>
</div>
</article>`;
}
/* ———- render grid ———- */
function renderGrid(){
const filtered = getFiltered();
const visibleItems = filtered.slice(0, state.visible);
document.getElementById(‘grid’).innerHTML = visibleItems.map(cardHTML).join(”);
const shown = visibleItems.length;
document.getElementById(‘showingText’).textContent = `Showing ${shown} of ${filtered.length}`;
const loadMoreBtn = document.getElementById(‘loadMoreBtn’);
const endNote = document.getElementById(‘endNote’);
if (shown >= filtered.length){
loadMoreBtn.hidden = true;
endNote.hidden = filtered.length === 0;
} else {
loadMoreBtn.hidden = false;
endNote.hidden = true;
}
}
/* ———- toast ———- */
let toastTimer = null;
function showToast(msg){
const toast = document.getElementById(‘toast’);
toast.textContent = msg;
toast.classList.add(‘show’);
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toast.classList.remove(‘show’), 1800);
}
/* ———- events ———- */
document.getElementById(‘grid’).addEventListener(‘click’, (e) => {
const btn = e.target.closest(‘.add-btn’);
if (!btn) return;
const id = Number(btn.dataset.id);
const product = PRODUCTS.find(p => p.id === id);
if (!product) return;
bagCount++;
document.getElementById(‘bagCount’).textContent = bagCount;
showToast(`Added “${product.name}” to bag`);
btn.classList.add(‘added’);
btn.textContent = ‘Added’;
setTimeout(() => { btn.classList.remove(‘added’); btn.textContent = ‘+ Add’; }, 1100);
});
document.getElementById(‘searchInput’).addEventListener(‘input’, (e) => {
state.search = e.target.value;
state.visible = 24;
renderGrid();
});
document.getElementById(‘sortSelect’).addEventListener(‘change’, (e) => {
state.sort = e.target.value;
state.visible = 24;
renderGrid();
});
document.getElementById(‘loadMoreBtn’).addEventListener(‘click’, () => {
state.visible += 24;
renderGrid();
});
document.getElementById(‘bagBtn’).addEventListener(‘click’, () => {
showToast(bagCount === 0 ? ‘Your bag is empty’ : `${bagCount} item${bagCount === 1 ? ” : ‘s’} in your bag`);
});
/* ———- init ———- */
renderLegendAndPills();
renderGrid();
</script>
</body>
</html>