/* ═══════════════════════════════════════════════
FATCAT CARDS — Complete App Logic
═══════════════════════════════════════════════ */
// ── STORE STATE ────────────────────────────────────────────────────────────
const Store = {
cart: JSON.parse(localStorage.getItem('fc_cart') || '[]'),
user: JSON.parse(localStorage.getItem('fc_user') || 'null'),
currentPack: null,
adminLoggedIn: localStorage.getItem('fc_admin') === 'true',
// Products database (in production this comes from your backend API)
products: {
singles: [
{ id:'s1', type:'single', name:'Charizard ex Alt Art', set:'Obsidian Flames', number:'125/197', rarity:'Alt Art', condition:'NM', price:89.99, originalPrice:null, stock:2, images:[], emoji:'🔥', featured:true, description:'One of the most sought-after cards from Obsidian Flames. Stunning alternate art depicting Charizard in a dramatic pose. Near mint condition.' },
{ id:'s2', type:'single', name:'Umbreon VMAX Rainbow', set:'Evolving Skies', number:'215/203', rarity:'Rainbow Rare', condition:'NM', price:54.00, originalPrice:null, stock:5, images:[], emoji:'🌙', featured:true, description:'Rainbow Rare Umbreon VMAX from Evolving Skies. Fan favorite secret rare. Near mint.' },
{ id:'s3', type:'single', name:'Pikachu V Promo', set:'SWSH Promo', number:'SWSH061', rarity:'Promo Holo', condition:'NM', price:14.99, originalPrice:null, stock:12, images:[], emoji:'⚡', featured:false, description:'Official Pokémon Center promo Pikachu V. Comes in original promo packaging.' },
{ id:'s4', type:'single', name:'Mew ex Alt Art', set:'151', number:'205/165', rarity:'Special Illustration', condition:'NM', price:67.50, originalPrice:null, stock:3, images:[], emoji:'💜', featured:true, description:'Gorgeous special illustration rare Mew ex from the 151 set. One of the most beautiful cards in the set.' },
{ id:'s5', type:'single', name:'Gardevoir ex Alt Art', set:'Scarlet & Violet', number:'228/198', rarity:'Special Illustration', condition:'NM', price:38.00, originalPrice:null, stock:4, images:[], emoji:'💗', featured:false, description:'Beautiful alt art Gardevoir ex from the base Scarlet & Violet set.' },
{ id:'s6', type:'single', name:'Eevee Gallery Rare', set:'Sword & Shield Promo', number:'GG33/GG70', rarity:'Gallery Rare', condition:'NM', price:22.00, originalPrice:null, stock:7, images:[], emoji:'🦊', featured:false, description:'Super cute Eevee gallery rare. Perfect for collectors and Eevee fans.' },
],
graded: [
{ id:'g1', type:'graded', name:'PSA 10 Charizard ex', set:'Obsidian Flames', gradingCompany:'PSA', grade:10, certNumber:'12345678', price:199.99, originalPrice:null, stock:1, images:[], emoji:'🔥', featured:true, description:'Gem Mint PSA 10 Charizard ex Alt Art. The highest possible grade. Certified authentic by PSA.' },
{ id:'g2', type:'graded', name:'PSA 9 Umbreon VMAX', set:'Evolving Skies', gradingCompany:'PSA', grade:9, certNumber:'87654321', price:89.99, originalPrice:null, stock:1, images:[], emoji:'🌙', featured:true, description:'PSA 9 Mint Umbreon VMAX Rainbow Rare. Near perfect condition certified by PSA.' },
{ id:'g3', type:'graded', name:'CGC 10 Pikachu V', set:'SWSH Promo', gradingCompany:'CGC', grade:10, certNumber:'11223344', price:55.00, originalPrice:null, stock:2, images:[], emoji:'⚡', featured:false, description:'CGC 10 Pristine Pikachu V Promo. Perfect condition certified by CGC.' },
{ id:'g4', type:'graded', name:'BGS 9.5 Mew ex', set:'151', gradingCompany:'BGS', grade:9.5, certNumber:'44332211', price:145.00, originalPrice:null, stock:1, images:[], emoji:'💜', featured:true, description:'BGS 9.5 Gem Mint Mew ex Special Illustration Rare. Near perfect subgrades.' },
],
sealed: [
{ id:'p1', type:'sealed', name:'Shrouded Fable Elite Trainer Box', set:'Shrouded Fable', price:49.99, originalPrice:64.99, stock:8, images:[], emoji:'📦', featured:true, description:'Sealed Shrouded Fable Elite Trainer Box. Contains 9 booster packs, 65 card sleeves, 45 energy cards, and more. Factory sealed.' },
{ id:'p2', type:'sealed', name:'Temporal Forces Booster Bundle', set:'Temporal Forces', price:27.99, originalPrice:null, stock:15, images:[], emoji:'🎴', featured:false, description:'6-pack Temporal Forces booster bundle. Factory sealed direct from distributor.' },
{ id:'p3', type:'sealed', name:'Obsidian Flames Booster Box', set:'Obsidian Flames', price:159.99, originalPrice:189.99, stock:3, images:[], emoji:'📦', featured:true, description:'Full Obsidian Flames booster box. 36 packs of the fan-favorite Charizard set. Factory sealed.' },
{ id:'p4', type:'sealed', name:'151 Elite Trainer Box', set:'Pokémon 151', price:44.99, originalPrice:null, stock:5, images:[], emoji:'📦', featured:true, description:'Official 151 Elite Trainer Box. Contains 9 booster packs with original 151 Pokémon. Perfect nostalgia hit.' },
{ id:'p5', type:'sealed', name:'Evolving Skies Single Pack', set:'Evolving Skies', price:8.99, originalPrice:null, stock:24, images:[], emoji:'🎴', featured:false, description:'Single Evolving Skies booster pack. Home of the sought-after Umbreon and Espeon VMAXs.' },
],
merch: [
{ id:'m1', type:'merch', name:'9-Pocket Binder (Pink)', set:'Accessories', price:18.99, originalPrice:null, stock:20, images:[], emoji:'📓', featured:false, description:'360-card capacity 9-pocket binder in pink. Perfect for showing off your collection.' },
{ id:'m2', type:'merch', name:'Perfect Fit Inner Sleeves 100pk', set:'Accessories', price:8.99, originalPrice:null, stock:50, images:[], emoji:'🛡️', featured:false, description:'100-pack crystal clear perfect fit inner sleeves. Essential protection for your valuable cards.' },
{ id:'m3', type:'merch', name:'KMC Standard Sleeves 80pk', set:'Accessories', price:11.99, originalPrice:null, stock:30, images:[], emoji:'🃏', featured:false, description:'80 KMC Hyper Mat standard sleeves. Tournament-legal, excellent shuffle feel.' },
],
},
// Mystery packs (configurable by admin)
mysteryPacks: JSON.parse(localStorage.getItem('fc_packs') || JSON.stringify([
{
id:'mp1',
name:'Vintage Treasure Hunt',
emoji:'🎁',
price:29.99,
floorValue:18,
avgValue:27,
description:'5 hand-selected cards from our vintage and modern collection. Guaranteed floor value with chances at massive hits!',
cardsPerPack:5,
openTypes:['instant','live','sealed'],
isActive:true,
image:null,
tiers:{
floor:{ pct:70, minValue:8, maxValue:25, label:'Base Pull', examples:['Holo Rare','Reverse Holo','Promo Card'], color:'#4ade80' },
bonus:{ pct:25, minValue:25, maxValue:60, label:'Bonus Hit', examples:['V Card','ex Card','VMAX'], color:'#60a5fa' },
chase:{ pct:5, minValue:60, maxValue:200, label:'Chase Hit', examples:['Alt Art','Gold Card','PSA 10 Slab'], color:'#D4A017' },
},
poolCards:[
{ name:'Pikachu V', value:14.99, tier:'floor', emoji:'⚡', set:'SWSH Promo' },
{ name:'Bulbasaur Rev Holo', value:8.50, tier:'floor', emoji:'🌿', set:'Base Set' },
{ name:'Mew ex', value:12.00, tier:'floor', emoji:'💜', set:'151' },
{ name:'Slowpoke Gallery', value:9.50, tier:'floor', emoji:'🌸', set:'Lost Origin' },
{ name:'Umbreon VMAX', value:54.00, tier:'bonus', emoji:'🌙', set:'Evolving Skies' },
{ name:'Gardevoir ex', value:38.00, tier:'bonus', emoji:'💗', set:'Scarlet & Violet' },
{ name:'Charizard ex', value:89.99, tier:'chase', emoji:'🔥', set:'Obsidian Flames' },
{ name:'Mew Gold Secret', value:78.00, tier:'chase', emoji:'✨', set:'Fusion Strike' },
]
},
{
id:'mp2',
name:'Gold Chase Pack',
emoji:'💛',
price:49.99,
floorValue:30,
avgValue:47,
description:'Premium mystery pack focusing on high-value hits. Higher floor value, better chase odds.',
cardsPerPack:3,
openTypes:['instant','live'],
isActive:true,
image:null,
tiers:{
floor:{ pct:60, minValue:20, maxValue:40, label:'Base Pull', examples:['VMAX','ex Card','Alt Art'], color:'#4ade80' },
bonus:{ pct:30, minValue:40, maxValue:80, label:'Bonus Hit', examples:['Gold Card','Special Illustration','PSA 9'], color:'#60a5fa' },
chase:{ pct:10, minValue:80, maxValue:300, label:'Chase Hit', examples:['PSA 10 Slab','Vintage Holo','Full Art Gold'], color:'#D4A017' },
},
poolCards:[
{ name:'Gardevoir ex Alt Art', value:38.00, tier:'floor', emoji:'💗', set:'Scarlet & Violet' },
{ name:'Miraidon ex', value:28.00, tier:'floor', emoji:'⚡', set:'Scarlet & Violet' },
{ name:'Umbreon Gold', value:65.00, tier:'bonus', emoji:'🌙', set:'Evolving Skies' },
{ name:'Charizard ex Alt Art', value:89.99, tier:'chase', emoji:'🔥', set:'Obsidian Flames' },
{ name:'PSA 10 Pikachu V', value:129.99, tier:'chase', emoji:'⚡', set:'PSA 10' },
]
},
{
id:'mp3',
name:'Live Break Slot',
emoji:'📺',
price:19.99,
floorValue:12,
avgValue:20,
description:'Buy a slot in our next live stream break! We open your pack on camera.',
cardsPerPack:5,
openTypes:['live'],
isActive:true,
image:null,
tiers:{
floor:{ pct:70, minValue:5, maxValue:18, label:'Base Pull', examples:['Holo','Reverse Holo','Common'], color:'#4ade80' },
bonus:{ pct:25, minValue:18, maxValue:45, label:'Bonus Hit', examples:['V Card','ex Card'], color:'#60a5fa' },
chase:{ pct:5, minValue:45, maxValue:150, label:'Chase Hit', examples:['Alt Art','Secret Rare'], color:'#D4A017' },
},
poolCards:[
{ name:'Various Holos', value:10.00, tier:'floor', emoji:'✨', set:'Mixed' },
{ name:'Various V Cards', value:25.00, tier:'bonus', emoji:'⚡', set:'Mixed' },
{ name:'Chase Hit', value:80.00, tier:'chase', emoji:'🔥', set:'Mixed' },
]
}
])),
// Save functions
saveCart() { localStorage.setItem('fc_cart', JSON.stringify(this.cart)); },
savePacks() { localStorage.setItem('fc_packs', JSON.stringify(this.mysteryPacks)); },
// Get all products flat
getAllProducts() {
return [
...this.products.singles,
...this.products.graded,
...this.products.sealed,
...this.products.merch,
...this.mysteryPacks.filter(p=>p.isActive).map(p=>({...p, type:'mystery'}))
];
},
getProduct(id) {
return this.getAllProducts().find(p=>p.id===id);
},
getPack(id) {
return this.mysteryPacks.find(p=>p.id===id);
}
};
// ── CART FUNCTIONS ──────────────────────────────────────────────────────────
const Cart = {
add(productId, qty=1) {
const product = Store.getProduct(productId);
if (!product) return;
const existing = Store.cart.find(i=>i.id===productId);
if (existing) {
existing.qty = Math.min(existing.qty + qty, product.stock || 99);
} else {
Store.cart.push({
id: product.id, name: product.name, price: product.price,
emoji: product.emoji || '🃏', type: product.type,
image: product.images?.[0] || null, qty
});
}
Store.saveCart();
UI.updateCartBadge();
UI.renderCartItems();
Toast.show(`Added ${product.name} to cart 🛒`);
},
remove(productId) {
Store.cart = Store.cart.filter(i=>i.id!==productId);
Store.saveCart();
UI.updateCartBadge();
UI.renderCartItems();
},
updateQty(productId, qty) {
const item = Store.cart.find(i=>i.id===productId);
if (!item) return;
if (qty <= 0) { this.remove(productId); return; }
item.qty = qty;
Store.saveCart();
UI.updateCartBadge();
UI.renderCartItems();
},
getTotal() {
return Store.cart.reduce((sum,i)=>sum+(i.price*i.qty), 0);
},
getCount() {
return Store.cart.reduce((sum,i)=>sum+i.qty, 0);
},
clear() {
Store.cart = [];
Store.saveCart();
UI.updateCartBadge();
UI.renderCartItems();
}
};
// ── UI FUNCTIONS ────────────────────────────────────────────────────────────
const UI = {
updateCartBadge() {
const count = Cart.getCount();
document.querySelectorAll('.cart-badge').forEach(el => {
el.textContent = count;
el.style.display = count > 0 ? 'flex' : 'none';
});
},
renderCartItems() {
const container = document.getElementById('cartItems');
const total = document.getElementById('cartTotal');
if (!container) return;
if (Store.cart.length === 0) {
container.innerHTML = `
`;
} else {
container.innerHTML = Store.cart.map(item => `
${item.image ? `

` : item.emoji}
${item.name}
$${item.price.toFixed(2)}
${item.qty}
`).join('');
}
if (total) total.textContent = '$' + Cart.getTotal().toFixed(2);
},
openCart() {
document.getElementById('cartOverlay')?.classList.add('open');
this.renderCartItems();
},
closeCart() {
document.getElementById('cartOverlay')?.classList.remove('open');
},
openMobileNav() {
document.getElementById('mobileNav')?.classList.add('open');
},
closeMobileNav() {
document.getElementById('mobileNav')?.classList.remove('open');
},
renderProductGrid(products, containerId) {
const container = document.getElementById(containerId);
if (!container) return;
if (!products.length) {
container.innerHTML = `🔍
No products found
Try adjusting your filters
`;
return;
}
container.innerHTML = products.map(p => this.productCardHTML(p)).join('');
},
productCardHTML(p) {
const isMystery = p.type === 'mystery';
const isGraded = p.type === 'graded';
const isOutOfStock = p.stock === 0;
const isLow = p.stock > 0 && p.stock <= 3;
const detailPage = isMystery ? `mystery-detail.html?id=${p.id}` : `product.html?id=${p.id}`;
let badge = '';
if (p.featured && !isOutOfStock) badge = `⭐ Featured`;
if (p.originalPrice) badge = `🔥 Sale`;
if (isMystery) badge = `✨ Mystery`;
if (isGraded) badge = `💎 ${p.gradingCompany} ${p.grade}`;
let openTag = '';
if (isMystery) {
const types = p.openTypes || [];
if (types.includes('instant')) openTag = `⚡ Instant Reveal`;
else if (types.includes('live')) openTag = `🔴 Live-Opened`;
}
return `
${p.images?.[0] ? `

` : `
${p.emoji || '🃏'}`}
${badge}
${openTag}
${p.name}
${p.set}${isGraded ? ` · ${p.gradingCompany} ${p.grade}` : ''}
${isLow ? `
⚠️ Only ${p.stock} left!
` : ''}
`;
}
};
// ── TOAST ───────────────────────────────────────────────────────────────────
const Toast = {
show(msg, type='default') {
let container = document.getElementById('toastContainer');
if (!container) {
container = document.createElement('div');
container.id = 'toastContainer';
container.className = 'toast-container';
document.body.appendChild(container);
}
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = msg;
container.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateY(10px)';
toast.style.transition = 'all 0.3s';
setTimeout(() => toast.remove(), 300);
}, 2800);
}
};
// ── PACK OPENING ENGINE ──────────────────────────────────────────────────────
const PackOpener = {
currentPack: null,
results: [],
open(packId) {
const pack = Store.getPack(packId);
if (!pack) return;
this.currentPack = pack;
this.results = [];
const modal = document.getElementById('packOpeningModal');
if (!modal) return;
modal.classList.add('open');
this.showStage('idle');
// Set pack info
const titleEl = document.getElementById('packOpenTitle');
const subEl = document.getElementById('packOpenSub');
const chestEl = document.getElementById('packChest');
if (titleEl) titleEl.textContent = pack.name;
if (subEl) subEl.textContent = `${pack.cardsPerPack} cards · Floor: $${pack.floorValue} · Avg: ~$${pack.avgValue}`;
if (chestEl) chestEl.textContent = pack.emoji || '🎁';
},
showStage(stage) {
['idle','revealing','summary'].forEach(s => {
const el = document.getElementById(`packStage${s.charAt(0).toUpperCase()+s.slice(1)}`);
if (el) el.style.display = s === stage ? 'block' : 'none';
});
},
startReveal() {
const pack = this.currentPack;
if (!pack) return;
const fast = document.getElementById('fastModeCheck')?.checked || false;
// Generate results server-side style (weighted random)
this.results = Array.from({length: pack.cardsPerPack}, () => this.weightedPick(pack));
this.showStage('revealing');
const grid = document.getElementById('revealCardsGrid');
if (!grid) return;
// Create card slots
grid.innerHTML = Array.from({length: pack.cardsPerPack}, (_,i) => `
`).join('');
// Populate faces
this.results.forEach((card, i) => {
const face = document.getElementById(`card-face-${i}`);
if (face) {
face.innerHTML = `
${card.tier==='chase' ? `⭐ CHASE!` : ''}
${card.emoji || '🃏'}
${card.name}
$${card.value.toFixed(2)}
`;
}
});
// Flip cards one by one
this.results.forEach((card, i) => {
const delay = fast ? i * 150 : (i === 0 ? 300 : i * (card.tier==='chase' ? 2500 : card.tier==='bonus' ? 1300 : 800));
setTimeout(() => {
const inner = document.getElementById(`card-inner-${i}`);
if (inner) inner.classList.add('flipped');
if (card.tier === 'chase') this.spawnParticles();
// Update progress
const prog = document.getElementById('revealProgress');
if (prog) prog.textContent = `${i+1} / ${pack.cardsPerPack} cards revealed`;
}, delay);
});
// Show summary
const lastDelay = fast ? pack.cardsPerPack * 150 + 500 : pack.cardsPerPack * 1000 + 2000;
setTimeout(() => this.showSummary(), lastDelay);
},
weightedPick(pack) {
const { tiers, poolCards } = pack;
const roll = Math.random() * 100;
let tier;
if (roll < tiers.chase.pct) tier = 'chase';
else if (roll < tiers.chase.pct + tiers.bonus.pct) tier = 'bonus';
else tier = 'floor';
const tierCards = poolCards.filter(c => c.tier === tier);
if (tierCards.length === 0) {
// Fallback: pick any card from pool
return poolCards[Math.floor(Math.random() * poolCards.length)];
}
return tierCards[Math.floor(Math.random() * tierCards.length)];
},
showSummary() {
this.showStage('summary');
const total = this.results.reduce((s,c) => s+c.value, 0);
const buyback = total * 0.65;
const totalEl = document.getElementById('packTotalAmount');
const sellBtn = document.getElementById('sellAllBtn');
const summaryGrid = document.getElementById('summaryCardsGrid');
if (totalEl) totalEl.textContent = '$' + total.toFixed(2);
if (sellBtn) sellBtn.textContent = `Sell All Back — $${buyback.toFixed(2)} Credit`;
if (summaryGrid) {
summaryGrid.innerHTML = this.results.map(card => `
${card.emoji||'🃏'}
${card.name}
$${card.value.toFixed(2)}
`).join('');
}
},
close() {
document.getElementById('packOpeningModal')?.classList.remove('open');
this.currentPack = null;
this.results = [];
},
reset() {
this.showStage('idle');
const grid = document.getElementById('revealCardsGrid');
if (grid) grid.innerHTML = '';
const prog = document.getElementById('revealProgress');
if (prog) prog.textContent = '0 / 5 cards revealed';
},
spawnParticles() {
const colors = ['#D4A017','#E8B94F','#ffffff','#ffd700','#E07B30'];
for (let i = 0; i < 24; i++) {
const p = document.createElement('div');
p.className = 'particle';
p.style.cssText = `
left:${25+Math.random()*50}%;
top:${20+Math.random()*60}%;
width:${6+Math.random()*6}px;
height:${6+Math.random()*6}px;
background:${colors[Math.floor(Math.random()*colors.length)]};
--dx:${(Math.random()-.5)*300}px;
--dy:${(Math.random()-.5)*300}px;
animation-delay:${Math.random()*0.3}s;
animation-duration:${0.8+Math.random()*0.6}s`;
document.body.appendChild(p);
setTimeout(() => p.remove(), 1500);
}
},
share() {
const total = this.results.reduce((s,c)=>s+c.value,0);
const text = `Just opened a ${this.currentPack?.name} from Fatcat Cards and pulled $${total.toFixed(2)} in cards! 🐱🔥 fatcatcardstcg.com`;
if (navigator.share) navigator.share({text});
else if (navigator.clipboard) { navigator.clipboard.writeText(text); Toast.show('Copied to clipboard! 📋'); }
}
};
// ── CHECKOUT / STRIPE ────────────────────────────────────────────────────────
const Checkout = {
async startStripe() {
if (Store.cart.length === 0) {
Toast.show('Your cart is empty!', 'error');
return;
}
// In production: POST to your backend /api/create-checkout-session
// Your backend creates a Stripe session and returns the URL
// For now we show instructions
Toast.show('Connecting to Stripe...', 'default');
// TODO: Replace with your actual Stripe checkout endpoint
// const response = await fetch('/api/checkout', {
// method: 'POST',
// headers: {'Content-Type':'application/json'},
// body: JSON.stringify({ items: Store.cart })
// });
// const { url } = await response.json();
// window.location.href = url;
alert('STRIPE SETUP NEEDED:\n\n1. Create a Stripe account at stripe.com\n2. Add your Stripe Secret Key to your .env file\n3. Deploy the /api/checkout endpoint from the Next.js code\n\nYour Stripe account is already set up — just connect it!');
}
};
// ── ADMIN ────────────────────────────────────────────────────────────────────
const Admin = {
isLoggedIn() {
return Store.adminLoggedIn;
},
login(password) {
// In production this should be a real auth check against your backend
// Default password: fatcat2025 (change this!)
if (password === 'fatcat2025') {
Store.adminLoggedIn = true;
localStorage.setItem('fc_admin', 'true');
return true;
}
return false;
},
logout() {
Store.adminLoggedIn = false;
localStorage.removeItem('fc_admin');
window.location.href = 'index.html';
},
savePack(packData) {
const existing = Store.mysteryPacks.findIndex(p=>p.id===packData.id);
if (existing >= 0) {
Store.mysteryPacks[existing] = packData;
} else {
packData.id = 'mp' + Date.now();
Store.mysteryPacks.push(packData);
}
Store.savePacks();
Toast.show('Pack saved! ✅', 'success');
},
deletePack(id) {
if (!confirm('Delete this pack?')) return;
Store.mysteryPacks = Store.mysteryPacks.filter(p=>p.id!==id);
Store.savePacks();
Toast.show('Pack deleted', 'default');
},
// Calculate expected value for a pack
calculateEV(pack) {
const { tiers, poolCards } = pack;
let ev = 0;
poolCards.forEach(card => {
const tierData = tiers[card.tier];
const tierCards = poolCards.filter(c=>c.tier===card.tier);
const cardWeight = (tierData.pct / 100) / tierCards.length;
ev += card.value * cardWeight;
});
return ev * pack.cardsPerPack;
}
};
// ── SEARCH & FILTER ──────────────────────────────────────────────────────────
const Search = {
filter(products, { query='', sort='featured', maxPrice=9999, condition='' }={}) {
let list = [...products];
if (query) {
const q = query.toLowerCase();
list = list.filter(p =>
p.name.toLowerCase().includes(q) ||
(p.set||'').toLowerCase().includes(q) ||
(p.rarity||'').toLowerCase().includes(q)
);
}
if (maxPrice < 9999) list = list.filter(p => p.price <= maxPrice);
if (condition) list = list.filter(p => p.condition === condition);
switch(sort) {
case 'price-asc': list.sort((a,b)=>a.price-b.price); break;
case 'price-desc': list.sort((a,b)=>b.price-a.price); break;
case 'newest': list.reverse(); break;
case 'featured': list.sort((a,b)=>(b.featured?1:0)-(a.featured?1:0)); break;
}
return list;
}
};
// ── URL PARAMS ───────────────────────────────────────────────────────────────
const Params = {
get(key) {
return new URLSearchParams(window.location.search).get(key);
}
};
// ── EMAIL SIGNUP ─────────────────────────────────────────────────────────────
const EmailSignup = {
submit(email, source='website') {
if (!email || !email.includes('@')) {
Toast.show('Please enter a valid email', 'error');
return false;
}
// Save locally + TODO: POST to your backend
const subs = JSON.parse(localStorage.getItem('fc_subscribers') || '[]');
if (!subs.includes(email)) {
subs.push(email);
localStorage.setItem('fc_subscribers', JSON.stringify(subs));
}
Toast.show('Welcome to the crew! 🐱', 'success');
return true;
}
};
// ── INIT ─────────────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
UI.updateCartBadge();
UI.renderCartItems();
// Cart overlay click to close
document.getElementById('cartOverlay')?.addEventListener('click', function(e) {
if (e.target === this) UI.closeCart();
});
// Close mobile nav on link click
document.querySelectorAll('.mobile-nav-links a').forEach(link => {
link.addEventListener('click', () => UI.closeMobileNav());
});
// Mark active nav link
const path = window.location.pathname.split('/').pop();
document.querySelectorAll('.nav-links a').forEach(link => {
const href = link.getAttribute('href');
if (href === path || (path === '' && href === 'index.html')) {
link.classList.add('active');
}
});
});