/* ============================================ GrubFlip — Post Meal Form Image upload, dietary tag picker, meal creation with preview. ============================================ */ const PostMeal = (() => { 'use strict'; const { api, $, $$, el, toast } = GrubFlip; let uploadedImage = null; // { url, thumb_url, upload_id } let submitting = false; const DIETARY_TAGS = [ 'vegan', 'vegetarian', 'gluten-free', 'dairy', 'dairy-free', 'nut-free', 'halal', 'kosher', 'high-protein', 'low-carb', 'spicy' ]; const CUISINES = [ 'Italian', 'Mexican', 'Chinese', 'Japanese', 'Indian', 'Thai', 'Mediterranean', 'American', 'Korean', 'Vietnamese', 'French', 'Middle Eastern', 'Caribbean', 'Ethiopian', 'Other' ]; // --- Image Upload --- function setupImageUpload() { const zone = $('#upload-zone'); const fileInput = $('#image-input'); if (!zone || !fileInput) return; // Click to select zone.addEventListener('click', (e) => { if (e.target.closest('.btn--remove')) return; fileInput.click(); }); // File selected fileInput.addEventListener('change', () => { if (fileInput.files.length) handleFile(fileInput.files[0]); }); // Drag and drop zone.addEventListener('dragover', (e) => { e.preventDefault(); zone.classList.add('dragover'); }); zone.addEventListener('dragleave', () => { zone.classList.remove('dragover'); }); zone.addEventListener('drop', (e) => { e.preventDefault(); zone.classList.remove('dragover'); const file = e.dataTransfer.files[0]; if (file) handleFile(file); }); // Remove button const removeBtn = zone.querySelector('.btn--remove'); if (removeBtn) { removeBtn.addEventListener('click', (e) => { e.stopPropagation(); clearUpload(); }); } } async function handleFile(file) { // Validate const validTypes = ['image/jpeg', 'image/png', 'image/webp']; if (!validTypes.includes(file.type)) { toast('Please upload a JPG, PNG, or WebP image', 'error'); return; } if (file.size > 20 * 1024 * 1024) { toast('Image must be under 20MB', 'error'); return; } const zone = $('#upload-zone'); const previewImg = $('#upload-preview-img'); // Show local preview immediately const reader = new FileReader(); reader.onload = (e) => { previewImg.src = e.target.result; zone.classList.add('has-preview'); }; reader.readAsDataURL(file); // Upload to server try { zone.style.opacity = '0.7'; const data = await api.upload('/uploads/image', file); uploadedImage = { url: data.url, thumb_url: data.thumb_url, upload_id: data.upload_id, }; toast('Image uploaded!', 'success'); } catch (err) { toast('Upload failed — ' + err.message, 'error'); clearUpload(); } finally { zone.style.opacity = ''; } } function clearUpload() { uploadedImage = null; const zone = $('#upload-zone'); const fileInput = $('#image-input'); const previewImg = $('#upload-preview-img'); if (zone) zone.classList.remove('has-preview'); if (fileInput) fileInput.value = ''; if (previewImg) previewImg.src = ''; } // --- Expiry Picker --- function setupExpiryPicker() { const select = $('#expires-select'); if (!select) return; // Populate with reasonable time windows const options = [ { value: '2', label: '2 hours' }, { value: '4', label: '4 hours' }, { value: '6', label: '6 hours' }, { value: '8', label: '8 hours' }, { value: '12', label: '12 hours' }, { value: '24', label: '24 hours' }, ]; select.innerHTML = ''; for (const opt of options) { select.appendChild(el('option', { value: opt.value, textContent: opt.label })); } } // --- Form Validation --- function validate() { const errors = []; const title = $('#meal-title')?.value.trim(); const description = $('#meal-description')?.value.trim(); const expiresHours = $('#expires-select')?.value; if (!title || title.length < 3) errors.push({ field: 'meal-title', msg: 'Title must be at least 3 characters' }); if (title && title.length > 100) errors.push({ field: 'meal-title', msg: 'Title is too long (max 100 chars)' }); if (!description) errors.push({ field: 'meal-description', msg: 'Add a short description' }); if (!expiresHours) errors.push({ field: 'expires-select', msg: 'Set a pickup window' }); if (!uploadedImage) errors.push({ field: 'upload-zone', msg: 'Add a photo of your meal' }); // Clear previous errors $$('.form-error').forEach(el => el.remove()); $$('.form-input, .form-textarea, .form-select').forEach(el => el.style.borderColor = ''); // Show errors for (const error of errors) { const fieldEl = $(`#${error.field}`); if (fieldEl) { fieldEl.style.borderColor = 'var(--gf-error)'; const errEl = document.createElement('p'); errEl.className = 'form-error'; errEl.textContent = error.msg; fieldEl.parentElement.appendChild(errEl); } } return errors.length === 0; } // --- Submit --- async function handleSubmit(e) { e.preventDefault(); if (submitting) return; if (!validate()) return; submitting = true; const submitBtn = $('#submit-btn'); if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Posting...'; } // Gather selected dietary tags const selectedTags = $$('.tag-picker__option:checked').map(cb => cb.value); const expiresHours = parseInt($('#expires-select').value, 10); const expiresAt = new Date(Date.now() + expiresHours * 3600000).toISOString(); const payload = { title: $('#meal-title').value.trim(), description: $('#meal-description').value.trim(), upload_id: uploadedImage.upload_id, dietary_tags: selectedTags, cuisine: $('#cuisine-select').value || null, portion_size: $('#portion-select').value || 'single', trade_type: $('#trade-type-select')?.value || 'swap', expires_at: expiresAt, }; try { const result = await api.post('/meals', payload); toast('Meal posted! 🎉', 'success'); // Redirect to feed after short delay setTimeout(() => { window.location.href = '/grubflip/index.html'; }, 1000); } catch (err) { toast('Failed to post — ' + err.message, 'error'); } finally { submitting = false; if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Post Meal'; } } } // --- Init --- function init() { const form = $('#post-meal-form'); if (!form) return; setupImageUpload(); setupExpiryPicker(); form.addEventListener('submit', handleSubmit); // Character counter for description const desc = $('#meal-description'); const counter = $('#desc-counter'); if (desc && counter) { desc.addEventListener('input', () => { const len = desc.value.length; counter.textContent = `${len}/300`; counter.style.color = len > 300 ? 'var(--gf-error)' : 'var(--gf-neutral-400)'; }); } } return { init }; })(); document.addEventListener('DOMContentLoaded', PostMeal.init);