Skip to content

Commit

Permalink
feat: update app to use email content templates
Browse files Browse the repository at this point in the history
  • Loading branch information
Anton Lilleby authored and Anton Lilleby committed Jan 15, 2025
1 parent 9aa4cd9 commit e7533eb
Show file tree
Hide file tree
Showing 14 changed files with 219 additions and 348 deletions.
15 changes: 9 additions & 6 deletions app/src/lib/actions/external/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import {
registrationSchemaExternal,
unregistrationSchemaExternal,
} from "$lib/schemas/external/schema";
import { sendRegistrationConfirmed } from "$lib/email/event/registration";
import { sendConfirmUnregistration } from "$lib/email/event/unregistration";
import { RateLimiter } from "sveltekit-rate-limiter/server";
import { sendEmailAccepted } from "$lib/email/event/accepted";
import { sendEmailConfirmDecline } from "$lib/email/event/confirm-decline";

const limiter = new RateLimiter({
IP: [20, "h"], // 20 rquests per hour from the same IP
Expand Down Expand Up @@ -155,17 +155,20 @@ export const submitRegistrationExternal: Actions["submitRegistrationExternal"] =

const emailPayload = {
id,
mailTo: email,
to: email,
summary: eventContent.title,
description: eventContent.summary,
start: eventContent.start,
end: eventContent.end,
location: eventContent.place,
organiser: eventContent.organisers.join(" | "),
subject: eventContent.emailTemplate.registrationSubject,
message: eventContent.emailTemplate.registrationMessage,
reminder: eventContent.emailReminder,
};

if (process.env.NODE_ENV !== "development") {
const { error: emailError } = await sendRegistrationConfirmed(emailPayload);
const { error: emailError } = await sendEmailAccepted(emailPayload);

if (emailError) {
console.error("Error: Failed to send email");
Expand Down Expand Up @@ -263,14 +266,14 @@ export const submitUnregistrationExternal: Actions["submitUnregistrationExternal

const emailPayload = {
id,
mailTo: email,
to: email,
summary: eventContent.title,
organiser: eventContent.organisers.join(" | "),
token,
};

if (process.env.NODE_ENV !== "development") {
const { error: emailError } = await sendConfirmUnregistration(emailPayload);
const { error: emailError } = await sendEmailConfirmDecline(emailPayload);

if (emailError) {
console.error("Error: Failed to send email");
Expand Down
43 changes: 24 additions & 19 deletions app/src/lib/actions/internal/action.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import validator from "validator";
import { type Actions } from "@sveltejs/kit";
import { superValidate, message } from "sveltekit-superforms/server";
import { zod } from "sveltekit-superforms/adapters";
import { getEventContent } from "$lib/server/sanity/queries";
import { sendEmailAccepted } from "$lib/email/event/accepted";
import { sendEmailDeclined } from "$lib/email/event/declined";
import {
getEvent,
getEventParticipant,
setParticipantNotAttending,
} from "$lib/server/supabase/queries";
registrationSchemaInternal,
unregistrationSchemaInternal,
} from "$lib/schemas/internal/schema";
import {
deleteEventParticipant,
executeTransaction,
insertAndGetEventParticipant,
insertEventFoodPreference,
insertEventParticipantOptions,
} from "$lib/server/kysley/transactions";
import { getEventContent } from "$lib/server/sanity/queries";
import {
registrationSchemaInternal,
unregistrationSchemaInternal,
} from "$lib/schemas/internal/schema";
import { sendRegistrationConfirmed } from "$lib/email/event/registration";
import { sendUnregistrationConfirmed } from "$lib/email/event/unregistration";
getEvent,
getEventParticipant,
setParticipantNotAttending,
} from "$lib/server/supabase/queries";
import { type Actions } from "@sveltejs/kit";
import { RateLimiter } from "sveltekit-rate-limiter/server";
import { zod } from "sveltekit-superforms/adapters";
import { message, superValidate } from "sveltekit-superforms/server";
import validator from "validator";

/**
** IP: Allows 40 requests per hour from the same IP address.
Expand Down Expand Up @@ -175,17 +175,20 @@ export const submitRegistrationInternal: Actions["submitRegistrationInternal"] =

const emailPayload = {
id,
mailTo: email,
to: email,
summary: eventContent.title,
description: eventContent.summary,
start: eventContent.start,
end: eventContent.end,
location: eventContent.place,
organiser: eventContent.organisers.join(" | "),
subject: eventContent.emailTemplate.registrationSubject,
message: eventContent.emailTemplate.registrationMessage,
reminder: eventContent.emailReminder,
};

if (process.env.NODE_ENV !== "development") {
const { error: emailError } = await sendRegistrationConfirmed(emailPayload);
const { error: emailError } = await sendEmailAccepted(emailPayload);

if (emailError) {
console.error("Error: Failed to send email");
Expand Down Expand Up @@ -291,23 +294,25 @@ export const submitUnregistrationInternal: Actions["submitUnregistrationInternal

const emailPayload = {
id,
mailTo: email,
to: email,
summary: eventContent.title,
description: eventContent.summary,
start: eventContent.start,
end: eventContent.end,
location: eventContent.place,
organiser: eventContent.organisers.join(" | "),
subject: eventContent.emailTemplate.unregistrationSubject,
message: eventContent.emailTemplate.unregistrationMessage,
};

if (process.env.NODE_ENV !== "development") {
const { error: emailError } = await sendUnregistrationConfirmed(emailPayload);
const { error: emailError } = await sendEmailDeclined(emailPayload);

if (emailError) {
console.error("Error: Failed to send email");

return message(unregistrationForm, {
text: "Du er nå meldt av arrangement 👋 Vi kunne dessverre ikke sende e-post bekreftelse.",
text: "Du er nå meldt av arrangementet 👋 Vi kunne dessverre ikke sende en e-post bekreftelse.",
warning: true,
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@ import ical, {
ICalCalendarMethod,
} from "ical-generator";
import { toHTML } from "@portabletext/to-html";
import { sendMail as sendEmail } from "../nodemailer";
import { composeEmail, sendEmail, wrapWithStyles } from "../nodemailer";
import { PUBLIC_APP_BASE_URL } from "$env/static/public";
import type { EventUpdatedProps } from "../../../routes/api/send-event-update/+server";
import type { EventUpdatedProps } from "../../../routes/api/send-event-updated/+server";

interface EventUpdatedExtendedProps extends EventUpdatedProps {
interface EmailAcceptedProps extends EventUpdatedProps {
to: string;
}
export const sendEmailUpdated = async (props: EventUpdatedExtendedProps) => {

export const sendEmailAccepted = async (props: EmailAcceptedProps) => {
const icsFile = createIcsFile(props);

const emailTemplate = createEmailTemplate({
const emailTemplate = composeEmail({
...props,
subject: `${props.subject} ${props.summary}`,
icsFile,
});

Expand All @@ -34,7 +36,7 @@ const createIcsFile = ({
location,
organiser,
reminder,
}: EventUpdatedExtendedProps) => {
}: EmailAcceptedProps) => {
const url = `${PUBLIC_APP_BASE_URL}/event/${id}`;
const calendar = ical({ name: organiser, method: ICalCalendarMethod.REQUEST });
const alarms = [];
Expand All @@ -43,7 +45,9 @@ const createIcsFile = ({
alarms.push({
type: ICalAlarmType.email,
summary: reminder.threeDaysSubject,
description: reminder.threeDaysMessage ? toHTML(reminder.threeDaysMessage) : "",
description: reminder.threeDaysMessage
? wrapWithStyles(toHTML(reminder.threeDaysMessage))
: "",
trigger: 259200, // ->3 days before event starts
});
}
Expand All @@ -52,7 +56,7 @@ const createIcsFile = ({
alarms.push({
type: ICalAlarmType.email,
summary: reminder.oneHourSubject,
description: reminder.oneHourMessage ? toHTML(reminder.oneHourMessage) : "",
description: reminder.oneHourMessage ? wrapWithStyles(toHTML(reminder.oneHourMessage)) : "",
trigger: 3600, // -> 1 hour before event starts
});
}
Expand Down Expand Up @@ -81,21 +85,3 @@ const createIcsFile = ({

return Buffer.from(calendar.toString());
};

interface EmailProps
extends Pick<EventUpdatedExtendedProps, "to" | "subject" | "message" | "summary"> {
icsFile: Buffer;
}

const createEmailTemplate = ({ to, subject, message, summary, icsFile }: EmailProps) => {
return {
to,
from: "Skjer <[email protected]>",
subject: `${subject} ${summary}`,
html: toHTML(message),
icalEvent: {
method: "request",
content: icsFile,
},
};
};
32 changes: 7 additions & 25 deletions app/src/lib/email/event/canceled.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { PUBLIC_APP_BASE_URL } from "$env/static/public";
import { sendMail as sendEmail } from "$lib/email/nodemailer";
import { toHTML } from "@portabletext/to-html";
import { composeEmail, sendEmail } from "$lib/email/nodemailer";
import ical, { ICalAttendeeRole, ICalAttendeeStatus, ICalCalendarMethod } from "ical-generator";
import type { EventCanceledProps } from "../../../routes/api/send-event-canceled/+server";

interface EventCanceledExtendedProps extends EventCanceledProps {
interface EmailCanceledProps extends EventCanceledProps {
to: string;
}

export const sendEmailCanceled = async (props: EventCanceledExtendedProps) => {
export const sendEmailCanceled = async (props: EmailCanceledProps) => {
const icsFile = createIcsFile(props);

const emailTemplate = createEmailTemplate({
const email = composeEmail({
...props,
subject: `${props.subject} ${props.summary}`,
icsFile,
});

const result = await sendEmail(emailTemplate);
const result = await sendEmail(email);
return result;
};

Expand All @@ -29,7 +29,7 @@ const createIcsFile = ({
end,
location,
organiser,
}: EventCanceledExtendedProps) => {
}: EmailCanceledProps) => {
const url = `${PUBLIC_APP_BASE_URL}/event/${id}`;
const calendar = ical({ name: organiser, method: ICalCalendarMethod.CANCEL });
calendar.createEvent({
Expand All @@ -55,21 +55,3 @@ const createIcsFile = ({

return Buffer.from(calendar.toString());
};

interface EmailProps
extends Pick<EventCanceledExtendedProps, "to" | "subject" | "message" | "summary"> {
icsFile: Buffer;
}

const createEmailTemplate = ({ to, subject, message, summary, icsFile }: EmailProps) => {
return {
to,
from: "Skjer <[email protected]>",
subject: `${subject} ${summary}`,
html: toHTML(message),
icalEvent: {
method: "request",
content: icsFile,
},
};
};
30 changes: 30 additions & 0 deletions app/src/lib/email/event/confirm-decline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { PUBLIC_APP_BASE_URL } from "$env/static/public";
import { sendEmail } from "$lib/email/nodemailer";

interface EmailConfirmDeclineProps {
to: string;
summary: string;
organiser: string;
token: string;
}

// TODO: Consider making this a function that takes a template from the Event Schema
export const sendEmailConfirmDecline = async (props: EmailConfirmDeclineProps) => {
const url = `${PUBLIC_APP_BASE_URL}/event/unregistration/${props.token}`;
const html = `<span>
<p>Hei,</p>
<p>Vi har mottatt en forespørsel om å melde deg av «${props.summary}».</p>
<p>For å bekrefte denne handlingen, vennligst klikk på følgende lenke:</p>
<p><a href="${url}">Bekreft avregistrering</a></p>
</span>`;

const emailTemplate = {
to: props.to,
from: "Skjer <[email protected]>",
subject: `Bekreft avregistrering: ${props.summary}`,
html,
};

const result = await sendEmail(emailTemplate);
return result;
};
67 changes: 67 additions & 0 deletions app/src/lib/email/event/declined.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import ical, { ICalAttendeeRole, ICalAttendeeStatus, ICalCalendarMethod } from "ical-generator";
import { composeEmail, sendEmail } from "../nodemailer";
import { PUBLIC_APP_BASE_URL } from "$env/static/public";
import type { BlockContent } from "$models/sanity.model";

interface EmailDeclinedProps {
id: string;
to: string;
summary: string;
description?: string;
start: string;
end: string;
location: string;
organiser: string;
subject: string;
message: BlockContent;
}

export const sendEmailDeclined = async (props: EmailDeclinedProps) => {
const icsFile = createIcsFile(props);

const emailTemplate = composeEmail({
...props,
subject: `${props.subject} ${props.summary}`,
icsFile,
});

const result = await sendEmail(emailTemplate);
return result;
};

const createIcsFile = ({
id,
to,
summary,
description,
start,
end,
location,
organiser,
}: EmailDeclinedProps) => {
const url = `${PUBLIC_APP_BASE_URL}/event/${id}`;
const calendar = ical({ name: organiser, method: ICalCalendarMethod.REQUEST });

calendar.createEvent({
id,
summary,
description,
location,
start,
end,
url,
attendees: [
{
email: to,
status: ICalAttendeeStatus.DECLINED,
role: ICalAttendeeRole.REQ,
},
],
organizer: {
name: organiser,
email: "[email protected]",
},
});

return Buffer.from(calendar.toString());
};
Loading

0 comments on commit e7533eb

Please sign in to comment.