/* ============================================ GrubFlip — Meal Detail Modal Shows full meal info in a bottom sheet modal. Triggered from feed card clicks. ============================================ */ const MealDetail = (() => { 'use strict'; const { api, $, el, timeUntil, timeAgo, starsHTML, toast, isLoggedIn } = GrubFlip; let overlay = null; let currentMeal = null; // --- Build Modal DOM (once) --- function ensureModal() { if (overlay) return overlay; overlay = el('div', { className: 'modal-overlay', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Meal details', }); const sheet = el('div', { className: 'modal-sheet meal-detail' }); sheet.innerHTML = `

`; overlay.appendChild(sheet); document.body.appendChild(overlay); // Close on overlay click (not sheet) overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); // Close button sheet.querySelector('.meal-detail__close').addEventListener('click', close); // Close on Escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && overlay.classList.contains('open')) close(); }); return overlay; } // --- Populate Modal with Meal Data --- function populate(meal) { currentMeal = meal; const modal = ensureModal(); const sheet = modal.querySelector('.modal-sheet'); // Image const img = sheet.querySelector('.meal-detail__image'); img.src = meal.image_url || meal.image_thumb_url; img.alt = meal.title; // Badges (trade type + expiry) const badges = sheet.querySelector('.meal-detail__badges'); badges.innerHTML = ''; const tradeLabel = meal.trade_type === 'swap' ? 'Swap' : meal.trade_type === 'free' ? 'Free' : meal.trade_type; const tradeClass = meal.trade_type === 'free' ? 'meal-detail__badge--free' : ''; badges.innerHTML += `${tradeLabel}`; if (meal.expires_at) { const remaining = timeUntil(meal.expires_at); if (remaining !== 'expired') { badges.innerHTML += ` ${remaining} `; } else { badges.innerHTML += `Expired`; } } // Title sheet.querySelector('.meal-detail__title').textContent = meal.title; // Description (full, no truncation) sheet.querySelector('.meal-detail__desc').textContent = meal.description; // Meta line (posted time + servings) const meta = sheet.querySelector('.meal-detail__meta'); const metaParts = []; if (meal.created_at) metaParts.push(`Posted ${timeAgo(meal.created_at)}`); if (meal.servings) metaParts.push(`${meal.servings} serving${meal.servings > 1 ? 's' : ''}`); if (meal.distance_km != null) metaParts.push(`${meal.distance_km} km away`); meta.innerHTML = metaParts.map(p => `${p}` ).join(''); // Tags (all of them — no limit) const tagsWrap = sheet.querySelector('.meal-detail__tags'); tagsWrap.innerHTML = ''; if (meal.cuisine) { tagsWrap.innerHTML += `${meal.cuisine}`; } for (const tag of (meal.dietary_tags || [])) { tagsWrap.innerHTML += `${tag}`; } // User section const userSection = sheet.querySelector('.meal-detail__user-section'); userSection.innerHTML = `
${meal.user.display_name}
${meal.user.display_name} ${starsHTML(meal.user.rating)} · ${meal.user.trade_count} trade${meal.user.trade_count !== 1 ? 's' : ''}
`; // Message button handler const msgBtn = userSection.querySelector('.meal-detail__message-btn'); msgBtn.addEventListener('click', () => { if (!isLoggedIn()) { toast('Sign in to message this person', 'warning'); return; } // Navigate to chat with context window.location.href = `chat.html?user=${meal.user.id}&meal=${meal.id}`; }); // Details grid (pickup info, preferences, etc.) const detailsGrid = sheet.querySelector('.meal-detail__details-grid'); detailsGrid.innerHTML = ''; if (meal.pickup_location || meal.pickup_notes) { detailsGrid.innerHTML += `
Pickup ${meal.pickup_location || meal.pickup_notes || 'Contact for details'}
`; } if (meal.looking_for) { detailsGrid.innerHTML += `
Looking for ${meal.looking_for}
`; } if (meal.allergen_info) { detailsGrid.innerHTML += `
Allergen info ${meal.allergen_info}
`; } // Actions const actions = sheet.querySelector('.meal-detail__actions'); const isExpired = meal.expires_at && timeUntil(meal.expires_at) === 'expired'; const isOwnMeal = parseInt(localStorage.getItem('gf_user_id') || '0', 10) === meal.user.id; actions.innerHTML = ''; if (isOwnMeal) { actions.innerHTML = ` `; } else if (isExpired) { actions.innerHTML = `
This meal listing has expired
`; } else { actions.innerHTML = ` `; } // Action handlers const proposeBtn = actions.querySelector('[data-action="propose"]'); if (proposeBtn) { proposeBtn.addEventListener('click', () => { if (!isLoggedIn()) { toast('Sign in to propose a trade', 'warning'); return; } close(); window.location.href = `trades.html?propose=${meal.id}`; }); } const editBtn = actions.querySelector('[data-action="edit"]'); if (editBtn) { editBtn.addEventListener('click', () => { close(); window.location.href = `post.html?edit=${meal.id}`; }); } const deleteBtn = actions.querySelector('[data-action="delete"]'); if (deleteBtn) { deleteBtn.addEventListener('click', async () => { if (!confirm('Remove this meal listing?')) return; try { await api.delete(`/meals/${meal.id}`); toast('Meal removed', 'success'); close(); // Refresh feed if (typeof Feed !== 'undefined' && Feed.loadMeals) { Feed.loadMeals(true); } } catch (err) { toast(err.message, 'error'); } }); } } // --- Open / Close --- function open(meal) { populate(meal); const modal = ensureModal(); // Prevent body scroll document.body.style.overflow = 'hidden'; // Small delay for transition requestAnimationFrame(() => { modal.classList.add('open'); }); } function close() { if (!overlay) return; overlay.classList.remove('open'); document.body.style.overflow = ''; currentMeal = null; } // --- Public API --- return { open, close }; })();