| <!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> |
|