| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <link rel="manifest" href="/manifest.json"> |
| <meta name="theme-color" content="#ffffff"> |
| <link rel="apple-touch-icon" href="/static/images/icon.svg"> |
| <meta name="apple-mobile-web-app-capable" content="yes"> |
| <meta name="apple-mobile-web-app-status-bar-style" content="default"> |
| <title>{% block title %}GhostBoard{% endblock %}</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> |
| <style> |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; |
| -webkit-font-smoothing: antialiased; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-50 text-gray-900 min-h-screen pb-20"> |
| <nav class="bg-white border-b border-gray-200 sticky top-0 z-50 bg-opacity-90 backdrop-blur-sm"> |
| <div class="container mx-auto px-4 py-3 flex justify-between items-center"> |
| <a href="{{ url_for('index') }}" class="text-xl font-bold text-gray-900 tracking-tight">GhostBoard</a> |
| <div class="flex space-x-3 text-sm font-medium overflow-x-auto no-scrollbar"> |
| {% for board in global_boards %} |
| <a href="{{ url_for('board', slug=board.slug) }}" class="text-gray-600 hover:text-blue-600 whitespace-nowrap">/{{ board.slug }}/</a> |
| {% endfor %} |
| </div> |
| </div> |
| </nav> |
|
|
| <main class="container mx-auto p-4"> |
| {% block content %}{% endblock %} |
| </main> |
|
|
| |
| <div class="container mx-auto px-4 mt-8 mb-4"> |
| <button onclick="shareAppOnWhatsApp()" class="w-full sm:w-auto mx-auto block bg-[#25D366] hover:bg-[#128C7E] text-white font-bold py-4 px-8 rounded-full shadow-lg transform transition hover:scale-105 flex items-center justify-center space-x-3"> |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="currentColor" viewBox="0 0 24 24"> |
| <path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.008-.57-.008-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 1.032 5.122 3.022 6.989 5.615.119 5.269 2.768 9.825 8.526 9.881-.14 0-.28 0-.42-.01l-.001-.001z"/> |
| </svg> |
| <span class="text-xl">Partager l'application</span> |
| </button> |
| </div> |
|
|
| <footer class="text-center text-gray-400 text-xs py-8 mt-8"> |
| <p>GhostBoard © 2025</p> |
| </footer> |
|
|
| |
| <div id="share-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-gray-900 bg-opacity-40 backdrop-blur-sm p-4"> |
| <div class="absolute inset-0" onclick="closeShareModal()"></div> |
| <div class="bg-white w-full max-w-sm rounded-2xl shadow-2xl transform transition-all relative z-10 overflow-hidden"> |
| <div class="p-6 text-center"> |
| <h3 class="text-xl font-bold text-gray-900 mb-6">Partager ce post</h3> |
| <div class="space-y-3"> |
| <button onclick="copyCurrentLink()" class="w-full bg-gray-100 hover:bg-gray-200 text-gray-800 font-semibold py-3 px-4 rounded-xl flex items-center justify-center transition-colors"> |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-3 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /> |
| </svg> |
| Copier le lien |
| </button> |
| <button onclick="shareToWhatsApp()" class="w-full bg-[#25D366] hover:bg-[#128C7E] text-white font-semibold py-3 px-4 rounded-xl flex items-center justify-center transition-colors"> |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-3" viewBox="0 0 24 24" fill="currentColor"> |
| <path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.008-.57-.008-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 1.032 5.122 3.022 6.989 5.615.119 5.269 2.768 9.825 8.526 9.881-.14 0-.28 0-.42-.01l-.001-.001z"/> |
| </svg> |
| Partager sur WhatsApp |
| </button> |
| </div> |
| <button onclick="closeShareModal()" class="mt-6 text-gray-500 hover:text-gray-700 text-sm font-medium">Fermer</button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| function shareAppOnWhatsApp() { |
| const url = window.location.origin; |
| const text = "Découvre GhostBoard, c'est génial ! " + url; |
| const whatsappUrl = `https://wa.me/?text=${encodeURIComponent(text)}`; |
| window.open(whatsappUrl, '_blank'); |
| } |
| |
| let currentShareId = null; |
| let currentShareUrl = null; |
| |
| function replyTo(id) { |
| const textarea = document.getElementById('content'); |
| const formContainer = document.getElementById('post-form-container'); |
| |
| if (formContainer && formContainer.classList.contains('hidden')) { |
| togglePostForm(); |
| } |
| |
| if (textarea) { |
| const currentVal = textarea.value; |
| textarea.value = currentVal + (currentVal ? '\n' : '') + '>>' + id + '\n'; |
| textarea.focus(); |
| textarea.scrollIntoView({ behavior: 'smooth', block: 'center' }); |
| } |
| } |
| |
| function togglePostForm() { |
| const container = document.getElementById('post-form-container'); |
| const fab = document.getElementById('fab-button'); |
| if (container.classList.contains('hidden')) { |
| container.classList.remove('hidden'); |
| |
| setTimeout(() => document.getElementById('content').focus(), 100); |
| if(fab) fab.classList.add('hidden'); |
| } else { |
| container.classList.add('hidden'); |
| if(fab) fab.classList.remove('hidden'); |
| } |
| } |
| |
| function highlightPost(id) { |
| const post = document.getElementById('post-' + id); |
| if (post) { |
| post.classList.add('bg-blue-50'); |
| post.classList.add('ring-2'); |
| post.classList.add('ring-blue-200'); |
| setTimeout(() => { |
| post.classList.remove('bg-blue-50'); |
| post.classList.remove('ring-2'); |
| post.classList.remove('ring-blue-200'); |
| }, 2000); |
| } |
| } |
| |
| function sharePost(id, url) { |
| currentShareId = id; |
| currentShareUrl = url; |
| |
| const modal = document.getElementById('share-modal'); |
| modal.classList.remove('hidden'); |
| } |
| |
| function closeShareModal() { |
| document.getElementById('share-modal').classList.add('hidden'); |
| currentShareId = null; |
| currentShareUrl = null; |
| } |
| |
| async function copyCurrentLink() { |
| if (!currentShareUrl) return; |
| try { |
| await navigator.clipboard.writeText(currentShareUrl); |
| alert('Lien copié dans le presse-papier !'); |
| closeShareModal(); |
| } catch (err) { |
| console.error('Failed to copy: ', err); |
| alert('Impossible de copier le lien'); |
| } |
| } |
| |
| async function shareToWhatsApp() { |
| if (!currentShareId || !currentShareUrl) return; |
| |
| const id = currentShareId; |
| const url = currentShareUrl; |
| |
| |
| closeShareModal(); |
| |
| |
| const toast = document.createElement('div'); |
| toast.className = 'fixed bottom-4 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white px-4 py-2 rounded-lg shadow-lg z-50'; |
| toast.textContent = 'Génération de l\'image...'; |
| document.body.appendChild(toast); |
| |
| const originalPost = document.getElementById('post-' + id); |
| if (!originalPost) { |
| toast.remove(); |
| return; |
| } |
| |
| |
| const wrapper = document.createElement('div'); |
| wrapper.style.width = '600px'; |
| wrapper.style.padding = '40px'; |
| wrapper.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'; |
| wrapper.style.display = 'flex'; |
| wrapper.style.flexDirection = 'column'; |
| wrapper.style.alignItems = 'center'; |
| wrapper.style.justifyContent = 'center'; |
| wrapper.style.position = 'absolute'; |
| wrapper.style.top = '-9999px'; |
| wrapper.style.left = '-9999px'; |
| |
| |
| const clone = originalPost.cloneNode(true); |
| |
| |
| const actions = clone.querySelectorAll('button, a[href^="#"], .no-capture'); |
| actions.forEach(el => el.remove()); |
| |
| |
| clone.id = 'clone-capture'; |
| clone.style.width = '100%'; |
| clone.style.maxWidth = '100%'; |
| clone.style.margin = '0'; |
| clone.style.backgroundColor = 'rgba(255, 255, 255, 0.95)'; |
| clone.style.borderRadius = '16px'; |
| clone.style.boxShadow = '0 10px 25px rgba(0,0,0,0.2)'; |
| clone.style.padding = '20px'; |
| clone.style.position = 'relative'; |
| clone.style.top = 'auto'; |
| clone.style.left = 'auto'; |
| |
| |
| const branding = document.createElement('div'); |
| branding.className = 'mt-4 pt-4 border-t border-gray-200 flex items-center justify-center text-gray-500 text-xs font-bold uppercase tracking-widest'; |
| branding.innerHTML = 'GhostBoard'; |
| |
| const contentDiv = clone.querySelector('.flex-grow') || clone; |
| contentDiv.appendChild(branding); |
| |
| wrapper.appendChild(clone); |
| document.body.appendChild(wrapper); |
| |
| try { |
| const canvas = await html2canvas(wrapper, { |
| scale: 2, |
| backgroundColor: null, |
| useCORS: true, |
| logging: false |
| }); |
| |
| document.body.removeChild(wrapper); |
| toast.remove(); |
| |
| canvas.toBlob(async (blob) => { |
| if (!blob) return; |
| |
| const file = new File([blob], `ghostboard-${id}.png`, { type: 'image/png' }); |
| |
| |
| const shareData = { |
| files: [file], |
| title: 'GhostBoard', |
| text: url |
| }; |
| |
| if (navigator.share && navigator.canShare && navigator.canShare({ files: [file] })) { |
| try { |
| await navigator.share(shareData); |
| } catch (err) { |
| console.log('Share dismissed or failed', err); |
| } |
| } else { |
| |
| const link = document.createElement('a'); |
| link.download = `ghostboard-${id}.png`; |
| link.href = canvas.toDataURL(); |
| link.click(); |
| alert("Partage natif non supporté sur cet appareil. L'image a été téléchargée. Vous pouvez maintenant la partager sur WhatsApp manuellement avec le lien : " + url); |
| } |
| }); |
| } catch (err) { |
| console.error('Capture failed', err); |
| if (wrapper.parentNode) document.body.removeChild(wrapper); |
| toast.remove(); |
| alert("Impossible de générer l'image. Veuillez réessayer."); |
| } |
| } |
| |
| |
| document.querySelectorAll('img.post-image').forEach(img => { |
| img.addEventListener('click', function(e) { |
| if (this.dataset.full) { |
| e.preventDefault(); |
| if (this.src === this.dataset.thumb) { |
| this.src = this.dataset.full; |
| this.style.maxWidth = '100%'; |
| this.style.maxHeight = 'none'; |
| } else { |
| this.src = this.dataset.thumb; |
| this.style.maxWidth = ''; |
| this.style.maxHeight = ''; |
| } |
| } |
| }); |
| }); |
| |
| |
| if ('serviceWorker' in navigator) { |
| window.addEventListener('load', () => { |
| navigator.serviceWorker.register('/sw.js') |
| .then(reg => console.log('SW Registered')) |
| .catch(err => console.log('SW Registration Failed', err)); |
| }); |
| } |
| |
| function previewFiles(input, previewId) { |
| const preview = document.getElementById(previewId); |
| if (!preview) return; |
| preview.innerHTML = ''; |
| if (input.files) { |
| Array.from(input.files).forEach(file => { |
| const reader = new FileReader(); |
| reader.onload = function(e) { |
| if (file.type.startsWith('image/')) { |
| const img = document.createElement('img'); |
| img.src = e.target.result; |
| img.className = 'w-20 h-20 object-cover rounded shadow-sm'; |
| preview.appendChild(img); |
| } else if (file.type.startsWith('video/')) { |
| const vid = document.createElement('video'); |
| vid.src = e.target.result; |
| vid.className = 'w-20 h-20 object-cover rounded shadow-sm bg-black'; |
| preview.appendChild(vid); |
| } |
| } |
| reader.readAsDataURL(file); |
| }); |
| } |
| } |
| |
| |
| function setNickname(event) { |
| event.preventDefault(); |
| const nickname = document.getElementById('nickname-input').value.trim(); |
| |
| if (!nickname) { |
| alert('Veuillez entrer un pseudo'); |
| return; |
| } |
| |
| fetch('/api/set-nickname', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify({ nickname: nickname }) |
| }) |
| .then(response => response.json()) |
| .then(data => { |
| if (data.success) { |
| localStorage.setItem('ghostboard_nickname', data.nickname); |
| closeNicknameModal(); |
| location.reload(); |
| } else { |
| alert(data.error || 'Erreur lors de la sauvegarde'); |
| } |
| }) |
| .catch(error => { |
| console.error('Error:', error); |
| alert('Erreur de connexion'); |
| }); |
| } |
| |
| function getNickname() { |
| return localStorage.getItem('ghostboard_nickname'); |
| } |
| |
| function checkNicknameAndShowModal() { |
| |
| const nickname = getNickname(); |
| if (!nickname) { |
| |
| fetch('/api/me') |
| .then(response => response.json()) |
| .then(data => { |
| if (data.nickname) { |
| localStorage.setItem('ghostboard_nickname', data.nickname); |
| } else { |
| |
| document.getElementById('nickname-modal').classList.remove('hidden'); |
| document.getElementById('nickname-input').focus(); |
| } |
| }) |
| .catch(error => { |
| console.error('Error checking user:', error); |
| |
| document.getElementById('nickname-modal').classList.remove('hidden'); |
| document.getElementById('nickname-input').focus(); |
| }); |
| } |
| } |
| |
| function closeNicknameModal() { |
| document.getElementById('nickname-modal').classList.add('hidden'); |
| } |
| |
| |
| function autoFillNickname() { |
| const nickname = getNickname(); |
| if (nickname) { |
| |
| const inputs = document.querySelectorAll('input[name="nickname"]'); |
| inputs.forEach(input => { |
| if (!input.value) { |
| input.value = nickname; |
| } |
| }); |
| |
| |
| const nicknameFields = document.querySelectorAll('#nickname-field, #nickname-input'); |
| nicknameFields.forEach(field => { |
| if (field) field.style.display = 'none'; |
| }); |
| } else { |
| |
| const nicknameFields = document.querySelectorAll('#nickname-field, #nickname-input'); |
| nicknameFields.forEach(field => { |
| if (field) field.style.display = 'block'; |
| }); |
| } |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', function() { |
| |
| if (window.location.pathname === '/') { |
| checkNicknameAndShowModal(); |
| } |
| |
| |
| autoFillNickname(); |
| }); |
| </script> |
| </body> |
| </html> |
|
|