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