RoadsideRestock / index.html
Honda219's picture
🐳 14/06 - 19:22 - make the products square tiles and fit 5 per row
494296a verified
Raw
History Blame Contribute Delete
25.8 kB
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Roadside Restock List 👽</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700;800;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
alien: ['Orbitron', 'sans-serif'],
mono: ['Share Tech Mono', 'monospace'],
},
colors: {
alien: {
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
glow: '#39ff14',
},
cosmic: {
900: '#0a0a12',
800: '#0f0f1a',
700: '#161625',
600: '#1e1e30',
500: '#2a2a40',
}
}
}
}
}
</script>
<style>
* { box-sizing: border-box; }
body {
font-family: 'Share Tech Mono', monospace;
background-color: #0a0a12;
color: #c4c4d4;
}
.font-alien { font-family: 'Orbitron', sans-serif; }
/* Alien Glow Effects */
.glow-text {
text-shadow: 0 0 10px rgba(57, 255, 20, 0.7), 0 0 20px rgba(57, 255, 20, 0.4), 0 0 40px rgba(57, 255, 20, 0.2);
}
.glow-box {
box-shadow: 0 0 15px rgba(57, 255, 20, 0.15), inset 0 0 15px rgba(57, 255, 20, 0.05);
}
.glow-box-strong {
box-shadow: 0 0 20px rgba(57, 255, 20, 0.3), inset 0 0 20px rgba(57, 255, 20, 0.08);
}
.glow-border {
border-color: rgba(57, 255, 20, 0.3);
}
/* Scanline overlay */
.scanlines::after {
content: '';
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0, 0, 0, 0.03) 2px,
rgba(0, 0, 0, 0.03) 4px
);
pointer-events: none;
z-index: 9999;
}
/* Category pill colors */
.cat-drinks { --cat-color: #3b82f6; --cat-glow: rgba(59, 130, 246, 0.4); }
.cat-fuel { --cat-color: #f59e0b; --cat-glow: rgba(245, 158, 11, 0.4); }
.cat-fresh { --cat-color: #22c55e; --cat-glow: rgba(34, 197, 94, 0.4); }
.cat-dry { --cat-color: #eab308; --cat-glow: rgba(234, 179, 8, 0.4); }
.cat-drugs { --cat-color: #a855f7; --cat-glow: rgba(168, 85, 247, 0.4); }
.cat-cans { --cat-color: #ef4444; --cat-glow: rgba(239, 68, 68, 0.4); }
.cat-car { --cat-color: #64748b; --cat-glow: rgba(100, 116, 139, 0.4); }
.cat-snack { --cat-color: #ec4899; --cat-glow: rgba(236, 72, 153, 0.4); }
.cat-bakery { --cat-color: #d97706; --cat-glow: rgba(217, 119, 6, 0.4); }
.cat-alien { --cat-color: #39ff14; --cat-glow: rgba(57, 255, 20, 0.5); }
.cat-freezer { --cat-color: #06b6d4; --cat-glow: rgba(6, 182, 212, 0.4); }
.cat-ready { --cat-color: #6366f1; --cat-glow: rgba(99, 102, 241, 0.4); }
.cat-all { --cat-color: #f8fafc; --cat-glow: rgba(248, 250, 252, 0.3); }
.category-pill {
background: rgba(var(--cat-color-rgb, 255,255,255), 0.08);
border: 1px solid var(--cat-color);
color: var(--cat-color);
transition: all 0.3s ease;
}
.category-pill:hover {
background: rgba(var(--cat-color-rgb, 255,255,255), 0.15);
box-shadow: 0 0 12px var(--cat-glow);
}
.category-pill.active {
background: var(--cat-color);
color: #0a0a12;
box-shadow: 0 0 20px var(--cat-glow), 0 0 40px var(--cat-glow);
font-weight: 700;
}
.product-row {
transition: all 0.3s ease;
border-left: 3px solid transparent;
}
.product-row:hover {
background: rgba(57, 255, 20, 0.03);
border-left-color: #39ff14;
}
.qty-btn {
transition: all 0.15s ease;
}
.qty-btn:hover {
transform: scale(1.15);
}
.qty-btn:active {
transform: scale(0.95);
}
.add-form input, .add-form select {
background: #161625;
border: 1px solid rgba(57, 255, 20, 0.2);
color: #c4c4d4;
transition: all 0.3s ease;
}
.add-form input:focus, .add-form select:focus {
border-color: #39ff14;
box-shadow: 0 0 10px rgba(57, 255, 20, 0.2);
outline: none;
}
.pulse-dot {
animation: pulse-alien 2s ease-in-out infinite;
}
@keyframes pulse-alien {
0%, 100% { opacity: 0.4; transform: scale(1); }
50% { opacity: 1; transform: scale(1.3); }
}
.float-anim {
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-6px); }
}
/* Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: #0a0a12; }
::-webkit-scrollbar-thumb { background: rgba(57, 255, 20, 0.3); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(57, 255, 20, 0.5); }
.needed-badge {
animation: glow-pulse 1.5s ease-in-out infinite alternate;
}
@keyframes glow-pulse {
from { box-shadow: 0 0 5px rgba(57, 255, 20, 0.3); }
to { box-shadow: 0 0 15px rgba(57, 255, 20, 0.6); }
}
.cat-icon-wrapper {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
background: var(--cat-color);
color: #0a0a12;
flex-shrink: 0;
}
.empty-state {
opacity: 0.4;
}
select option {
background: #161625;
color: #c4c4d4;
}
</style>
</head>
<body class="scanlines min-h-screen">
<div class="max-w-6xl mx-auto px-4 py-6 md:py-10">
<!-- Header -->
<header class="text-center mb-8">
<div class="float-anim inline-block mb-3">
<span class="text-4xl">🛸</span>
</div>
<h1 class="font-alien text-2xl md:text-4xl font-bold text-alien-glow glow-text tracking-wider mb-2">
ROADSIDE RESTOCK
</h1>
<p class="text-sm md:text-base text-gray-500 tracking-widest font-alien uppercase">
Inventory Management System
</p>
<div class="flex items-center justify-center gap-2 mt-3">
<span class="pulse-dot inline-block w-2 h-2 rounded-full bg-alien-glow"></span>
<span class="text-xs text-alien-glow/60 font-alien tracking-widest">SYSTEM ONLINE</span>
<span class="pulse-dot inline-block w-2 h-2 rounded-full bg-alien-glow"></span>
</div>
</header>
<!-- Stats Bar -->
<div id="stats-bar" class="grid grid-cols-3 gap-3 mb-6">
<div class="bg-cosmic-700 rounded-xl p-3 text-center glow-box border border-alien-glow/10">
<div class="text-xs text-gray-500 font-alien uppercase tracking-wider mb-1">Products</div>
<div id="stat-total" class="text-xl font-alien font-bold text-alien-glow">0</div>
</div>
<div class="bg-cosmic-700 rounded-xl p-3 text-center glow-box border border-alien-glow/10">
<div class="text-xs text-gray-500 font-alien uppercase tracking-wider mb-1">Needed</div>
<div id="stat-needed" class="text-xl font-alien font-bold text-yellow-400">0</div>
</div>
<div class="bg-cosmic-700 rounded-xl p-3 text-center glow-box border border-alien-glow/10">
<div class="text-xs text-gray-500 font-alien uppercase tracking-wider mb-1">Total Qty</div>
<div id="stat-qty" class="text-xl font-alien font-bold text-cyan-400">0</div>
</div>
</div>
<!-- View Toggle -->
<div class="flex items-center justify-between mb-4 bg-cosmic-700 rounded-xl p-3 glow-box border border-alien-glow/10">
<div class="flex items-center gap-2">
<i data-lucide="eye" class="w-4 h-4 text-alien-glow/60"></i>
<span class="text-xs font-alien text-gray-400 uppercase tracking-wider">View Mode</span>
</div>
<div class="flex bg-cosmic-900 rounded-lg p-1 gap-1">
<button id="btn-all" onclick="setViewMode('all')" class="px-3 py-1.5 rounded-md text-xs font-alien uppercase tracking-wider transition-all bg-alien-glow text-cosmic-900 font-bold">
All Items
</button>
<button id="btn-needed" onclick="setViewMode('needed')" class="px-3 py-1.5 rounded-md text-xs font-alien uppercase tracking-wider transition-all text-gray-500 hover:text-gray-300">
Needed Only
</button>
</div>
</div>
<!-- Category Tabs -->
<div class="mb-6 -mx-4 px-4">
<div id="category-tabs" class="flex flex-wrap gap-2">
<!-- Generated by JS -->
</div>
</div>
<!-- Add Product Form -->
<div class="add-form bg-cosmic-700 rounded-xl p-4 mb-6 glow-box border border-alien-glow/10">
<div class="flex items-center gap-2 mb-3">
<i data-lucide="plus-circle" class="w-4 h-4 text-alien-glow"></i>
<span class="font-alien text-sm text-alien-glow uppercase tracking-wider">Add Product</span>
</div>
<div class="flex flex-col sm:flex-row gap-3">
<input type="text" id="input-name" placeholder="Product name..." class="flex-1 px-4 py-2.5 rounded-lg text-sm font-mono" onkeydown="if(event.key==='Enter')addProduct()">
<select id="input-category" class="px-4 py-2.5 rounded-lg text-sm font-mono sm:w-44">
<!-- Generated by JS -->
</select>
<div class="flex items-center gap-2">
<label class="text-xs text-gray-500 font-alien">QTY:</label>
<input type="number" id="input-qty" value="0" min="0" class="w-20 px-3 py-2.5 rounded-lg text-sm font-mono text-center">
</div>
<button onclick="addProduct()" class="px-5 py-2.5 bg-alien-glow text-cosmic-900 rounded-lg font-alien font-bold text-sm uppercase tracking-wider hover:shadow-[0_0_20px_rgba(57,255,20,0.5)] transition-all active:scale-95 flex items-center gap-2 justify-center">
<i data-lucide="plus" class="w-4 h-4"></i>
Add
</button>
</div>
</div>
<!-- Product List -->
<div id="product-list" class="grid grid-cols-5 gap-3 mb-6">
<!-- Generated by JS -->
</div>
<!-- Empty State -->
<div id="empty-state" class="hidden text-center py-16 empty-state">
<div class="text-5xl mb-4">📡</div>
<p class="font-alien text-sm text-gray-600 uppercase tracking-widest mb-2">No Products Found</p>
<p class="text-xs text-gray-700">Add products above or switch category</p>
</div>
<!-- Needed Summary (shown in needed view) -->
<div id="needed-summary" class="hidden bg-cosmic-700 rounded-xl p-4 glow-box-strong border border-alien-glow/20 mb-6">
<div class="flex items-center gap-2 mb-3">
<i data-lucide="clipboard-list" class="w-4 h-4 text-yellow-400"></i>
<span class="font-alien text-sm text-yellow-400 uppercase tracking-wider">Restock Summary</span>
</div>
<div id="summary-content" class="space-y-1 text-sm font-mono">
<!-- Generated by JS -->
</div>
</div>
<!-- Footer -->
<footer class="text-center py-6 border-t border-cosmic-600">
<p class="text-xs text-gray-700 font-alien tracking-widest uppercase">
Roadside Research · Restock System v1.0
</p>
</footer>
</div>
<script>
// ========== CATEGORY CONFIG ==========
const CATEGORIES = [
{ id: 'all', name: 'All', icon: 'layout-grid', cssClass: 'cat-all', color: '#f8fafc' },
{ id: 'drinks', name: 'Drinks', icon: 'cup-soda', cssClass: 'cat-drinks', color: '#3b82f6' },
{ id: 'fuel', name: 'Fuel', icon: 'fuel', cssClass: 'cat-fuel', color: '#f59e0b' },
{ id: 'fresh', name: 'Fresh', icon: 'apple', cssClass: 'cat-fresh', color: '#22c55e' },
{ id: 'dry', name: 'Dry', icon: 'wheat', cssClass: 'cat-dry', color: '#eab308' },
{ id: 'drugs', name: 'Drugs', icon: 'pill', cssClass: 'cat-drugs', color: '#a855f7' },
{ id: 'cans', name: 'Cans', icon: 'package', cssClass: 'cat-cans', color: '#ef4444' },
{ id: 'car', name: 'Car', icon: 'car', cssClass: 'cat-car', color: '#64748b' },
{ id: 'snack', name: 'Snack', icon: 'cookie', cssClass: 'cat-snack', color: '#ec4899' },
{ id: 'bakery', name: 'Bakery', icon: 'croissant', cssClass: 'cat-bakery', color: '#d97706' },
{ id: 'alien', name: 'Alien', icon: 'rocket', cssClass: 'cat-alien', color: '#39ff14' },
{ id: 'freezer', name: 'Freezer', icon: 'snowflake', cssClass: 'cat-freezer', color: '#06b6d4' },
{ id: 'ready', name: 'Ready Meals', icon: 'cooking-pot', cssClass: 'cat-ready', color: '#6366f1' },
];
// ========== STATE ==========
let products = JSON.parse(localStorage.getItem('roadside-products') || '[]');
let activeCategory = 'all';
let viewMode = 'all'; // 'all' or 'needed'
// ========== INIT ==========
function init() {
renderCategoryTabs();
renderCategorySelect();
renderProducts();
updateStats();
lucide.createIcons();
}
// ========== CATEGORY TABS ==========
function renderCategoryTabs() {
const container = document.getElementById('category-tabs');
container.innerHTML = CATEGORIES.map(cat => {
const isActive = activeCategory === cat.id;
return `
<button onclick="setCategory('${cat.id}')"
class="${cat.cssClass} category-pill flex items-center gap-2 px-3 py-2 rounded-xl text-xs font-alien uppercase tracking-wider whitespace-nowrap ${isActive ? 'active' : ''}"
style="--cat-color: ${cat.color}; --cat-glow: ${cat.color}40;">
<i data-lucide="${cat.icon}" class="w-3.5 h-3.5"></i>
<span>${cat.name}</span>
<span class="cat-count-${cat.id} text-[10px] opacity-70"></span>
</button>
`;
}).join('');
lucide.createIcons();
updateCategoryCounts();
}
function updateCategoryCounts() {
CATEGORIES.forEach(cat => {
const el = document.querySelector(`.cat-count-${cat.id}`);
if (el) {
const count = cat.id === 'all' ? products.length : products.filter(p => p.category === cat.id).length;
el.textContent = count > 0 ? `(${count})` : '';
}
});
}
function setCategory(catId) {
activeCategory = catId;
renderCategoryTabs();
renderProducts();
}
// ========== CATEGORY SELECT ==========
function renderCategorySelect() {
const select = document.getElementById('input-category');
select.innerHTML = CATEGORIES.filter(c => c.id !== 'all').map(cat =>
`<option value="${cat.id}">${cat.name}</option>`
).join('');
}
// ========== VIEW MODE ==========
function setViewMode(mode) {
viewMode = mode;
const btnAll = document.getElementById('btn-all');
const btnNeeded = document.getElementById('btn-needed');
if (mode === 'all') {
btnAll.className = 'px-3 py-1.5 rounded-md text-xs font-alien uppercase tracking-wider transition-all bg-alien-glow text-cosmic-900 font-bold';
btnNeeded.className = 'px-3 py-1.5 rounded-md text-xs font-alien uppercase tracking-wider transition-all text-gray-500 hover:text-gray-300';
} else {
btnNeeded.className = 'px-3 py-1.5 rounded-md text-xs font-alien uppercase tracking-wider transition-all bg-yellow-500 text-cosmic-900 font-bold';
btnAll.className = 'px-3 py-1.5 rounded-md text-xs font-alien uppercase tracking-wider transition-all text-gray-500 hover:text-gray-300';
}
renderProducts();
}
// ========== ADD PRODUCT ==========
function addProduct() {
const nameInput = document.getElementById('input-name');
const catInput = document.getElementById('input-category');
const qtyInput = document.getElementById('input-qty');
const name = nameInput.value.trim();
if (!name) {
nameInput.style.borderColor = '#ef4444';
nameInput.style.boxShadow = '0 0 10px rgba(239, 68, 68, 0.3)';
setTimeout(() => {
nameInput.style.borderColor = 'rgba(57, 255, 20, 0.2)';
nameInput.style.boxShadow = 'none';
}, 1500);
return;
}
const product = {
id: Date.now() + Math.random(),
name: name,
category: catInput.value,
quantity: Math.max(0, parseInt(qtyInput.value) || 0)
};
products.push(product);
saveProducts();
nameInput.value = '';
qtyInput.value = '0';
renderProducts();
updateStats();
updateCategoryCounts();
}
// ========== UPDATE QUANTITY ==========
function updateQty(productId, delta) {
const product = products.find(p => p.id === productId);
if (product) {
product.quantity = Math.max(0, product.quantity + delta);
saveProducts();
renderProducts();
updateStats();
}
}
function setQty(productId, value) {
const product = products.find(p => p.id === productId);
if (product) {
product.quantity = Math.max(0, parseInt(value) || 0);
saveProducts();
updateStats();
}
}
// ========== DELETE PRODUCT ==========
function deleteProduct(productId) {
products = products.filter(p => p.id !== productId);
saveProducts();
renderProducts();
updateStats();
updateCategoryCounts();
}
// ========== RENDER PRODUCTS ==========
function renderProducts() {
const container = document.getElementById('product-list');
const emptyState = document.getElementById('empty-state');
const neededSummary = document.getElementById('needed-summary');
let filtered = activeCategory === 'all' ? [...products] : products.filter(p => p.category === activeCategory);
if (viewMode === 'needed') {
filtered = filtered.filter(p => p.quantity >= 1);
}
if (filtered.length === 0) {
container.innerHTML = '';
emptyState.classList.remove('hidden');
neededSummary.classList.add('hidden');
return;
}
emptyState.classList.add('hidden');
// Group by category if in "all" category view
if (activeCategory === 'all' && viewMode === 'all') {
const grouped = {};
filtered.forEach(p => {
if (!grouped[p.category]) grouped[p.category] = [];
grouped[p.category].push(p);
});
container.innerHTML = Object.entries(grouped).map(([catId, items]) => {
const cat = CATEGORIES.find(c => c.id === catId);
return `
<div class="col-span-5 flex items-center gap-2 mt-4 mb-1 px-1 first:mt-0">
<div class="cat-icon-wrapper ${cat.cssClass}" style="--cat-color: ${cat.color}; width:24px; height:24px; border-radius:6px;">
<i data-lucide="${cat.icon}" class="w-3 h-3"></i>
</div>
<span class="font-alien text-xs uppercase tracking-widest" style="color: ${cat.color}">${cat.name}</span>
<span class="text-[10px] text-gray-600">(${items.length})</span>
<div class="flex-1 h-px" style="background: ${cat.color}20"></div>
</div>
${items.map(p => renderProductTile(p, cat)).join('')}
`;
}).join('');
} else {
container.innerHTML = filtered.map(p => {
const pCat = CATEGORIES.find(c => c.id === p.category);
return renderProductTile(p, pCat);
}).join('');
}
// Needed summary
if (viewMode === 'needed') {
const neededProducts = (activeCategory === 'all' ? products : products.filter(p => p.category === activeCategory)).filter(p => p.quantity >= 1);
if (neededProducts.length > 0) {
neededSummary.classList.remove('hidden');
const summaryContent = document.getElementById('summary-content');
const totalItems = neededProducts.reduce((sum, p) => sum + p.quantity, 0);
// Group summary by category
const grouped = {};
neededProducts.forEach(p => {
if (!grouped[p.category]) grouped[p.category] = [];
grouped[p.category].push(p);
});
summaryContent.innerHTML = Object.entries(grouped).map(([catId, items]) => {
const cat = CATEGORIES.find(c => c.id === catId);
const catTotal = items.reduce((sum, p) => sum + p.quantity, 0);
return `
<div class="flex items-center gap-2 py-1" style="color: ${cat.color}">
<i data-lucide="${cat.icon}" class="w-3.5 h-3.5 flex-shrink-0"></i>
<span class="font-alien text-xs uppercase">${cat.name}:</span>
<span class="text-gray-400">${items.map(p => `${p.name}×${p.quantity}`).join(', ')}</span>
<span class="ml-auto font-alien text-xs opacity-70">${catTotal}</span>
</div>
`;
}).join('') + `
<div class="border-t border-cosmic-500 mt-2 pt-2 flex items-center justify-between">
<span class="font-alien text-xs text-alien-glow uppercase">Total Restock Items</span>
<span class="font-alien text-lg text-alien-glow glow-text">${totalItems}</span>
</div>
`;
} else {
neededSummary.classList.add('hidden');
}
} else {
neededSummary.classList.add('hidden');
}
lucide.createIcons();
}
function renderProductTile(product, cat) {
const isNeeded = product.quantity >= 1;
return `
<div class="bg-cosmic-700 rounded-xl aspect-square border border-cosmic-500/50 flex flex-col items-center justify-between p-3 relative ${isNeeded && viewMode === 'needed' ? 'needed-badge border-alien-glow/20' : ''}" style="border-top: 3px solid ${cat.color};">
<!-- Delete button -->
<button onclick="deleteProduct(${product.id})" class="absolute top-1.5 right-1.5 w-6 h-6 rounded-md flex items-center justify-center text-gray-600 hover:text-red-500 hover:bg-red-500/10 transition-colors z-10">
<i data-lucide="x" class="w-3 h-3"></i>
</button>
<!-- Category Icon -->
<div class="cat-icon-wrapper ${cat.cssClass}" style="--cat-color: ${cat.color}; width:36px; height:36px; border-radius:10px; margin-top: 4px;">
<i data-lucide="${cat.icon}" class="w-5 h-5"></i>
</div>
<!-- Product Name -->
<div class="text-xs font-mono text-center w-full px-1 leading-tight ${isNeeded ? 'text-alien-glow' : 'text-gray-400'}" style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;">
${escapeHtml(product.name)}
</div>
<!-- Quantity Controls -->
<div class="flex items-center gap-1 w-full">
<button onclick="updateQty(${product.id}, -1)" class="qty-btn flex-1 h-7 rounded-lg bg-cosmic-900 border border-cosmic-500 flex items-center justify-center text-gray-400 hover:text-red-400 hover:border-red-400/50 transition-colors">
<i data-lucide="minus" class="w-3 h-3"></i>
</button>
<input type="number" value="${product.quantity}" min="0"
onchange="setQty(${product.id}, this.value)"
class="w-10 h-7 text-center rounded-lg bg-cosmic-900 border border-cosmic-500 text-xs font-alien font-bold focus:border-alien-glow focus:outline-none transition-colors ${product.quantity >= 1 ? 'text-alien-glow' : 'text-gray-500'}">
<button onclick="updateQty(${product.id}, 1)" class="qty-btn flex-1 h-7 rounded-lg bg-cosmic-900 border border-cosmic-500 flex items-center justify-center text-gray-400 hover:text-alien-glow hover:border-alien-glow/50 transition-colors">
<i data-lucide="plus" class="w-3 h-3"></i>
</button>
</div>
<!-- Needed / Full indicator -->
${isNeeded ? `
<div class="flex items-center gap-1 w-full justify-center">
<span class="w-1.5 h-1.5 rounded-full bg-alien-glow pulse-dot"></span>
<span class="text-[9px] font-alien uppercase text-alien-glow/70">Needed</span>
</div>
` : `
<div class="flex items-center gap-1 w-full justify-center">
<span class="text-[9px] font-alien uppercase text-gray-600">Full</span>
</div>
`}
</div>
`;
}
// ========== STATS ==========
function updateStats() {
const total = products.length;
const needed = products.filter(p => p.quantity >= 1).length;
const totalQty = products.reduce((sum, p) => sum + p.quantity, 0);
document.getElementById('stat-total').textContent = total;
document.getElementById('stat-needed').textContent = needed;
document.getElementById('stat-qty').textContent = totalQty;
}
// ========== SAVE ==========
function saveProducts() {
localStorage.setItem('roadside-products', JSON.stringify(products));
}
// ========== UTILS ==========
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ========== START ==========
document.addEventListener('DOMContentLoaded', init);
</script>
<script src="https://deepsite.hf.co/deepsite-badge.js"></script>
</body>
</html>