/* ============================================
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}
${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 };
})();