- 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>
21 lines
1.7 KiB
HTML
21 lines
1.7 KiB
HTML
{% load static %}
|
|
<form method="post" action="{% url 'comment_post' %}" class="mt-4 pt-4 border-t border-zinc-100 dark:border-zinc-800"
|
|
hx-post="{% url 'comment_post' %}" hx-target="#comments-list" hx-swap="beforeend" hx-on::after-request="if(event.detail.successful) this.reset()">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="article_id" value="{{ page.id }}" />
|
|
<input type="hidden" name="parent_id" value="{{ comment.id }}" />
|
|
<div class="flex gap-3 mb-3">
|
|
<input type="text" name="author_name" required placeholder="Your name"
|
|
class="flex-1 bg-transparent border border-zinc-300 dark:border-zinc-700 px-3 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink focus:shadow-neon-pink transition-colors" />
|
|
<input type="email" name="author_email" required placeholder="your@email.com"
|
|
class="flex-1 bg-transparent border border-zinc-300 dark:border-zinc-700 px-3 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink focus:shadow-neon-pink transition-colors" />
|
|
</div>
|
|
<textarea name="body" required placeholder="Write a reply..." rows="2"
|
|
class="w-full bg-transparent border border-zinc-300 dark:border-zinc-700 px-3 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink focus:shadow-neon-pink transition-colors mb-3 resize-none"></textarea>
|
|
<input type="text" name="honeypot" hidden />
|
|
{% if turnstile_site_key %}
|
|
<div class="cf-turnstile mb-3" data-sitekey="{{ turnstile_site_key }}" data-theme="auto" data-size="compact"></div>
|
|
{% endif %}
|
|
<button type="submit" class="px-4 py-2 bg-zinc-200 dark:bg-zinc-800 font-display font-bold text-sm hover:bg-brand-pink hover:text-white transition-colors">Reply</button>
|
|
</form>
|