Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5ed1194
test
christianhelp Apr 7, 2025
56fc866
updates gitignore
christianhelp Apr 8, 2025
06f5e0b
updates config for emails
christianhelp Apr 8, 2025
1a4ef28
adds plunk and react email
christianhelp Apr 8, 2025
b7e20fc
adds registration confirmation
christianhelp Apr 8, 2025
ed87960
adds account connection email
christianhelp Apr 8, 2025
8274051
updates env
christianhelp Apr 8, 2025
33ae714
adds email functionality
christianhelp Apr 8, 2025
8cc2cb1
updates edit
christianhelp Apr 8, 2025
e23a0a3
formatter
christianhelp Apr 8, 2025
4e5687d
updates data cleaning
christianhelp Apr 8, 2025
cabcc66
updates email components
christianhelp Apr 8, 2025
0177218
refactor emails to own package
christianhelp Apr 9, 2025
72f4640
fixes imports
christianhelp Apr 9, 2025
0739f44
updates component styling
christianhelp Apr 9, 2025
f2d4ffc
formatter
christianhelp Apr 9, 2025
a03696d
updates shared
christianhelp Apr 9, 2025
cee109d
updates email format
christianhelp Apr 10, 2025
94e4061
update styling
christianhelp Apr 10, 2025
b7a9c39
reformat
christianhelp Apr 10, 2025
aac60bc
updates config
christianhelp Apr 10, 2025
2a84441
formatter
christianhelp Apr 10, 2025
b4504e2
updates wording
christianhelp Apr 10, 2025
f1d4f50
update wording
christianhelp Apr 10, 2025
5f62581
adds try-catch
christianhelp Apr 10, 2025
fc34411
reword
christianhelp Apr 10, 2025
a03acfc
updates config
christianhelp Apr 10, 2025
eb34f3d
use useplunk
christianhelp Apr 10, 2025
6ab4c52
updates env
christianhelp Apr 14, 2025
bfca91c
prettier
christianhelp Apr 14, 2025
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,6 @@ yarn-error.log*
# vscode
.vscode
.env*.local

# infisical
.infisical.json
6 changes: 5 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"with-env": "dotenv -e ../../.env --",
"pages:build": "pnpm dlx @cloudflare/next-on-pages",
"preview": "pnpm run pages:build && wrangler pages dev",
"deploy": "pnpm run pages:build && wrangler pages deploy"
"deploy": "pnpm run pages:build && wrangler pages deploy",
"email-dev": "email dev --dir ./src/components/emails --port 3001"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.758.0",
Expand All @@ -35,6 +36,7 @@
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.1.0",
"@react-email/components": "0.0.36",
"@t3-oss/env-nextjs": "^0.10.1",
"@tanstack/match-sorter-utils": "^8.15.1",
"@tanstack/react-table": "^8.17.3",
Expand All @@ -48,6 +50,7 @@
"db": "workspace:*",
"dotenv": "^16.3.1",
"dotenv-cli": "^7.4.1",
"emails": "workspace:*",
"jiti": "^1.21.0",
"lucide-react": "^0.377.0",
"nanoid": "^5.0.6",
Expand Down Expand Up @@ -78,6 +81,7 @@
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"postcss": "^8",
"react-email": "4.0.4",
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
Expand Down
40 changes: 40 additions & 0 deletions apps/web/src/actions/email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"use server";
import { authenticatedAction } from "@/lib/safe-action";
import z from "zod";
import { sendEmail } from "emails/utils";

const emailSchema = z
.string({
invalid_type_error: "Email needs to be a string",
required_error: "Email is required",
})
.email({ message: "Invalid email address" })
.transform((e) => e.toLowerCase());

const emailPropsSchema = z.object({
subscribed: z
.boolean({ invalid_type_error: "Subscribed should be a boolean" })
.optional(),
from: emailSchema.optional(),
name: z.string().optional(),
reply: emailSchema.optional(),
to: z
.array(emailSchema)
.max(5, "You can only send transactional emails to 5 people at a time")
.or(emailSchema.transform((e) => [e])),
subject: z.string({
required_error:
"Subject is required. Read more: https://docs.useplunk.com/api-reference/transactional/send",
}),
body: z.string({
required_error:
"Body is required. Read more: https://docs.useplunk.com/api-reference/transactional/send",
}),
headers: z.record(z.string()).optional(),
});

export const sendEmailAction = authenticatedAction
.schema(emailPropsSchema)
.action(async ({ parsedInput }) => {
await sendEmail(parsedInput);
});
36 changes: 31 additions & 5 deletions apps/web/src/actions/register/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ import { db } from "db";
import { and, eq, isNull } from "db/drizzle";
import { users, data } from "db/schema";
import { currentUser } from "@clerk/nextjs/server";
import { AccountConnected } from "emails/components";
import { sendEmail } from "emails/utils";
import c from "config";

export const doPortalLookupCheck = authenticatedAction
.schema(
z.object({ universityID: z.string().min(1), email: z.string().min(1) }),
z.object({
universityID: z.string().min(1).max(255),
email: z.string().min(1).max(255),
}),
)
.action(async ({ parsedInput: { email, universityID } }) => {
const lookup = await db
Expand Down Expand Up @@ -40,18 +45,23 @@ export const doPortalLookupCheck = authenticatedAction

export const doPortalLink = authenticatedAction
.schema(
z.object({ universityID: z.string().min(1), email: z.string().min(1) }),
z.object({
universityID: z.string().min(1).max(255),
email: z.string().min(1).max(255),
}),
)
.action(
async ({ ctx: { clerkID }, parsedInput: { email, universityID } }) => {
const emailLower = email.toLowerCase();
const universityIDLower = universityID.toLowerCase();
const lookup = await db
.select()
.from(users)
.where(
and(
eq(users.email, email.toLowerCase()),
eq(users.email, emailLower),
isNull(users.clerkID),
eq(users.universityID, universityID.toLowerCase()),
eq(users.universityID, universityIDLower),
),
)
.limit(1);
Expand All @@ -65,8 +75,24 @@ export const doPortalLink = authenticatedAction
if (lookup[0]) {
await db
.update(users)
.set({ clerkID, email: userEmail })
.set({
clerkID,
email: userEmail,
universityID: universityIDLower,
})
.where(eq(users.userID, lookup[0].userID));
try {
await sendEmail({
to: userEmail,
subject: `Welcome back to ${c.clubName}!`,
name: `${c.universityName} ${c.clubName}`,
body: AccountConnected({
firstName: lookup[0].firstName,
}),
});
} catch (e) {
console.error("Error sending email:", e);
}
return {
success: true,
};
Expand Down
17 changes: 17 additions & 0 deletions apps/web/src/actions/register/new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { insertUserWithDataSchemaFormified } from "db/zod";
import { db } from "db";
import { eq, or } from "db/drizzle";
import { users, data } from "db/schema";
import { RegistrationConfirmation } from "emails/components";
import { sendEmail } from "emails/utils";
import c from "config";

export const createRegistration = authenticatedAction
.schema(insertUserWithDataSchemaFormified)
Expand Down Expand Up @@ -65,6 +68,20 @@ export const createRegistration = authenticatedAction
});
});

try {
console.log("Sending email to: ", lowerCasedEmail);
await sendEmail({
to: lowerCasedEmail,
subject: `Welcome to ${c.clubName}!`,
name: `${c.universityName} ${c.clubName}`,
body: RegistrationConfirmation({
firstName: usersSchemaInputs.firstName,
}),
});
} catch (e) {
console.log("Error sending email: ", e);
}

return {
success: true,
code: "success",
Expand Down
17 changes: 8 additions & 9 deletions apps/web/src/env.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
import { emailsConfig } from "config";

export const env = createEnv({
server: {
CLERK_SECRET_KEY: z.string().min(1),
CLERK_SECRET_KEY: z.string(),
},
client: {
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1),
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string(),
NEXT_PUBLIC_BASE_URL: emailsConfig.useEmailService
? z.string()
: z.string().optional(),
},
// If you're using Next.js < 13.4.4, you'll need to specify the runtimeEnv manually
// runtimeEnv: {
// DATABASE_URL: process.env.DATABASE_URL,
// OPEN_AI_API_KEY: process.env.OPEN_AI_API_KEY,
// NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY,
// },
// For Next.js >= 13.4.4, you only need to destructure client variables:
experimental__runtimeEnv: {
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:
process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL,
},
emptyStringAsUndefined: true,
});
2 changes: 1 addition & 1 deletion apps/web/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"@/*": ["./src/*"]
},
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "../../packages/emails/utils.ts"],
"exclude": ["node_modules"],
"types": [
"@cloudflare/workers-types/2024-07-29"
Expand Down
18 changes: 17 additions & 1 deletion packages/config/clubkit.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const c = {
clubName: "ClubKit",
universityName: "UTSA",
discordLink: "https://go.acmutsa.org/discord",
universityID: {
name: "ABC123",
maxLength: 6,
Expand Down Expand Up @@ -101,6 +102,15 @@ const c = {
memberRoles: ["member", "admin", "super_admin"] as const,
} as const;

const emailsConfig = {
useEmailService: true,
//If self hosted, the api route should look like: https://base_url_hosted/api/v1/ - Do NOT omit the final forward slash or it will not be included
isSelfHosted: true,
rightsReservedString: `© Association of Computing Machinery at UTSA 2015 -
${new Date().getFullYear()}. All Rights Reserved.`,
publicLogoLink: "https://static.acmutsa.org/acm-logo.png",
};

export const defaultTheme = "light";

const bucketBaseUrl = `${c.clubName}-${c.universityName}`;
Expand Down Expand Up @@ -228,4 +238,10 @@ const staticUploads = {
} as const;

export default c;
export { majors, staticUploads, bucketEventThumbnailBaseUrl, bucketBaseUrl };
export {
majors,
staticUploads,
bucketEventThumbnailBaseUrl,
bucketBaseUrl,
emailsConfig,
};
2 changes: 2 additions & 0 deletions packages/emails/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as AccountConnected } from "./templates/AccountConnected";
export { default as RegistrationConfirmation } from "./templates/RegistrationConfirmation";
116 changes: 116 additions & 0 deletions packages/emails/components/templates/AccountConnected.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import {
Body,
Button,
Column,
Container,
Head,
Heading,
Html,
Img,
Link,
Preview,
Row,
Section,
Tailwind,
Text,
} from "@react-email/components";
import type * as React from "react";
import c, { emailsConfig } from "config";
import { DefaultFooter, DefaultHeader, baseUrl } from "./shared";

export default function AccountConnected({ firstName }: { firstName: string }) {
return (
<Html>
<Head />
<Tailwind
config={{
theme: {
extend: {
colors: {
brand: "#2250f4",
offwhite: "#fafbfb",
},
spacing: {
0: "0px",
20: "20px",
45: "45px",
},
},
},
}}
>
<Preview>{`Welcome back to ${c.universityName} ${c.clubName}. Thanks for connecting your account. We are
glad to have you back with us. Things have
changed since you were last here. Lots of
updates and enhancements that we hope you
will enjoy.`}</Preview>
<Body className="bg-offwhite font-sans text-base">
<DefaultHeader />
<Container className="p-45 bg-white">
<Heading className="my-0 text-center leading-8">
{`Welcome back to ${c.universityName} ${c.clubName}`}
</Heading>

<Section className="pt-5">
<Row>
<Text className="text-base">
{`Hi ${firstName},`}
</Text>
<Text className="text-base">
Thanks for connecting your account. We are
glad to have you back with us. Things have
changed since you were last here. Lots of
updates and enhancements that we hope you
will enjoy.
</Text>
<Text className="mt-4 text-base">
Here's how to get started:
</Text>
</Row>
</Section>
<ul className="mt-0 pt-0">
<li className="mb-20">
<strong>
Go update your account information.{" "}
</strong>
We know it might have been a bit difficult to
update all of your information in the previous
iteration, but we have made it much easier for
you to do so now. Click the link to{" "}
<Link href={`${baseUrl}/settings`}>
update your settings.
</Link>
</li>
<li className="mb-20">
<strong>Check out our upcoming events. </strong>
{`${c.clubName} is always hosting cool events that give you an opportunity to come learn, eat snacks, and meet new people so be sure to `}
<Link href={`${baseUrl}/events`}>
check out what is happening soon.
</Link>
</li>
<li className="mb-20">
<strong>Join our discord. </strong>
We are always chatting and sharing cool stuff
there. You can also talk with our officers and
ask any questions you may have.{" "}
<Link href={`${c.discordLink}`}>
{" "}
Click the here to join.
</Link>
</li>
</ul>
<Section className="mt-10 text-center">
<Button
className="bg-brand rounded-lg px-[18px] py-3 text-white"
href={`${baseUrl}/dash`}
>
Go to your dashboard
</Button>
</Section>
</Container>
<DefaultFooter />
</Body>
</Tailwind>
</Html>
);
}
Loading