Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"date-fns": "^3.6.0",
"date-fns-tz": "^3.1.3",
"db": "workspace:*",
"email": "workspace:*",
"dotenv": "^16.4.5",
"drizzle-orm": "^0.39.3",
"drizzle-zod": "^0.7.0",
Expand Down
16 changes: 16 additions & 0 deletions apps/web/src/actions/registration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
UNIQUE_KEY_CONSTRAINT_VIOLATION_CODE,
UNIQUE_KEY_MAPPER_DEFAULT_KEY,
} from "@/lib/constants";
import { sendRegistrationEmail } from "email/sender";

const registerUserSchema = hackerRegistrationFormValidator;

Expand Down Expand Up @@ -81,6 +82,21 @@ export const registerHacker = authenticatedAction
hasSharedDataWithMLH,
isEmailable,
});

const emailSendSuccess = await sendRegistrationEmail({
subject: `Thanks for registering for ${c.hackathonName}`,
to: email,
body: {
email,
firstName: userData.firstName,
lastName: userData.lastName,
hackerTag,
},
});

if (!emailSendSuccess) {
console.log("Unable to send email to " + email);
}
});
} catch (e) {
// Catch duplicates because they will be based off of the error code 23505
Expand Down
150 changes: 150 additions & 0 deletions packages/email/emails/test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import {
Body,
Button,
Column,
Container,
Head,
Heading,
Hr,
Html,
Img,
Link,
Preview,
Row,
Section,
Tailwind,
Text,
} from "@react-email/components";
import * as React from "react";

interface VercelInviteUserEmailProps {
username?: string;
userImage?: string;
invitedByUsername?: string;
invitedByEmail?: string;
teamName?: string;
teamImage?: string;
inviteLink?: string;
inviteFromIp?: string;
inviteFromLocation?: string;
}

const baseUrl = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: "";

export const VercelInviteUserEmail = ({
username = "zenorocha",
userImage = `${baseUrl}/static/vercel-user.png`,
invitedByUsername = "bukinoshita",
invitedByEmail = "[email protected]",
teamName = "My Project",
teamImage = `${baseUrl}/static/vercel-team.png`,
inviteLink = "https://vercel.com/teams/invite/foo",
inviteFromIp = "204.13.186.218",
inviteFromLocation = "São Paulo, Brazil",
}: VercelInviteUserEmailProps) => {
const previewText = `Join ${invitedByUsername} on Vercel`;

return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind>
<Body className="mx-auto my-auto bg-white font-sans">
<Container className="mx-auto my-[40px] w-[465px] rounded border border-solid border-[#eaeaea] p-[20px]">
<Section className="mt-[32px]">
<Img
src={`${baseUrl}/static/vercel-logo.png`}
width="40"
height="37"
alt="Vercel"
className="mx-auto my-0"
/>
</Section>
<Heading className="mx-0 my-[30px] p-0 text-center text-[24px] font-normal text-black">
Join <strong>{teamName}</strong> on{" "}
<strong>Vercel</strong>
</Heading>
<Text className="text-[14px] leading-[24px] text-black">
Hello {username},
</Text>
<Text className="text-[14px] leading-[24px] text-black">
<strong>bukinoshita</strong> (
<Link
href={`mailto:${invitedByEmail}`}
className="text-blue-600 no-underline"
>
{invitedByEmail}
</Link>
) has invited you to the <strong>{teamName}</strong>{" "}
team on <strong>Vercel</strong>.
</Text>
<Section>
<Row>
<Column align="right">
<Img
className="rounded-full"
src={userImage}
width="64"
height="64"
/>
</Column>
<Column align="center">
<Img
src={`${baseUrl}/static/vercel-arrow.png`}
width="12"
height="9"
alt="invited you to"
/>
</Column>
<Column align="left">
<Img
className="rounded-full"
src={teamImage}
width="64"
height="64"
/>
</Column>
</Row>
</Section>
<Section className="mb-[32px] mt-[32px] text-center">
<Button
className="rounded bg-[#000000] px-4 py-3 text-center text-[12px] font-semibold text-white no-underline"
href={inviteLink}
>
Join the team
</Button>
</Section>
<Text className="text-[14px] leading-[24px] text-black">
or copy and paste this URL into your browser:{" "}
<Link
href={inviteLink}
className="text-blue-600 no-underline"
>
{inviteLink}
</Link>
</Text>
<Hr className="mx-0 my-[26px] w-full border border-solid border-[#eaeaea]" />
<Text className="text-[12px] leading-[24px] text-[#666666]">
This invitation was intended for{" "}
<span className="text-black">{username} </span>.This
invite was sent from{" "}
<span className="text-black">{inviteFromIp}</span>{" "}
located in{" "}
<span className="text-black">
{inviteFromLocation}
</span>
. If you were not expecting this invitation, you can
ignore this email. If you are concerned about your
account's safety, please reply to this email to get
in touch with us.
</Text>
</Container>
</Body>
</Tailwind>
</Html>
);
};

export default VercelInviteUserEmail;
27 changes: 27 additions & 0 deletions packages/email/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "email",
"version": "1.0.0",
"description": "",
"scripts": {
"email-dev": "email dev --dir ./templates --port 3001",
"email-export": "email export --dir ./templates",
"send-test-email": "ts-node scripts/send-test-email.tsx"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@plunk/node": "^3.0.3",
"@react-email/components": "^0.0.25",
"@react-email/render": "^1.0.4",
"@types/node": "20.14.11",
"@types/react": "18.3.3",
"config": "workspace:*",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"react-email": "^3.0.6",
"ts-node": "^10.9.2"
}
}
34 changes: 34 additions & 0 deletions packages/email/scripts/send-test-email.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { createInterface } from "readline";
import { sendRegistrationEmail } from "../sender";

const reader = createInterface({
input: process.stdin,
output: process.stdout,
});

reader.question(
"Enter the email address you would like to recieve the email: ",
async (email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

if (!emailRegex.test(email)) {
reader.write("Invalid email address entered.\n");
reader.close();
return;
}

await sendRegistrationEmail({
to: email,
subject: "Test Email",
body: {
email,
firstName: "Someone",
lastName: "Important",
hackerTag: "jdoe",
},
});

reader.write(`Email sent check the email address: ${email}.\n`);
reader.close();
},
);
15 changes: 15 additions & 0 deletions packages/email/sender/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Plunk from "@plunk/node";

if (!process.env.PLUNK_API_URL || !process.env.PLUNK_API_KEY) {
console.warn(
"Plunk API information is not defined... Did you add the relevant environment variables to the project?",
);
}

export const plunk = new Plunk(process.env.PLUNK_API_KEY as string, {
baseUrl: process.env.PLUNK_API_URL as string,
});

export { render } from "@react-email/render";

export * from "./utils";
30 changes: 30 additions & 0 deletions packages/email/sender/utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { RegistrationSuccessEmail } from "../templates/registration";
import { type ComponentProps } from "react";
import { plunk, render } from ".";

type RegistrationEmailBody = ComponentProps<typeof RegistrationSuccessEmail>;

interface SendEmailParams<T> {
body: T;
to: string;
subject: string;
}

export async function sendRegistrationEmail({
body,
to,
subject,
}: SendEmailParams<RegistrationEmailBody>) {
const renderedBody = await render(<RegistrationSuccessEmail {...body} />);
let success = true;

await plunk.emails
.send({
to,
subject,
body: renderedBody,
})
.catch(() => (success = false));

return success;
}
Loading