feat(mail): redesign email templates with TeamWallet branding and shared layout partial

This commit is contained in:
Bastian Wagner
2026-07-31 22:49:40 +02:00
parent ae9de39ac4
commit 0a0990c6f4
5 changed files with 125 additions and 71 deletions

View File

@@ -1,33 +1,8 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=">
<title>{{title}}</title>
</head>
<body style="margin:0;font-family:arial">
<table style="border:0;width:100%">
<tr style="background:#eeeeee">
<td style="padding:20px;color:#808080;text-align:center;font-size:40px;font-weight:600">
{{app_name}}
</td>
</tr>
<tr>
<td style="padding:20px;color:#808080;font-size:16px;font-weight:100">
{{text1}}<br>
{{text2}} {{app_name}}.<br>
{{text3}}
</td>
</tr>
<tr>
<td style="text-align:center">
<a href="{{url}}"
style="display:inline-block;padding:20px;background:#00838f;text-decoration:none;color:#ffffff">{{actionTitle}}</a>
</td>
</tr>
</table>
</body>
</html>
{{#> layout}}
<p>Hallo{{#if firstName}} {{firstName}}{{/if}},</p>
<p>schön, dass du bei TeamWallet dabei bist! Bestätige deine E-Mail-Adresse, um dein Konto zu aktivieren.</p>
<div class="tw-button-row">
<a class="tw-button" href="{{url}}">{{actionTitle}}</a>
</div>
<p>Falls der Button nicht funktioniert, kopiere diesen Link in deinen Browser:<br><a href="{{url}}">{{url}}</a></p>
{{/layout}}

View File

@@ -0,0 +1,63 @@
import * as fs from 'fs';
import * as path from 'path';
import * as Handlebars from 'handlebars';
describe('mail templates rendering', () => {
const templatesDir = __dirname;
beforeAll(() => {
const layoutSource = fs.readFileSync(
path.join(templatesDir, 'partials', 'layout.hbs'),
'utf-8',
);
Handlebars.registerPartial('layout', layoutSource);
});
const baseContext = {
title: 'Test-Betreff',
year: 2026,
firstName: 'Max',
url: 'https://app.example.com/confirm-email/abc123',
actionTitle: 'Jetzt bestätigen',
};
it('renders activation.hbs with greeting, link and button text', () => {
const source = fs.readFileSync(
path.join(templatesDir, 'activation.hbs'),
'utf-8',
);
const html = Handlebars.compile(source, { strict: true })(baseContext);
expect(html).toContain('TeamWallet');
expect(html).toContain('Hallo Max,');
expect(html).toContain(baseContext.url);
expect(html).toContain(baseContext.actionTitle);
});
it('renders reset-password.hbs with greeting, link and button text', () => {
const source = fs.readFileSync(
path.join(templatesDir, 'reset-password.hbs'),
'utf-8',
);
const html = Handlebars.compile(source, { strict: true })(baseContext);
expect(html).toContain('TeamWallet');
expect(html).toContain('Hallo Max,');
expect(html).toContain(baseContext.url);
expect(html).toContain(baseContext.actionTitle);
});
it('falls back to a generic greeting when firstName is missing', () => {
const source = fs.readFileSync(
path.join(templatesDir, 'activation.hbs'),
'utf-8',
);
const html = Handlebars.compile(source, { strict: true })({
...baseContext,
firstName: undefined,
});
expect(html).toContain('Hallo,');
expect(html).not.toContain('Hallo Max,');
});
});

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}}</title>
<style>
body { margin: 0; padding: 0; background: #f4f6f4; font-family: Roboto, Helvetica, Arial, sans-serif; }
.tw-container { max-width: 600px; margin: 0 auto; padding: 32px 16px; }
.tw-card { background: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08); }
.tw-header { background: #2e7d32; padding: 28px 32px; text-align: center; }
.tw-wordmark { color: #ffffff; font-size: 22px; font-weight: 700; letter-spacing: 0.5px; }
.tw-body { padding: 32px; color: #20251f; font-size: 15px; line-height: 1.6; }
.tw-body p { margin: 0 0 16px; }
.tw-button-row { text-align: center; padding: 8px 0 24px; }
.tw-button { display: inline-block; background: #2e7d32; color: #ffffff !important; text-decoration: none; padding: 14px 32px; border-radius: 8px; font-weight: 600; font-size: 15px; }
.tw-footer { text-align: center; padding: 20px 16px 0; color: #8a938a; font-size: 12px; line-height: 1.6; }
.tw-footer a { color: #8a938a; }
</style>
</head>
<body>
<div class="tw-container">
<div class="tw-card">
<div class="tw-header">
<span class="tw-wordmark">TeamWallet</span>
</div>
<div class="tw-body">
{{> @partial-block }}
</div>
</div>
<div class="tw-footer">
<p>Diese E-Mail wurde automatisch von TeamWallet verschickt.<br>&copy; {{year}} TeamWallet</p>
</div>
</div>
</body>
</html>

View File

@@ -1,38 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=">
<title>{{title}}</title>
</head>
<body style="margin:0;font-family:arial">
<table style="border:0;width:100%">
<tr style="background:#eeeeee">
<td style="padding:20px;color:#808080;text-align:center;font-size:40px;font-weight:600">
{{app_name}}
</td>
</tr>
<tr>
<td style="padding:20px;color:#808080;font-size:16px;font-weight:100">
{{text1}}<br>
{{text2}}<br>
{{text3}}
</td>
</tr>
<tr>
<td style="text-align:center">
<a href="{{url}}"
style="display:inline-block;padding:20px;background:#00838f;text-decoration:none;color:#ffffff">{{actionTitle}}</a>
</td>
</tr>
<tr>
<td style="padding:20px;color:#808080;font-size:16px;font-weight:100">
{{text4}}
</td>
</tr>
</table>
</body>
</html>
{{#> layout}}
<p>Hallo{{#if firstName}} {{firstName}}{{/if}},</p>
<p>du hast angefragt, dein TeamWallet-Passwort zurückzusetzen. Klicke auf den Button, um ein neues Passwort zu vergeben.</p>
<div class="tw-button-row">
<a class="tw-button" href="{{url}}">{{actionTitle}}</a>
</div>
<p>Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail einfach ignorieren — es wird nichts verändert.</p>
<p>Falls der Button nicht funktioniert, kopiere diesen Link in deinen Browser:<br><a href="{{url}}">{{url}}</a></p>
{{/layout}}