Spaces:
Running
Running
| <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> |