From 97db9d0ec1f94fd75f6070a4b0fd715186259486 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Mon, 26 Jan 2026 18:23:06 +0000 Subject: [PATCH] Autosave: 20260126-182305 --- api/analyze.php | 133 ++++++++++++++ api/analyze_debug.log | 4 + assets/css/custom.css | 407 +++++++++--------------------------------- assets/js/main.js | 321 ++++++++++++++++++++++++++------- index.php | 363 ++++++++++++++++++++++--------------- 5 files changed, 708 insertions(+), 520 deletions(-) create mode 100644 api/analyze.php create mode 100644 api/analyze_debug.log diff --git a/api/analyze.php b/api/analyze.php new file mode 100644 index 0000000..67957d5 --- /dev/null +++ b/api/analyze.php @@ -0,0 +1,133 @@ + false, 'error' => 'No image or barcode provided']); + exit; +} + +$productData = null; +$errorDetails = null; + +// Try Barcode Lookup via Open Food Facts if barcode is present +if ($barcode) { + debugLog("Attempting Open Food Facts lookup for $barcode"); + $url = "https://world.openfoodfacts.org/api/v0/product/" . urlencode($barcode) . ".json"; + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 5); + curl_setopt($ch, CURLOPT_USERAGENT, 'HomePantryTracker/1.0'); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode === 200 && $response) { + $offData = json_decode($response, true); + if (isset($offData['status']) && $offData['status'] == 1) { + $p = $offData['product']; + $productData = [ + 'name' => $p['product_name'] ?? ($p['product_name_en'] ?? 'Unknown Product'), + 'category' => 'Pantry', // Default + 'quantity' => $p['quantity'] ?? '', + 'expiration_date' => null + ]; + debugLog("Barcode found: " . $productData['name']); + + // Try to map category + if (!empty($p['categories_tags'])) { + $tags = implode(' ', $p['categories_tags']); + if (stripos($tags, 'dairy') !== false || stripos($tags, 'milk') !== false) $productData['category'] = 'Dairy'; + elseif (stripos($tags, 'meat') !== false) $productData['category'] = 'Meat'; + elseif (stripos($tags, 'bakery') !== false || stripos($tags, 'bread') !== false) $productData['category'] = 'Bakery'; + elseif (stripos($tags, 'fruit') !== false || stripos($tags, 'vegetable') !== false) $productData['category'] = 'Produce'; + elseif (stripos($tags, 'frozen') !== false) $productData['category'] = 'Frozen'; + } + } else { + debugLog("Barcode not found in Open Food Facts"); + } + } else { + debugLog("Open Food Facts API error: $httpCode"); + } +} + +// If barcode lookup failed or we have an image, use AI +if (!$productData) { + debugLog("Using AI for identification..."); + $prompt = "You are a professional pantry organizer. Identify the product from the image provided. + Return ONLY a valid JSON object. + Keys: + - name: Specific brand and product name. + - category: Dairy, Meat, Bakery, Produce, Pantry, Frozen. + - quantity: e.g., '1.5 L', '500g'. + - expiration_date: Look for 'Best Before', 'Use By', or 'EXP' date in YYYY-MM-DD format, or null. + If the package is not in English, translate name/category to English."; + + if ($image) { + // Use "Simple" approach: data URL directly in the content string + $content = $prompt . "\nAnalyze this image: " . $image; + $messages = [ + ['role' => 'user', 'content' => $content] + ]; + } else { + $messages = [ + ['role' => 'user', 'content' => $prompt . " Product barcode: $barcode"] + ]; + } + + $resp = LocalAIApi::createResponse([ + 'input' => $messages, + 'temperature' => 0.1 + ]); + + if (!empty($resp['success'])) { + debugLog("AI response successful"); + $result = LocalAIApi::decodeJsonFromResponse($resp); + if ($result) { + $productData = $result; + } else { + $text = LocalAIApi::extractText($resp); + debugLog("AI returned text instead of JSON. Extracting..."); + if (preg_match('/\{.*\}/s', $text, $matches)) { + $productData = json_decode($matches[0], true); + } else { + $errorDetails = "AI returned text instead of JSON: " . substr($text, 0, 100); + debugLog("Error: $errorDetails"); + } + } + } else { + $errorDetails = $resp['error'] ?? $resp['message'] ?? 'AI request failed'; + $httpStatus = $resp['status'] ?? 'unknown'; + debugLog("AI Request failed. Status: $httpStatus, Error: $errorDetails"); + + if (isset($resp['response'])) { + debugLog("Full proxy response: " . json_encode($resp['response'])); + } + } +} + +if ($productData) { + if (isset($productData['expiration_date']) && ($productData['expiration_date'] === 'null' || $productData['expiration_date'] === '')) { + $productData['expiration_date'] = null; + } + debugLog("Returning success for " . ($productData['name'] ?? 'unknown')); + echo json_encode(['success' => true, 'data' => $productData]); +} else { + debugLog("Returning failure: " . ($errorDetails ?? 'Could not identify product')); + echo json_encode(['success' => false, 'error' => $errorDetails ?: 'Could not identify product']); +} \ No newline at end of file diff --git a/api/analyze_debug.log b/api/analyze_debug.log new file mode 100644 index 0000000..b8b227f --- /dev/null +++ b/api/analyze_debug.log @@ -0,0 +1,4 @@ +[2026-01-26 18:22:50] Request received. Barcode: none, Image size: 32143 +[2026-01-26 18:22:50] Using AI for identification... +[2026-01-26 18:22:51] AI Request failed. Status: 500, Error: the server responded with status 400 +[2026-01-26 18:22:51] Returning failure: the server responded with status 400 diff --git a/assets/css/custom.css b/assets/css/custom.css index 65a1626..4fe7ff1 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,346 +1,117 @@ +/* Custom styles for Home Pantry Tracker */ + :root { - --color-bg: #ffffff; - --color-text: #1a1a1a; - --color-primary: #2563EB; /* Vibrant Blue */ - --color-secondary: #000000; - --color-accent: #A3E635; /* Lime Green */ - --color-surface: #f8f9fa; - --font-heading: 'Space Grotesk', sans-serif; - --font-body: 'Inter', sans-serif; - --border-width: 2px; - --shadow-hard: 5px 5px 0px #000; - --shadow-hover: 8px 8px 0px #000; - --radius-pill: 50rem; - --radius-card: 1rem; + --primary-color: #2c3e50; + --accent-color: #27ae60; + --danger-color: #c53030; + --warning-color: #f6ad55; + --bg-light: #f8fafc; } body { - font-family: var(--font-body); - background-color: var(--color-bg); - color: var(--color-text); - overflow-x: hidden; + font-family: 'Inter', system-ui, -apple-system, sans-serif; + background-color: var(--bg-light); + color: var(--primary-color); } -h1, h2, h3, h4, h5, h6, .navbar-brand { - font-family: var(--font-heading); - letter-spacing: -0.03em; -} - -/* Utilities */ -.text-primary { color: var(--color-primary) !important; } -.bg-black { background-color: #000 !important; } -.text-white { color: #fff !important; } -.shadow-hard { box-shadow: var(--shadow-hard); } -.border-2-black { border: var(--border-width) solid #000; } -.py-section { padding-top: 5rem; padding-bottom: 5rem; } - -/* Navbar */ .navbar { - background: rgba(255, 255, 255, 0.9); - backdrop-filter: blur(10px); - border-bottom: var(--border-width) solid transparent; - transition: all 0.3s; - padding-top: 1rem; - padding-bottom: 1rem; + background-color: white; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); } -.navbar.scrolled { - border-bottom-color: #000; - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} - -.brand-text { - font-size: 1.5rem; - font-weight: 800; -} - -.nav-link { - font-weight: 500; - color: var(--color-text); - margin-left: 1rem; - position: relative; -} - -.nav-link:hover, .nav-link.active { - color: var(--color-primary); -} - -/* Buttons */ -.btn { +.navbar-brand { font-weight: 700; - font-family: var(--font-heading); - padding: 0.8rem 2rem; - border-radius: var(--radius-pill); - border: var(--border-width) solid #000; - transition: all 0.2s cubic-bezier(0.25, 1, 0.5, 1); - box-shadow: var(--shadow-hard); + color: var(--primary-color); } -.btn:hover { - transform: translate(-2px, -2px); - box-shadow: var(--shadow-hover); +.card { + border: none; + border-radius: 12px; + box-shadow: 0 4px 6px rgba(0,0,0,0.02); } -.btn:active { - transform: translate(2px, 2px); - box-shadow: 0 0 0 #000; +.table thead th { + background-color: #f1f5f9; + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.05em; + font-weight: 700; + border: none; +} + +.table tbody td { + vertical-align: middle; + padding: 1rem 0.75rem; +} + +.badge-fresh { background-color: #e6fffa; color: #234e52; } +.badge-warning { background-color: #fffaf0; color: #7b341e; } +.badge-expired { background-color: #fff5f5; color: #822727; } + +.status-expired { background-color: #fff5f5; } +.status-warning { background-color: #fffaf0; } + +#reader { + background: #000; + border: 2px solid #e2e8f0; + min-height: 250px; + position: relative; + overflow: hidden; +} + +#reader video { + border-radius: 8px; + width: 100% !important; +} + +/* Camera mode viewfinder look */ +#reader.camera-mode::after { + content: ''; + position: absolute; + top: 20px; + left: 20px; + right: 20px; + bottom: 20px; + border: 1px solid rgba(255,255,255,0.3); + pointer-events: none; +} + +.ai-loading { + background: rgba(255,255,255,0.9); + border-radius: 8px; + margin-top: 10px; + border: 1px solid #e2e8f0; } .btn-primary { - background-color: var(--color-primary); - border-color: #000; - color: #fff; + background-color: var(--accent-color); + border-color: var(--accent-color); } .btn-primary:hover { - background-color: #1d4ed8; - border-color: #000; - color: #fff; + background-color: #229954; + border-color: #229954; } -.btn-outline-dark { - background-color: #fff; - color: #000; -} - -.btn-cta { - background-color: var(--color-accent); - color: #000; -} - -.btn-cta:hover { - background-color: #8cc629; - color: #000; -} - -/* Hero Section */ -.hero-section { - min-height: 100vh; - padding-top: 80px; -} - -.background-blob { +/* Shutter effect for photo capture */ +.shutter-flash { position: absolute; - border-radius: 50%; - filter: blur(80px); - opacity: 0.6; - z-index: 1; -} - -.blob-1 { - top: -10%; - right: -10%; - width: 600px; - height: 600px; - background: radial-gradient(circle, var(--color-accent), transparent); -} - -.blob-2 { - bottom: 10%; - left: -10%; - width: 500px; - height: 500px; - background: radial-gradient(circle, var(--color-primary), transparent); -} - -.highlight-text { - background: linear-gradient(120deg, transparent 0%, transparent 40%, var(--color-accent) 40%, var(--color-accent) 100%); - background-repeat: no-repeat; - background-size: 100% 40%; - background-position: 0 88%; - padding: 0 5px; -} - -.dot { color: var(--color-primary); } - -.badge-pill { - display: inline-block; - padding: 0.5rem 1rem; - border: 2px solid #000; - border-radius: 50px; - font-weight: 700; - background: #fff; - box-shadow: 4px 4px 0 #000; - font-family: var(--font-heading); - font-size: 0.9rem; -} - -/* Marquee */ -.marquee-container { - overflow: hidden; - white-space: nowrap; - border-top: 2px solid #000; - border-bottom: 2px solid #000; -} - -.rotate-divider { - transform: rotate(-2deg) scale(1.05); - z-index: 10; - position: relative; - margin-top: -50px; - margin-bottom: 30px; -} - -.marquee-content { - display: inline-block; - animation: marquee 20s linear infinite; - font-family: var(--font-heading); - font-weight: 700; - font-size: 1.5rem; - letter-spacing: 2px; -} - -@keyframes marquee { - 0% { transform: translateX(0); } - 100% { transform: translateX(-50%); } -} - -/* Portfolio Cards */ -.project-card { - border: 2px solid #000; - border-radius: var(--radius-card); - overflow: hidden; - background: #fff; - transition: transform 0.3s ease; - box-shadow: var(--shadow-hard); - height: 100%; - display: flex; - flex-direction: column; -} - -.project-card:hover { - transform: translateY(-10px); - box-shadow: 8px 8px 0 #000; -} - -.card-img-holder { - height: 250px; - display: flex; - align-items: center; - justify-content: center; - border-bottom: 2px solid #000; - position: relative; - font-size: 4rem; -} - -.placeholder-art { - transition: transform 0.3s ease; -} - -.project-card:hover .placeholder-art { - transform: scale(1.2) rotate(10deg); -} - -.bg-soft-blue { background-color: #e0f2fe; } -.bg-soft-green { background-color: #dcfce7; } -.bg-soft-purple { background-color: #f3e8ff; } -.bg-soft-yellow { background-color: #fef9c3; } - -.category-tag { - position: absolute; - top: 15px; - right: 15px; - background: #000; - color: #fff; - padding: 5px 12px; - border-radius: 20px; - font-size: 0.75rem; - font-weight: 700; -} - -.card-body { padding: 1.5rem; } - -.link-arrow { - text-decoration: none; - color: #000; - font-weight: 700; - display: inline-flex; - align-items: center; - margin-top: auto; -} - -.link-arrow i { transition: transform 0.2s; margin-left: 5px; } -.link-arrow:hover i { transform: translateX(5px); } - -/* About */ -.about-image-stack { - position: relative; - height: 400px; - width: 100%; -} - -.stack-card { - position: absolute; - width: 80%; - height: 100%; - border-radius: var(--radius-card); - border: 2px solid #000; - box-shadow: var(--shadow-hard); - left: 10%; - transform: rotate(-3deg); - background-size: cover; -} - -/* Forms */ -.form-control { - border: 2px solid #000; - border-radius: 0.5rem; - padding: 1rem; - font-weight: 500; - background: #f8f9fa; -} - -.form-control:focus { - box-shadow: 4px 4px 0 var(--color-primary); - border-color: #000; - background: #fff; -} - -/* Animations */ -.animate-up { + top: 0; + left: 0; + right: 0; + bottom: 0; + background: white; opacity: 0; - transform: translateY(30px); - animation: fadeUp 0.8s ease forwards; + pointer-events: none; + transition: opacity 0.1s; + z-index: 100; } -.delay-100 { animation-delay: 0.1s; } -.delay-200 { animation-delay: 0.2s; } - -@keyframes fadeUp { - to { - opacity: 1; - transform: translateY(0); - } +.shutter-flash.active { + opacity: 1; } -/* Social */ -.social-links a { - transition: transform 0.2s; - display: inline-block; -} -.social-links a:hover { - transform: scale(1.2) rotate(10deg); - color: var(--color-accent) !important; -} - -/* Responsive */ -@media (max-width: 991px) { - .rotate-divider { - transform: rotate(0); - margin-top: 0; - margin-bottom: 2rem; - } - - .hero-section { - padding-top: 120px; - text-align: center; - min-height: auto; - padding-bottom: 100px; - } - - .display-1 { font-size: 3.5rem; } - - .blob-1 { width: 300px; height: 300px; right: -20%; } - .blob-2 { width: 300px; height: 300px; left: -20%; } -} +.is-valid { + border-color: var(--accent-color) !important; + background-color: #f0fff4 !important; +} \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index fdf2cfd..3150ba4 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,73 +1,268 @@ -document.addEventListener('DOMContentLoaded', () => { - - // Smooth scrolling for navigation links - document.querySelectorAll('a[href^="#"]').forEach(anchor => { - anchor.addEventListener('click', function (e) { - e.preventDefault(); - const targetId = this.getAttribute('href'); - if (targetId === '#') return; - - const targetElement = document.querySelector(targetId); - if (targetElement) { - // Close mobile menu if open - const navbarToggler = document.querySelector('.navbar-toggler'); - const navbarCollapse = document.querySelector('.navbar-collapse'); - if (navbarCollapse.classList.contains('show')) { - navbarToggler.click(); - } +document.addEventListener('DOMContentLoaded', function() { + let html5QrCode; + const btnScanBarcode = document.getElementById('btnScanBarcode'); + const btnTakePhoto = document.getElementById('btnTakePhoto'); + const reader = document.getElementById('reader'); + const aiLoading = document.getElementById('aiLoading'); - // Scroll with offset - const offset = 80; - const elementPosition = targetElement.getBoundingClientRect().top; - const offsetPosition = elementPosition + window.pageYOffset - offset; + const pName = document.getElementById('p_name'); + const pCategory = document.getElementById('p_category'); + const pQuantity = document.getElementById('p_quantity'); + const pExpiration = document.getElementById('p_expiration'); - window.scrollTo({ - top: offsetPosition, - behavior: "smooth" - }); - } - }); - }); - - // Navbar scroll effect - const navbar = document.querySelector('.navbar'); - window.addEventListener('scroll', () => { - if (window.scrollY > 50) { - navbar.classList.add('scrolled', 'shadow-sm', 'bg-white'); - navbar.classList.remove('bg-transparent'); - } else { - navbar.classList.remove('scrolled', 'shadow-sm', 'bg-white'); - navbar.classList.add('bg-transparent'); + function checkHttps() { + if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') { + alert("Camera access requires HTTPS. Please ensure you are using a secure connection."); + return false; } - }); + return true; + } - // Intersection Observer for fade-up animations - const observerOptions = { - threshold: 0.1, - rootMargin: "0px 0px -50px 0px" - }; - - const observer = new IntersectionObserver((entries) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - entry.target.classList.add('animate-up'); - entry.target.style.opacity = "1"; - observer.unobserve(entry.target); // Only animate once + function stopScanner() { + return new Promise((resolve) => { + console.log("Stopping scanner..."); + cleanupUI(); + if (html5QrCode && html5QrCode.isScanning) { + html5QrCode.stop().then(() => { + console.log("Scanner stopped."); + reader.style.display = 'none'; + resolve(); + }).catch(err => { + console.error("Failed to stop scanner", err); + reader.style.display = 'none'; + resolve(); + }); + } else { + console.log("Scanner was not active."); + reader.style.display = 'none'; + resolve(); } }); - }, observerOptions); + } - // Select elements to animate (add a class 'reveal' to them in HTML if not already handled by CSS animation) - // For now, let's just make sure the hero animations run. - // If we want scroll animations, we'd add opacity: 0 to elements in CSS and reveal them here. - // Given the request, the CSS animation I added runs on load for Hero. - // Let's make the project cards animate in. - - const projectCards = document.querySelectorAll('.project-card'); - projectCards.forEach((card, index) => { - card.style.opacity = "0"; - card.style.animationDelay = `${index * 0.1}s`; - observer.observe(card); + function cleanupUI() { + const capBtn = document.getElementById('btnCaptureFrame'); + if (capBtn) capBtn.remove(); + const stopBtn = document.getElementById('btnStopScanner'); + if (stopBtn) stopBtn.remove(); + const flash = document.querySelector('.shutter-flash'); + if (flash) flash.classList.remove('active'); + reader.classList.remove('camera-mode'); + } + + function addStopButton() { + if (!document.getElementById('btnStopScanner')) { + const stopBtn = document.createElement('button'); + stopBtn.id = 'btnStopScanner'; + stopBtn.innerText = "Stop Camera"; + stopBtn.className = "btn btn-danger btn-sm w-100 mt-2"; + stopBtn.onclick = stopScanner; + reader.after(stopBtn); + } + } + + function triggerFlash() { + let flash = document.querySelector('.shutter-flash'); + if (!flash) { + flash = document.createElement('div'); + flash.className = 'shutter-flash'; + reader.appendChild(flash); + } + flash.classList.add('active'); + setTimeout(() => flash.classList.remove('active'), 150); + } + + btnScanBarcode.addEventListener('click', function() { + if (!checkHttps()) return; + + stopScanner().then(() => { + reader.style.display = 'block'; + if (!html5QrCode) { + html5QrCode = new Html5Qrcode("reader"); + } + + addStopButton(); + + const config = { + fps: 10, + qrbox: (viewfinderWidth, viewfinderHeight) => { + return { width: viewfinderWidth * 0.8, height: viewfinderHeight * 0.4 }; + } + }; + + html5QrCode.start( + { facingMode: "environment" }, + config, + (decodedText) => { + console.log(`Barcode detected: ${decodedText}`); + triggerFlash(); + stopScanner().then(() => { + analyzeProduct({ barcode: decodedText }); + }); + } + ).catch(err => { + console.error("Scanner start error:", err); + alert("Could not start camera. Please check permissions."); + reader.style.display = 'none'; + cleanupUI(); + }); + }); }); + btnTakePhoto.addEventListener('click', function() { + if (!checkHttps()) return; + + stopScanner().then(() => { + reader.style.display = 'block'; + reader.classList.add('camera-mode'); + if (!html5QrCode) { + html5QrCode = new Html5Qrcode("reader"); + } + + addStopButton(); + + html5QrCode.start( + { facingMode: "environment" }, + { fps: 10 }, + () => { /* ignore QR scans in photo mode unless we want both */ } + ).then(() => { + if (!document.getElementById('btnCaptureFrame')) { + const capBtn = document.createElement('button'); + capBtn.id = 'btnCaptureFrame'; + capBtn.innerHTML = ' Take Photo & Identify'; + capBtn.className = "btn btn-primary btn-lg w-100 mt-2 mb-1 py-3"; + capBtn.onclick = capturePhoto; + // Insert before stop button if exists + const stopBtn = document.getElementById('btnStopScanner'); + if (stopBtn) { + stopBtn.before(capBtn); + } else { + reader.after(capBtn); + } + } + }).catch(err => { + console.error("Camera start error:", err); + alert("Camera error: " + err); + reader.style.display = 'none'; + cleanupUI(); + }); + }); + }); + + function capturePhoto() { + console.log("Capturing photo..."); + const video = document.querySelector('#reader video'); + if (!video) { + console.error("Video element not found in reader"); + alert("Camera not ready. Video element missing."); + return; + } + + triggerFlash(); + + const canvas = document.getElementById('photoCanvas'); + if (!canvas) { + console.error("Canvas element #photoCanvas not found"); + alert("Application error: missing canvas."); + return; + } + + const MAX_DIM = 1024; + let width = video.videoWidth; + let height = video.videoHeight; + + if (width === 0 || height === 0) { + console.error("Video dimensions are 0", {width, height}); + alert("Camera error: invalid video dimensions. Try moving the camera."); + return; + } + + if (width > height) { + if (width > MAX_DIM) { + height *= MAX_DIM / width; + width = MAX_DIM; + } + } else { + if (height > MAX_DIM) { + width *= MAX_DIM / height; + height = MAX_DIM; + } + } + + canvas.width = width; + canvas.height = height; + const context = canvas.getContext('2d'); + context.drawImage(video, 0, 0, width, height); + + const dataUrl = canvas.toDataURL('image/jpeg', 0.8); + console.log("Data URL generated, length:", dataUrl.length); + + if (dataUrl.length < 1000) { + console.error("Data URL seems too short, capture might have failed"); + } + + setTimeout(() => { + console.log("Proceeding to identification..."); + stopScanner().then(() => { + analyzeProduct({ image: dataUrl }); + }); + }, 300); + } + + function analyzeProduct(params) { + console.log("Sending request to api/analyze.php", params.barcode ? "with barcode" : "with image"); + aiLoading.style.display = 'block'; + pName.placeholder = "Identifying..."; + + // Clear previous values to show something is happening + pName.value = ''; + + fetch('api/analyze.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(params) + }) + .then(res => { + console.log("Received response status:", res.status); + return res.json(); + }) + .then(res => { + console.log("Received data:", res); + aiLoading.style.display = 'none'; + pName.placeholder = "e.g. Milk, Eggs, Bread"; + + if (res.success && res.data) { + const data = res.data; + if (data.name) pName.value = data.name; + if (data.category) pCategory.value = data.category; + if (data.quantity) pQuantity.value = data.quantity; + if (data.expiration_date) pExpiration.value = data.expiration_date; + + // Visual highlight of changed fields + [pName, pCategory, pQuantity, pExpiration].forEach(el => { + if (el.value) { + el.classList.add('is-valid'); + setTimeout(() => el.classList.remove('is-valid'), 2000); + } + }); + + } else { + console.error("Identification failed:", res.error); + alert("Could not identify the product: " + (res.error || "Please try again.")); + } + }) + .catch(err => { + aiLoading.style.display = 'none'; + pName.placeholder = "e.g. Milk, Eggs, Bread"; + console.error("Fetch error:", err); + alert("Analysis failed. Connection error or server issue."); + }); + } + + const addItemModal = document.getElementById('addItemModal'); + if (addItemModal) { + addItemModal.addEventListener('hidden.bs.modal', function () { + stopScanner(); + }); + } }); \ No newline at end of file diff --git a/index.php b/index.php index 7205f3d..80a46f3 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,235 @@ prepare("INSERT INTO products (name, category, quantity, expiration_date) VALUES (?, ?, ?, ?)"); + $stmt->execute([$name, $category, $quantity, $expiration_date]); + } + } elseif ($_POST['action'] === 'delete') { + $id = (int)($_POST['id'] ?? 0); + if ($id) { + $stmt = db()->prepare("DELETE FROM products WHERE id = ?"); + $stmt->execute([$id]); + } + } + header('Location: index.php'); + exit; +} + +// Fetch Products +$products = db()->query("SELECT * FROM products ORDER BY expiration_date ASC")->fetchAll(); + +$today = new DateTime(); +$expiring_soon_days = 7; ?> - - - New Style - - - - - - - - - - - - - - - - - - - + + + Home Pantry Tracker + + + + + + + + + -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+ +
- + + +
+
+ diff($exp_date); + $days = (int)$diff->format("%r%a"); + if ($days < 0) $expired_count++; + elseif ($days <= $expiring_soon_days) $warning_count++; + } + } + ?> +
+
+
Total Items
+
+
+
+
+
+
Expired
+
+
+
+
+
+
Expiring Soon
+
+
+
+
+ +
+
+ Inventory List +
Sorted by expiration date
+
+
+ + + + + + + + + + + + + + + + + + diff($exp_date); + $days = (int)$diff->format("%r%a"); + + if ($days < 0) { + $status_class = 'status-expired'; + $badge_class = 'badge-expired'; + $status_text = 'Expired'; + } elseif ($days <= $expiring_soon_days) { + $status_class = 'status-warning'; + $badge_class = 'badge-warning'; + $status_text = 'Expiring Soon'; + } + } + ?> + + + + + + + + + + +
Product NameCategoryQuantityExpirationStatusActions
No items in your pantry yet. Add some!
+
+ + + +
+
+
+
+
+ + + + + + + + - + \ No newline at end of file