wip
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
import { Resend } from 'resend'
|
||||
|
||||
import type { RestaurantTable } from '#/db/schema'
|
||||
|
||||
const dateTimeFormatter = new Intl.DateTimeFormat('de-DE', {
|
||||
dateStyle: 'full',
|
||||
timeStyle: 'short',
|
||||
})
|
||||
|
||||
const COLORS = {
|
||||
bg: '#17120f',
|
||||
bgRaised: '#1e1712',
|
||||
line: '#3a2f26',
|
||||
text: '#f3ead9',
|
||||
textMuted: '#c9baa3',
|
||||
gold: '#c9a15a',
|
||||
goldBright: '#e0bb78',
|
||||
}
|
||||
|
||||
function escapeHtml(value: string) {
|
||||
return value
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''')
|
||||
}
|
||||
|
||||
let resendClient: Resend | undefined
|
||||
|
||||
function getResendClient() {
|
||||
const apiKey = process.env.RESEND_API_KEY
|
||||
|
||||
if (!apiKey) {
|
||||
return null
|
||||
}
|
||||
|
||||
resendClient ??= new Resend(apiKey)
|
||||
return resendClient
|
||||
}
|
||||
|
||||
type ReservationEmailInfo = {
|
||||
customerName: string
|
||||
customerEmail: string
|
||||
message?: string | null
|
||||
partySize: number
|
||||
reservationTime: Date
|
||||
cancelToken: string
|
||||
table: Pick<RestaurantTable, 'name'>
|
||||
}
|
||||
|
||||
const CONTACT_EMAIL = 'info@steakhaus-meppen.de'
|
||||
|
||||
function getSiteUrl() {
|
||||
return process.env.BETTER_AUTH_URL ?? 'http://localhost:3000'
|
||||
}
|
||||
|
||||
function buildCancelUrl(cancelToken: string) {
|
||||
return `${getSiteUrl()}/stornieren/${cancelToken}`
|
||||
}
|
||||
|
||||
function renderReservationEmailHtml(
|
||||
heading: string,
|
||||
intro: string,
|
||||
reservation: ReservationEmailInfo,
|
||||
showActions: boolean,
|
||||
) {
|
||||
const formattedTime = dateTimeFormatter.format(reservation.reservationTime)
|
||||
const name = escapeHtml(reservation.customerName)
|
||||
const tableName = escapeHtml(reservation.table.name)
|
||||
const cancelUrl = buildCancelUrl(reservation.cancelToken)
|
||||
|
||||
const row = (label: string, value: string) => `
|
||||
<tr>
|
||||
<td style="padding:10px 0;border-top:1px solid ${COLORS.line};color:${COLORS.textMuted};font:400 14px/1.4 'Helvetica Neue',Arial,sans-serif;letter-spacing:0.03em;text-transform:uppercase;white-space:nowrap;">
|
||||
${label}
|
||||
</td>
|
||||
<td style="padding:10px 0;border-top:1px solid ${COLORS.line};color:${COLORS.text};font:600 15px/1.4 Georgia,'Times New Roman',serif;text-align:right;">
|
||||
${value}
|
||||
</td>
|
||||
</tr>`
|
||||
|
||||
return `
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>${heading}</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:32px 16px;background:${COLORS.bg};font:400 16px/1.5 'Helvetica Neue',Arial,sans-serif;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom:28px;">
|
||||
<span style="font:600 20px/1.3 Georgia,'Times New Roman',serif;letter-spacing:0.03em;color:${COLORS.text};">
|
||||
Steakhaus <span style="color:${COLORS.goldBright};">bei Mirko</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="background:${COLORS.bgRaised};border:1px solid ${COLORS.line};border-radius:10px;padding:32px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom:20px;">
|
||||
<span style="display:inline-block;width:36px;height:2px;background:${COLORS.gold};"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font:600 22px/1.3 Georgia,'Times New Roman',serif;color:${COLORS.goldBright};padding-bottom:12px;">
|
||||
${heading}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color:${COLORS.textMuted};font-size:15px;padding-bottom:8px;">
|
||||
Hallo ${name},
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color:${COLORS.textMuted};font-size:15px;padding-bottom:20px;">
|
||||
${intro}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
${row('Tisch', tableName)}
|
||||
${row('Datum & Uhrzeit', formattedTime)}
|
||||
${row('Personen', String(reservation.partySize))}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
${
|
||||
reservation.message
|
||||
? `
|
||||
<tr>
|
||||
<td style="padding-top:16px;">
|
||||
<div style="color:${COLORS.textMuted};font:400 12px/1.4 'Helvetica Neue',Arial,sans-serif;letter-spacing:0.03em;text-transform:uppercase;padding-bottom:4px;">
|
||||
Nachricht
|
||||
</div>
|
||||
<div style="color:${COLORS.text};font-size:14px;">${escapeHtml(reservation.message)}</div>
|
||||
</td>
|
||||
</tr>`
|
||||
: ''
|
||||
}
|
||||
${
|
||||
showActions
|
||||
? `
|
||||
<tr>
|
||||
<td align="center" style="padding-top:28px;">
|
||||
<a href="${cancelUrl}" style="display:inline-block;padding:0.7rem 1.4rem;border:1px solid ${COLORS.line};border-radius:4px;color:${COLORS.text};font:600 13px/1 'Helvetica Neue',Arial,sans-serif;text-decoration:none;">
|
||||
Reservierung stornieren
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding-top:16px;color:${COLORS.textMuted};font-size:13px;">
|
||||
Möchten Sie etwas ändern? Schreiben Sie uns einfach eine E-Mail an
|
||||
<a href="mailto:${CONTACT_EMAIL}" style="color:${COLORS.goldBright};">${CONTACT_EMAIL}</a>.
|
||||
</td>
|
||||
</tr>`
|
||||
: ''
|
||||
}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding-top:28px;color:${COLORS.textMuted};font-size:13px;">
|
||||
Wir freuen uns auf Sie!
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding-top:16px;color:${COLORS.textMuted};font-size:12px;">
|
||||
Meppener Straße 8 · 49716 Meppen · 05931 - 12750
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
function renderReservationEmailText(
|
||||
heading: string,
|
||||
intro: string,
|
||||
reservation: ReservationEmailInfo,
|
||||
showActions: boolean,
|
||||
) {
|
||||
const formattedTime = dateTimeFormatter.format(reservation.reservationTime)
|
||||
|
||||
return [
|
||||
'Steakhaus bei Mirko',
|
||||
'',
|
||||
heading,
|
||||
'',
|
||||
`Hallo ${reservation.customerName},`,
|
||||
'',
|
||||
intro,
|
||||
'',
|
||||
`Tisch: ${reservation.table.name}`,
|
||||
`Datum & Uhrzeit: ${formattedTime}`,
|
||||
`Personen: ${reservation.partySize}`,
|
||||
...(reservation.message ? [`Nachricht: ${reservation.message}`] : []),
|
||||
'',
|
||||
...(showActions
|
||||
? [
|
||||
`Reservierung stornieren: ${buildCancelUrl(reservation.cancelToken)}`,
|
||||
`Möchten Sie etwas ändern? Schreiben Sie uns eine E-Mail an ${CONTACT_EMAIL}.`,
|
||||
'',
|
||||
]
|
||||
: []),
|
||||
'Wir freuen uns auf Sie!',
|
||||
'Steakhaus bei Mirko',
|
||||
'Meppener Straße 8 · 49716 Meppen · 05931 - 12750',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
async function sendReservationEmail(
|
||||
subject: string,
|
||||
intro: string,
|
||||
reservation: ReservationEmailInfo,
|
||||
showActions: boolean,
|
||||
) {
|
||||
const client = getResendClient()
|
||||
|
||||
if (!client) {
|
||||
console.warn('RESEND_API_KEY is not set — skipping reservation email')
|
||||
return
|
||||
}
|
||||
|
||||
const fromAddress = process.env.RESERVATIONS_FROM_EMAIL ?? 'Steakhaus bei Mirko <reservierungen@steakhaus-meppen.de>'
|
||||
|
||||
try {
|
||||
await client.emails.send({
|
||||
from: fromAddress,
|
||||
to: reservation.customerEmail,
|
||||
subject,
|
||||
html: renderReservationEmailHtml(subject, intro, reservation, showActions),
|
||||
text: renderReservationEmailText(subject, intro, reservation, showActions),
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Failed to send reservation email', err)
|
||||
}
|
||||
}
|
||||
|
||||
export function sendReservationConfirmationEmail(reservation: ReservationEmailInfo) {
|
||||
return sendReservationEmail(
|
||||
'Ihre Reservierung ist bestätigt',
|
||||
'vielen Dank für Ihre Reservierung. Hier sind die Details:',
|
||||
reservation,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
export function sendReservationUpdateEmail(reservation: ReservationEmailInfo) {
|
||||
return sendReservationEmail(
|
||||
'Ihre Reservierung wurde geändert',
|
||||
'Ihre Reservierung wurde aktualisiert. Hier sind die neuen Details:',
|
||||
reservation,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
export function sendReservationCancellationEmail(reservation: ReservationEmailInfo) {
|
||||
return sendReservationEmail(
|
||||
'Ihre Reservierung wurde storniert',
|
||||
'Ihre folgende Reservierung wurde storniert:',
|
||||
reservation,
|
||||
false,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user