feat(ui): optimize interface for small tablet with Main-Scan-Manage navigation
Build and Push Fridger Container / Build & Push Fridger Image (push) Successful in 1m23s
Build and Push Fridger Container / Build & Push Fridger Image (push) Successful in 1m23s
This commit is contained in:
+103
-90
@@ -1,10 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Fridger Frontend Application
|
* Fridger Frontend Application
|
||||||
* Simplified UI: Fridge View (drawer-free) and 3 primary actions: Manage Grams, Add Pack, Remove Pack.
|
* Tablet-optimized UI: Main -> Scan -> Manage navigation and 3-button product management.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
currentMode: 'view', // 'view' or 'manage'
|
currentTab: 'main', // 'main' or 'manage'
|
||||||
items: [],
|
items: [],
|
||||||
meta: {
|
meta: {
|
||||||
categories: [],
|
categories: [],
|
||||||
@@ -19,8 +19,7 @@ const state = {
|
|||||||
activeGramsItem: null,
|
activeGramsItem: null,
|
||||||
activeGramsValue: 0,
|
activeGramsValue: 0,
|
||||||
html5QrCode: null,
|
html5QrCode: null,
|
||||||
isScanning: false,
|
isScanning: false
|
||||||
openedFromModal: false
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize Application
|
// Initialize Application
|
||||||
@@ -88,25 +87,28 @@ async function loadItems() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
// Mode Switching & Filtering
|
// Navigation Tab Switching (Main -> Scan -> Manage)
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
|
|
||||||
function switchMode(mode) {
|
function switchTab(tab) {
|
||||||
state.currentMode = mode;
|
state.currentTab = tab;
|
||||||
|
|
||||||
const viewBtn = document.getElementById('mode-view-btn');
|
const mainBtn = document.getElementById('nav-main-btn');
|
||||||
const manageBtn = document.getElementById('mode-manage-btn');
|
const manageBtn = document.getElementById('nav-manage-btn');
|
||||||
const viewContainer = document.getElementById('view-mode-container');
|
const viewContainer = document.getElementById('view-tab-container');
|
||||||
const manageContainer = document.getElementById('manage-mode-container');
|
const manageContainer = document.getElementById('manage-tab-container');
|
||||||
|
|
||||||
if (mode === 'view') {
|
const activeClasses = 'flex items-center justify-center gap-2 px-3 sm:px-6 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all bg-teal-500 text-white shadow-md active:scale-95 touch-manipulation min-h-[44px]';
|
||||||
viewBtn.className = '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';
|
const inactiveClasses = 'flex items-center justify-center gap-2 px-3 sm:px-6 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all text-slate-300 hover:text-white hover:bg-slate-800/80 active:scale-95 touch-manipulation min-h-[44px]';
|
||||||
manageBtn.className = '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';
|
|
||||||
|
if (tab === 'main') {
|
||||||
|
mainBtn.className = activeClasses;
|
||||||
|
manageBtn.className = inactiveClasses;
|
||||||
viewContainer.classList.remove('hidden');
|
viewContainer.classList.remove('hidden');
|
||||||
manageContainer.classList.add('hidden');
|
manageContainer.classList.add('hidden');
|
||||||
} else {
|
} else if (tab === 'manage') {
|
||||||
manageBtn.className = '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';
|
manageBtn.className = activeClasses;
|
||||||
viewBtn.className = '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';
|
mainBtn.className = inactiveClasses;
|
||||||
manageContainer.classList.remove('hidden');
|
manageContainer.classList.remove('hidden');
|
||||||
viewContainer.classList.add('hidden');
|
viewContainer.classList.add('hidden');
|
||||||
}
|
}
|
||||||
@@ -141,9 +143,9 @@ function setStatusFilter(status) {
|
|||||||
state.filters.status = status;
|
state.filters.status = status;
|
||||||
document.querySelectorAll('[data-status-btn]').forEach(btn => {
|
document.querySelectorAll('[data-status-btn]').forEach(btn => {
|
||||||
if (btn.getAttribute('data-status-btn') === status) {
|
if (btn.getAttribute('data-status-btn') === status) {
|
||||||
btn.className = 'status-chip active px-3 py-1.5 rounded-lg bg-slate-800 border border-slate-700 text-white font-semibold';
|
btn.className = 'status-chip active px-3.5 py-2 rounded-xl bg-slate-800 border border-slate-700 text-white font-bold min-h-[38px] touch-manipulation';
|
||||||
} else {
|
} else {
|
||||||
btn.className = 'status-chip px-3 py-1.5 rounded-lg bg-slate-900 border border-slate-800 text-slate-400 hover:text-slate-200';
|
btn.className = 'status-chip px-3.5 py-2 rounded-xl bg-slate-900 border border-slate-800 text-slate-400 hover:text-slate-200 min-h-[38px] touch-manipulation';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
loadItems();
|
loadItems();
|
||||||
@@ -160,9 +162,9 @@ function renderCategoryFilters() {
|
|||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
let html = `
|
let html = `
|
||||||
<button onclick="setCategoryFilter('All')" class="px-2.5 py-1 rounded-lg border whitespace-nowrap transition text-xs ${
|
<button onclick="setCategoryFilter('All')" class="px-3.5 py-2 rounded-xl border whitespace-nowrap transition text-xs sm:text-sm min-h-[38px] touch-manipulation ${
|
||||||
state.filters.category === 'All'
|
state.filters.category === 'All'
|
||||||
? 'bg-teal-500/20 text-teal-300 border-teal-500/40 font-semibold'
|
? 'bg-teal-500/20 text-teal-300 border-teal-500/50 font-bold'
|
||||||
: 'bg-slate-900 text-slate-400 border-slate-800 hover:text-slate-200'
|
: 'bg-slate-900 text-slate-400 border-slate-800 hover:text-slate-200'
|
||||||
}">
|
}">
|
||||||
All Categories
|
All Categories
|
||||||
@@ -172,9 +174,9 @@ function renderCategoryFilters() {
|
|||||||
state.meta.categories.forEach(cat => {
|
state.meta.categories.forEach(cat => {
|
||||||
const isSelected = state.filters.category === cat.id;
|
const isSelected = state.filters.category === cat.id;
|
||||||
html += `
|
html += `
|
||||||
<button onclick="setCategoryFilter('${escapeHtml(cat.id)}')" class="px-2.5 py-1 rounded-lg border whitespace-nowrap transition text-xs ${
|
<button onclick="setCategoryFilter('${escapeHtml(cat.id)}')" class="px-3.5 py-2 rounded-xl border whitespace-nowrap transition text-xs sm:text-sm min-h-[38px] touch-manipulation ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-teal-500/20 text-teal-300 border-teal-500/40 font-semibold'
|
? 'bg-teal-500/20 text-teal-300 border-teal-500/50 font-bold'
|
||||||
: 'bg-slate-900 text-slate-400 border-slate-800 hover:text-slate-200'
|
: 'bg-slate-900 text-slate-400 border-slate-800 hover:text-slate-200'
|
||||||
}">
|
}">
|
||||||
${escapeHtml(cat.name)}
|
${escapeHtml(cat.name)}
|
||||||
@@ -195,7 +197,7 @@ function populateModalSelects() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
// Rendering UI
|
// UI Rendering
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
|
|
||||||
function renderApp() {
|
function renderApp() {
|
||||||
@@ -210,7 +212,7 @@ function renderApp() {
|
|||||||
|
|
||||||
emptyState.classList.add('hidden');
|
emptyState.classList.add('hidden');
|
||||||
|
|
||||||
if (state.currentMode === 'view') {
|
if (state.currentTab === 'main') {
|
||||||
renderFridgeView();
|
renderFridgeView();
|
||||||
} else {
|
} else {
|
||||||
renderManageView();
|
renderManageView();
|
||||||
@@ -219,7 +221,7 @@ function renderApp() {
|
|||||||
initLucide();
|
initLucide();
|
||||||
}
|
}
|
||||||
|
|
||||||
// MODE 1: Fridge View Mode (No drawers/shelves - clean grid)
|
// MAIN TAB: Fridge View (Tablet-optimized cards)
|
||||||
function renderFridgeView() {
|
function renderFridgeView() {
|
||||||
const container = document.getElementById('fridge-view-content');
|
const container = document.getElementById('fridge-view-content');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -235,65 +237,65 @@ function renderFridgeView() {
|
|||||||
else if (percent <= 50) progressBarColor = 'bg-amber-500';
|
else if (percent <= 50) progressBarColor = 'bg-amber-500';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="shelf-card rounded-2xl p-4 flex flex-col justify-between transition hover:border-teal-500/40 space-y-3">
|
<div class="shelf-card rounded-2xl sm:rounded-3xl p-4 sm:p-5 flex flex-col justify-between transition hover:border-teal-500/40 space-y-3.5">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<!-- Header: Brand & Expiry -->
|
<!-- Header: Brand & Expiry -->
|
||||||
<div class="flex items-center justify-between gap-2 mb-2">
|
<div class="flex items-center justify-between gap-2 mb-2">
|
||||||
<span class="text-[11px] font-semibold text-teal-400 bg-teal-500/10 px-2 py-0.5 rounded-md truncate max-w-[130px]">
|
<span class="text-xs font-bold text-teal-400 bg-teal-500/10 px-2.5 py-1 rounded-lg truncate max-w-[140px]">
|
||||||
${escapeHtml(item.brand || item.category)}
|
${escapeHtml(item.brand || item.category)}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-[11px] font-semibold px-2 py-0.5 rounded-md flex items-center gap-1 shrink-0 ${expiryInfo.badgeClass}">
|
<span class="text-xs font-bold px-2.5 py-1 rounded-lg flex items-center gap-1.5 shrink-0 ${expiryInfo.badgeClass}">
|
||||||
<i data-lucide="${expiryInfo.icon}" class="w-3 h-3"></i>
|
<i data-lucide="${expiryInfo.icon}" class="w-3.5 h-3.5"></i>
|
||||||
<span>${expiryInfo.label}</span>
|
<span>${expiryInfo.label}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Product Details & Image -->
|
<!-- Product Details & Image -->
|
||||||
<div class="flex items-start gap-3 my-1">
|
<div class="flex items-center gap-3.5 my-2">
|
||||||
${item.image_url ? `
|
${item.image_url ? `
|
||||||
<img src="${escapeHtml(item.image_url)}" alt="${escapeHtml(item.name)}" class="w-12 h-12 object-contain rounded-xl bg-white/5 p-1 border border-slate-700/60 shrink-0">
|
<img src="${escapeHtml(item.image_url)}" alt="${escapeHtml(item.name)}" class="w-14 h-14 object-contain rounded-2xl bg-white/5 p-1 border border-slate-700/60 shrink-0">
|
||||||
` : `
|
` : `
|
||||||
<div class="w-12 h-12 rounded-xl bg-slate-800/80 border border-slate-700 text-teal-400 flex items-center justify-center shrink-0">
|
<div class="w-14 h-14 rounded-2xl bg-slate-800/80 border border-slate-700 text-teal-400 flex items-center justify-center shrink-0">
|
||||||
<i data-lucide="${getCategoryIcon(item.category)}" class="w-6 h-6"></i>
|
<i data-lucide="${getCategoryIcon(item.category)}" class="w-7 h-7"></i>
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
|
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<h4 class="font-bold text-slate-100 text-sm leading-snug">${escapeHtml(item.name)}</h4>
|
<h4 class="font-bold text-white text-base leading-snug line-clamp-2">${escapeHtml(item.name)}</h4>
|
||||||
${item.notes ? `<p class="text-xs text-slate-400 italic mt-0.5 truncate">${escapeHtml(item.notes)}</p>` : ''}
|
${item.notes ? `<p class="text-xs text-slate-400 italic mt-0.5 truncate">${escapeHtml(item.notes)}</p>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Grams / Quantity Progress -->
|
<!-- Grams / Quantity Progress -->
|
||||||
<div class="mt-2.5 bg-slate-950/70 rounded-xl p-2.5 border border-slate-800/80">
|
<div class="mt-3 bg-slate-950/80 rounded-2xl p-3 border border-slate-800/80">
|
||||||
<div class="flex items-center justify-between text-xs mb-1">
|
<div class="flex items-center justify-between text-xs sm:text-sm mb-1.5">
|
||||||
<span class="font-bold text-white flex items-center gap-1">
|
<span class="font-bold text-white flex items-center gap-1">
|
||||||
<span class="text-base font-black text-teal-300">${item.quantity}</span>
|
<span class="text-lg font-black text-teal-300">${item.quantity}</span>
|
||||||
<span class="text-slate-400 font-normal">${item.unit}</span>
|
<span class="text-slate-400 font-normal">${item.unit}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="text-[11px] text-slate-400">
|
<span class="text-xs text-slate-400">
|
||||||
Pack: ${item.initial_quantity} ${item.unit} (${percent}%)
|
Pack: ${item.initial_quantity} ${item.unit} (${percent}%)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full bg-slate-800 h-1.5 rounded-full overflow-hidden">
|
<div class="w-full bg-slate-800 h-2 rounded-full overflow-hidden">
|
||||||
<div class="${progressBarColor} h-full rounded-full transition-all duration-300" style="width: ${percent}%;"></div>
|
<div class="${progressBarColor} h-full rounded-full transition-all duration-300" style="width: ${percent}%;"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 3 Primary Action Buttons: Add Pack, Remove Pack, Manage Grams -->
|
<!-- 3 Quick Tablet Action Buttons -->
|
||||||
<div class="grid grid-cols-3 gap-1.5 pt-2 border-t border-slate-800/80">
|
<div class="grid grid-cols-3 gap-2 pt-2 border-t border-slate-800/80">
|
||||||
<button onclick="addWholePacket(${item.id})" class="flex items-center justify-center gap-1 py-1.5 px-2 rounded-xl bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 text-xs font-semibold transition active:scale-95" title="Add 1 whole packet (+${item.initial_quantity}${item.unit})">
|
<button onclick="addWholePacket(${item.id})" class="flex items-center justify-center gap-1.5 py-2.5 px-2 rounded-xl bg-emerald-500/15 hover:bg-emerald-500/25 text-emerald-300 border border-emerald-500/30 text-xs sm:text-sm font-bold transition active:scale-95 touch-manipulation min-h-[42px]" title="Add 1 whole packet">
|
||||||
<i data-lucide="plus" class="w-3.5 h-3.5"></i>
|
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||||
<span>Add</span>
|
<span>Add</span>
|
||||||
</button>
|
</button>
|
||||||
<button onclick="removeWholePacket(${item.id})" class="flex items-center justify-center gap-1 py-1.5 px-2 rounded-xl bg-rose-500/10 hover:bg-rose-500/20 text-rose-300 border border-rose-500/30 text-xs font-semibold transition active:scale-95" title="Remove 1 whole packet (-${item.initial_quantity}${item.unit})">
|
<button onclick="removeWholePacket(${item.id})" class="flex items-center justify-center gap-1.5 py-2.5 px-2 rounded-xl bg-rose-500/15 hover:bg-rose-500/25 text-rose-300 border border-rose-500/30 text-xs sm:text-sm font-bold transition active:scale-95 touch-manipulation min-h-[42px]" title="Remove 1 whole packet">
|
||||||
<i data-lucide="minus" class="w-3.5 h-3.5"></i>
|
<i data-lucide="minus" class="w-4 h-4"></i>
|
||||||
<span>Remove</span>
|
<span>Remove</span>
|
||||||
</button>
|
</button>
|
||||||
<button onclick="openGramsModal(${item.id})" class="flex items-center justify-center gap-1 py-1.5 px-2 rounded-xl bg-teal-500/20 hover:bg-teal-500/30 text-teal-300 border border-teal-500/40 text-xs font-semibold transition active:scale-95" title="Adjust grams when not used up">
|
<button onclick="openGramsModal(${item.id})" class="flex items-center justify-center gap-1.5 py-2.5 px-2 rounded-xl bg-teal-500/20 hover:bg-teal-500/30 text-teal-300 border border-teal-500/40 text-xs sm:text-sm font-bold transition active:scale-95 touch-manipulation min-h-[42px]" title="Manage grams remaining">
|
||||||
<i data-lucide="sliders" class="w-3.5 h-3.5"></i>
|
<i data-lucide="sliders" class="w-4 h-4"></i>
|
||||||
<span>Manage</span>
|
<span>Manage</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -303,7 +305,7 @@ function renderFridgeView() {
|
|||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// MODE 2: Manage Mode (Simplified list with the 3 main buttons)
|
// MANAGE TAB: Manage Products Interface (Optimized for Tablet with Large Controls)
|
||||||
function renderManageView() {
|
function renderManageView() {
|
||||||
const container = document.getElementById('manage-items-list');
|
const container = document.getElementById('manage-items-list');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -315,61 +317,73 @@ function renderManageView() {
|
|||||||
: 100;
|
: 100;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="bg-slate-900 border border-slate-800 rounded-2xl p-3.5 shadow-md transition hover:border-slate-700 space-y-3">
|
<div class="bg-slate-900 border border-slate-800 rounded-2xl sm:rounded-3xl p-4 sm:p-5 shadow-lg transition hover:border-slate-700 space-y-4">
|
||||||
|
|
||||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
<div class="flex flex-col tablet:flex-row items-start tablet:items-center justify-between gap-4">
|
||||||
|
|
||||||
<!-- Item info -->
|
<!-- Product info -->
|
||||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
<div class="flex items-center gap-3.5 min-w-0 flex-1">
|
||||||
${item.image_url ? `
|
${item.image_url ? `
|
||||||
<img src="${escapeHtml(item.image_url)}" alt="${escapeHtml(item.name)}" class="w-11 h-11 object-contain rounded-xl bg-white/5 p-1 border border-slate-800 shrink-0">
|
<img src="${escapeHtml(item.image_url)}" alt="${escapeHtml(item.name)}" class="w-14 h-14 object-contain rounded-2xl bg-white/5 p-1 border border-slate-800 shrink-0">
|
||||||
` : `
|
` : `
|
||||||
<div class="w-11 h-11 rounded-xl bg-slate-800 border border-slate-700 text-teal-400 flex items-center justify-center shrink-0">
|
<div class="w-14 h-14 rounded-2xl bg-slate-800 border border-slate-700 text-teal-400 flex items-center justify-center shrink-0">
|
||||||
<i data-lucide="${getCategoryIcon(item.category)}" class="w-5 h-5"></i>
|
<i data-lucide="${getCategoryIcon(item.category)}" class="w-7 h-7"></i>
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
|
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="flex items-center gap-2 flex-wrap">
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
<h4 class="font-bold text-white text-sm truncate">${escapeHtml(item.name)}</h4>
|
<h4 class="font-extrabold text-white text-base sm:text-lg truncate">${escapeHtml(item.name)}</h4>
|
||||||
${item.brand ? `<span class="text-[11px] px-1.5 py-0.5 rounded bg-slate-800 text-slate-300">${escapeHtml(item.brand)}</span>` : ''}
|
${item.brand ? `<span class="text-xs px-2 py-0.5 rounded-lg bg-slate-800 text-slate-300 font-medium">${escapeHtml(item.brand)}</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-slate-400 flex items-center gap-2 mt-0.5 flex-wrap">
|
<div class="text-xs sm:text-sm text-slate-400 flex items-center gap-2.5 mt-1 flex-wrap">
|
||||||
<span class="font-semibold text-teal-300">${item.quantity} ${item.unit}</span>
|
<span class="font-black text-teal-300 text-sm sm:text-base">${item.quantity} ${item.unit}</span>
|
||||||
<span>(Pack size: ${item.initial_quantity} ${item.unit})</span>
|
<span>(Pack size: ${item.initial_quantity} ${item.unit})</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span class="${expiryInfo.badgeClass} px-1.5 py-0.2 rounded font-medium text-[11px]">${expiryInfo.label}</span>
|
<span class="${expiryInfo.badgeClass} px-2 py-0.5 rounded-lg font-bold text-xs">${expiryInfo.label}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- The 3 Core Action Buttons: Manage, Add Pack, Remove Pack (+ Edit/Delete icons) -->
|
<!-- The 3 Core Tablet Action Buttons: Add, Remove, Manage Product Grams -->
|
||||||
<div class="flex items-center gap-2 w-full sm:w-auto justify-end">
|
<div class="flex items-center gap-2 w-full tablet:w-auto justify-end flex-wrap">
|
||||||
|
|
||||||
<!-- 1. Add Whole Packet -->
|
<!-- 1. Add Whole Packet -->
|
||||||
<button onclick="addWholePacket(${item.id})" class="px-3 py-1.5 rounded-xl bg-emerald-500/15 hover:bg-emerald-500/25 text-emerald-300 border border-emerald-500/30 text-xs font-semibold flex items-center gap-1 transition active:scale-95" title="Add 1 Whole Packet">
|
<button
|
||||||
<i data-lucide="plus" class="w-3.5 h-3.5"></i>
|
onclick="addWholePacket(${item.id})"
|
||||||
|
class="flex-1 tablet:flex-initial px-4 py-3 rounded-2xl bg-emerald-500/15 hover:bg-emerald-500/25 text-emerald-300 border border-emerald-500/30 text-xs sm:text-sm font-bold flex items-center justify-center gap-1.5 transition active:scale-95 touch-manipulation min-h-[46px]"
|
||||||
|
title="Add 1 Whole Packet"
|
||||||
|
>
|
||||||
|
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||||
<span>Add</span>
|
<span>Add</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- 2. Remove Whole Packet -->
|
<!-- 2. Remove Whole Packet -->
|
||||||
<button onclick="removeWholePacket(${item.id})" class="px-3 py-1.5 rounded-xl bg-rose-500/15 hover:bg-rose-500/25 text-rose-300 border border-rose-500/30 text-xs font-semibold flex items-center gap-1 transition active:scale-95" title="Remove 1 Whole Packet">
|
<button
|
||||||
<i data-lucide="minus" class="w-3.5 h-3.5"></i>
|
onclick="removeWholePacket(${item.id})"
|
||||||
|
class="flex-1 tablet:flex-initial px-4 py-3 rounded-2xl bg-rose-500/15 hover:bg-rose-500/25 text-rose-300 border border-rose-500/30 text-xs sm:text-sm font-bold flex items-center justify-center gap-1.5 transition active:scale-95 touch-manipulation min-h-[46px]"
|
||||||
|
title="Remove 1 Whole Packet"
|
||||||
|
>
|
||||||
|
<i data-lucide="minus" class="w-4 h-4"></i>
|
||||||
<span>Remove</span>
|
<span>Remove</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- 3. Manage Grams / Partial Amount -->
|
<!-- 3. Manage Grams / Partial -->
|
||||||
<button onclick="openGramsModal(${item.id})" class="px-3.5 py-1.5 rounded-xl bg-teal-500 hover:bg-teal-400 text-white text-xs font-semibold flex items-center gap-1 shadow-sm transition active:scale-95" title="Adjust grams when partially used">
|
<button
|
||||||
<i data-lucide="sliders" class="w-3.5 h-3.5"></i>
|
onclick="openGramsModal(${item.id})"
|
||||||
|
class="flex-1 tablet:flex-initial px-5 py-3 rounded-2xl bg-teal-500 hover:bg-teal-400 text-white text-xs sm:text-sm font-bold flex items-center justify-center gap-1.5 shadow-md shadow-teal-500/20 transition active:scale-95 touch-manipulation min-h-[46px]"
|
||||||
|
title="Manage product grams"
|
||||||
|
>
|
||||||
|
<i data-lucide="sliders" class="w-4 h-4"></i>
|
||||||
<span>Manage</span>
|
<span>Manage</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Secondary: Edit / Delete -->
|
<!-- Secondary: Edit / Delete -->
|
||||||
<button onclick="openItemModal(${item.id})" class="p-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-400 hover:text-white border border-slate-700 text-xs ml-1" title="Edit Item Details">
|
<button onclick="openItemModal(${item.id})" class="p-3 rounded-2xl bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white border border-slate-700 min-h-[46px] min-w-[46px] flex items-center justify-center touch-manipulation" title="Edit details">
|
||||||
<i data-lucide="pencil" class="w-3.5 h-3.5"></i>
|
<i data-lucide="pencil" class="w-4 h-4"></i>
|
||||||
</button>
|
</button>
|
||||||
<button onclick="confirmDeleteItem(${item.id}, '${escapeHtml(item.name)}')" class="p-1.5 rounded-xl bg-slate-800 hover:bg-rose-900/40 text-slate-400 hover:text-rose-300 border border-slate-700 text-xs" title="Delete">
|
<button onclick="confirmDeleteItem(${item.id}, '${escapeHtml(item.name)}')" class="p-3 rounded-2xl bg-slate-800 hover:bg-rose-900/40 text-slate-400 hover:text-rose-300 border border-slate-700 min-h-[46px] min-w-[46px] flex items-center justify-center touch-manipulation" title="Delete">
|
||||||
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
|
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -382,7 +396,7 @@ function renderManageView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
// 3 Primary Action Handlers: Add Pack, Remove Pack, Manage Grams
|
// 3 Primary Action Handlers
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
|
|
||||||
async function addWholePacket(itemId) {
|
async function addWholePacket(itemId) {
|
||||||
@@ -395,7 +409,7 @@ async function addWholePacket(itemId) {
|
|||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const updated = await res.json();
|
const updated = await res.json();
|
||||||
showToast(`Added 1 packet of ${updated.name} (Now: ${updated.quantity} ${updated.unit})`, 'success');
|
showToast(`+1 Pack of ${updated.name} (Total: ${updated.quantity} ${updated.unit})`, 'success');
|
||||||
await loadStats();
|
await loadStats();
|
||||||
await loadItems();
|
await loadItems();
|
||||||
} else {
|
} else {
|
||||||
@@ -417,7 +431,7 @@ async function removeWholePacket(itemId) {
|
|||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const updated = await res.json();
|
const updated = await res.json();
|
||||||
showToast(`Removed 1 packet of ${updated.name} (Remaining: ${updated.quantity} ${updated.unit})`, 'info');
|
showToast(`-1 Pack of ${updated.name} (Remaining: ${updated.quantity} ${updated.unit})`, 'info');
|
||||||
await loadStats();
|
await loadStats();
|
||||||
await loadItems();
|
await loadItems();
|
||||||
} else {
|
} else {
|
||||||
@@ -520,7 +534,7 @@ async function saveGramsAdjustment() {
|
|||||||
showToast('Failed to update quantity', 'error');
|
showToast('Failed to update quantity', 'error');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error('Save grams error:', err);
|
||||||
showToast('Network error while updating quantity', 'error');
|
showToast('Network error while updating quantity', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -559,7 +573,7 @@ function openItemModal(itemId = null) {
|
|||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|
||||||
document.getElementById('modal-item-id').value = item.id;
|
document.getElementById('modal-item-id').value = item.id;
|
||||||
document.getElementById('item-modal-title').textContent = 'Edit Item';
|
document.getElementById('item-modal-title').textContent = 'Edit Product';
|
||||||
document.getElementById('modal-barcode').value = item.barcode || '';
|
document.getElementById('modal-barcode').value = item.barcode || '';
|
||||||
document.getElementById('modal-name').value = item.name || '';
|
document.getElementById('modal-name').value = item.name || '';
|
||||||
document.getElementById('modal-brand').value = item.brand || '';
|
document.getElementById('modal-brand').value = item.brand || '';
|
||||||
@@ -578,7 +592,7 @@ function openItemModal(itemId = null) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('modal-item-id').value = '';
|
document.getElementById('modal-item-id').value = '';
|
||||||
document.getElementById('item-modal-title').textContent = 'Add Item';
|
document.getElementById('item-modal-title').textContent = 'Add Product';
|
||||||
document.getElementById('modal-unit').value = 'g';
|
document.getElementById('modal-unit').value = 'g';
|
||||||
document.getElementById('modal-quantity').value = '';
|
document.getElementById('modal-quantity').value = '';
|
||||||
document.getElementById('modal-initial-quantity').value = '';
|
document.getElementById('modal-initial-quantity').value = '';
|
||||||
@@ -676,17 +690,17 @@ async function handleItemFormSubmit(event) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
showToast(itemId ? 'Item updated' : 'Item added to fridge', 'success');
|
showToast(itemId ? 'Product updated' : 'Product added to fridge', 'success');
|
||||||
closeItemModal();
|
closeItemModal();
|
||||||
await loadStats();
|
await loadStats();
|
||||||
await loadItems();
|
await loadItems();
|
||||||
} else {
|
} else {
|
||||||
const err = await res.json();
|
const err = await res.json();
|
||||||
showToast(err.error || 'Failed to save item', 'error');
|
showToast(err.error || 'Failed to save product', 'error');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
showToast('Network error while saving item', 'error');
|
showToast('Network error while saving product', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -701,17 +715,16 @@ async function confirmDeleteItem(itemId, name) {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
showToast('Failed to delete item', 'error');
|
showToast('Failed to delete product', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
// Live Camera Barcode Scanner
|
// Tablet Live Camera Barcode Scanner
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
|
|
||||||
function openScannerModal(fromModal = false) {
|
function openScannerModal() {
|
||||||
state.openedFromModal = fromModal;
|
|
||||||
document.getElementById('scanner-modal').classList.remove('hidden');
|
document.getElementById('scanner-modal').classList.remove('hidden');
|
||||||
document.getElementById('scanner-manual-input').value = '';
|
document.getElementById('scanner-manual-input').value = '';
|
||||||
document.getElementById('scanner-status').textContent = 'Initializing camera...';
|
document.getElementById('scanner-status').textContent = 'Initializing camera...';
|
||||||
@@ -739,7 +752,7 @@ async function startCameraScanner() {
|
|||||||
state.html5QrCode = new Html5Qrcode("barcode-reader");
|
state.html5QrCode = new Html5Qrcode("barcode-reader");
|
||||||
const config = {
|
const config = {
|
||||||
fps: 10,
|
fps: 10,
|
||||||
qrbox: { width: 240, height: 150 },
|
qrbox: { width: 260, height: 160 },
|
||||||
aspectRatio: 1.333
|
aspectRatio: 1.333
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -751,7 +764,7 @@ async function startCameraScanner() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
state.isScanning = true;
|
state.isScanning = true;
|
||||||
document.getElementById('scanner-status').textContent = 'Point camera at barcode.';
|
document.getElementById('scanner-status').textContent = 'Point tablet camera at barcode.';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Camera start error:', err);
|
console.warn('Camera start error:', err);
|
||||||
document.getElementById('scanner-status').textContent = 'Camera unavailable. Please enter barcode number below.';
|
document.getElementById('scanner-status').textContent = 'Camera unavailable. Please enter barcode number below.';
|
||||||
@@ -898,7 +911,7 @@ function showToast(message, type = 'info') {
|
|||||||
info: 'bg-slate-800 text-slate-100 border-slate-700'
|
info: 'bg-slate-800 text-slate-100 border-slate-700'
|
||||||
};
|
};
|
||||||
|
|
||||||
toast.className = `p-3 rounded-xl border shadow-xl text-xs font-medium flex items-center gap-2 transform transition-all duration-300 translate-y-2 opacity-0 pointer-events-auto ${colors[type] || colors.info}`;
|
toast.className = `p-3.5 rounded-2xl border shadow-2xl text-xs sm:text-sm font-semibold flex items-center gap-2 transform transition-all duration-300 translate-y-2 opacity-0 pointer-events-auto ${colors[type] || colors.info}`;
|
||||||
toast.innerHTML = `<span>${escapeHtml(message)}</span>`;
|
toast.innerHTML = `<span>${escapeHtml(message)}</span>`;
|
||||||
|
|
||||||
container.appendChild(toast);
|
container.appendChild(toast);
|
||||||
|
|||||||
+186
-159
@@ -18,6 +18,9 @@
|
|||||||
500: '#14b8a6',
|
500: '#14b8a6',
|
||||||
600: '#0d9488',
|
600: '#0d9488',
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
screens: {
|
||||||
|
'tablet': '640px',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,81 +35,102 @@
|
|||||||
|
|
||||||
<link rel="stylesheet" href="/styles.css">
|
<link rel="stylesheet" href="/styles.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="h-full flex flex-col bg-slate-950 text-slate-100 antialiased selection:bg-teal-500 selection:text-white">
|
<body class="h-full flex flex-col bg-slate-950 text-slate-100 antialiased selection:bg-teal-500 selection:text-white select-none">
|
||||||
|
|
||||||
<!-- TOP APP BAR -->
|
<!-- TOP TABLET APP BAR -->
|
||||||
<header class="sticky top-0 z-30 bg-slate-900/95 backdrop-blur-md border-b border-slate-800">
|
<header class="sticky top-0 z-30 bg-slate-900/95 backdrop-blur-md border-b border-slate-800 shadow-md">
|
||||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
<div class="max-w-5xl mx-auto px-3 sm:px-6">
|
||||||
<div class="flex items-center justify-between h-16 gap-3">
|
<div class="flex items-center justify-between h-16 sm:h-20 gap-2">
|
||||||
|
|
||||||
<!-- Brand -->
|
<!-- Brand Logo (Compact & Clean) -->
|
||||||
<div class="flex items-center gap-2.5 shrink-0">
|
<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 class="w-10 h-10 sm:w-11 sm:h-11 rounded-2xl bg-gradient-to-tr from-teal-500 to-cyan-400 flex items-center justify-center shadow-lg shadow-teal-500/20 text-white font-black text-xl">
|
||||||
🧊
|
🧊
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="hidden xs:block">
|
||||||
<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>
|
<h1 class="font-extrabold text-lg sm:text-xl 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>
|
<p class="text-[11px] text-slate-400 font-medium">Smart Fridge</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 2 MODES: VIEW & MANAGE -->
|
<!-- 3 MAIN TABLET BUTTONS: MAIN -> SCAN -> MANAGE -->
|
||||||
<div class="flex items-center bg-slate-950/80 p-1 rounded-xl border border-slate-800 shadow-inner">
|
<nav class="flex items-center bg-slate-950 p-1.5 rounded-2xl border border-slate-800 shadow-inner gap-1">
|
||||||
<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>
|
<!-- 1. MAIN (Fridge View) -->
|
||||||
<span>Fridge View</span>
|
<button
|
||||||
|
id="nav-main-btn"
|
||||||
|
onclick="switchTab('main')"
|
||||||
|
class="flex items-center justify-center gap-2 px-3 sm:px-6 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all bg-teal-500 text-white shadow-md active:scale-95 touch-manipulation min-h-[44px]"
|
||||||
|
>
|
||||||
|
<i data-lucide="home" class="w-4 h-4 sm:w-5 sm:h-5"></i>
|
||||||
|
<span>Main</span>
|
||||||
</button>
|
</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>
|
<!-- 2. SCAN (Live Barcode Camera) -->
|
||||||
|
<button
|
||||||
|
id="nav-scan-btn"
|
||||||
|
onclick="openScannerModal()"
|
||||||
|
class="flex items-center justify-center gap-2 px-3 sm:px-6 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all text-slate-300 hover:text-white hover:bg-slate-800/80 active:scale-95 touch-manipulation min-h-[44px]"
|
||||||
|
>
|
||||||
|
<i data-lucide="scan-barcode" class="w-4 h-4 sm:w-5 sm:h-5 text-teal-400"></i>
|
||||||
|
<span>Scan</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 3. MANAGE (Manage Products) -->
|
||||||
|
<button
|
||||||
|
id="nav-manage-btn"
|
||||||
|
onclick="switchTab('manage')"
|
||||||
|
class="flex items-center justify-center gap-2 px-3 sm:px-6 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all text-slate-300 hover:text-white hover:bg-slate-800/80 active:scale-95 touch-manipulation min-h-[44px]"
|
||||||
|
>
|
||||||
|
<i data-lucide="package" class="w-4 h-4 sm:w-5 sm:h-5"></i>
|
||||||
<span>Manage</span>
|
<span>Manage</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- QUICK SCAN / ADD -->
|
</nav>
|
||||||
<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">
|
<!-- Quick Add Item Button -->
|
||||||
<i data-lucide="scan-barcode" class="w-4 h-4"></i>
|
<button
|
||||||
<span class="hidden sm:inline">Scan</span>
|
onclick="openItemModal()"
|
||||||
</button>
|
class="flex items-center justify-center gap-1.5 px-3 sm:px-4 py-2.5 sm:py-3 rounded-xl bg-slate-800 hover:bg-slate-700 border border-slate-700 text-slate-200 text-xs sm:text-sm font-semibold transition active:scale-95 min-h-[44px] touch-manipulation shrink-0"
|
||||||
<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">
|
title="Add Product Manually"
|
||||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
>
|
||||||
<span class="hidden sm:inline">Add</span>
|
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||||
</button>
|
<span class="hidden sm:inline">Add</span>
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- SUMMARY STATS -->
|
<!-- TABLET QUICK STATS BAR -->
|
||||||
<section class="bg-slate-900/40 border-b border-slate-800/60 py-2.5">
|
<section class="bg-slate-900/50 border-b border-slate-800/60 py-2 sm:py-2.5">
|
||||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
<div class="max-w-5xl mx-auto px-3 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 justify-between text-xs sm:text-sm text-slate-400 overflow-x-auto gap-4 scrollbar-none">
|
||||||
<div class="flex items-center gap-1.5 shrink-0">
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
<span class="w-2 h-2 rounded-full bg-teal-400"></span>
|
<span class="w-2.5 h-2.5 rounded-full bg-teal-400"></span>
|
||||||
<span>In Fridge: <strong id="stat-total" class="text-white font-bold">0</strong></span>
|
<span>In Fridge: <strong id="stat-total" class="text-white font-black text-sm sm:text-base">0</strong></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5 shrink-0">
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
<span class="w-2 h-2 rounded-full bg-emerald-400"></span>
|
<span class="w-2.5 h-2.5 rounded-full bg-emerald-400"></span>
|
||||||
<span>Fresh: <strong id="stat-fresh" class="text-emerald-300 font-bold">0</strong></span>
|
<span>Fresh: <strong id="stat-fresh" class="text-emerald-300 font-bold">0</strong></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5 shrink-0">
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
<span class="w-2 h-2 rounded-full bg-amber-400"></span>
|
<span class="w-2.5 h-2.5 rounded-full bg-amber-400"></span>
|
||||||
<span>Expiring Soon: <strong id="stat-expiring" class="text-amber-300 font-bold">0</strong></span>
|
<span>Expiring Soon: <strong id="stat-expiring" class="text-amber-300 font-bold">0</strong></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5 shrink-0">
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
<span class="w-2 h-2 rounded-full bg-indigo-400"></span>
|
<span class="w-2.5 h-2.5 rounded-full bg-indigo-400"></span>
|
||||||
<span>Opened: <strong id="stat-opened" class="text-indigo-300 font-bold">0</strong></span>
|
<span>Opened: <strong id="stat-opened" class="text-indigo-300 font-bold">0</strong></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- SEARCH & STATUS BAR -->
|
<!-- SEARCH & FILTER BAR -->
|
||||||
<section class="max-w-6xl mx-auto px-4 sm:px-6 pt-4 pb-2 w-full space-y-3">
|
<section class="max-w-5xl mx-auto px-3 sm:px-6 pt-3 sm:pt-4 pb-2 w-full space-y-2.5">
|
||||||
<div class="flex flex-col sm:flex-row gap-2.5 items-stretch sm:items-center justify-between">
|
<div class="flex flex-col sm:flex-row gap-2.5 items-stretch sm:items-center justify-between">
|
||||||
|
|
||||||
<!-- Search Input -->
|
<!-- Search Input (Large tablet hit area) -->
|
||||||
<div class="relative flex-1">
|
<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>
|
<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
|
<input
|
||||||
@@ -114,91 +138,93 @@
|
|||||||
id="search-input"
|
id="search-input"
|
||||||
placeholder="Search items by name, barcode or brand..."
|
placeholder="Search items by name, barcode or brand..."
|
||||||
oninput="handleSearch(this.value)"
|
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"
|
class="w-full bg-slate-900 border border-slate-800 focus:border-teal-500 rounded-xl pl-9 pr-9 py-2.5 text-sm sm:text-base 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">
|
<button id="clear-search" onclick="clearSearch()" class="hidden absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-white p-1">
|
||||||
<i data-lucide="x" class="w-4 h-4"></i>
|
<i data-lucide="x" class="w-4 h-4"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quick Status Filters -->
|
<!-- Quick Status Filters -->
|
||||||
<div class="flex items-center gap-1 overflow-x-auto pb-0.5 text-xs font-medium scrollbar-none">
|
<div class="flex items-center gap-1.5 overflow-x-auto pb-0.5 text-xs sm:text-sm 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('all')" data-status-btn="all" class="status-chip active px-3.5 py-2 rounded-xl bg-slate-800 border border-slate-700 text-white font-bold min-h-[38px] touch-manipulation">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('fresh')" data-status-btn="fresh" class="status-chip px-3.5 py-2 rounded-xl bg-slate-900 border border-slate-800 text-slate-400 hover:text-slate-200 min-h-[38px] touch-manipulation">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('expiring_soon')" data-status-btn="expiring_soon" class="status-chip px-3.5 py-2 rounded-xl bg-slate-900 border border-slate-800 text-amber-400 hover:text-amber-300 min-h-[38px] touch-manipulation">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('low')" data-status-btn="low" class="status-chip px-3.5 py-2 rounded-xl bg-slate-900 border border-slate-800 text-indigo-400 hover:text-indigo-300 min-h-[38px] touch-manipulation">≤25%</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>
|
<button onclick="setStatusFilter('expired')" data-status-btn="expired" class="status-chip px-3.5 py-2 rounded-xl bg-slate-900 border border-slate-800 text-rose-400 hover:text-rose-300 min-h-[38px] touch-manipulation">Expired</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Category Filter Bar -->
|
<!-- 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">
|
<div id="category-filter-bar" class="flex items-center gap-1.5 overflow-x-auto pb-1 scrollbar-none text-xs sm:text-sm font-medium">
|
||||||
<!-- Injected by JS -->
|
<!-- Populated dynamically -->
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- MAIN CONTENT AREA -->
|
<!-- MAIN VIEWPORT -->
|
||||||
<main class="flex-1 max-w-6xl mx-auto px-4 sm:px-6 py-3 w-full">
|
<main class="flex-1 max-w-5xl mx-auto px-3 sm:px-6 py-3 w-full">
|
||||||
|
|
||||||
<!-- MODE 1: FRIDGE VIEW (Visual cards without drawers) -->
|
<!-- 1. MAIN TAB: FRIDGE VIEW (Visual touch-friendly grid) -->
|
||||||
<div id="view-mode-container">
|
<div id="view-tab-container">
|
||||||
<div id="fridge-view-content" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div id="fridge-view-content" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3.5 sm:gap-4">
|
||||||
<!-- Rendered items -->
|
<!-- Rendered items -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- MODE 2: MANAGE INVENTORY (Simpler list with 3 primary buttons: Manage Grams, Add Pack, Remove Pack) -->
|
<!-- 2. MANAGE TAB: MANAGE PRODUCTS (Large tablet touch actions: Add Pack, Remove Pack, Manage Grams) -->
|
||||||
<div id="manage-mode-container" class="hidden space-y-3">
|
<div id="manage-tab-container" class="hidden space-y-3">
|
||||||
<div id="manage-items-list" class="space-y-3">
|
<div id="manage-items-list" class="space-y-3">
|
||||||
<!-- Rendered management items -->
|
<!-- Rendered management rows -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- EMPTY STATE -->
|
<!-- EMPTY STATE -->
|
||||||
<div id="empty-state" class="hidden text-center py-16 px-4">
|
<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 class="w-16 h-16 sm:w-20 sm:h-20 rounded-2xl bg-slate-900 text-slate-500 flex items-center justify-center mx-auto mb-3 border border-slate-800 text-3xl">
|
||||||
🧊
|
🧊
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-base font-bold text-white mb-1">Fridge is empty</h3>
|
<h3 class="text-lg font-bold text-white mb-1">Your 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>
|
<p class="text-xs sm:text-sm text-slate-400 max-w-xs mx-auto mb-6">Scan a barcode or add an item to track quantities and grams.</p>
|
||||||
<div class="flex items-center justify-center gap-2">
|
<div class="flex items-center justify-center gap-3">
|
||||||
<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">
|
<button onclick="openScannerModal()" class="px-5 py-3 rounded-xl bg-teal-500 hover:bg-teal-400 text-white text-sm font-bold flex items-center gap-2 shadow-lg shadow-teal-500/20 active:scale-95 touch-manipulation">
|
||||||
<i data-lucide="scan-barcode" class="w-4 h-4"></i>
|
<i data-lucide="scan-barcode" class="w-5 h-5"></i>
|
||||||
<span>Scan Barcode</span>
|
<span>Scan Barcode</span>
|
||||||
</button>
|
</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">
|
<button onclick="seedDemoData()" class="px-4 py-3 rounded-xl bg-slate-800 hover:bg-slate-700 border border-slate-700 text-slate-300 text-sm font-medium active:scale-95 touch-manipulation">
|
||||||
Load Sample Data
|
Load Sample Fridge
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- MANAGE GRAMS / WEIGHT ADJUSTMENT MODAL -->
|
<!-- MANAGE GRAMS / WEIGHT ADJUSTMENT MODAL (Optimized for Tablet Touch) -->
|
||||||
<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 id="grams-modal" class="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm hidden flex items-center justify-center p-3 sm: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="bg-slate-900 border border-slate-800 rounded-3xl max-w-md w-full p-5 sm:p-6 shadow-2xl space-y-5 animate-scale-up">
|
||||||
|
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<span class="text-[11px] uppercase tracking-wider font-bold text-teal-400">Manage Amount (Grams / Partial)</span>
|
<span class="text-xs uppercase tracking-wider font-bold text-teal-400">Manage Grams / Amount</span>
|
||||||
<h3 id="grams-modal-title" class="text-base font-bold text-white mt-0.5">Item Name</h3>
|
<h3 id="grams-modal-title" class="text-lg sm:text-xl font-extrabold text-white mt-0.5">Item Name</h3>
|
||||||
<p id="grams-modal-sub" class="text-xs text-slate-400">1 Full Packet = 250g</p>
|
<p id="grams-modal-sub" class="text-xs sm:text-sm text-slate-400">1 Full Packet = 250g</p>
|
||||||
</div>
|
</div>
|
||||||
<button onclick="closeGramsModal()" class="text-slate-400 hover:text-white p-1">
|
<button onclick="closeGramsModal()" class="text-slate-400 hover:text-white p-2 rounded-xl bg-slate-800/60 touch-manipulation">
|
||||||
<i data-lucide="x" class="w-5 h-5"></i>
|
<i data-lucide="x" class="w-5 h-5"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Current Remaining Display & Slider -->
|
<!-- Live Remaining Big Gauge & Slider -->
|
||||||
<div class="bg-slate-950 p-4 rounded-xl border border-slate-800/80 space-y-3.5 text-center">
|
<div class="bg-slate-950 p-4 sm:p-5 rounded-2xl border border-slate-800/90 space-y-4 text-center">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-3xl font-black text-white flex items-center justify-center gap-1">
|
<div class="text-4xl sm:text-5xl font-black text-white flex items-center justify-center gap-1.5">
|
||||||
<span id="grams-current-val">250</span>
|
<span id="grams-current-val">250</span>
|
||||||
<span id="grams-modal-unit" class="text-sm font-normal text-teal-400">g</span>
|
<span id="grams-modal-unit" class="text-base sm:text-lg font-normal text-teal-400">g</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="grams-percent-text" class="text-xs text-slate-400 mt-0.5">Remaining in open pack</div>
|
<div id="grams-percent-text" class="text-xs sm:text-sm text-slate-400 mt-1 font-medium">Remaining in open pack</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Big touch-friendly slider -->
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
id="grams-slider"
|
id="grams-slider"
|
||||||
@@ -207,23 +233,23 @@
|
|||||||
step="5"
|
step="5"
|
||||||
value="250"
|
value="250"
|
||||||
oninput="onGramsSliderChange(this.value)"
|
oninput="onGramsSliderChange(this.value)"
|
||||||
class="w-full accent-teal-500 cursor-pointer h-2 bg-slate-800 rounded-lg"
|
class="w-full accent-teal-500 cursor-pointer h-3.5 bg-slate-800 rounded-xl"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- Quick Decrement Buttons -->
|
<!-- Large Tablet Touch Quick Buttons -->
|
||||||
<div class="flex items-center justify-center gap-1.5 pt-1 flex-wrap">
|
<div class="grid grid-cols-3 sm:grid-cols-6 gap-2 pt-1">
|
||||||
<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(-10)" class="py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs sm:text-sm text-slate-200 border border-slate-700 font-bold active:scale-95 touch-manipulation">-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(-25)" class="py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs sm:text-sm text-slate-200 border border-slate-700 font-bold active:scale-95 touch-manipulation">-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(-50)" class="py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs sm:text-sm text-slate-200 border border-slate-700 font-bold active:scale-95 touch-manipulation">-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="applyGramsDelta(-100)" class="py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs sm:text-sm text-slate-200 border border-slate-700 font-bold active:scale-95 touch-manipulation">-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.5)" class="py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs sm:text-sm text-slate-200 border border-slate-700 font-bold active:scale-95 touch-manipulation">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>
|
<button onclick="setGramsPercent(0)" class="py-2.5 rounded-xl bg-rose-950/40 hover:bg-rose-900 text-xs sm:text-sm text-rose-300 border border-rose-800 font-bold active:scale-95 touch-manipulation">Empty (0)</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Exact Input -->
|
<!-- Exact Numeric Input -->
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<label class="text-xs text-slate-400 shrink-0">Exact amount left:</label>
|
<label class="text-xs sm:text-sm text-slate-300 font-medium shrink-0">Exact amount:</label>
|
||||||
<div class="relative flex-1">
|
<div class="relative flex-1">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -231,65 +257,66 @@
|
|||||||
min="0"
|
min="0"
|
||||||
step="any"
|
step="any"
|
||||||
oninput="onGramsExactInput(this.value)"
|
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"
|
class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2.5 text-base sm:text-lg font-bold text-white focus:border-teal-500 outline-none"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between pt-2">
|
<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">
|
<button onclick="consumeAllAndRemove()" class="text-xs sm:text-sm text-rose-400 hover:text-rose-300 flex items-center gap-1.5 p-2 rounded-xl hover:bg-rose-500/10 touch-manipulation">
|
||||||
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
|
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||||
<span>Finished / Remove</span>
|
<span>Remove Item</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex items-center gap-2">
|
<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="closeGramsModal()" class="px-4 py-3 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs sm:text-sm font-medium touch-manipulation">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>
|
<button onclick="saveGramsAdjustment()" class="px-6 py-3 rounded-xl bg-teal-500 hover:bg-teal-400 text-white text-xs sm:text-sm font-bold shadow-lg shadow-teal-500/20 active:scale-95 touch-manipulation">Save Grams</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ADD / EDIT ITEM MODAL -->
|
<!-- 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 id="item-modal" class="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm hidden flex items-center justify-center p-3 sm: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="bg-slate-900 border border-slate-800 rounded-3xl max-w-lg w-full p-5 sm:p-6 shadow-2xl space-y-4 my-8 animate-scale-up">
|
||||||
|
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 id="item-modal-title" class="text-base font-bold text-white">Add Item</h3>
|
<h3 id="item-modal-title" class="text-base sm:text-lg font-bold text-white">Add Product</h3>
|
||||||
<p class="text-xs text-slate-400">Enter product or scan barcode</p>
|
<p class="text-xs text-slate-400">Scan or enter product details</p>
|
||||||
</div>
|
</div>
|
||||||
<button onclick="closeItemModal()" class="text-slate-400 hover:text-white p-1">
|
<button onclick="closeItemModal()" class="text-slate-400 hover:text-white p-2 rounded-xl bg-slate-800/60 touch-manipulation">
|
||||||
<i data-lucide="x" class="w-5 h-5"></i>
|
<i data-lucide="x" class="w-5 h-5"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Barcode Search strip -->
|
<!-- Barcode Search strip -->
|
||||||
<div class="bg-slate-950 p-2.5 rounded-xl border border-slate-800 flex items-center gap-2">
|
<div class="bg-slate-950 p-2.5 sm:p-3 rounded-2xl border border-slate-800 flex items-center gap-2">
|
||||||
<div class="relative flex-1">
|
<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>
|
<i data-lucide="barcode" class="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="modal-barcode"
|
id="modal-barcode"
|
||||||
placeholder="Barcode number (e.g. 4008452023451)"
|
placeholder="Barcode (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"
|
class="w-full bg-slate-900 border border-slate-800 focus:border-teal-500 rounded-xl pl-9 pr-3 py-2 text-xs sm:text-sm text-white outline-none"
|
||||||
>
|
>
|
||||||
</div>
|
</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">
|
<button type="button" onclick="lookupCurrentBarcode()" id="modal-barcode-lookup-btn" class="px-3.5 py-2 rounded-xl bg-teal-600 hover:bg-teal-500 text-white text-xs sm:text-sm font-bold shrink-0 min-h-[38px] touch-manipulation">
|
||||||
Lookup
|
Lookup
|
||||||
</button>
|
</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">
|
<button type="button" onclick="openScannerModal(true)" class="p-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 shrink-0 min-h-[38px] min-w-[38px] flex items-center justify-center touch-manipulation" title="Scan with Camera">
|
||||||
<i data-lucide="camera" class="w-4 h-4"></i>
|
<i data-lucide="camera" class="w-4 h-4"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="item-form" onsubmit="handleItemFormSubmit(event)" class="space-y-3 text-xs">
|
<form id="item-form" onsubmit="handleItemFormSubmit(event)" class="space-y-3.5 text-xs sm:text-sm">
|
||||||
<input type="hidden" id="modal-item-id">
|
<input type="hidden" id="modal-item-id">
|
||||||
<input type="hidden" id="modal-image-url">
|
<input type="hidden" id="modal-image-url">
|
||||||
|
|
||||||
<!-- Product Image Preview if found -->
|
<!-- Image Preview if available -->
|
||||||
<div id="modal-image-container" class="hidden items-center gap-3 p-2 bg-slate-950 rounded-xl border border-slate-800">
|
<div id="modal-image-container" class="hidden items-center gap-3 p-2.5 bg-slate-950 rounded-2xl border border-slate-800">
|
||||||
<img id="modal-image-preview" src="" alt="Product" class="w-10 h-10 object-contain rounded-lg bg-white/5">
|
<img id="modal-image-preview" src="" alt="Product" class="w-12 h-12 object-contain rounded-xl bg-white/5">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<span class="text-[11px] text-teal-400 font-semibold">Matched product image</span>
|
<span class="text-xs text-teal-400 font-bold">Product Match</span>
|
||||||
<p id="modal-image-label" class="text-xs text-slate-400 truncate"></p>
|
<p id="modal-image-label" class="text-xs text-slate-400 truncate"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -297,28 +324,28 @@
|
|||||||
<!-- Name & Brand -->
|
<!-- Name & Brand -->
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
|
||||||
<div class="sm:col-span-2">
|
<div class="sm:col-span-2">
|
||||||
<label class="block text-slate-300 font-medium mb-1">Item Name *</label>
|
<label class="block text-slate-300 font-semibold mb-1">Product 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">
|
<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.5 py-2.5 text-white outline-none">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-slate-300 font-medium mb-1">Brand</label>
|
<label class="block text-slate-300 font-semibold 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">
|
<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.5 py-2.5 text-white outline-none">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quantity & Unit (Whole Packet Size & Current Amount) -->
|
<!-- Quantity & Unit -->
|
||||||
<div class="grid grid-cols-3 gap-2.5">
|
<div class="grid grid-cols-3 gap-2.5">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-slate-300 font-medium mb-1">Current Amount *</label>
|
<label class="block text-slate-300 font-semibold 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">
|
<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.5 py-2.5 text-white outline-none">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-slate-300 font-medium mb-1">1 Full Packet Size *</label>
|
<label class="block text-slate-300 font-semibold mb-1">1 Full Pack 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">
|
<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.5 py-2.5 text-white outline-none">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-slate-300 font-medium mb-1">Unit</label>
|
<label class="block text-slate-300 font-semibold 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">
|
<select id="modal-unit" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
||||||
<option value="g">Grams (g)</option>
|
<option value="g">Grams (g)</option>
|
||||||
<option value="ml">Milliliters (ml)</option>
|
<option value="ml">Milliliters (ml)</option>
|
||||||
<option value="pcs">Pieces (Stk)</option>
|
<option value="pcs">Pieces (Stk)</option>
|
||||||
@@ -332,70 +359,70 @@
|
|||||||
|
|
||||||
<!-- Category -->
|
<!-- Category -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-slate-300 font-medium mb-1">Category</label>
|
<label class="block text-slate-300 font-semibold 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">
|
<select id="modal-category" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
||||||
<!-- Populated dynamically -->
|
<!-- Populated dynamically -->
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Expiration Date & Helpers -->
|
<!-- Expiry Date -->
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between mb-1">
|
<div class="flex items-center justify-between mb-1">
|
||||||
<label class="block text-slate-300 font-medium">Expiration Date (MHD)</label>
|
<label class="block text-slate-300 font-semibold">Expiration Date</label>
|
||||||
<div class="flex items-center gap-1.5 text-[11px]">
|
<div class="flex items-center gap-2 text-xs">
|
||||||
<button type="button" onclick="setModalExpiryDays(3)" class="text-slate-400 hover:text-teal-400">+3d</button>
|
<button type="button" onclick="setModalExpiryDays(3)" class="text-slate-400 hover:text-teal-400 font-medium">+3d</button>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<button type="button" onclick="setModalExpiryDays(7)" class="text-slate-400 hover:text-teal-400">+1w</button>
|
<button type="button" onclick="setModalExpiryDays(7)" class="text-slate-400 hover:text-teal-400 font-medium">+1w</button>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<button type="button" onclick="setModalExpiryDays(14)" class="text-slate-400 hover:text-teal-400">+2w</button>
|
<button type="button" onclick="setModalExpiryDays(14)" class="text-slate-400 hover:text-teal-400 font-medium">+2w</button>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<button type="button" onclick="setModalExpiryDays(30)" class="text-slate-400 hover:text-teal-400">+1m</button>
|
<button type="button" onclick="setModalExpiryDays(30)" class="text-slate-400 hover:text-teal-400 font-medium">+1m</button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<input type="date" id="modal-expiry" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Notes -->
|
<!-- Notes -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-slate-300 font-medium mb-1">Notes (Optional)</label>
|
<label class="block text-slate-300 font-semibold 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">
|
<input type="text" id="modal-notes" placeholder="e.g. For pasta dinner" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-end gap-2.5 pt-3 border-t border-slate-800">
|
<div class="flex items-center justify-end gap-3 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="button" onclick="closeItemModal()" class="px-5 py-3 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 text-sm font-medium touch-manipulation">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>
|
<button type="submit" class="px-6 py-3 rounded-xl bg-teal-500 hover:bg-teal-400 text-white text-sm font-bold shadow-lg shadow-teal-500/20 active:scale-95 touch-manipulation">Save Product</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- LIVE CAMERA BARCODE SCANNER MODAL -->
|
<!-- TABLET 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 id="scanner-modal" class="fixed inset-0 z-50 bg-black/90 backdrop-blur-md hidden flex items-center justify-center p-3 sm: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="bg-slate-900 border border-slate-800 rounded-3xl max-w-lg 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="p-4 bg-slate-850 border-b border-slate-800 flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2.5">
|
||||||
<div class="w-7 h-7 rounded-lg bg-teal-500/10 text-teal-400 flex items-center justify-center">
|
<div class="w-8 h-8 rounded-xl bg-teal-500/10 text-teal-400 flex items-center justify-center">
|
||||||
<i data-lucide="scan-barcode" class="w-4 h-4"></i>
|
<i data-lucide="scan-barcode" class="w-5 h-5"></i>
|
||||||
</div>
|
</div>
|
||||||
<span class="font-bold text-white text-sm">Scan Product Barcode</span>
|
<span class="font-extrabold text-white text-base">Barcode Scanner</span>
|
||||||
</div>
|
</div>
|
||||||
<button onclick="closeScannerModal()" class="text-slate-400 hover:text-white p-1">
|
<button onclick="closeScannerModal()" class="text-slate-400 hover:text-white p-2 rounded-xl bg-slate-800/60 touch-manipulation">
|
||||||
<i data-lucide="x" class="w-5 h-5"></i>
|
<i data-lucide="x" class="w-5 h-5"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Camera Preview Box -->
|
<!-- Camera Preview -->
|
||||||
<div class="relative bg-black flex items-center justify-center min-h-[280px] w-full overflow-hidden">
|
<div class="relative bg-black flex items-center justify-center min-h-[300px] sm:min-h-[360px] w-full overflow-hidden">
|
||||||
<div id="barcode-reader" class="w-full"></div>
|
<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 id="scanner-overlay" class="absolute inset-0 pointer-events-none border-2 border-teal-400/50 rounded-3xl m-8 sm:m-12 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 class="w-full h-1 bg-red-500/90 animate-pulse shadow-lg shadow-red-500"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Scanner Status / Manual Fallback -->
|
<!-- Scanner Status & Manual Input -->
|
||||||
<div class="p-3.5 bg-slate-900 space-y-2.5">
|
<div class="p-4 bg-slate-900 space-y-3">
|
||||||
<div id="scanner-status" class="text-xs text-center text-slate-400">
|
<div id="scanner-status" class="text-xs sm:text-sm text-center text-slate-400 font-medium">
|
||||||
Point camera at barcode
|
Point camera at barcode
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -404,10 +431,10 @@
|
|||||||
type="text"
|
type="text"
|
||||||
id="scanner-manual-input"
|
id="scanner-manual-input"
|
||||||
placeholder="Or type barcode number manually..."
|
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"
|
class="flex-1 bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-xs sm:text-sm text-white outline-none"
|
||||||
onkeydown="if(event.key === 'Enter') handleManualBarcodeSubmit()"
|
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">
|
<button onclick="handleManualBarcodeSubmit()" class="px-4 py-2.5 rounded-xl bg-teal-600 hover:bg-teal-500 text-white text-xs sm:text-sm font-bold min-h-[42px] touch-manipulation">
|
||||||
Lookup
|
Lookup
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+17
-11
@@ -1,4 +1,4 @@
|
|||||||
/* Custom styling & animations for Fridger */
|
/* Custom styling & tablet optimizations for Fridger */
|
||||||
|
|
||||||
@keyframes scaleUp {
|
@keyframes scaleUp {
|
||||||
from {
|
from {
|
||||||
@@ -12,19 +12,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.animate-scale-up {
|
.animate-scale-up {
|
||||||
animation: scaleUp 0.18s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
animation: scaleUp 0.16s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Glass Shelf Card effect */
|
/* Glass Shelf Card effect for Tablet Touch */
|
||||||
.shelf-card {
|
.shelf-card {
|
||||||
background: linear-gradient(135deg, rgba(30, 41, 59, 0.7) 0%, rgba(15, 23, 42, 0.8) 100%);
|
background: linear-gradient(135deg, rgba(30, 41, 59, 0.75) 0%, rgba(15, 23, 42, 0.85) 100%);
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
border: 1px solid rgba(51, 65, 85, 0.6);
|
border: 1px solid rgba(51, 65, 85, 0.65);
|
||||||
box-shadow: 0 4px 20px -2px rgba(0, 0, 0, 0.4);
|
box-shadow: 0 4px 20px -2px rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shelf-card:hover {
|
.shelf-card:hover, .shelf-card:active {
|
||||||
border-color: rgba(45, 212, 191, 0.4);
|
border-color: rgba(45, 212, 191, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Touch manipulation & Fast Tap */
|
||||||
|
button, input, select {
|
||||||
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom Scrollbar */
|
/* Custom Scrollbar */
|
||||||
@@ -60,7 +65,7 @@
|
|||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
object-fit: cover !important;
|
object-fit: cover !important;
|
||||||
border-radius: 0.5rem;
|
border-radius: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#barcode-reader {
|
#barcode-reader {
|
||||||
@@ -74,9 +79,10 @@
|
|||||||
#barcode-reader__dashboard_section_csr button {
|
#barcode-reader__dashboard_section_csr button {
|
||||||
background-color: #0d9488 !important;
|
background-color: #0d9488 !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
border-radius: 0.5rem !important;
|
border-radius: 0.75rem !important;
|
||||||
padding: 0.35rem 0.75rem !important;
|
padding: 0.5rem 1rem !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
font-size: 0.75rem !important;
|
font-size: 0.875rem !important;
|
||||||
|
font-weight: bold !important;
|
||||||
margin: 0.25rem !important;
|
margin: 0.25rem !important;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user