feat: markdown in email body for mailing and transactional

This commit is contained in:
Björn Fromme
2026-08-20 14:00:53 +02:00
parent f0e850978b
commit 5912a75c81
23 changed files with 1251 additions and 160 deletions
@@ -4,6 +4,8 @@
{{ form_row(form.headline) }}
{{ form_row(form.body) }}
{{ include('_partials/_markdown_hint.html.twig') }}
<div class="bg-gray-100 rounded-md p-4 text-sm">
<div class="font-bold pb-2">
Verfügbare Platzhalter
@@ -6,9 +6,9 @@
{% if text.headline is not empty %}
<h1 class="text-xl font-bold pb-2">{{ text.headline | raw }}</h1>
{% endif %}
{# The paragraphs come from the mail body, where email/layout.html.twig styles every
<p> with padding-bottom:16px - pb-4 is the same spacing, so the preview matches. #}
<div class="[&>p]:pb-4 [&_a]:underline">
{# .mail-body carries the spacing and list styling of email/layout.html.twig, so this
pane and the mailing pane both show what the mail will look like. #}
<div class="mail-body">
{{ text.bodyHtml | raw }}
</div>
<p>
@@ -0,0 +1,15 @@
<div class="pb-4">
<span class="font-bold">Betreff:</span> {{ subject }}
</div>
{# App\Email\MailBodyRenderer escaped everything before it introduced a tag, which is what
makes |raw safe here - see the contract on that class. #}
<div class="mail-body border border-gray-200 rounded-md p-4">
{{ bodyHtml | raw }}
<p>
<span class="btn inline-block">Zum Portal</span>
</p>
</div>
<div class="pt-4 text-sm text-gray-500">
Platzhalter sind mit deinem eigenen Namen gefüllt, beim Versand steht dort der Name der
jeweiligen Person. Logo und Fußzeile der E-Mail sind hier nicht abgebildet.
</div>
+56 -21
View File
@@ -27,29 +27,64 @@
{% endif %}
</div>
{# the draft is parked on every keystroke so that a trip to the filter and back keeps it,
hx-trigger replaces htmx' default submit trigger, the preview button still posts natively #}
{{ form_start(form, { 'attr': {
'hx-post': path('app_admin_teamer_mailing_draft'),
'hx-trigger': 'input changed delay:500ms',
'hx-swap': 'none',
} }) }}
<div class="flex flex-col space-y-4 pb-4 max-w-3xl">
{{ form_row(form.subject) }}
{{ form_row(form.message) }}
</div>
{# One request per keystroke parks the draft - so that a trip to the filter and back
keeps it - and answers with the preview, which is why the response is swapped into
the pane instead of being thrown away. hx-trigger replaces htmx' default submit
trigger, the "Vorschau an mich senden" button still posts natively.
<div class="pb-8 max-w-3xl text-sm text-gray-500">
<div class="pb-2">
Platzhalter für Betreff und Nachricht, sie werden beim Senden pro Person ersetzt:
Deliberately without htmx' "changed" modifier: it compares the listening element's
.value between events, and a <form> has none, so every event compares undefined to
undefined and the request is never sent. The 500ms delay is what keeps the typing
from flooding the route. #}
{{ form_start(form, { 'attr': {
'id': 'mailing-form',
'hx-post': path('app_admin_teamer_mailing_draft'),
'hx-trigger': 'input delay:500ms',
'hx-target': '#mailing-preview',
'hx-swap': 'innerHTML',
'hx-indicator': '#mailing-preview-indicator',
} }) }}
<div class="grid lg:grid-cols-2 gap-8 items-start">
<div>
<div class="flex flex-col space-y-4 pb-4">
{{ form_row(form.subject) }}
{{ form_row(form.message) }}
</div>
<div class="pb-6">
{{ include('_partials/_markdown_hint.html.twig') }}
</div>
<div class="pb-8 text-sm text-gray-500">
<div class="pb-2">
Platzhalter für Betreff und Nachricht, sie werden beim Senden pro Person ersetzt:
</div>
<div class="flex flex-wrap gap-2">
{% for placeholder, label in placeholders %}
<span class="inline-flex items-center space-x-1 py-1 px-2 bg-gray-50 border border-gray-200 rounded">
<code>{{ placeholder }}</code>
<span>{{ label }}</span>
</span>
{% endfor %}
</div>
</div>
</div>
<div class="flex flex-wrap gap-2">
{% for placeholder, label in placeholders %}
<span class="inline-flex items-center space-x-1 py-1 px-2 bg-gray-50 border border-gray-200 rounded">
<code>{{ placeholder }}</code>
<span>{{ label }}</span>
</span>
{% endfor %}
<div class="lg:sticky lg:top-4">
<div class="font-bold pb-2">
Vorschau
</div>
<div class="relative">
<div id="mailing-preview">
{{ include('admin/teamer/_mailing_preview.html.twig', preview) }}
</div>
{# Deliberately a sibling of the swap target, not a child: hx-swap replaces
the target's contents, which would carry the indicator away with them. #}
<div class="htmx-indicator absolute inset-0 bg-white/75 flex items-center justify-center"
id="mailing-preview-indicator">
{{ include('_partials/_spinner.html.twig', { 'class': 'w-8 h-8' }) }}
</div>
</div>
</div>
</div>