Merge branch 'feature/event-timezone' into main
This commit is contained in:
commit
877ef1a220
13 changed files with 318 additions and 159 deletions
|
|
@ -18,6 +18,8 @@ const eventSchema = new mongoose.Schema({
|
|||
enum: ["community", "workshop", "social", "showcase"],
|
||||
default: "community",
|
||||
},
|
||||
// IANA timezone for interpreting datetime input and rendering the event time.
|
||||
displayTimezone: { type: String, default: "America/Toronto" },
|
||||
// Online-first location handling
|
||||
location: {
|
||||
type: String,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -173,6 +173,7 @@ export const adminEventCreateSchema = z.object({
|
|||
description: z.string().min(1).max(50000),
|
||||
startDate: z.string().min(1),
|
||||
endDate: z.string().min(1),
|
||||
displayTimezone: z.string().max(100).optional(),
|
||||
location: z.string().max(500).optional(),
|
||||
maxAttendees: z.preprocess(emptyStringToUndefined, z.number().int().positive().optional().nullable()),
|
||||
membersOnly: z.boolean().optional(),
|
||||
|
|
@ -204,6 +205,7 @@ export const adminEventUpdateSchema = z.object({
|
|||
description: z.string().min(1).max(50000),
|
||||
startDate: z.string().min(1),
|
||||
endDate: z.string().min(1),
|
||||
displayTimezone: z.string().max(100).optional(),
|
||||
location: z.string().max(500).optional(),
|
||||
maxAttendees: z.preprocess(emptyStringToUndefined, z.number().int().positive().optional().nullable()),
|
||||
membersOnly: z.boolean().optional(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue