mirror of
https://gitlab.com/Zer0interrupt/recipe-manager.git
synced 2026-11-01 23:24:11 -05:00
316 lines
10 KiB
JavaScript
316 lines
10 KiB
JavaScript
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 = `
|
||
<div class="shopping-category-header">
|
||
<div class="shopping-category-title">
|
||
<i data-lucide="${iconName.toLowerCase()}"></i>
|
||
<span>${groupName}</span>
|
||
</div>
|
||
<span class="shopping-category-count">${uncheckedCount} / ${groupEntries.length}</span>
|
||
</div>
|
||
<ul class="shopping-items-list"></ul>
|
||
`;
|
||
|
||
const ul = card.querySelector('.shopping-items-list');
|
||
groupEntries.forEach(entry => {
|
||
const li = document.createElement('li');
|
||
li.className = `shopping-item-row ${entry.checked ? 'checked' : ''}`;
|
||
const sourcesHtml = entry.sources.length > 0
|
||
? `<div class="shopping-item-sources">${entry.sources.map(s => `<span class="shopping-source-tag">${s}</span>`).join('')}</div>`
|
||
: '';
|
||
|
||
li.innerHTML = `
|
||
<div class="shopping-item-main">
|
||
<div class="checkbox-custom"><i data-lucide="check" style="width:12px;height:12px;"></i></div>
|
||
<div class="shopping-item-text-wrap">
|
||
<span class="shopping-item-text">${entry.displayText}</span>
|
||
${sourcesHtml}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
li.addEventListener('click', () => toggleSharedItem(entry.itemIds, !entry.checked));
|
||
ul.appendChild(li);
|
||
});
|
||
|
||
contentEl.appendChild(card);
|
||
}
|
||
lucide.createIcons();
|
||
}
|
||
|
||
async function toggleSharedItem(itemIds, checked) {
|
||
try {
|
||
const res = await fetch(`/api/share/shopping-lists/${encodeURIComponent(sharedShareToken)}/items/check`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ itemIds, checked })
|
||
});
|
||
if (res.ok) {
|
||
sharedListData = await res.json();
|
||
renderSharedList();
|
||
}
|
||
} catch (err) {
|
||
console.error('Error toggling shared shopping list item:', err);
|
||
}
|
||
}
|
||
|
||
let copyTimeout = null;
|
||
async function handleCopySharedText() {
|
||
if (!sharedListData || !Array.isArray(sharedListData.items)) return;
|
||
const entries = buildDisplayEntries(sharedListData.items, consolidateItems);
|
||
const grouped = groupEntries(entries, groupByMode);
|
||
|
||
const lines = [`🛒 ${sharedListData.name}`];
|
||
for (const [groupName, groupEntries] of grouped.entries()) {
|
||
lines.push(`\n[${groupName}]`);
|
||
groupEntries.forEach(entry => {
|
||
lines.push(`${entry.checked ? '[x]' : '[ ]'} ${entry.displayText}`);
|
||
});
|
||
}
|
||
|
||
const text = lines.join('\n');
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
await navigator.clipboard.writeText(text);
|
||
} else {
|
||
const ta = document.createElement('textarea');
|
||
ta.value = text;
|
||
ta.style.position = 'fixed';
|
||
ta.style.opacity = '0';
|
||
document.body.appendChild(ta);
|
||
ta.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(ta);
|
||
}
|
||
|
||
const btn = document.getElementById('btn-shared-copy-text');
|
||
if (btn) {
|
||
if (copyTimeout) clearTimeout(copyTimeout);
|
||
btn.innerHTML = `<i data-lucide="check"></i> <span>Copied!</span>`;
|
||
btn.style.borderColor = 'var(--accent)';
|
||
btn.style.color = 'var(--accent)';
|
||
lucide.createIcons();
|
||
copyTimeout = setTimeout(() => {
|
||
btn.innerHTML = `<i data-lucide="copy"></i> <span>Copy Text</span>`;
|
||
btn.style.borderColor = '';
|
||
btn.style.color = '';
|
||
lucide.createIcons();
|
||
}, 2000);
|
||
}
|
||
}
|