Added poc of new email renderer

This commit is contained in:
Karl Ludwig Weise
2025-11-21 15:19:25 +01:00
parent 56b7d4a9b2
commit 80662c5da4
8 changed files with 229 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
<script lang="js">
import { m } from "$i18n/messages";
import { getLocale, setLocale } from "$i18n/runtime";
import EmailButton from "./components/EmailButton.svelte";
import EmailLayout from "./components/EmailLayout.svelte";
import EmailText from "./components/EmailText.svelte";
let { name, locale } = $props();
setLocale(locale);
console.log("Locale in AppointmentBooked:", getLocale());
</script>
<EmailLayout>
<p>{m["absences.add.description"]()} {name},</p>
<p>your appointment has been booked.</p>
<h2>Vaccination Appointment</h2>
<p>
Dr. John Doe<br />
June 30, 2024<br />
10:00 AM - 10:15 AM
</p>
<h2>Praxis Dr. Jane Doe</h2>
<p>
Musterstraße 10<br />
Etage 4<br />
12345 Musterstadt
</p>
<EmailButton href="https://open-reception.org/appointments">View Appointments</EmailButton>
<EmailText variant="md" color="text-light">
You are receiving this email because someone booked an appointment with your e-mail address.
</EmailText>
</EmailLayout>
@@ -0,0 +1,31 @@
<script lang="js">
let { children, href } = $props();
</script>
<div class="button-container">
<a class="button" {href}>{@render children?.()}</a>
</div>
<svelte:head>
<style>
.button-container {
margin: 24px 0;
margin: 1.5rem 0;
}
.button {
font-size: 16px;
line-height: 1.5;
color: #ffffff;
background-color: #000000;
padding: 12px 32px;
padding: 0.75rem 2rem;
text-decoration: none;
display: inline-block;
border-radius: 12px;
}
.button:hover {
background-color: #333333;
cursor: pointer;
}
</style>
</svelte:head>
@@ -0,0 +1,88 @@
<script lang="js">
import EmailText from "./EmailText.svelte";
let { children } = $props();
</script>
<div class="page">
<div class="container">
<div class="paper">
<div class="content">
{@render children?.()}
</div>
</div>
<div class="branding">
<EmailText variant="sm" color="text-light">
Powered by <a href="https://open-reception.org">OpenReception</a>
</EmailText>
</div>
</div>
</div>
<svelte:head>
<style>
body {
margin: 0;
min-height: 100%;
min-height: 100vh;
box-sizing: border-box;
font-family: "Inter", Arial, sans-serif;
}
a {
color: inherit;
text-decoration: underline;
}
h1 {
font-size: 18px;
line-height: 1.5;
font-weight: 600;
}
h2 {
font-size: 16px;
line-height: 1.5;
font-weight: 600;
margin-top: 24px;
margin-bottom: 8px;
}
h3 {
font-size: 14px;
line-height: 1.5;
font-weight: 500;
}
h2 + p {
margin-top: 0;
}
.page {
background-color: #f6f6f6;
min-height: 100%;
min-height: 100vh;
box-sizing: border-box;
width: 100%;
padding: 0 16px 16px;
padding: 0 1rem 1rem;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.content {
padding: 24px;
padding: 1.5rem;
}
.paper {
background-color: white;
width: 100%;
box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.1);
overflow: hidden;
border-bottom-right-radius: 6px;
border-bottom-left-radius: 6px;
}
.branding {
margin: 16px 0;
margin: 1rem 0;
padding: 0 16px;
padding: 0 1rem;
text-align: center;
}
</style>
</svelte:head>
@@ -0,0 +1,29 @@
<script lang="js">
let { children, variant, color } = $props();
</script>
<span class={[variant, color].join(" ")}>{@render children?.()}</span>
<svelte:head>
<style>
.lg {
font-size: 16px;
line-height: 1.5;
}
.md {
font-size: 14px;
line-height: 1.5;
}
.sm {
font-size: 12px;
line-height: 1.5;
}
.xs {
font-size: 10px;
line-height: 1.25;
}
.text-light {
color: #888888;
}
</style>
</svelte:head>
+21
View File
@@ -0,0 +1,21 @@
// is not exported from svelte
export type RenderOutput = {
head: string;
body: string;
};
export const renderOutputToHtml = (renderOutput: RenderOutput) => {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
${renderOutput.head}
</head>
<body>
${renderOutput.body}
</body>
</html>
`;
};
+7
View File
@@ -0,0 +1,7 @@
import { dev } from "$app/environment";
import type { ParamMatcher } from "@sveltejs/kit";
export const match: ParamMatcher = (param) => {
// Doesn't have to be include, but needs to return a boolean
return dev && param.includes("local-only");
};
@@ -0,0 +1,14 @@
import AppointmentBooked from "$lib/emails/AppointmentBooked.svelte";
import { renderOutputToHtml } from "$lib/emails/utils";
import type { RequestHandler } from "@sveltejs/kit";
import { render } from "svelte/server";
export const GET: RequestHandler = async () => {
const emailRender = render(AppointmentBooked, { props: { name: "John Doe", locale: "de" } });
return new Response(renderOutputToHtml(emailRender), {
headers: {
"Content-Type": "text/html",
},
});
};
+6
View File
@@ -11,6 +11,12 @@ import type { Handle } from "@sveltejs/kit";
* @returns {Promise<Response>} The response with applied headers and rate limiting
*/
export const i18nHandle: Handle = ({ event, resolve }) => {
// Disable i18n for API and local-only routes
// Avoids using i18n middleware where emails are rendered
if (event.url.pathname.startsWith("/api") || event.url.pathname.startsWith("/local-only")) {
return resolve(event);
}
// Determine browser locale and use it
const cookieLocale = event.cookies.get(cookieName);
let locale: Locale | undefined = cookieLocale as Locale;