an / templates /base.html
Docfile's picture
Upload 23 files
75ba54e verified
<!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"> <!-- pb-20 for FAB space -->
<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>
<!-- Share App Button -->
<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 &copy; 2025</p>
</footer>
<!-- Share Modal -->
<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');
// Focus textarea
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;
// Close modal immediately to avoid capturing it or blocking UI
closeShareModal();
// Notify user generating image
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;
}
// Wrapper for style (NGL like background)
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';
// Clone the post
const clone = originalPost.cloneNode(true);
// Remove actions/buttons from clone
const actions = clone.querySelectorAll('button, a[href^="#"], .no-capture');
actions.forEach(el => el.remove());
// Style clone card
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';
// Add Branding
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' });
// Share data with text link
const shareData = {
files: [file],
title: 'GhostBoard',
text: url // Description text for WhatsApp
};
if (navigator.share && navigator.canShare && navigator.canShare({ files: [file] })) {
try {
await navigator.share(shareData);
} catch (err) {
console.log('Share dismissed or failed', err);
}
} else {
// Fallback download if web share not available (e.g. desktop)
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.");
}
}
// Image expansion logic
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 = ''; // Reset to class default
this.style.maxHeight = '';
}
}
});
});
// Register Service Worker
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);
});
}
}
// User nickname management
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(); // Refresh to update UI
} 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() {
// Check if user has a nickname
const nickname = getNickname();
if (!nickname) {
// Check with server if user exists
fetch('/api/me')
.then(response => response.json())
.then(data => {
if (data.nickname) {
localStorage.setItem('ghostboard_nickname', data.nickname);
} else {
// No nickname, show modal
document.getElementById('nickname-modal').classList.remove('hidden');
document.getElementById('nickname-input').focus();
}
})
.catch(error => {
console.error('Error checking user:', error);
// On error, show modal anyway
document.getElementById('nickname-modal').classList.remove('hidden');
document.getElementById('nickname-input').focus();
});
}
}
function closeNicknameModal() {
document.getElementById('nickname-modal').classList.add('hidden');
}
// Auto-fill nickname fields
function autoFillNickname() {
const nickname = getNickname();
if (nickname) {
// Fill nickname inputs
const inputs = document.querySelectorAll('input[name="nickname"]');
inputs.forEach(input => {
if (!input.value) {
input.value = nickname;
}
});
// Hide nickname fields since user has a nickname
const nicknameFields = document.querySelectorAll('#nickname-field, #nickname-input');
nicknameFields.forEach(field => {
if (field) field.style.display = 'none';
});
} else {
// Show nickname fields for anonymous users
const nicknameFields = document.querySelectorAll('#nickname-field, #nickname-input');
nicknameFields.forEach(field => {
if (field) field.style.display = 'block';
});
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
// Check nickname on index page
if (window.location.pathname === '/') {
checkNicknameAndShowModal();
}
// Auto-fill nickname fields
autoFillNickname();
});
</script>
</body>
</html>