/* ============================================ GrubFlip — Trade Flow Trade state machine, trade cards, actions ============================================ */ const Trades = (() => { 'use strict'; const { api, $, $$, el, timeAgo, toast } = GrubFlip; // --- State Machine Definition --- const TRANSITIONS = { pending: ['accepted', 'declined', 'cancelled', 'expired'], accepted: ['confirmed', 'declined', 'cancelled'], confirmed: ['completed', 'cancelled'], completed: [], // terminal declined: [], // terminal cancelled: [], // terminal expired: [], // terminal }; const STATUS_META = { pending: { label: 'Pending', icon: '⏳', actionable: true }, accepted: { label: 'Accepted', icon: '👍', actionable: true }, confirmed: { label: 'Confirmed', icon: '📍', actionable: true }, completed: { label: 'Completed', icon: '✅', actionable: false }, declined: { label: 'Declined', icon: '❌', actionable: false }, cancelled: { label: 'Cancelled', icon: '🚫', actionable: false }, expired: { label: 'Expired', icon: '⏰', actionable: false }, }; let trades = []; let activeTab = 'active'; // 'active' | 'completed' let loading = false; // --- Trade Card Rendering --- function renderTradeCard(trade, currentUserId) { const card = el('div', { className: 'trade-card', dataset: { tradeId: trade.id } }); // Status bar const statusBar = el('div', { className: 'trade-card__status-bar' }); const meta = STATUS_META[trade.status] || STATUS_META.pending; statusBar.appendChild(el('span', { className: `trade-card__status trade-card__status--${trade.status}`, textContent: `${meta.icon} ${meta.label}`, })); statusBar.appendChild(el('span', { className: 'trade-card__time', textContent: timeAgo(trade.updated_at || trade.created_at), })); card.appendChild(statusBar); // Meals being traded const mealsSection = el('div', { className: 'trade-card__meals' }); // Offered meal (what the requester is offering) const offeredMeal = el('div', { className: 'trade-card__meal' }); offeredMeal.appendChild(el('img', { className: 'trade-card__meal-img', src: trade.offered_meal.image_thumb_url, alt: trade.offered_meal.title, loading: 'lazy', })); const offeredInfo = el('div', { className: 'trade-card__meal-info' }); offeredInfo.appendChild(el('p', { className: 'trade-card__meal-title', textContent: trade.offered_meal.title })); offeredInfo.appendChild(el('p', { className: 'trade-card__meal-user', textContent: trade.requester.display_name })); offeredMeal.appendChild(offeredInfo); mealsSection.appendChild(offeredMeal); // Swap icon mealsSection.appendChild(el('span', { className: 'trade-card__swap-icon', innerHTML: ``, })); // Requested meal (what the requester wants) const requestedMeal = el('div', { className: 'trade-card__meal' }); requestedMeal.appendChild(el('img', { className: 'trade-card__meal-img', src: trade.requested_meal.image_thumb_url, alt: trade.requested_meal.title, loading: 'lazy', })); const requestedInfo = el('div', { className: 'trade-card__meal-info' }); requestedInfo.appendChild(el('p', { className: 'trade-card__meal-title', textContent: trade.requested_meal.title })); requestedInfo.appendChild(el('p', { className: 'trade-card__meal-user', textContent: trade.owner.display_name })); requestedMeal.appendChild(requestedInfo); mealsSection.appendChild(requestedMeal); card.appendChild(mealsSection); // Message if (trade.message) { card.appendChild(el('p', { className: 'trade-card__message', textContent: `"${trade.message}"`, })); } // Meetup info (if confirmed) if (trade.status === 'confirmed' && (trade.meetup_location || trade.meetup_time)) { const meetup = el('div', { className: 'trade-card__message', style: 'font-style:normal; background:var(--gf-secondary-bg);', }); let meetupText = '📍 Meetup: '; if (trade.meetup_location) meetupText += trade.meetup_location; if (trade.meetup_time) meetupText += ` at ${new Date(trade.meetup_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`; meetup.textContent = meetupText; card.appendChild(meetup); } // Action buttons — based on status and role const actions = getActions(trade, currentUserId); if (actions.length > 0) { const actionsWrap = el('div', { className: 'trade-card__actions' }); for (const action of actions) { actionsWrap.appendChild(el('button', { className: `btn btn--${action.style} btn--sm`, textContent: action.label, onClick: () => handleAction(trade.id, action.action), })); } card.appendChild(actionsWrap); } return card; } // --- Determine Available Actions --- function getActions(trade, currentUserId) { const isOwner = trade.owner.id === currentUserId; const isRequester = trade.requester.id === currentUserId; const actions = []; switch (trade.status) { case 'pending': if (isOwner) { actions.push({ label: 'Accept', action: 'accept', style: 'primary' }); actions.push({ label: 'Decline', action: 'decline', style: 'ghost' }); } if (isRequester) { actions.push({ label: 'Cancel Request', action: 'cancel', style: 'ghost' }); } break; case 'accepted': actions.push({ label: 'Set Meetup', action: 'set-meetup', style: 'primary' }); actions.push({ label: 'Cancel', action: 'cancel', style: 'ghost' }); break; case 'confirmed': actions.push({ label: 'Mark Complete', action: 'complete', style: 'secondary' }); actions.push({ label: 'Cancel', action: 'cancel', style: 'ghost' }); break; } return actions; } // --- Handle Trade Actions --- async function handleAction(tradeId, action) { try { switch (action) { case 'accept': await api.put(`/trades/${tradeId}`, { status: 'accepted' }); toast('Trade accepted! Set up a meetup time.', 'success'); break; case 'decline': if (!confirm('Decline this trade request?')) return; await api.put(`/trades/${tradeId}`, { status: 'declined' }); toast('Trade declined.', 'default'); break; case 'cancel': if (!confirm('Cancel this trade?')) return; await api.put(`/trades/${tradeId}`, { status: 'cancelled' }); toast('Trade cancelled.', 'default'); break; case 'complete': await api.put(`/trades/${tradeId}`, { status: 'completed' }); toast('Trade completed! 🎉', 'success'); break; case 'set-meetup': openMeetupModal(tradeId); return; // Don't reload — modal handles it default: return; } loadTrades(); // Refresh } catch (err) { toast('Action failed: ' + err.message, 'error'); } } // --- Meetup Modal --- function openMeetupModal(tradeId) { const overlay = $('#meetup-modal'); if (!overlay) return; overlay.classList.add('open'); const form = $('#meetup-form'); form.onsubmit = async (e) => { e.preventDefault(); const location = $('#meetup-location').value.trim(); const time = $('#meetup-time').value; if (!location) { toast('Please enter a meetup location', 'warning'); return; } try { await api.put(`/trades/${tradeId}`, { status: 'confirmed', meetup_location: location, meetup_time: time || null, }); toast('Meetup confirmed! 📍', 'success'); overlay.classList.remove('open'); loadTrades(); } catch (err) { toast('Failed to confirm meetup: ' + err.message, 'error'); } }; // Close on overlay click overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.classList.remove('open'); }); } // --- Load Trades --- async function loadTrades() { if (loading) return; loading = true; const list = $('#trades-list'); if (!list) return; list.innerHTML = '
'; try { const data = await api.get('/trades', { status: activeTab === 'active' ? 'active' : 'completed' }); trades = data.trades || []; list.innerHTML = ''; // TODO: get actual current user ID from auth const currentUserId = parseInt(localStorage.getItem('gf_user_id') || '0', 10); if (trades.length === 0) { list.innerHTML = `

No ${activeTab} trades

${activeTab === 'active' ? 'Browse the feed and request a swap!' : 'Your completed trades will show here.'}

`; return; } for (const trade of trades) { list.appendChild(renderTradeCard(trade, currentUserId)); } } catch (err) { toast('Failed to load trades: ' + err.message, 'error'); list.innerHTML = ''; } finally { loading = false; } } // --- Tab Switching --- function setupTabs() { const tabs = $$('.trade-tab'); for (const tab of tabs) { tab.addEventListener('click', () => { tabs.forEach(t => t.classList.remove('active')); tab.classList.add('active'); activeTab = tab.dataset.tab; loadTrades(); }); } } // --- Init --- function init() { const tradesContainer = $('#trades-container'); if (!tradesContainer) return; setupTabs(); loadTrades(); } return { init, renderTradeCard, loadTrades }; })(); document.addEventListener('DOMContentLoaded', Trades.init);