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…
-
-
= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.
-
This page will update automatically as the plan is implemented.
-
Runtime: PHP = htmlspecialchars($phpVersion) ?> — UTC = htmlspecialchars($now) ?>
+
+
+
+
+
+ diff($exp_date);
+ $days = (int)$diff->format("%r%a");
+ if ($days < 0) $expired_count++;
+ elseif ($days <= $expiring_soon_days) $warning_count++;
+ }
+ }
+ ?>
+
+
+
Total Items
+
= $total_items ?>
+
+
+
+
+
Expired
+
= $expired_count ?>
+
+
+
+
+
Expiring Soon
+
= $warning_count ?>
+
+
+
+
+
+
+
+
+
+
+ | Product Name |
+ Category |
+ Quantity |
+ Expiration |
+ Status |
+ Actions |
+
+
+
+
+
+ | No items in your pantry yet. Add some! |
+
+
+ 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';
+ }
+ }
+ ?>
+
+ | = htmlspecialchars($p['name']) ?> |
+ = htmlspecialchars($p['category']) ?> |
+ = htmlspecialchars($p['quantity']) ?> |
+ = $p['expiration_date'] ? date('M d, Y', strtotime($p['expiration_date'])) : '-' ?> |
+ = $status_text ?> |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+