276 lines
9.0 KiB
TypeScript
276 lines
9.0 KiB
TypeScript
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,
|
|
)
|
|
}
|