| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Kitchen Stock - Track Out-of-Stock Items</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap'); |
| |
| body { |
| font-family: 'Poppins', sans-serif; |
| background-color: #f8fafc; |
| } |
| |
| .gradient-bg { |
| background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); |
| } |
| |
| .item-card { |
| transition: all 0.3s ease; |
| } |
| |
| .item-card:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); |
| } |
| |
| .pulse { |
| animation: pulse 2s infinite; |
| } |
| |
| @keyframes pulse { |
| 0% { |
| transform: scale(0.95); |
| box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.7); |
| } |
| 70% { |
| transform: scale(1); |
| box-shadow: 0 0 0 10px rgba(74, 222, 128, 0); |
| } |
| 100% { |
| transform: scale(0.95); |
| box-shadow: 0 0 0 0 rgba(74, 222, 128, 0); |
| } |
| } |
| </style> |
| </head> |
| <body class="min-h-screen gradient-bg"> |
| <div class="container mx-auto px-4 py-8"> |
| |
| <header class="flex justify-between items-center mb-8"> |
| <div class="flex items-center"> |
| <div class="w-12 h-12 rounded-full bg-white shadow-md flex items-center justify-center mr-4"> |
| <i class="fas fa-utensils text-2xl text-emerald-500"></i> |
| </div> |
| <h1 class="text-3xl font-bold text-gray-800">Kitchen Stock</h1> |
| </div> |
| <button id="addItemBtn" class="bg-emerald-500 hover:bg-emerald-600 text-white px-6 py-2 rounded-full shadow-md flex items-center transition-all"> |
| <i class="fas fa-plus mr-2"></i> Add Item |
| </button> |
| </header> |
| |
| |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8"> |
| <div class="bg-white rounded-xl shadow-md p-6"> |
| <div class="flex items-center"> |
| <div class="p-3 rounded-full bg-emerald-100 text-emerald-600 mr-4"> |
| <i class="fas fa-box-open text-xl"></i> |
| </div> |
| <div> |
| <p class="text-gray-500 text-sm">Out of Stock</p> |
| <h3 class="text-2xl font-bold" id="outOfStockCount">0</h3> |
| </div> |
| </div> |
| </div> |
| <div class="bg-white rounded-xl shadow-md p-6"> |
| <div class="flex items-center"> |
| <div class="p-3 rounded-full bg-blue-100 text-blue-600 mr-4"> |
| <i class="fas fa-list text-xl"></i> |
| </div> |
| <div> |
| <p class="text-gray-500 text-sm">Total Items</p> |
| <h3 class="text-2xl font-bold" id="totalItemsCount">0</h3> |
| </div> |
| </div> |
| </div> |
| <div class="bg-white rounded-xl shadow-md p-6"> |
| <div class="flex items-center"> |
| <div class="p-3 rounded-full bg-purple-100 text-purple-600 mr-4"> |
| <i class="fas fa-history text-xl"></i> |
| </div> |
| <div> |
| <p class="text-gray-500 text-sm">Recently Added</p> |
| <h3 class="text-2xl font-bold" id="recentItemsCount">0</h3> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="bg-white rounded-xl shadow-md overflow-hidden"> |
| <div class="p-4 border-b border-gray-200 flex justify-between items-center"> |
| <h2 class="text-xl font-semibold text-gray-800">Out of Stock Items</h2> |
| <div class="relative"> |
| <input type="text" id="searchInput" placeholder="Search items..." class="pl-10 pr-4 py-2 border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"> |
| <i class="fas fa-search absolute left-3 top-3 text-gray-400"></i> |
| </div> |
| </div> |
| |
| <div class="p-4" id="itemsContainer"> |
| |
| <div id="emptyState" class="text-center py-12"> |
| <div class="w-24 h-24 mx-auto mb-4 rounded-full bg-gray-100 flex items-center justify-center"> |
| <i class="fas fa-box-open text-3xl text-gray-400"></i> |
| </div> |
| <h3 class="text-xl font-medium text-gray-700 mb-2">No out-of-stock items</h3> |
| <p class="text-gray-500 mb-6">Add items that are out of stock to keep track</p> |
| <button id="addFirstItemBtn" class="bg-emerald-500 hover:bg-emerald-600 text-white px-6 py-2 rounded-full shadow-md inline-flex items-center transition-all"> |
| <i class="fas fa-plus mr-2"></i> Add Your First Item |
| </button> |
| </div> |
| |
| |
| </div> |
| </div> |
| </div> |
| |
| |
| <div id="addItemModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> |
| <div class="bg-white rounded-xl shadow-xl w-full max-w-md mx-4"> |
| <div class="p-6"> |
| <div class="flex justify-between items-center mb-4"> |
| <h3 class="text-xl font-bold text-gray-800">Add Out-of-Stock Item</h3> |
| <button id="closeModalBtn" class="text-gray-500 hover:text-gray-700"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| |
| <form id="addItemForm"> |
| <div class="mb-4"> |
| <label for="itemName" class="block text-sm font-medium text-gray-700 mb-1">Item Name</label> |
| <input type="text" id="itemName" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent" placeholder="e.g. Milk, Eggs, Bread" required> |
| </div> |
| |
| <div class="mb-4"> |
| <label for="itemCategory" class="block text-sm font-medium text-gray-700 mb-1">Category</label> |
| <select id="itemCategory" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"> |
| <option value="Dairy">Dairy</option> |
| <option value="Produce">Produce</option> |
| <option value="Pantry">Pantry</option> |
| <option value="Meat">Meat</option> |
| <option value="Beverages">Beverages</option> |
| <option value="Frozen">Frozen</option> |
| <option value="Other">Other</option> |
| </select> |
| </div> |
| |
| <div class="mb-4"> |
| <label for="itemNotes" class="block text-sm font-medium text-gray-700 mb-1">Notes (Optional)</label> |
| <textarea id="itemNotes" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent" placeholder="Any specific details..."></textarea> |
| </div> |
| |
| <div class="flex justify-end space-x-3"> |
| <button type="button" id="cancelAddBtn" class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-all"> |
| Cancel |
| </button> |
| <button type="submit" class="px-6 py-2 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg shadow-md transition-all flex items-center"> |
| <i class="fas fa-save mr-2"></i> Save Item |
| </button> |
| </div> |
| </form> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div id="restockModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> |
| <div class="bg-white rounded-xl shadow-xl w-full max-w-md mx-4"> |
| <div class="p-6"> |
| <div class="flex justify-between items-center mb-4"> |
| <h3 class="text-xl font-bold text-gray-800">Item Restocked</h3> |
| <button id="closeRestockModalBtn" class="text-gray-500 hover:text-gray-700"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| |
| <p class="mb-6 text-gray-700">Are you sure you want to mark <span id="restockItemName" class="font-semibold"></span> as restocked? This will remove it from your out-of-stock list.</p> |
| |
| <input type="hidden" id="restockItemId"> |
| |
| <div class="flex justify-end space-x-3"> |
| <button type="button" id="cancelRestockBtn" class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-all"> |
| Cancel |
| </button> |
| <button type="button" id="confirmRestockBtn" class="px-6 py-2 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg shadow-md transition-all flex items-center"> |
| <i class="fas fa-check mr-2"></i> Confirm Restock |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <script> |
| |
| let items = JSON.parse(localStorage.getItem('kitchenStockItems')) || []; |
| let recentItems = JSON.parse(localStorage.getItem('recentKitchenItems')) || []; |
| |
| |
| if (items.length === 0) { |
| recentItems = []; |
| localStorage.setItem('recentKitchenItems', JSON.stringify(recentItems)); |
| } |
| |
| |
| const itemsContainer = document.getElementById('itemsContainer'); |
| const emptyState = document.getElementById('emptyState'); |
| const addItemBtn = document.getElementById('addItemBtn'); |
| const addFirstItemBtn = document.getElementById('addFirstItemBtn'); |
| const addItemModal = document.getElementById('addItemModal'); |
| const closeModalBtn = document.getElementById('closeModalBtn'); |
| const cancelAddBtn = document.getElementById('cancelAddBtn'); |
| const addItemForm = document.getElementById('addItemForm'); |
| const searchInput = document.getElementById('searchInput'); |
| const restockModal = document.getElementById('restockModal'); |
| const closeRestockModalBtn = document.getElementById('closeRestockModalBtn'); |
| const cancelRestockBtn = document.getElementById('cancelRestockBtn'); |
| const confirmRestockBtn = document.getElementById('confirmRestockBtn'); |
| const restockItemName = document.getElementById('restockItemName'); |
| const restockItemId = document.getElementById('restockItemId'); |
| const outOfStockCount = document.getElementById('outOfStockCount'); |
| const totalItemsCount = document.getElementById('totalItemsCount'); |
| const recentItemsCount = document.getElementById('recentItemsCount'); |
| |
| |
| addItemBtn.addEventListener('click', openAddItemModal); |
| addFirstItemBtn.addEventListener('click', openAddItemModal); |
| closeModalBtn.addEventListener('click', closeAddItemModal); |
| cancelAddBtn.addEventListener('click', closeAddItemModal); |
| addItemForm.addEventListener('submit', handleAddItem); |
| searchInput.addEventListener('input', filterItems); |
| closeRestockModalBtn.addEventListener('click', closeRestockModal); |
| cancelRestockBtn.addEventListener('click', closeRestockModal); |
| confirmRestockBtn.addEventListener('click', handleRestock); |
| |
| |
| document.addEventListener('DOMContentLoaded', init); |
| |
| function init() { |
| renderItems(); |
| updateStats(); |
| } |
| |
| |
| function renderItems(filteredItems = null) { |
| const itemsToRender = filteredItems || items; |
| |
| if (itemsToRender.length === 0) { |
| emptyState.classList.remove('hidden'); |
| itemsContainer.innerHTML = ''; |
| itemsContainer.appendChild(emptyState); |
| return; |
| } |
| |
| emptyState.classList.add('hidden'); |
| |
| itemsContainer.innerHTML = ''; |
| |
| itemsToRender.forEach((item, index) => { |
| const itemCard = document.createElement('div'); |
| itemCard.className = 'item-card bg-white rounded-lg shadow-sm p-4 mb-3 border-l-4 border-red-500 hover:shadow-md'; |
| itemCard.innerHTML = ` |
| <div class="flex justify-between items-start"> |
| <div> |
| <h3 class="font-semibold text-lg text-gray-800">${item.name}</h3> |
| <div class="flex items-center mt-1"> |
| <span class="text-xs px-2 py-1 rounded-full ${getCategoryColor(item.category)} mr-2">${item.category}</span> |
| <span class="text-xs text-gray-500"><i class="far fa-clock mr-1"></i> ${formatDate(item.dateAdded)}</span> |
| </div> |
| ${item.notes ? `<p class="text-sm text-gray-600 mt-2">${item.notes}</p>` : ''} |
| </div> |
| <button class="restock-btn px-3 py-1 bg-emerald-500 hover:bg-emerald-600 text-white text-sm rounded-full transition-all" data-id="${index}"> |
| <i class="fas fa-check mr-1"></i> Restocked |
| </button> |
| </div> |
| `; |
| |
| itemsContainer.appendChild(itemCard); |
| }); |
| |
| |
| document.querySelectorAll('.restock-btn').forEach(btn => { |
| btn.addEventListener('click', (e) => { |
| const itemId = e.currentTarget.getAttribute('data-id'); |
| openRestockModal(itemId); |
| }); |
| }); |
| } |
| |
| |
| function getCategoryColor(category) { |
| const colors = { |
| 'Dairy': 'bg-blue-100 text-blue-800', |
| 'Produce': 'bg-green-100 text-green-800', |
| 'Pantry': 'bg-yellow-100 text-yellow-800', |
| 'Meat': 'bg-red-100 text-red-800', |
| 'Beverages': 'bg-indigo-100 text-indigo-800', |
| 'Frozen': 'bg-purple-100 text-purple-800', |
| 'Other': 'bg-gray-100 text-gray-800' |
| }; |
| return colors[category] || 'bg-gray-100 text-gray-800'; |
| } |
| |
| |
| function formatDate(dateString) { |
| const date = new Date(dateString); |
| return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); |
| } |
| |
| |
| function openAddItemModal() { |
| addItemModal.classList.remove('hidden'); |
| document.getElementById('itemName').focus(); |
| } |
| |
| |
| function closeAddItemModal() { |
| addItemModal.classList.add('hidden'); |
| addItemForm.reset(); |
| } |
| |
| |
| function openRestockModal(itemId) { |
| const item = items[itemId]; |
| restockItemName.textContent = item.name; |
| restockItemId.value = itemId; |
| restockModal.classList.remove('hidden'); |
| } |
| |
| |
| function closeRestockModal() { |
| restockModal.classList.add('hidden'); |
| } |
| |
| |
| function handleAddItem(e) { |
| e.preventDefault(); |
| |
| const name = document.getElementById('itemName').value.trim(); |
| const category = document.getElementById('itemCategory').value; |
| const notes = document.getElementById('itemNotes').value.trim(); |
| |
| const newItem = { |
| name, |
| category, |
| notes, |
| dateAdded: new Date().toISOString() |
| }; |
| |
| |
| items.unshift(newItem); |
| |
| |
| if (!recentItems.some(item => item.name === newItem.name && item.dateAdded === newItem.dateAdded)) { |
| recentItems.unshift(newItem); |
| |
| |
| if (recentItems.length > 5) { |
| recentItems = recentItems.slice(0, 5); |
| } |
| } |
| |
| saveItems(); |
| renderItems(); |
| updateStats(); |
| closeAddItemModal(); |
| |
| |
| const successIcon = document.createElement('div'); |
| successIcon.className = 'fixed bottom-6 right-6 w-16 h-16 rounded-full bg-emerald-500 flex items-center justify-center text-white text-2xl shadow-lg pulse'; |
| successIcon.innerHTML = '<i class="fas fa-check"></i>'; |
| document.body.appendChild(successIcon); |
| |
| setTimeout(() => { |
| successIcon.classList.remove('pulse'); |
| setTimeout(() => { |
| successIcon.remove(); |
| }, 300); |
| }, 2000); |
| } |
| |
| |
| function handleRestock() { |
| const itemId = restockItemId.value; |
| const restockedItem = items[itemId]; |
| |
| |
| items.splice(itemId, 1); |
| |
| |
| if (!recentItems.some(item => item.name === restockedItem.name && item.dateAdded === restockedItem.dateAdded)) { |
| recentItems.unshift(restockedItem); |
| |
| |
| if (recentItems.length > 5) { |
| recentItems = recentItems.slice(0, 5); |
| } |
| } |
| |
| saveItems(); |
| renderItems(); |
| updateStats(); |
| closeRestock |
| </html> |