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 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
|
* Send event registration confirmation email
|
||||||
* @param {Object} options - { requiresSignIn?: boolean } — when true, appends a
|
* @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.
|
* registered via the public form and did not receive an auto-login cookie.
|
||||||
*/
|
*/
|
||||||
export async function sendEventRegistrationEmail(registration, eventData, options = {}) {
|
export async function sendEventRegistrationEmail(registration, eventData, options = {}) {
|
||||||
const formatDate = (dateString) => {
|
const tz = eventData.displayTimezone || "America/Toronto";
|
||||||
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 baseUrl = process.env.BASE_URL || "https://ghostguild.org";
|
const baseUrl = process.env.BASE_URL || "https://ghostguild.org";
|
||||||
const eventUrl = `${baseUrl}/events/${eventData.slug || eventData._id}`;
|
const eventUrl = `${baseUrl}/events/${eventData.slug || eventData._id}`;
|
||||||
const signInSection = options.requiresSignIn
|
const signInSection = options.requiresSignIn
|
||||||
|
|
@ -66,8 +67,8 @@ Paid: $${registration.amountPaid.toFixed(2)} CAD`;
|
||||||
|
|
||||||
You're now registered for ${eventData.title}.
|
You're now registered for ${eventData.title}.
|
||||||
|
|
||||||
Date: ${formatDate(eventData.startDate)}
|
Date: ${formatEventDate(eventData.startDate, tz)}
|
||||||
Time: ${formatTime(eventData.startDate, eventData.endDate)}
|
Time: ${formatEventTime(eventData.startDate, eventData.endDate, tz)}
|
||||||
Location: ${eventData.location}
|
Location: ${eventData.location}
|
||||||
${eventData.description ? `\n${eventData.description}\n` : ""}${ticketSection}${signInSection}
|
${eventData.description ? `\n${eventData.description}\n` : ""}${ticketSection}${signInSection}
|
||||||
View event: ${eventUrl}
|
View event: ${eventUrl}
|
||||||
|
|
@ -124,29 +125,7 @@ ${baseUrl}/events`,
|
||||||
* Send waitlist notification email when a spot opens up
|
* Send waitlist notification email when a spot opens up
|
||||||
*/
|
*/
|
||||||
export async function sendWaitlistNotificationEmail(waitlistEntry, eventData) {
|
export async function sendWaitlistNotificationEmail(waitlistEntry, eventData) {
|
||||||
const formatDate = (dateString) => {
|
const tz = eventData.displayTimezone || "America/Toronto";
|
||||||
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 baseUrl = process.env.BASE_URL || "https://ghostguild.org";
|
const baseUrl = process.env.BASE_URL || "https://ghostguild.org";
|
||||||
const eventUrl = `${baseUrl}/events/${eventData.slug || eventData._id}`;
|
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}.
|
A spot opened up for ${eventData.title}.
|
||||||
|
|
||||||
Date: ${formatDate(eventData.startDate)}
|
Date: ${formatEventDate(eventData.startDate, tz)}
|
||||||
Time: ${formatTime(eventData.startDate, eventData.endDate)}
|
Time: ${formatEventTime(eventData.startDate, eventData.endDate, tz)}
|
||||||
Location: ${eventData.location}
|
Location: ${eventData.location}
|
||||||
|
|
||||||
Register now: ${eventUrl}
|
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) {
|
export async function sendSeriesPassConfirmation(options) {
|
||||||
const { to, name, series, ticket, events, paymentId } = 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") => {
|
const formatPrice = (price, currency = "CAD") => {
|
||||||
if (price === 0) return "Free";
|
if (price === 0) return "Free";
|
||||||
return new Intl.NumberFormat("en-CA", {
|
return new Intl.NumberFormat("en-CA", {
|
||||||
|
|
@ -223,13 +179,13 @@ export async function sendSeriesPassConfirmation(options) {
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const eventList = events
|
const eventList = events
|
||||||
.map(
|
.map((evt, index) => {
|
||||||
(evt, index) =>
|
const tz = evt.displayTimezone || "America/Toronto";
|
||||||
` ${index + 1}. ${evt.title}
|
return ` ${index + 1}. ${evt.title}
|
||||||
${formatDate(evt.startDate)}
|
${formatEventDate(evt.startDate, tz)}
|
||||||
${formatTime(evt.startDate, evt.endDate)}
|
${formatEventTime(evt.startDate, evt.endDate, tz)}
|
||||||
${evt.location}`,
|
${evt.location}`;
|
||||||
)
|
})
|
||||||
.join("\n\n");
|
.join("\n\n");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue