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(),
|
adapter: new HandlebarsAdapter(),
|
||||||
options: {
|
options: {
|
||||||
strict: true,
|
strict: true,
|
||||||
|
partials: {
|
||||||
|
dir: path.join(
|
||||||
|
this.configService.get('app.workingDirectory'),
|
||||||
|
'src',
|
||||||
|
'mail',
|
||||||
|
'mail-templates',
|
||||||
|
'partials',
|
||||||
|
),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as MailerOptions;
|
} as MailerOptions;
|
||||||
|
|||||||
@@ -1,33 +1,8 @@
|
|||||||
<!DOCTYPE html>
|
{{#> layout}}
|
||||||
<html lang="en">
|
<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>
|
||||||
<head>
|
<div class="tw-button-row">
|
||||||
<meta charset="UTF-8">
|
<a class="tw-button" href="{{url}}">{{actionTitle}}</a>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=">
|
</div>
|
||||||
<title>{{title}}</title>
|
<p>Falls der Button nicht funktioniert, kopiere diesen Link in deinen Browser:<br><a href="{{url}}">{{url}}</a></p>
|
||||||
</head>
|
{{/layout}}
|
||||||
|
|
||||||
<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>
|
|
||||||
@@ -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>
|
{{#> layout}}
|
||||||
<html lang="en">
|
<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>
|
||||||
<head>
|
<div class="tw-button-row">
|
||||||
<meta charset="UTF-8">
|
<a class="tw-button" href="{{url}}">{{actionTitle}}</a>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=">
|
</div>
|
||||||
<title>{{title}}</title>
|
<p>Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail einfach ignorieren — es wird nichts verändert.</p>
|
||||||
</head>
|
<p>Falls der Button nicht funktioniert, kopiere diesen Link in deinen Browser:<br><a href="{{url}}">{{url}}</a></p>
|
||||||
|
{{/layout}}
|
||||||
<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>
|
|
||||||
Reference in New Issue
Block a user