|
|
{% extends 'base.html' %} |
|
|
|
|
|
{% block title %}/{{ board.slug }}/ - Thread #{{ thread.id }}{% endblock %} |
|
|
|
|
|
{% block content %} |
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
|
|
|
<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 }}"> |
|
|
|
|
|
<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"> |
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<div class="space-y-6"> |
|
|
|
|
|
<div class="prose prose-slate max-w-none text-slate-800 text-lg leading-relaxed break-words"> |
|
|
{{ thread.content|format_post(post_context) }} |
|
|
</div> |
|
|
|
|
|
|
|
|
{% 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> |
|
|
|
|
|
|
|
|
|
|
|
<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 }}"> |
|
|
|
|
|
<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"> |
|
|
|
|
|
|
|
|
<div class="relative max-w-full sm:max-w-[85%]"> |
|
|
|
|
|
|
|
|
|
|
|
<span class="text-[13px] text-slate-900 font-bold ml-3 mb-1 block"> |
|
|
{{ reply.nickname }} |
|
|
|
|
|
<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"> |
|
|
|
|
|
{% 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> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
|
|
|
<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"> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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"> |
|
|
|
|
|
</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> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
@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> |
|
|
|
|
|
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">`; |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
|
|
|
container.appendChild(div); |
|
|
} |
|
|
reader.readAsDataURL(file); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
function scrollToReply(id) { |
|
|
const textarea = document.querySelector('textarea[name="content"]'); |
|
|
textarea.focus(); |
|
|
textarea.value += '>>' + id + '\n'; |
|
|
|
|
|
textarea.style.height = 'auto'; |
|
|
textarea.style.height = textarea.scrollHeight + 'px'; |
|
|
|
|
|
|
|
|
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'); |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
const textarea = document.querySelector('textarea[name="content"]'); |
|
|
if (textarea.value) { |
|
|
textarea.style.height = textarea.scrollHeight + 'px'; |
|
|
} |
|
|
}); |
|
|
</script> |
|
|
|
|
|
{% endblock %} |
|
|
|