let sharedListData = null; let sharedShareToken = null; let consolidateItems = true; let groupByMode = 'aisle'; const AISLE_ORDER = [ 'Produce', 'Meat & Seafood', 'Dairy & Eggs', 'Canned & Jarred', 'Bakery & Grains', 'Pantry & Spices', 'Frozen', 'Other' ]; const AISLE_ICONS = { 'Produce': 'apple', 'Meat & Seafood': 'beef', 'Dairy & Eggs': 'milk', 'Canned & Jarred': 'archive', 'Bakery & Grains': 'wheat', 'Pantry & Spices': 'flame', 'Frozen': 'snowflake', 'Other': 'shopping-bag' }; window.addEventListener('DOMContentLoaded', () => { initTheme(); lucide.createIcons(); document.getElementById('theme-toggle').addEventListener('click', toggleTheme); document.getElementById('shared-toggle-consolidate').addEventListener('change', (e) => { consolidateItems = e.target.checked; renderSharedList(); }); document.getElementById('shared-group-by').addEventListener('change', (e) => { groupByMode = e.target.value; renderSharedList(); }); document.getElementById('btn-shared-copy-text').addEventListener('click', handleCopySharedText); const params = new URLSearchParams(window.location.search); sharedShareToken = params.get('token'); if (!sharedShareToken) { showNotFound(); return; } loadSharedShoppingList(); }); function initTheme() { const savedTheme = localStorage.getItem('gourmet-theme') || 'dark'; if (savedTheme === 'dark') { document.body.classList.add('dark-theme'); } else { document.body.classList.remove('dark-theme'); } } function toggleTheme() { document.body.classList.toggle('dark-theme'); const isDark = document.body.classList.contains('dark-theme'); localStorage.setItem('gourmet-theme', isDark ? 'dark' : 'light'); } function showNotFound() { document.getElementById('share-list-block').style.display = 'none'; document.getElementById('list-not-found-block').style.display = 'flex'; lucide.createIcons(); } async function loadSharedShoppingList() { try { const res = await fetch(`/api/share/shopping-lists/${encodeURIComponent(sharedShareToken)}`); if (!res.ok) { showNotFound(); return; } sharedListData = await res.json(); document.title = `${sharedListData.name} – Shared Shopping List | ForkFolio`; document.getElementById('shared-list-title').textContent = sharedListData.name; document.getElementById('shared-list-creator').textContent = `@${sharedListData.createdBy || 'chef'}`; document.getElementById('share-list-block').style.display = 'block'; renderSharedList(); } catch (err) { console.error('Failed to load shared shopping list:', err); showNotFound(); } } function formatScaledNumber(num) { if (num % 1 === 0) return num.toString(); const rounded = Math.round(num * 100) / 100; const decimal = rounded % 1; const integer = Math.floor(rounded); if (Math.abs(decimal - 0.25) < 0.05) return (integer > 0 ? integer + ' ' : '') + '1/4'; if (Math.abs(decimal - 0.5) < 0.05) return (integer > 0 ? integer + ' ' : '') + '1/2'; if (Math.abs(decimal - 0.75) < 0.05) return (integer > 0 ? integer + ' ' : '') + '3/4'; if (Math.abs(decimal - 0.33) < 0.05) return (integer > 0 ? integer + ' ' : '') + '1/3'; if (Math.abs(decimal - 0.66) < 0.05) return (integer > 0 ? integer + ' ' : '') + '2/3'; return rounded.toString(); } function buildDisplayEntries(items, shouldConsolidate) { if (!shouldConsolidate) { return items.map(item => ({ itemIds: [item.id], displayText: item.rawText, category: item.category || 'Other', sources: item.recipeTitle ? [item.recipeTitle] : [], checked: !!item.checked })); } const groups = new Map(); for (const item of items) { const normName = (item.itemName || item.rawText || '').toLowerCase().trim(); const normUnit = (item.unit || '').toLowerCase().trim(); const key = `${item.checked ? '1' : '0'}::${normName}::${normUnit}`; if (!groups.has(key)) { groups.set(key, { itemIds: [item.id], rawTexts: [item.rawText], itemName: normName, unit: normUnit, totalQty: item.quantity !== null && item.quantity !== undefined ? Number(item.quantity) : null, hasNullQty: item.quantity === null || item.quantity === undefined, category: item.category || 'Other', sources: item.recipeTitle ? [item.recipeTitle] : [], checked: !!item.checked }); } else { const g = groups.get(key); g.itemIds.push(item.id); g.rawTexts.push(item.rawText); if (item.quantity !== null && item.quantity !== undefined && !g.hasNullQty) { g.totalQty = (g.totalQty || 0) + Number(item.quantity); } else { g.hasNullQty = true; } if (item.recipeTitle && !g.sources.includes(item.recipeTitle)) { g.sources.push(item.recipeTitle); } } } const results = []; for (const g of groups.values()) { let displayText = g.rawTexts[0]; if (g.itemIds.length > 1) { if (!g.hasNullQty && g.totalQty !== null && g.totalQty > 0) { const qtyStr = formatScaledNumber(g.totalQty); const unitStr = g.unit ? (g.totalQty > 1 && !g.unit.endsWith('s') && ['clove', 'can', 'jar', 'bunch', 'slice', 'piece', 'head', 'stalk', 'sprig', 'stick', 'cup'].includes(g.unit) ? `${g.unit}s ` : `${g.unit} `) : ''; const displayItem = g.itemName.charAt(0).toUpperCase() + g.itemName.slice(1); displayText = `${qtyStr} ${unitStr}${displayItem}`; } else { const displayItem = g.itemName.charAt(0).toUpperCase() + g.itemName.slice(1); displayText = `${displayItem} (×${g.itemIds.length})`; } } results.push({ itemIds: g.itemIds, displayText, category: g.category, sources: g.sources, checked: g.checked }); } return results; } function groupEntries(entries, mode) { const grouped = new Map(); if (mode === 'recipe') { for (const entry of entries) { const groupLabels = entry.sources.length > 0 ? [entry.sources.join(' + ')] : ['Manual / Household Staples']; for (const label of groupLabels) { if (!grouped.has(label)) grouped.set(label, []); grouped.get(label).push(entry); } } return grouped; } for (const aisle of AISLE_ORDER) { const matching = entries.filter(e => (e.category || 'Other') === aisle); if (matching.length > 0) { matching.sort((a, b) => Number(a.checked) - Number(b.checked)); grouped.set(aisle, matching); } } return grouped; } function renderSharedList() { const contentEl = document.getElementById('shared-list-content'); const emptyEl = document.getElementById('shared-list-empty'); if (!sharedListData || !Array.isArray(sharedListData.items) || sharedListData.items.length === 0) { contentEl.innerHTML = ''; emptyEl.style.display = 'flex'; lucide.createIcons(); return; } emptyEl.style.display = 'none'; contentEl.innerHTML = ''; const entries = buildDisplayEntries(sharedListData.items, consolidateItems); const grouped = groupEntries(entries, groupByMode); for (const [groupName, groupEntries] of grouped.entries()) { const card = document.createElement('div'); card.className = 'shopping-category-card'; const iconName = AISLE_ICONS[groupName] || 'utensils'; const uncheckedCount = groupEntries.filter(e => !e.checked).length; card.innerHTML = `