Build and Push Fridger Container / Build & Push Fridger Image (push) Successful in 1m36s
426 lines
22 KiB
HTML
426 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" class="h-full bg-slate-950 text-slate-100">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
<title>Fridger - Smart Fridge Manager</title>
|
|
|
|
<!-- Tailwind CSS -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
darkMode: 'class',
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
brand: {
|
|
400: '#2dd4bf',
|
|
500: '#14b8a6',
|
|
600: '#0d9488',
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<!-- Lucide Icons -->
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
|
|
<!-- HTML5 QR Code Scanner -->
|
|
<script src="https://unpkg.com/html5-qrcode" type="text/javascript"></script>
|
|
|
|
<link rel="stylesheet" href="/styles.css">
|
|
</head>
|
|
<body class="h-full flex flex-col bg-slate-950 text-slate-100 antialiased selection:bg-teal-500 selection:text-white">
|
|
|
|
<!-- TOP APP BAR -->
|
|
<header class="sticky top-0 z-30 bg-slate-900/95 backdrop-blur-md border-b border-slate-800">
|
|
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
|
<div class="flex items-center justify-between h-16 gap-3">
|
|
|
|
<!-- Brand -->
|
|
<div class="flex items-center gap-2.5 shrink-0">
|
|
<div class="w-9 h-9 rounded-xl bg-gradient-to-tr from-teal-500 to-cyan-400 flex items-center justify-center shadow-md shadow-teal-500/20 text-white font-black text-lg">
|
|
🧊
|
|
</div>
|
|
<div>
|
|
<h1 class="font-bold text-lg leading-none bg-gradient-to-r from-teal-300 to-cyan-300 bg-clip-text text-transparent">Fridger</h1>
|
|
<p class="text-[11px] text-slate-400 font-medium">Fridge Manager</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 2 MODES: VIEW & MANAGE -->
|
|
<div class="flex items-center bg-slate-950/80 p-1 rounded-xl border border-slate-800 shadow-inner">
|
|
<button id="mode-view-btn" onclick="switchMode('view')" class="flex items-center gap-2 px-3 sm:px-5 py-2 rounded-lg text-sm font-semibold transition bg-teal-500 text-white shadow-md">
|
|
<i data-lucide="eye" class="w-4 h-4"></i>
|
|
<span>Fridge View</span>
|
|
</button>
|
|
<button id="mode-manage-btn" onclick="switchMode('manage')" class="flex items-center gap-2 px-3 sm:px-5 py-2 rounded-lg text-sm font-semibold transition text-slate-300 hover:text-white">
|
|
<i data-lucide="sliders" class="w-4 h-4"></i>
|
|
<span>Manage</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- QUICK SCAN / ADD -->
|
|
<div class="flex items-center gap-2">
|
|
<button onclick="openScannerModal()" class="flex items-center gap-1.5 px-3 py-2 rounded-xl bg-teal-500 hover:bg-teal-400 text-white text-xs sm:text-sm font-semibold shadow-md transition active:scale-95">
|
|
<i data-lucide="scan-barcode" class="w-4 h-4"></i>
|
|
<span class="hidden sm:inline">Scan</span>
|
|
</button>
|
|
<button onclick="openItemModal()" class="flex items-center gap-1.5 px-3 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 border border-slate-700 text-slate-200 text-xs sm:text-sm font-medium transition active:scale-95">
|
|
<i data-lucide="plus" class="w-4 h-4"></i>
|
|
<span class="hidden sm:inline">Add</span>
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- SUMMARY STATS -->
|
|
<section class="bg-slate-900/40 border-b border-slate-800/60 py-2.5">
|
|
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
|
<div class="flex items-center justify-between text-xs text-slate-400 overflow-x-auto gap-4 scrollbar-none">
|
|
<div class="flex items-center gap-1.5 shrink-0">
|
|
<span class="w-2 h-2 rounded-full bg-teal-400"></span>
|
|
<span>In Fridge: <strong id="stat-total" class="text-white font-bold">0</strong></span>
|
|
</div>
|
|
<div class="flex items-center gap-1.5 shrink-0">
|
|
<span class="w-2 h-2 rounded-full bg-emerald-400"></span>
|
|
<span>Fresh: <strong id="stat-fresh" class="text-emerald-300 font-bold">0</strong></span>
|
|
</div>
|
|
<div class="flex items-center gap-1.5 shrink-0">
|
|
<span class="w-2 h-2 rounded-full bg-amber-400"></span>
|
|
<span>Expiring Soon: <strong id="stat-expiring" class="text-amber-300 font-bold">0</strong></span>
|
|
</div>
|
|
<div class="flex items-center gap-1.5 shrink-0">
|
|
<span class="w-2 h-2 rounded-full bg-indigo-400"></span>
|
|
<span>Opened: <strong id="stat-opened" class="text-indigo-300 font-bold">0</strong></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- SEARCH & STATUS BAR -->
|
|
<section class="max-w-6xl mx-auto px-4 sm:px-6 pt-4 pb-2 w-full space-y-3">
|
|
<div class="flex flex-col sm:flex-row gap-2.5 items-stretch sm:items-center justify-between">
|
|
|
|
<!-- Search Input -->
|
|
<div class="relative flex-1">
|
|
<i data-lucide="search" class="w-4 h-4 text-slate-400 absolute left-3.5 top-1/2 -translate-y-1/2"></i>
|
|
<input
|
|
type="text"
|
|
id="search-input"
|
|
placeholder="Search items by name, barcode or brand..."
|
|
oninput="handleSearch(this.value)"
|
|
class="w-full bg-slate-900 border border-slate-800 focus:border-teal-500 rounded-xl pl-9 pr-8 py-2 text-sm text-slate-100 placeholder-slate-500 outline-none transition"
|
|
>
|
|
<button id="clear-search" onclick="clearSearch()" class="hidden absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-white">
|
|
<i data-lucide="x" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Quick Status Filters -->
|
|
<div class="flex items-center gap-1 overflow-x-auto pb-0.5 text-xs font-medium scrollbar-none">
|
|
<button onclick="setStatusFilter('all')" data-status-btn="all" class="status-chip active px-3 py-1.5 rounded-lg bg-slate-800 border border-slate-700 text-white font-semibold">All</button>
|
|
<button onclick="setStatusFilter('fresh')" data-status-btn="fresh" class="status-chip px-3 py-1.5 rounded-lg bg-slate-900 border border-slate-800 text-slate-400 hover:text-slate-200">Fresh</button>
|
|
<button onclick="setStatusFilter('expiring_soon')" data-status-btn="expiring_soon" class="status-chip px-3 py-1.5 rounded-lg bg-slate-900 border border-slate-800 text-amber-400 hover:text-amber-300">Expiring</button>
|
|
<button onclick="setStatusFilter('low')" data-status-btn="low" class="status-chip px-3 py-1.5 rounded-lg bg-slate-900 border border-slate-800 text-indigo-400 hover:text-indigo-300">≤25% Left</button>
|
|
<button onclick="setStatusFilter('expired')" data-status-btn="expired" class="status-chip px-3 py-1.5 rounded-lg bg-slate-900 border border-slate-800 text-rose-400 hover:text-rose-300">Expired</button>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Category Filter Bar -->
|
|
<div id="category-filter-bar" class="flex items-center gap-1.5 overflow-x-auto pb-1 scrollbar-none text-xs font-medium">
|
|
<!-- Injected by JS -->
|
|
</div>
|
|
</section>
|
|
|
|
<!-- MAIN CONTENT AREA -->
|
|
<main class="flex-1 max-w-6xl mx-auto px-4 sm:px-6 py-3 w-full">
|
|
|
|
<!-- MODE 1: FRIDGE VIEW (Visual cards without drawers) -->
|
|
<div id="view-mode-container">
|
|
<div id="fridge-view-content" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<!-- Rendered items -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MODE 2: MANAGE INVENTORY (Simpler list with 3 primary buttons: Manage Grams, Add Pack, Remove Pack) -->
|
|
<div id="manage-mode-container" class="hidden space-y-3">
|
|
<div id="manage-items-list" class="space-y-3">
|
|
<!-- Rendered management items -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- EMPTY STATE -->
|
|
<div id="empty-state" class="hidden text-center py-16 px-4">
|
|
<div class="w-16 h-16 rounded-2xl bg-slate-900 text-slate-500 flex items-center justify-center mx-auto mb-3 border border-slate-800 text-2xl">
|
|
🧊
|
|
</div>
|
|
<h3 class="text-base font-bold text-white mb-1">Fridge is empty</h3>
|
|
<p class="text-xs text-slate-400 max-w-xs mx-auto mb-5">Scan a barcode or add an item to start tracking what's inside.</p>
|
|
<div class="flex items-center justify-center gap-2">
|
|
<button onclick="openScannerModal()" class="px-4 py-2 rounded-xl bg-teal-500 hover:bg-teal-400 text-white text-xs font-semibold flex items-center gap-1.5">
|
|
<i data-lucide="scan-barcode" class="w-4 h-4"></i>
|
|
<span>Scan Barcode</span>
|
|
</button>
|
|
<button onclick="seedDemoData()" class="px-3.5 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 border border-slate-700 text-slate-300 text-xs font-medium">
|
|
Load Sample Data
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<!-- MANAGE GRAMS / WEIGHT ADJUSTMENT MODAL -->
|
|
<div id="grams-modal" class="fixed inset-0 z-50 bg-black/75 backdrop-blur-sm hidden flex items-center justify-center p-4">
|
|
<div class="bg-slate-900 border border-slate-800 rounded-2xl max-w-md w-full p-5 shadow-2xl space-y-4 animate-scale-up">
|
|
<div class="flex items-start justify-between">
|
|
<div>
|
|
<span class="text-[11px] uppercase tracking-wider font-bold text-teal-400">Manage Amount (Grams / Partial)</span>
|
|
<h3 id="grams-modal-title" class="text-base font-bold text-white mt-0.5">Item Name</h3>
|
|
<p id="grams-modal-sub" class="text-xs text-slate-400">1 Full Packet = 250g</p>
|
|
</div>
|
|
<button onclick="closeGramsModal()" class="text-slate-400 hover:text-white p-1">
|
|
<i data-lucide="x" class="w-5 h-5"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Current Remaining Display & Slider -->
|
|
<div class="bg-slate-950 p-4 rounded-xl border border-slate-800/80 space-y-3.5 text-center">
|
|
<div>
|
|
<div class="text-3xl font-black text-white flex items-center justify-center gap-1">
|
|
<span id="grams-current-val">250</span>
|
|
<span id="grams-modal-unit" class="text-sm font-normal text-teal-400">g</span>
|
|
</div>
|
|
<div id="grams-percent-text" class="text-xs text-slate-400 mt-0.5">Remaining in open pack</div>
|
|
</div>
|
|
|
|
<input
|
|
type="range"
|
|
id="grams-slider"
|
|
min="0"
|
|
max="500"
|
|
step="5"
|
|
value="250"
|
|
oninput="onGramsSliderChange(this.value)"
|
|
class="w-full accent-teal-500 cursor-pointer h-2 bg-slate-800 rounded-lg"
|
|
>
|
|
|
|
<!-- Quick Decrement Buttons -->
|
|
<div class="flex items-center justify-center gap-1.5 pt-1 flex-wrap">
|
|
<button onclick="applyGramsDelta(-10)" class="px-2.5 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-xs text-slate-200 border border-slate-700 font-medium">-10</button>
|
|
<button onclick="applyGramsDelta(-25)" class="px-2.5 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-xs text-slate-200 border border-slate-700 font-medium">-25</button>
|
|
<button onclick="applyGramsDelta(-50)" class="px-2.5 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-xs text-slate-200 border border-slate-700 font-medium">-50</button>
|
|
<button onclick="applyGramsDelta(-100)" class="px-2.5 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-xs text-slate-200 border border-slate-700 font-medium">-100</button>
|
|
<button onclick="setGramsPercent(0.5)" class="px-2.5 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-xs text-slate-200 border border-slate-700 font-medium">Half</button>
|
|
<button onclick="setGramsPercent(0)" class="px-2.5 py-1.5 rounded-lg bg-rose-950/40 hover:bg-rose-900 text-xs text-rose-300 border border-rose-800 font-medium">Empty (0)</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Exact Input -->
|
|
<div class="flex items-center gap-3">
|
|
<label class="text-xs text-slate-400 shrink-0">Exact amount left:</label>
|
|
<div class="relative flex-1">
|
|
<input
|
|
type="number"
|
|
id="grams-exact-input"
|
|
min="0"
|
|
step="any"
|
|
oninput="onGramsExactInput(this.value)"
|
|
class="w-full bg-slate-950 border border-slate-800 rounded-lg px-3 py-1.5 text-sm text-white focus:border-teal-500 outline-none"
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between pt-2">
|
|
<button onclick="consumeAllAndRemove()" class="text-xs text-rose-400 hover:text-rose-300 flex items-center gap-1">
|
|
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
|
|
<span>Finished / Remove</span>
|
|
</button>
|
|
<div class="flex items-center gap-2">
|
|
<button onclick="closeGramsModal()" class="px-3.5 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-medium">Cancel</button>
|
|
<button onclick="saveGramsAdjustment()" class="px-5 py-2 rounded-xl bg-teal-500 hover:bg-teal-400 text-white text-xs font-semibold shadow-md">Save Grams</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ADD / EDIT ITEM MODAL -->
|
|
<div id="item-modal" class="fixed inset-0 z-50 bg-black/75 backdrop-blur-sm hidden flex items-center justify-center p-4 overflow-y-auto">
|
|
<div class="bg-slate-900 border border-slate-800 rounded-2xl max-w-lg w-full p-5 shadow-2xl space-y-4 my-8 animate-scale-up">
|
|
<div class="flex items-start justify-between">
|
|
<div>
|
|
<h3 id="item-modal-title" class="text-base font-bold text-white">Add Item</h3>
|
|
<p class="text-xs text-slate-400">Enter product or scan barcode</p>
|
|
</div>
|
|
<button onclick="closeItemModal()" class="text-slate-400 hover:text-white p-1">
|
|
<i data-lucide="x" class="w-5 h-5"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Barcode Search strip -->
|
|
<div class="bg-slate-950 p-2.5 rounded-xl border border-slate-800 flex items-center gap-2">
|
|
<div class="relative flex-1">
|
|
<i data-lucide="barcode" class="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>
|
|
<input
|
|
type="text"
|
|
id="modal-barcode"
|
|
placeholder="Barcode number (e.g. 4008452023451)"
|
|
class="w-full bg-slate-900 border border-slate-800 focus:border-teal-500 rounded-lg pl-9 pr-3 py-1.5 text-xs text-white outline-none"
|
|
>
|
|
</div>
|
|
<button type="button" onclick="lookupCurrentBarcode()" id="modal-barcode-lookup-btn" class="px-3 py-1.5 rounded-lg bg-teal-600 hover:bg-teal-500 text-white text-xs font-semibold shrink-0">
|
|
Lookup
|
|
</button>
|
|
<button type="button" onclick="openScannerModal(true)" class="p-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 shrink-0" title="Scan with Camera">
|
|
<i data-lucide="camera" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<form id="item-form" onsubmit="handleItemFormSubmit(event)" class="space-y-3 text-xs">
|
|
<input type="hidden" id="modal-item-id">
|
|
<input type="hidden" id="modal-image-url">
|
|
|
|
<!-- Product Image Preview if found -->
|
|
<div id="modal-image-container" class="hidden items-center gap-3 p-2 bg-slate-950 rounded-xl border border-slate-800">
|
|
<img id="modal-image-preview" src="" alt="Product" class="w-10 h-10 object-contain rounded-lg bg-white/5">
|
|
<div class="flex-1 min-w-0">
|
|
<span class="text-[11px] text-teal-400 font-semibold">Matched product image</span>
|
|
<p id="modal-image-label" class="text-xs text-slate-400 truncate"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Name & Brand -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
|
|
<div class="sm:col-span-2">
|
|
<label class="block text-slate-300 font-medium mb-1">Item Name *</label>
|
|
<input type="text" id="modal-name" required placeholder="e.g. Gouda Cheese" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3 py-2 text-white outline-none">
|
|
</div>
|
|
<div>
|
|
<label class="block text-slate-300 font-medium mb-1">Brand</label>
|
|
<input type="text" id="modal-brand" placeholder="e.g. Milbona" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3 py-2 text-white outline-none">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quantity & Unit (Whole Packet Size & Current Amount) -->
|
|
<div class="grid grid-cols-3 gap-2.5">
|
|
<div>
|
|
<label class="block text-slate-300 font-medium mb-1">Current Amount *</label>
|
|
<input type="number" id="modal-quantity" step="any" min="0" required placeholder="250" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3 py-2 text-white outline-none">
|
|
</div>
|
|
<div>
|
|
<label class="block text-slate-300 font-medium mb-1">1 Full Packet Size *</label>
|
|
<input type="number" id="modal-initial-quantity" step="any" min="0" required placeholder="250" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3 py-2 text-white outline-none">
|
|
</div>
|
|
<div>
|
|
<label class="block text-slate-300 font-medium mb-1">Unit</label>
|
|
<select id="modal-unit" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3 py-2 text-white outline-none">
|
|
<option value="g">Grams (g)</option>
|
|
<option value="ml">Milliliters (ml)</option>
|
|
<option value="pcs">Pieces (Stk)</option>
|
|
<option value="kg">Kilograms (kg)</option>
|
|
<option value="l">Liters (L)</option>
|
|
<option value="pack">Pack</option>
|
|
<option value="slices">Slices</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Category -->
|
|
<div>
|
|
<label class="block text-slate-300 font-medium mb-1">Category</label>
|
|
<select id="modal-category" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3 py-2 text-white outline-none">
|
|
<!-- Populated dynamically -->
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Expiration Date & Helpers -->
|
|
<div>
|
|
<div class="flex items-center justify-between mb-1">
|
|
<label class="block text-slate-300 font-medium">Expiration Date (MHD)</label>
|
|
<div class="flex items-center gap-1.5 text-[11px]">
|
|
<button type="button" onclick="setModalExpiryDays(3)" class="text-slate-400 hover:text-teal-400">+3d</button>
|
|
<span>•</span>
|
|
<button type="button" onclick="setModalExpiryDays(7)" class="text-slate-400 hover:text-teal-400">+1w</button>
|
|
<span>•</span>
|
|
<button type="button" onclick="setModalExpiryDays(14)" class="text-slate-400 hover:text-teal-400">+2w</button>
|
|
<span>•</span>
|
|
<button type="button" onclick="setModalExpiryDays(30)" class="text-slate-400 hover:text-teal-400">+1m</button>
|
|
</div>
|
|
</div>
|
|
<input type="date" id="modal-expiry" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3 py-2 text-white outline-none">
|
|
</div>
|
|
|
|
<!-- Notes -->
|
|
<div>
|
|
<label class="block text-slate-300 font-medium mb-1">Notes (Optional)</label>
|
|
<input type="text" id="modal-notes" placeholder="e.g. Opened for dinner" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3 py-2 text-white outline-none">
|
|
</div>
|
|
|
|
<div class="flex items-center justify-end gap-2.5 pt-3 border-t border-slate-800">
|
|
<button type="button" onclick="closeItemModal()" class="px-4 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-medium">Cancel</button>
|
|
<button type="submit" class="px-5 py-2 rounded-xl bg-teal-500 hover:bg-teal-400 text-white text-xs font-semibold shadow-md">Save Item</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- LIVE CAMERA BARCODE SCANNER MODAL -->
|
|
<div id="scanner-modal" class="fixed inset-0 z-50 bg-black/85 backdrop-blur-md hidden flex items-center justify-center p-4">
|
|
<div class="bg-slate-900 border border-slate-800 rounded-3xl max-w-md w-full overflow-hidden shadow-2xl flex flex-col animate-scale-up">
|
|
|
|
<div class="p-3.5 bg-slate-850 border-b border-slate-800 flex items-center justify-between">
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-7 h-7 rounded-lg bg-teal-500/10 text-teal-400 flex items-center justify-center">
|
|
<i data-lucide="scan-barcode" class="w-4 h-4"></i>
|
|
</div>
|
|
<span class="font-bold text-white text-sm">Scan Product Barcode</span>
|
|
</div>
|
|
<button onclick="closeScannerModal()" class="text-slate-400 hover:text-white p-1">
|
|
<i data-lucide="x" class="w-5 h-5"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Camera Preview Box -->
|
|
<div class="relative bg-black flex items-center justify-center min-h-[280px] w-full overflow-hidden">
|
|
<div id="barcode-reader" class="w-full"></div>
|
|
<div id="scanner-overlay" class="absolute inset-0 pointer-events-none border-2 border-teal-400/40 rounded-2xl m-8 flex items-center justify-center">
|
|
<div class="w-full h-0.5 bg-red-500/80 animate-pulse shadow-lg shadow-red-500"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scanner Status / Manual Fallback -->
|
|
<div class="p-3.5 bg-slate-900 space-y-2.5">
|
|
<div id="scanner-status" class="text-xs text-center text-slate-400">
|
|
Point camera at barcode
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<input
|
|
type="text"
|
|
id="scanner-manual-input"
|
|
placeholder="Or type barcode number manually..."
|
|
class="flex-1 bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3 py-2 text-xs text-white outline-none"
|
|
onkeydown="if(event.key === 'Enter') handleManualBarcodeSubmit()"
|
|
>
|
|
<button onclick="handleManualBarcodeSubmit()" class="px-3 py-2 rounded-xl bg-teal-600 hover:bg-teal-500 text-white text-xs font-semibold">
|
|
Lookup
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TOAST NOTIFICATION CONTAINER -->
|
|
<div id="toast-container" class="fixed bottom-5 right-5 z-50 flex flex-col gap-2 max-w-sm pointer-events-none"></div>
|
|
|
|
<!-- MAIN CLIENT SCRIPT -->
|
|
<script src="/app.js"></script>
|
|
</body>
|
|
</html>
|