| | <!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> |
| |
|