feat(mail): redesign email templates with TeamWallet branding and shared layout partial
This commit is contained in:
@@ -36,6 +36,15 @@ export class MailConfigService implements MailerOptionsFactory {
|
||||
adapter: new HandlebarsAdapter(),
|
||||
options: {
|
||||
strict: true,
|
||||
partials: {
|
||||
dir: path.join(
|
||||
this.configService.get('app.workingDirectory'),
|
||||
'src',
|
||||
'mail',
|
||||
'mail-templates',
|
||||
'partials',
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as MailerOptions;
|
||||
|
||||
@@ -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}}
|
||||
@@ -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,');
|
||||
});
|
||||
});
|
||||
@@ -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>© {{year}} TeamWallet</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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}}
|
||||
Reference in New Issue
Block a user