Add a Settings > Email previews page showing every branded email with sample data
Renders the real templates every send function uses (claim approved, membership invoice, invoice reminder) against hand-built placeholder context -- no real Member/ClubMembership/DuesInvoice row needed, so nothing here can leak real data. Each card shows the subject line and toggles between the HTML render (in an iframe, so the email's own markup can't clash with the page's) and the plain-text body, using the same bg-steel pill-toggle pattern as the news preview's NL/EN switch (generalised to .view-toggle-btn, kept .news-lang-btn as an alias). Admin-only, alongside Club identity in Settings. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
@@ -93,6 +93,7 @@
|
||||
<div class="flex flex-col">
|
||||
{% if is_club_admin %}
|
||||
<a class="nav-subitem {% if nav == 'club_settings' %}active{% endif %}" href="{% url 'management:club_settings' %}">{% trans "Club identity" %}</a>
|
||||
<a class="nav-subitem {% if nav == 'email_preview_list' %}active{% endif %}" href="{% url 'management:email_preview_list' %}">{% trans "Email previews" %}</a>
|
||||
{% endif %}
|
||||
<a class="nav-subitem {% if nav == 'onboarding_requirement_list' %}active{% endif %}" href="{% url 'management:onboarding_requirement_list' %}">{% trans "Onboarding requirements" %}</a>
|
||||
{% if is_club_admin %}
|
||||
|
||||
43
management/templates/management/email_previews.html
Normal file
43
management/templates/management/email_previews.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{% extends "management/base.html" %}
|
||||
{% load i18n lucide %}
|
||||
|
||||
{% block heading %}{% trans "Email previews" %}{% endblock heading %}
|
||||
{% block topbar_context %}<span class="text-sm text-muted">{% trans "Exactly what a member or parent receives -- with sample data, nothing is sent." %}</span>{% endblock topbar_context %}
|
||||
|
||||
{% block panel %}
|
||||
{% for preview in previews %}
|
||||
<div class="card overflow-hidden" data-email-card>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3 border-b border-line px-4.5 py-3.5">
|
||||
<div>
|
||||
<div class="font-display text-sm font-extrabold tracking-wide text-ink uppercase">{{ preview.label }}</div>
|
||||
<p class="mt-1 max-w-2xl text-sm text-muted">{{ preview.description }}</p>
|
||||
<div class="mt-2 font-mono text-xs text-muted"><span class="text-dim">{% trans "Subject:" %}</span> {{ preview.subject }}</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 rounded-full bg-steel p-1">
|
||||
<button type="button" class="view-toggle-btn active" data-view-btn="html">{% trans "Email" %}</button>
|
||||
<button type="button" class="view-toggle-btn" data-view-btn="text">{% trans "Plain text" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-subhead p-4" data-view-panel="html">
|
||||
<iframe class="h-[560px] w-full rounded-lg border border-line bg-white" srcdoc="{{ preview.html }}" title="{{ preview.label }}"></iframe>
|
||||
</div>
|
||||
<pre class="hidden overflow-x-auto p-4.5 font-mono text-sm whitespace-pre-wrap text-ink" data-view-panel="text">{{ preview.text }}</pre>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock panel %}
|
||||
|
||||
{% block extra_body %}
|
||||
<script>
|
||||
(() => {
|
||||
document.querySelectorAll("[data-email-card]").forEach((card) => {
|
||||
card.querySelectorAll("[data-view-btn]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
card.querySelectorAll("[data-view-btn]").forEach((b) => b.classList.toggle("active", b === button));
|
||||
card.querySelectorAll("[data-view-panel]").forEach((el) => el.classList.toggle("hidden", el.dataset.viewPanel !== button.dataset.viewBtn));
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock extra_body %}
|
||||
Reference in New Issue
Block a user