Skip to content

Commit

Permalink
feat: add fields for email reminder and template (#139)
Browse files Browse the repository at this point in the history
Fixes #118 
Fixes #135 
Fixes #143

---------

Co-authored-by: Anton Lilleby <[email protected]>
  • Loading branch information
an2n and Anton Lilleby authored Jan 15, 2025
1 parent 6f5ede0 commit 55097df
Show file tree
Hide file tree
Showing 30 changed files with 1,180 additions and 477 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ Vil du klikke deg rundt i browser for å se hva som skjer i testene, sleng på `

## Slack

Når et arrangement publiseres for første gang, vil det automatisk genereres en Slack-melding til kanalen #tmp_arrangementer. For å bygge meldingen kan man benytte [Block Kit Builder](https://app.slack.com/block-kit-builder). Denne tjenesten lar deg visuelt designe layouten av meldingen med ulike blokker som knapper, tekstfelter og bilder.
Når et arrangement publiseres for første gang, vil det automatisk genereres en Slack-melding til kanalen #skjer. For å bygge meldingen kan man benytte [Block Kit Builder](https://app.slack.com/block-kit-builder). Denne tjenesten lar deg visuelt designe layouten av meldingen med ulike blokker som knapper, tekstfelter og bilder.

## E-posthåndtering

Expand Down
1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"dependencies": {
"@auth/core": "^0.34.1",
"@auth/sveltekit": "^1.4.1",
"@portabletext/to-html": "^2.0.13",
"@sanity/client": "^6.21.1",
"@sanity/image-url": "^1.0.2",
"@sanity/svelte-loader": "^1.11.33",
Expand Down
12 changes: 12 additions & 0 deletions app/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion app/src/components/shared/Deadline.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
export let deadline: string;
</script>

<div class="flex text-sm gap-1 rounded-xl bg-gray-100 py-2 px-4 w-fit">
<div class="flex w-fit gap-1 rounded-xl bg-gray-100 px-4 py-2 text-sm dark:text-black">
<p>
Påmeldingsfrist kl.{formatTime(deadline)}, {formatDate(deadline)}
</p>
Expand Down
15 changes: 10 additions & 5 deletions app/src/components/shared/EventLogos.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,20 @@
</script>

<div class="flex items-center gap-4">
{#if event.organisers.includes("Capra")}
{#if event.organisers === "Alle"}
<img class="block h-{height} dark:hidden" alt="Capra-logo" src={capraLogoBlack} />
<img class="hidden h-{height} dark:block" alt="Capra-logo" src={capraLogoWhite} />
{/if}
{#if event.organisers.includes("Liflig")}
<img class="block h-{height} dark:hidden" alt="Liflig-logo" src={lifligLogoBlack} />
<img class="hidden h-{height} dark:block" alt="Liflig-logo" src={lifligLogoWhite} />
{/if}
{#if event.organisers.includes("Fryde")}
<img class="block h-{height} dark:hidden" alt="Fryde-logo" src={frydeLogoBlack} />
<img class="hidden h-{height} dark:block" alt="Fryde-logo" src={frydeLogoWhite} />
{:else if event.organisers === "Capra"}
<img class="block h-{height} dark:hidden" alt="Capra-logo" src={capraLogoBlack} />
<img class="hidden h-{height} dark:block" alt="Capra-logo" src={capraLogoWhite} />
{:else if event.organisers === "Liflig"}
<img class="block h-{height} dark:hidden" alt="Liflig-logo" src={lifligLogoBlack} />
<img class="hidden h-{height} dark:block" alt="Liflig-logo" src={lifligLogoWhite} />
{:else if event.organisers === "Fryde"}
<img class="block h-{height} dark:hidden" alt="Fryde-logo" src={frydeLogoBlack} />
<img class="hidden h-{height} dark:block" alt="Fryde-logo" src={frydeLogoWhite} />
{/if}
Expand Down
18 changes: 10 additions & 8 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,19 @@ 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(" | "),
organiser: eventContent.organisers,
subject: eventContent.emailTemplate.registrationSubject,
message: eventContent.emailTemplate.registrationMessage,
};

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 +265,14 @@ export const submitUnregistrationExternal: Actions["submitUnregistrationExternal

const emailPayload = {
id,
mailTo: email,
to: email,
summary: eventContent.title,
organiser: eventContent.organisers.join(" | "),
organiser: eventContent.organisers,
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
46 changes: 25 additions & 21 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,19 @@ 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(" | "),
organiser: eventContent.organisers,
subject: eventContent.emailTemplate.registrationSubject,
message: eventContent.emailTemplate.registrationMessage,
};

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 +293,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(" | "),
organiser: eventContent.organisers,
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
58 changes: 58 additions & 0 deletions app/src/lib/email/event/accepted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import ical, { ICalAttendeeRole, ICalAttendeeStatus, ICalCalendarMethod } from "ical-generator";
import { composeEmail, sendEmail } from "../nodemailer";
import { PUBLIC_APP_BASE_URL } from "$env/static/public";
import type { EventUpdatedProps } from "../../../routes/api/send-event-updated/+server";

interface EmailAcceptedProps extends EventUpdatedProps {
to: string;
}

export const sendEmailAccepted = async (props: EmailAcceptedProps) => {
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,
}: EmailAcceptedProps) => {
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.ACCEPTED,
role: ICalAttendeeRole.REQ,
},
],
organizer: {
name: organiser === "Alle" ? "Capra Gruppen" : organiser,
email: "[email protected]",
},
});

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

1 comment on commit 55097df

@vercel
Copy link

@vercel vercel bot commented on 55097df Jan 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.