// public/message.js - VERSION DEBUG & UX CORRIGÉE function $(selector, root = document) { return root.querySelector(selector); } function showToast(message, type = 'success') { const container = $('#toast'); if (!container) return; const el = document.createElement('div'); el.className = `toast ${type}`; el.textContent = message; container.appendChild(el); requestAnimationFrame(() => el.classList.add('show')); setTimeout(() => { el.classList.remove('show'); el.addEventListener('transitionend', () => el.remove(), { once: true }); }, 4000); } function escapeHtml(unsafe) { return String(unsafe).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } function formatDate(ts) { return new Date(ts * 1000).toLocaleString('fr-FR', { day:'2-digit', month:'2-digit', hour:'2-digit', minute:'2-digit' }); } // --- Config --- const SLUG = window.location.pathname.slice(1); const MAX_FILES = 3, MAX_SIZE = 15 * 1024 * 1024, MAX_CHARS = 1000; const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif']; // --- State --- let selectedFiles = []; // --- DOM --- const slugDisplay = $('#slugDisplay'); const msgForm = $('#msgForm'); const contentEl = $('#content'); const charCount = $('#charCount'); const dropZone = $('#dropZone'); const fileInput = $('#fileInput'); const previews = $('#previews'); const dropText = $('#dropText'); const btnSend = $('#btnSend'); const legalCheck = $('#legalCheck'); const messagesList = $('#messagesList'); const imgModal = $('#imgModal'); const imgModalImg = $('#imgModal img'); const imgModalClose = $('#imgModal .close-btn'); // --- Init --- function init() { slugDisplay.textContent = SLUG; document.title = `Message pour /${SLUG}`; setupForm(); setupDropZone(); setupImageModal(); loadMessages(); } // --- Form --- function setupForm() { contentEl.addEventListener('input', () => { const len = contentEl.value.length; charCount.textContent = len; charCount.classList.toggle('warning', len > MAX_CHARS * 0.8); charCount.classList.toggle('danger', len >= MAX_CHARS); checkFormValidity(); }); legalCheck.addEventListener('change', checkFormValidity); msgForm.addEventListener('submit', async (e) => { e.preventDefault(); // --- DEBUG : Affiche l'état exact avant envoi --- const content = contentEl.value.trim(); const hasText = content.length > 0; const hasFiles = selectedFiles.length > 0; const legal = legalCheck.checked; console.log('[DEBUG SUBMIT]', { hasText, hasFiles, legal, contentLen: content.length, filesCount: selectedFiles.length }); // 1. Validation Contenu (Texte OU Image) if (!hasText && !hasFiles) { showToast('❌ Écrivez un message OU ajoutez une image', 'error'); return; } if (content.length > MAX_CHARS) return showToast(`Trop long (max ${MAX_CHARS})`, 'error'); if (selectedFiles.length > MAX_FILES) return showToast(`Max ${MAX_FILES} images`, 'error'); // 2. Validation Légale (Double check) if (!legal) { showToast('❌ Vous devez cocher la case d\'attestation légale', 'error'); legalCheck.focus(); return; } // 3. Envoi btnSend.disabled = true; btnSend.textContent = 'Envoi...'; const formData = new FormData(); formData.append('content', content); formData.append('legal_accepted', 'true'); // String "true" attendue par le backend selectedFiles.forEach(f => formData.append('images', f)); // DEBUG: Voir ce qui part dans FormData for (let pair of formData.entries()) { console.log('[FORMDATA]', pair[0], pair[1] instanceof File ? `(File: ${pair[1].name})` : pair[1]); } try { const res = await fetch(`/api/public/${SLUG}/message`, { method: 'POST', body: formData, credentials: 'include' }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || `Erreur ${res.status}`); showToast('✅ Message envoyé !', 'success'); msgForm.reset(); selectedFiles = []; updatePreviews(); charCount.textContent = '0'; charCount.classList.remove('warning', 'danger'); loadMessages(); } catch (err) { console.error('Send Error:', err); showToast(err.message, 'error'); } finally { btnSend.disabled = false; checkFormValidity(); } }); } function checkFormValidity() { const hasText = contentEl.value.trim().length > 0; const hasFiles = selectedFiles.length > 0; const legal = legalCheck.checked; const ready = legal && (hasText || hasFiles); btnSend.disabled = !ready; // UX : Change le texte du bouton pour expliquer pourquoi c'est grisé if (!legal) btnSend.textContent = '⚠️ Cochez la case légale pour activer'; else if (!hasText && !hasFiles) btnSend.textContent = '📝 Écrivez ou ajoutez une image'; else btnSend.textContent = '✅ Valider et Envoyer'; } // --- Dropzone & Files (Identique, robuste) --- function setupDropZone() { ['dragenter','dragover'].forEach(ev => dropZone.addEventListener(ev, e => { e.preventDefault(); e.stopPropagation(); dropZone.classList.add('drag'); })); ['dragleave','drop'].forEach(ev => dropZone.addEventListener(ev, e => { e.preventDefault(); e.stopPropagation(); dropZone.classList.remove('drag'); })); dropZone.addEventListener('drop', e => handleFiles(e.dataTransfer.files)); dropZone.addEventListener('click', e => { // Si on clique directement sur l'input file, on laisse le navigateur faire nativement if (e.target === fileInput) return; if (!e.target.closest('.preview-item button')) fileInput.click(); }); dropZone.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); fileInput.click(); } }); fileInput.addEventListener('change', e => { handleFiles(e.target.files); fileInput.value = ''; }); } function handleFiles(files) { const arr = [...files]; const slots = MAX_FILES - selectedFiles.length; if (slots <= 0) return showToast(`Max ${MAX_FILES} images`, 'error'); const valid = arr.filter(f => { if (!ALLOWED_TYPES.includes(f.type)) { showToast(`${f.name}: Format non supporté`, 'error'); return false; } if (f.size > MAX_SIZE) { showToast(`${f.name}: > 15 Mo`, 'error'); return false; } return true; }).slice(0, slots); if (valid.length < arr.length && arr.length > slots) showToast(`Max ${MAX_FILES} images`, 'error'); selectedFiles.push(...valid); updatePreviews(); checkFormValidity(); } function updatePreviews() { previews.innerHTML = ''; selectedFiles.forEach((file, i) => { const url = URL.createObjectURL(file); const div = document.createElement('div'); div.className = 'preview-item'; div.innerHTML = `${escapeHtml(file.name)}`; div.querySelector('button').onclick = e => { e.stopPropagation(); URL.revokeObjectURL(url); selectedFiles.splice(i, 1); updatePreviews(); checkFormValidity(); }; div.querySelector('img').onclick = () => openImgModal(url); previews.appendChild(div); }); dropText.textContent = selectedFiles.length ? `${selectedFiles.length}/${MAX_FILES} image(s)` : 'Cliquez ou glissez-déposez vos images ici'; } // --- Messages --- async function loadMessages() { try { const res = await fetch(`/api/public/${SLUG}`, { credentials: 'include' }); if (!res.ok) throw new Error(res.status === 404 ? 'Lien introuvable' : 'Erreur chargement'); const { link, messages } = await res.json(); slugDisplay.textContent = link.slug; document.title = `Messages pour /${link.slug}`; renderMessages(messages); } catch (e) { messagesList.innerHTML = `
${escapeHtml(e.message)}
`; } } function renderMessages(messages) { if (!messages.length) { messagesList.innerHTML = '
Aucun message. Soyez le premier !
'; return; } messagesList.innerHTML = messages.map(m => `
🕒 ${formatDate(m.created_at)}Anonyme
${m.content ? `
${escapeHtml(m.content)}
` : ''} ${m.images?.length ? `
${m.images.map(i => `${escapeHtml(i.filename)}`).join('')}
` : ''}
`).join(''); messagesList.querySelectorAll('.msg-images img').forEach(img => img.onclick = () => openImgModal(img.src)); } // --- Modal --- function openImgModal(src) { imgModalImg.src = src; imgModal.classList.add('open'); document.body.style.overflow = 'hidden'; } function closeImgModal() { imgModal.classList.remove('open'); imgModalImg.src = ''; document.body.style.overflow = ''; } function setupImageModal() { imgModalClose.onclick = closeImgModal; imgModal.onclick = e => { if (e.target === imgModal) closeImgModal(); }; document.addEventListener('keydown', e => { if (e.key === 'Escape' && imgModal.classList.contains('open')) closeImgModal(); }); } init();