Download templates/thread.html from Docfile/an: direct link, hf CLI and curl.
- Browser
- Download file 19.9 kB
-
https://huggingface.co/spaces/Docfile/an/resolve/main/templates/thread.html
- Command line
-
hf download hf://spaces/Docfile/an/templates/thread.html
-
curl -L -o thread.html https://huggingface.co/spaces/Docfile/an/resolve/main/templates/thread.html
19.9 kB
| {% extends 'base.html' %} | |
| {% block title %}/{{ board.slug }}/ - Thread #{{ thread.id }}{% endblock %} | |
| {% block content %} | |
| <!-- Add padding bottom to account for fixed bottom bar --> | |
| <div class="max-w-4xl mx-auto pb-40 px-3 sm:px-6"> | |
| <div class="mb-6 pt-6"> | |
| <a href="{{ url_for('board', slug=board.slug) }}" class="inline-flex items-center text-slate-500 hover:text-blue-600 transition-colors font-medium text-sm"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1.5" viewBox="0 0 20 20" fill="currentColor"> | |
| <path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd" /> | |
| </svg> | |
| Retour à /{{ board.slug }}/ | |
| </a> | |
| </div> | |
| <!-- Main Thread Post --> | |
| <!-- "Think Harder": Make OP look distinct, like a primary content block, not just another box --> | |
| <div class="bg-white rounded-2xl shadow-sm border border-slate-100 p-5 sm:p-6 mb-10 relative overflow-hidden" id="post-{{ thread.id }}"> | |
| <!-- Decorative subtle background gradient for OP --> | |
| <div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-500"></div> | |
| <div class="flex flex-col gap-6"> | |
| <!-- Header: Author & Meta --> | |
| <div class="flex items-center justify-between border-b border-slate-50 pb-4"> | |
| <div class="flex items-center gap-3"> | |
| <div class="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-white font-bold text-lg shadow-md select-none"> | |
| {{ thread.nickname[0] | upper }} | |
| </div> | |
| <div class="flex flex-col"> | |
| <span class="font-bold text-slate-900 text-sm">{{ thread.nickname }}</span> | |
| <div class="flex items-center text-xs text-slate-400 gap-2"> | |
| <span>{{ thread.created_at.strftime('%d %b %Y à %H:%M') }}</span> | |
| <span class="w-1 h-1 rounded-full bg-slate-300"></span> | |
| <a href="#post-{{ thread.id }}" class="hover:text-blue-500">N° {{ thread.id }}</a> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex gap-2"> | |
| <button class="text-slate-400 hover:text-blue-600 p-2 hover:bg-slate-50 rounded-full transition-colors" onclick="scrollToReply('{{ thread.id }}')" title="Répondre"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" /> | |
| </svg> | |
| </button> | |
| <button class="text-slate-400 hover:text-green-600 p-2 hover:bg-green-50 rounded-full transition-colors" onclick="sharePost('{{ thread.id }}', '{{ url_for('thread', thread_id=thread.id, _external=True) }}')" title="Partager"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Content Body --> | |
| <div class="space-y-6"> | |
| <!-- Text --> | |
| <div class="prose prose-slate max-w-none text-slate-800 text-lg leading-relaxed break-words"> | |
| {{ thread.content|format_post(post_context) }} | |
| </div> | |
| <!-- Media Grid --> | |
| {% if thread.files %} | |
| <div class="grid grid-cols-1 sm:grid-cols-2 gap-3"> | |
| {% for file in thread.files %} | |
| <div class="relative group rounded-xl overflow-hidden bg-slate-100 border border-slate-200 shadow-sm"> | |
| {% if file.filename.endswith(('.mp4', '.webm')) %} | |
| <video controls class="w-full h-full object-cover max-h-[500px]"> | |
| <source src="{{ url_for('static', filename='uploads/' + file.filename) }}" type="video/{{ file.filename.split('.')[-1] }}"> | |
| </video> | |
| {% else %} | |
| <img | |
| src="{{ url_for('static', filename='uploads/' + file.filename) }}" | |
| data-thumb="{{ url_for('static', filename='uploads/' + file.filename) }}" | |
| data-full="{{ url_for('static', filename='uploads/' + file.filename) }}" | |
| alt="Post image" | |
| class="post-image w-full h-auto object-contain max-h-[500px] cursor-pointer" | |
| > | |
| {% endif %} | |
| <a href="{{ url_for('static', filename='uploads/' + file.filename) }}" target="_blank" class="absolute bottom-2 right-2 bg-black/50 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity backdrop-blur-sm"> | |
| Full Size | |
| </a> | |
| </div> | |
| {% endfor %} | |
| </div> | |
| {% endif %} | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Replies List (Chat/Social Style) --> | |
| <!-- "Think Harder": Use spacing and grouping to make it feel like a flowing conversation --> | |
| <div class="space-y-6" id="comments-list"> | |
| {% for reply in replies %} | |
| <div class="group flex gap-3 sm:gap-4 animate-fade-in" id="post-{{ reply.id }}"> | |
| <!-- Avatar --> | |
| <div class="flex-shrink-0 pt-1"> | |
| <div class="w-9 h-9 rounded-full bg-slate-200 flex items-center justify-center text-slate-500 font-bold text-xs shadow-sm select-none border border-white"> | |
| {{ reply.nickname[0] | upper }} | |
| </div> | |
| </div> | |
| <div class="flex-grow min-w-0 max-w-full"> | |
| <div class="flex flex-col items-start"> | |
| <!-- Bubble Container --> | |
| <div class="relative max-w-full sm:max-w-[85%]"> | |
| <!-- Name (Facebook style: outside top left, or inside?) | |
| Let's do: Name slightly outside for clarity, bubble below. | |
| --> | |
| <span class="text-[13px] text-slate-900 font-bold ml-3 mb-1 block"> | |
| {{ reply.nickname }} | |
| <!-- Optional: ID pill --> | |
| <span class="text-[10px] text-slate-400 font-normal ml-1">#{{ reply.id }}</span> | |
| </span> | |
| <div class="bg-slate-100 rounded-2xl rounded-tl-md px-4 py-3 text-slate-800 break-words shadow-sm border border-slate-200/50"> | |
| <!-- Reply files --> | |
| {% if reply.files %} | |
| <div class="flex flex-wrap gap-2 mb-3"> | |
| {% for file in reply.files %} | |
| <div class="rounded-lg overflow-hidden border border-slate-200"> | |
| {% if file.filename.endswith(('.mp4', '.webm')) %} | |
| <video controls class="max-w-full max-h-64"> | |
| <source src="{{ url_for('static', filename='uploads/' + file.filename) }}" type="video/{{ file.filename.split('.')[-1] }}"> | |
| </video> | |
| {% else %} | |
| <img | |
| src="{{ url_for('static', filename='uploads/' + file.filename) }}" | |
| data-thumb="{{ url_for('static', filename='uploads/' + file.filename) }}" | |
| data-full="{{ url_for('static', filename='uploads/' + file.filename) }}" | |
| alt="Post image" | |
| class="post-image max-w-full max-h-64 object-cover cursor-pointer hover:opacity-95 transition-opacity" | |
| > | |
| {% endif %} | |
| </div> | |
| {% endfor %} | |
| </div> | |
| {% endif %} | |
| <div class="prose prose-sm max-w-none text-[15px] leading-snug text-slate-800"> | |
| {{ reply.content|format_post(post_context) }} | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Metadata / Actions Line --> | |
| <div class="flex items-center gap-4 mt-1.5 ml-3 text-xs text-slate-500 font-medium"> | |
| <span>{{ reply.created_at.strftime('%H:%M') }}</span> <!-- Simplify time --> | |
| <button class="text-slate-600 hover:text-blue-600 font-bold cursor-pointer transition-colors" onclick="scrollToReply('{{ reply.id }}')"> | |
| Répondre | |
| </button> | |
| <button class="hover:text-green-600 flex items-center gap-1 transition-colors" onclick="sharePost('{{ reply.id }}', '{{ url_for('thread', thread_id=thread.id, _external=True) }}#post-{{ reply.id }}')"> | |
| Partager | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {% endfor %} | |
| </div> | |
| </div> | |
| <!-- Sticky Bottom Input Bar --> | |
| <!-- "Think Harder": Floating Pill Design --> | |
| <div class="fixed bottom-0 left-0 right-0 z-50 pointer-events-none pb-safe"> | |
| <div class="bg-gradient-to-t from-white via-white/95 to-transparent pt-10 pb-2 px-2 sm:px-4 pointer-events-auto"> | |
| <div class="max-w-4xl mx-auto w-full"> | |
| <!-- Reply Context (Floating above bar) --> | |
| <div id="reply-context" class="hidden mb-2 mx-2 bg-white/90 backdrop-blur border border-blue-100 shadow-lg rounded-xl px-4 py-2 flex justify-between items-center text-xs text-blue-600 animate-slide-up"> | |
| <span class="flex items-center gap-2"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" /> | |
| </svg> | |
| Réponse au post #<span id="reply-to-id" class="font-bold"></span> | |
| </span> | |
| <button onclick="clearReplyContext()" class="text-slate-400 hover:text-slate-600 p-1 rounded-full hover:bg-slate-100"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> | |
| </svg> | |
| </button> | |
| </div> | |
| <!-- File Previews (Floating above bar) --> | |
| <div id="preview-reply" class="hidden mb-2 mx-2 flex gap-2 overflow-x-auto p-2 bg-white/90 backdrop-blur border border-slate-100 rounded-xl shadow-lg scrollbar-hide"> | |
| <!-- JS populates this --> | |
| </div> | |
| <form method="POST" enctype="multipart/form-data" class="bg-white shadow-[0_0_20px_rgba(0,0,0,0.1)] rounded-[2rem] p-1.5 flex items-end gap-2 border border-slate-100" id="sticky-form"> | |
| {{ form.hidden_tag() }} | |
| <div class="hidden"> | |
| {{ form.honeypot() }} | |
| </div> | |
| <!-- Nickname Toggle Button --> | |
| <div class="relative shrink-0"> | |
| <button type="button" onclick="document.getElementById('nickname-input').classList.toggle('hidden')" class="w-10 h-10 flex items-center justify-center text-slate-400 hover:text-blue-500 rounded-full hover:bg-slate-50 transition-colors" title="Identité"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /> | |
| </svg> | |
| </button> | |
| <!-- Popup Nickname Input --> | |
| <div id="nickname-input" class="hidden absolute bottom-full left-0 mb-4 ml-1 w-56 bg-white shadow-xl border border-slate-100 rounded-2xl p-3 animate-fade-in-up"> | |
| <label class="block text-xs font-bold text-slate-500 mb-1">Votre Pseudo</label> | |
| {{ form.nickname(class="w-full text-sm bg-slate-50 border-transparent rounded-lg focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all px-3 py-2", placeholder="Anonyme") }} | |
| </div> | |
| </div> | |
| <!-- File Upload Button --> | |
| <label for="files" class="w-10 h-10 flex items-center justify-center text-slate-400 hover:text-blue-500 rounded-full hover:bg-slate-50 cursor-pointer transition-colors shrink-0" title="Ajouter médias"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /> | |
| </svg> | |
| {{ form.files(class="hidden", multiple=True, accept="image/*,video/*", onchange="previewFiles(this, 'preview-reply')") }} | |
| </label> | |
| <!-- Text Input --> | |
| <div class="flex-grow py-2"> | |
| {{ form.content(class="w-full bg-transparent border-none focus:ring-0 p-0 text-slate-800 placeholder-slate-400 resize-none max-h-32 min-h-[1.5rem] leading-relaxed", placeholder="Ajouter un commentaire...", rows="1", oninput="this.style.height = ''; this.style.height = this.scrollHeight + 'px'") }} | |
| </div> | |
| <!-- Submit Button --> | |
| <button type="submit" class="w-10 h-10 flex items-center justify-center bg-blue-600 hover:bg-blue-700 text-white rounded-full transition-all shadow-md hover:shadow-lg active:scale-95 shrink-0"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-0.5" viewBox="0 0 20 20" fill="currentColor"> | |
| <path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z" /> | |
| </svg> | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| <style> | |
| /* Custom animations for smooth feel */ | |
| @keyframes fadeIn { | |
| from { opacity: 0; } | |
| to { opacity: 1; } | |
| } | |
| .animate-fade-in { | |
| animation: fadeIn 0.3s ease-out forwards; | |
| } | |
| @keyframes fadeInUp { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .animate-fade-in-up { | |
| animation: fadeInUp 0.2s ease-out forwards; | |
| } | |
| @keyframes slideUp { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .animate-slide-up { | |
| animation: slideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards; | |
| } | |
| .pb-safe { | |
| padding-bottom: env(safe-area-inset-bottom); | |
| } | |
| .scrollbar-hide::-webkit-scrollbar { | |
| display: none; | |
| } | |
| .scrollbar-hide { | |
| -ms-overflow-style: none; | |
| scrollbar-width: none; | |
| } | |
| </style> | |
| <script> | |
| // Custom file preview for the sticky bar | |
| function previewFiles(input, previewId) { | |
| const preview = document.getElementById(previewId); | |
| const container = preview; | |
| container.innerHTML = ''; | |
| container.classList.remove('hidden'); | |
| if (input.files) { | |
| if (input.files.length === 0) { | |
| container.classList.add('hidden'); | |
| return; | |
| } | |
| Array.from(input.files).forEach(file => { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| const div = document.createElement('div'); | |
| div.className = 'relative flex-shrink-0 w-16 h-16 group'; | |
| let content; | |
| if (file.type.startsWith('video/')) { | |
| content = `<video src="${e.target.result}" class="w-full h-full object-cover rounded-lg bg-black"></video>`; | |
| } else { | |
| content = `<img src="${e.target.result}" class="w-full h-full object-cover rounded-lg border border-slate-200 shadow-sm">`; | |
| } | |
| // Add remove button overlay | |
| const removeBtn = document.createElement('div'); | |
| removeBtn.innerHTML = ` | |
| <div class="absolute inset-0 bg-black/40 flex items-center justify-center rounded-lg opacity-0 group-hover:opacity-100 transition-opacity"> | |
| <svg class="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg> | |
| </div> | |
| `; | |
| div.innerHTML = content; | |
| div.appendChild(removeBtn); // Append the overlay | |
| // Note: Actual file removal from input requires DataTransfer API which is complex for this simple UI, | |
| // so we just visually show it. For now let's keep it simple. | |
| container.appendChild(div); | |
| } | |
| reader.readAsDataURL(file); | |
| }); | |
| } | |
| } | |
| function scrollToReply(id) { | |
| const textarea = document.querySelector('textarea[name="content"]'); | |
| textarea.focus(); | |
| textarea.value += '>>' + id + '\n'; | |
| // Trigger auto resize | |
| textarea.style.height = 'auto'; | |
| textarea.style.height = textarea.scrollHeight + 'px'; | |
| // Update UI for "Replying to..." | |
| const context = document.getElementById('reply-context'); | |
| const contextId = document.getElementById('reply-to-id'); | |
| context.classList.remove('hidden'); | |
| contextId.textContent = id; | |
| } | |
| function clearReplyContext() { | |
| document.getElementById('reply-context').classList.add('hidden'); | |
| } | |
| // Auto-resize textarea on page load if content exists (e.g. after error) | |
| document.addEventListener('DOMContentLoaded', function() { | |
| const textarea = document.querySelector('textarea[name="content"]'); | |
| if (textarea.value) { | |
| textarea.style.height = textarea.scrollHeight + 'px'; | |
| } | |
| }); | |
| </script> | |
| {% endblock %} | |