an / templates /thread.html
Docfile's picture
Upload 23 files
75ba54e verified
{% 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 %}