- Extract comment templates into reusable partials (_comment.html, _comment_form.html, _comment_list.html, _reply_form.html, etc.) - Add HTMX progressive enhancement: inline form submission with partial responses, delta polling for live updates, form reset on success, success/moderation toast feedback - Integrate Cloudflare Turnstile for invisible bot protection: server-side token validation with hostname check, fail-closed on errors/timeouts, feature-flagged via TURNSTILE_SECRET_KEY env var - Auto-approve comments that pass Turnstile; keep manual approval as fallback when Turnstile is disabled (model default stays False) - Add CommentReaction model with UniqueConstraint for session-based anonymous reactions (heart/thumbs-up), toggle support, separate rate-limit bucket (20/min) - Add comment poll endpoint (GET /comments/poll/<id>/?after_id=N) for HTMX delta polling without duplicates - Update CSP middleware to allow challenges.cloudflare.com in script-src, connect-src, and frame-src - Self-host htmx.min.js (v2.0.4) to minimize CSP surface area - Add django-htmx middleware and requests to dependencies - Add Unapprove bulk action to Wagtail admin for moderation - Extend PII purge command to anonymize reaction session_key - Design refresh: neon glow avatars, solid hover shadows, gradient section header, cyan reply borders, grid-pattern empty state, neon-pink focus glow on form inputs - Add turnstile_site_key to template context via context processor - 18 new tests covering HTMX contracts, Turnstile success/failure/ timeout/hostname-mismatch, polling deltas, reaction toggle/dedup/ rate-limit, CSP headers, and PII purge extension Closes #43 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
25 lines
1.6 KiB
HTML
25 lines
1.6 KiB
HTML
<article id="comment-{{ comment.id }}" class="bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 p-6 hover:shadow-solid-dark dark:hover:shadow-solid-light transition-all duration-300">
|
|
<div class="flex items-center gap-3 mb-3">
|
|
<div class="w-10 h-10 bg-gradient-to-tr from-brand-cyan to-brand-pink shrink-0 shadow-neon-cyan"></div>
|
|
<div>
|
|
<div class="font-display font-bold text-sm hover:text-brand-cyan transition-colors">{{ comment.author_name }}</div>
|
|
<div class="font-mono text-xs text-zinc-500">{{ comment.created_at|date:"M j, Y" }}</div>
|
|
</div>
|
|
</div>
|
|
<p class="text-zinc-700 dark:text-zinc-300 text-sm leading-relaxed">{{ comment.body }}</p>
|
|
{% include "comments/_reactions.html" with comment=comment counts=comment.reaction_counts user_reacted=comment.user_reacted %}
|
|
{% for reply in comment.replies.all %}
|
|
<article id="comment-{{ reply.id }}" class="mt-6 ml-8 bg-zinc-50 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 border-l-2 border-l-brand-cyan p-4">
|
|
<div class="flex items-center gap-3 mb-2">
|
|
<div class="w-7 h-7 bg-gradient-to-tr from-brand-pink to-brand-cyan shrink-0"></div>
|
|
<div>
|
|
<div class="font-display font-bold text-sm">{{ reply.author_name }}</div>
|
|
<div class="font-mono text-xs text-zinc-500">{{ reply.created_at|date:"M j, Y" }}</div>
|
|
</div>
|
|
</div>
|
|
<p class="text-zinc-700 dark:text-zinc-300 text-sm leading-relaxed">{{ reply.body }}</p>
|
|
</article>
|
|
{% endfor %}
|
|
{% include "comments/_reply_form.html" with page=page comment=comment %}
|
|
</article>
|