Download templates/base.html from Docfile/an: direct link, hf CLI and curl.
- Browser
- Download file 20.2 kB
-
https://huggingface.co/spaces/Docfile/an/resolve/main/templates/base.html
- Command line
-
hf download hf://spaces/Docfile/an/templates/base.html
-
curl -L -o base.html https://huggingface.co/spaces/Docfile/an/resolve/main/templates/base.html
20.2 kB
| <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 © 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> | |