feat(emails): render event reminders in event displayTimezone
Registration, waitlist, and series-pass confirmation emails formatted dates with Intl.DateTimeFormat in the server's local TZ. Switch to event.displayTimezone (fallback America/Toronto) so the email shows the event's intended wall-clock + zone suffix regardless of where the Resend worker runs. Inline formatters in three exports collapsed to two module-level helpers that take a timeZone argument.
This commit is contained in:
parent
9dd007657a
commit
75b1f84d18
1 changed files with 36 additions and 80 deletions
|
|
@ -2,6 +2,29 @@ import { Resend } from "resend";
|
|||
|
||||
const resend = new Resend(useRuntimeConfig().resendApiKey);
|
||||
|
||||
const formatEventDate = (dateString, timeZone = "America/Toronto") => {
|
||||
const date = new Date(dateString);
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
timeZone,
|
||||
}).format(date);
|
||||
};
|
||||
|
||||
const formatEventTime = (startDate, endDate, timeZone = "America/Toronto") => {
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
const timeFormat = new Intl.DateTimeFormat("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
timeZoneName: "short",
|
||||
timeZone,
|
||||
});
|
||||
return `${timeFormat.format(start)} - ${timeFormat.format(end)}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Send event registration confirmation email
|
||||
* @param {Object} options - { requiresSignIn?: boolean } — when true, appends a
|
||||
|
|
@ -9,29 +32,7 @@ const resend = new Resend(useRuntimeConfig().resendApiKey);
|
|||
* registered via the public form and did not receive an auto-login cookie.
|
||||
*/
|
||||
export async function sendEventRegistrationEmail(registration, eventData, options = {}) {
|
||||
const formatDate = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}).format(date);
|
||||
};
|
||||
|
||||
const formatTime = (startDate, endDate) => {
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
|
||||
const timeFormat = new Intl.DateTimeFormat("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
timeZoneName: "short",
|
||||
});
|
||||
|
||||
return `${timeFormat.format(start)} - ${timeFormat.format(end)}`;
|
||||
};
|
||||
|
||||
const tz = eventData.displayTimezone || "America/Toronto";
|
||||
const baseUrl = process.env.BASE_URL || "https://ghostguild.org";
|
||||
const eventUrl = `${baseUrl}/events/${eventData.slug || eventData._id}`;
|
||||
const signInSection = options.requiresSignIn
|
||||
|
|
@ -66,8 +67,8 @@ Paid: $${registration.amountPaid.toFixed(2)} CAD`;
|
|||
|
||||
You're now registered for ${eventData.title}.
|
||||
|
||||
Date: ${formatDate(eventData.startDate)}
|
||||
Time: ${formatTime(eventData.startDate, eventData.endDate)}
|
||||
Date: ${formatEventDate(eventData.startDate, tz)}
|
||||
Time: ${formatEventTime(eventData.startDate, eventData.endDate, tz)}
|
||||
Location: ${eventData.location}
|
||||
${eventData.description ? `\n${eventData.description}\n` : ""}${ticketSection}${signInSection}
|
||||
View event: ${eventUrl}
|
||||
|
|
@ -124,29 +125,7 @@ ${baseUrl}/events`,
|
|||
* Send waitlist notification email when a spot opens up
|
||||
*/
|
||||
export async function sendWaitlistNotificationEmail(waitlistEntry, eventData) {
|
||||
const formatDate = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}).format(date);
|
||||
};
|
||||
|
||||
const formatTime = (startDate, endDate) => {
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
|
||||
const timeFormat = new Intl.DateTimeFormat("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
timeZoneName: "short",
|
||||
});
|
||||
|
||||
return `${timeFormat.format(start)} - ${timeFormat.format(end)}`;
|
||||
};
|
||||
|
||||
const tz = eventData.displayTimezone || "America/Toronto";
|
||||
const baseUrl = process.env.BASE_URL || "https://ghostguild.org";
|
||||
const eventUrl = `${baseUrl}/events/${eventData.slug || eventData._id}`;
|
||||
|
||||
|
|
@ -159,8 +138,8 @@ export async function sendWaitlistNotificationEmail(waitlistEntry, eventData) {
|
|||
|
||||
A spot opened up for ${eventData.title}.
|
||||
|
||||
Date: ${formatDate(eventData.startDate)}
|
||||
Time: ${formatTime(eventData.startDate, eventData.endDate)}
|
||||
Date: ${formatEventDate(eventData.startDate, tz)}
|
||||
Time: ${formatEventTime(eventData.startDate, eventData.endDate, tz)}
|
||||
Location: ${eventData.location}
|
||||
|
||||
Register now: ${eventUrl}
|
||||
|
|
@ -186,29 +165,6 @@ If you can no longer attend, ignore this email and the spot goes to the next per
|
|||
export async function sendSeriesPassConfirmation(options) {
|
||||
const { to, name, series, ticket, events, paymentId } = options;
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}).format(date);
|
||||
};
|
||||
|
||||
const formatTime = (startDate, endDate) => {
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
|
||||
const timeFormat = new Intl.DateTimeFormat("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
timeZoneName: "short",
|
||||
});
|
||||
|
||||
return `${timeFormat.format(start)} - ${timeFormat.format(end)}`;
|
||||
};
|
||||
|
||||
const formatPrice = (price, currency = "CAD") => {
|
||||
if (price === 0) return "Free";
|
||||
return new Intl.NumberFormat("en-CA", {
|
||||
|
|
@ -223,13 +179,13 @@ export async function sendSeriesPassConfirmation(options) {
|
|||
: "";
|
||||
|
||||
const eventList = events
|
||||
.map(
|
||||
(evt, index) =>
|
||||
` ${index + 1}. ${evt.title}
|
||||
${formatDate(evt.startDate)}
|
||||
${formatTime(evt.startDate, evt.endDate)}
|
||||
${evt.location}`,
|
||||
)
|
||||
.map((evt, index) => {
|
||||
const tz = evt.displayTimezone || "America/Toronto";
|
||||
return ` ${index + 1}. ${evt.title}
|
||||
${formatEventDate(evt.startDate, tz)}
|
||||
${formatEventTime(evt.startDate, evt.endDate, tz)}
|
||||
${evt.location}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
try {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue