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:
@@ -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")
|
@bp.route("/emails/results")
|
||||||
@role_required("admin")
|
@role_required("admin")
|
||||||
async def email_results():
|
async def email_results():
|
||||||
|
|||||||
@@ -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>
|
<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
|
Compose
|
||||||
</a>
|
</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 %}">
|
<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>
|
<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
|
Audiences
|
||||||
|
|||||||
81
web/src/padelnomics/admin/templates/admin/email_gallery.html
Normal file
81
web/src/padelnomics/admin/templates/admin/email_gallery.html
Normal 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 →</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -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">← 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 →
|
||||||
|
</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 %}
|
||||||
Reference in New Issue
Block a user