feat(emails): subtask 6 — admin gallery (routes, templates, sidebar link)

- Add GET /admin/emails/gallery — card grid of all 11 email types
- Add GET /admin/emails/gallery/<slug>?lang=en|de — preview with lang toggle
- Add email_gallery.html: 3-column responsive card grid
- Add email_gallery_preview.html: full-width iframe + EN/DE toggle + log link
- Add Gallery sidebar link to base_admin.html (admin_page == 'gallery')

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-25 12:13:35 +01:00
parent 536d5c8f40
commit 4fafd3e80e
4 changed files with 185 additions and 0 deletions

View File

@@ -1249,6 +1249,45 @@ async def emails():
)
@bp.route("/emails/gallery")
@role_required("admin")
async def email_gallery():
"""Gallery of all email template types with sample previews."""
return await render_template(
"admin/email_gallery.html",
registry=EMAIL_TEMPLATE_REGISTRY,
)
@bp.route("/emails/gallery/<slug>")
@role_required("admin")
async def email_gallery_preview(slug: str):
"""Rendered preview of a single email template with sample data."""
entry = EMAIL_TEMPLATE_REGISTRY.get(slug)
if not entry:
await flash(f"Unknown email template: {slug!r}", "error")
return redirect(url_for("admin.email_gallery"))
lang = request.args.get("lang", "en")
if lang not in ("en", "de"):
lang = "en"
try:
sample = entry["sample_data"](lang)
rendered_html = render_email_template(entry["template"], lang=lang, **sample)
except Exception:
logger.exception("email_gallery_preview: render failed for %s (lang=%s)", slug, lang)
rendered_html = "<p style='padding:2rem;color:#DC2626;'>Render error — see logs.</p>"
return await render_template(
"admin/email_gallery_preview.html",
slug=slug,
entry=entry,
lang=lang,
rendered_html=rendered_html,
)
@bp.route("/emails/results")
@role_required("admin")
async def email_results():

View File

@@ -118,6 +118,10 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"/></svg>
Compose
</a>
<a href="{{ url_for('admin.email_gallery') }}" class="{% if admin_page == 'gallery' %}active{% endif %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 7.125C2.25 6.504 2.754 6 3.375 6h6c.621 0 1.125.504 1.125 1.125v3.75c0 .621-.504 1.125-1.125 1.125h-6a1.125 1.125 0 0 1-1.125-1.125v-3.75ZM14.25 8.625c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v8.25c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 0 1-1.125-1.125v-8.25ZM3.75 16.125c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 0 1-1.125-1.125v-2.25Z"/></svg>
Gallery
</a>
<a href="{{ url_for('admin.audiences') }}" class="{% if admin_page == 'audiences' %}active{% endif %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z"/></svg>
Audiences

View File

@@ -0,0 +1,81 @@
{% extends "admin/base_admin.html" %}
{% set admin_page = "gallery" %}
{% block title %}Email Gallery - Admin - {{ config.APP_NAME }}{% endblock %}
{% block admin_head %}
<style>
.gallery-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
@media (max-width: 1100px) { .gallery-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 720px) { .gallery-grid { grid-template-columns: 1fr; } }
.gallery-card {
display: block;
text-decoration: none;
background: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 10px;
padding: 1.25rem 1.25rem 1rem;
transition: border-color 0.15s, box-shadow 0.15s, transform 0.1s;
position: relative;
overflow: hidden;
}
.gallery-card::before {
content: '';
position: absolute;
left: 0; top: 0; bottom: 0;
width: 3px;
background: #1D4ED8;
transform: scaleY(0);
transform-origin: bottom;
transition: transform 0.2s ease;
}
.gallery-card:hover {
border-color: #BFDBFE;
box-shadow: 0 4px 16px rgba(29,78,216,0.08);
transform: translateY(-1px);
}
.gallery-card:hover::before { transform: scaleY(1); }
.gallery-card__icon {
width: 36px; height: 36px;
background: #EFF6FF;
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
margin-bottom: 0.875rem;
flex-shrink: 0;
}
.gallery-card__icon svg { width: 18px; height: 18px; color: #1D4ED8; }
.gallery-card__label { font-size: 0.9375rem; font-weight: 600; color: #0F172A; margin-bottom: 0.25rem; }
.gallery-card__desc { font-size: 0.8125rem; color: #64748B; line-height: 1.5; margin-bottom: 0.875rem; }
.gallery-card__cta { font-size: 0.8125rem; font-weight: 500; color: #1D4ED8; }
</style>
{% endblock %}
{% block admin_content %}
<header class="mb-6">
<div style="display:flex;align-items:baseline;gap:1rem;flex-wrap:wrap;">
<h1 class="text-2xl">Email Gallery</h1>
<span style="font-size:0.8125rem;color:#94A3B8;">{{ registry | length }} template{{ 's' if registry | length != 1 else '' }}</span>
</div>
<p style="margin-top:0.25rem;font-size:0.875rem;color:#64748B;">Rendered previews of all transactional email templates with sample data.</p>
</header>
<div class="gallery-grid">
{% for slug, entry in registry.items() %}
<a class="gallery-card" href="{{ url_for('admin.email_gallery_preview', slug=slug) }}">
<div class="gallery-card__icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75"/>
</svg>
</div>
<div class="gallery-card__label">{{ entry.label }}</div>
<div class="gallery-card__desc">{{ entry.description }}</div>
<div class="gallery-card__cta">Preview &rarr;</div>
</a>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,61 @@
{% extends "admin/base_admin.html" %}
{% set admin_page = "gallery" %}
{% block title %}{{ entry.label }} Preview - Email Gallery - Admin{% endblock %}
{% block admin_head %}
<style>
.preview-toolbar {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.preview-toolbar__title { flex: 1; min-width: 0; }
.lang-toggle { display: flex; gap: 2px; background: #F1F5F9; border-radius: 6px; padding: 2px; }
.lang-toggle a {
padding: 4px 12px; font-size: 0.75rem; font-weight: 600;
border-radius: 4px; text-decoration: none; color: #64748B; transition: all 0.1s;
}
.lang-toggle a.active { background: #FFFFFF; color: #1D4ED8; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.preview-iframe {
width: 100%; height: 700px;
border: 1px solid #E2E8F0; border-radius: 8px;
background: #F1F5F9;
}
</style>
{% endblock %}
{% block admin_content %}
<header class="mb-4">
<a href="{{ url_for('admin.email_gallery') }}" class="text-sm text-slate">&larr; Email Gallery</a>
<h1 class="text-2xl mt-1">{{ entry.label }}</h1>
<p style="margin-top:0.25rem;font-size:0.875rem;color:#64748B;">{{ entry.description }}</p>
</header>
<div class="preview-toolbar">
<div class="preview-toolbar__title">
{% if entry.email_type %}
<a href="{{ url_for('admin.emails') }}?email_type={{ entry.email_type }}"
style="font-size:0.8125rem;color:#1D4ED8;text-decoration:none;">
View in sent log &rarr;
</a>
{% endif %}
</div>
{# Language toggle #}
<div class="lang-toggle">
<a href="{{ url_for('admin.email_gallery_preview', slug=slug, lang='en') }}"
class="{{ 'active' if lang == 'en' else '' }}">EN</a>
<a href="{{ url_for('admin.email_gallery_preview', slug=slug, lang='de') }}"
class="{{ 'active' if lang == 'de' else '' }}">DE</a>
</div>
</div>
<iframe
class="preview-iframe"
srcdoc="{{ rendered_html | e }}"
sandbox="allow-same-origin"
title="{{ entry.label }} email preview"
></iframe>
{% endblock %}