Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: scheduled zoom email #99

Merged
16 changes: 16 additions & 0 deletions .github/workflows/weekly-zoom-reminder.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Weekly Zoom Reminder
on:
schedule:
# Runs at 12:00 PM UTC (noon) every Sunday
- cron: '0 12 * * 0'

jobs:
send-reminders:
runs-on: ubuntu-latest
steps:
- name: Send Zoom Reminder Emails
run: |
curl -X POST \
https://rudra-utkscf-fork.vercel.app/api/emails/actions/send-healthy-habits-zoom-link \
-H "x-api-key: ${{ secrets.API_KEY }}" \
-H "Content-Type: application/json"
55 changes: 55 additions & 0 deletions src/app/api/emails/actions/send-healthy-habits-zoom-link/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { sendZoomReminderEmail } from "@/server/api/emails/private-mutations";
import { getUsersByProgram } from "@/server/api/users/private-mutations";

export async function POST(request: Request) {
try {
const apiKeyHeader = request.headers.get("x-api-key");

if (!apiKeyHeader || apiKeyHeader !== process.env.API_KEY) {
return new Response(
JSON.stringify({ success: false, error: "Invalid request." }),
{
status: 401,
headers: { "Content-Type": "application/json" },
},
);
}

const programName = "Healthy Habits For The Long Haul";
const [users, usersError] = await getUsersByProgram(programName);

if (usersError !== null) {
return new Response(
JSON.stringify({ success: false, error: "Internal server error." }),
{
status: 500,
headers: { "Content-Type": "application/json" },
},
);
}

await Promise.all(
users.map((user) =>
sendZoomReminderEmail(
"Healthy Habits For The Long Haul",
"https://google.com",
user.email,
),
),
);

return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error(error);
return new Response(
JSON.stringify({ success: false, error: "Internal server error." }),
{
status: 500,
headers: { "Content-Type": "application/json" },
},
);
}
}
120 changes: 120 additions & 0 deletions src/components/emails/ZoomReminderEmail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import {
Body,
Button,
Container,
Head,
Hr,
Html,
Img,
Link,
Preview,
Section,
Text,
} from "@react-email/components";

import imgurLogoImageUrl from "@/utils/constants/imgurLogoImageUrl";

type ZoomReminderEmailProps = {
meetingName?: string;
meetingLink?: string;
};

export default function ZoomReminderEmail({
meetingName = "your SCF program",
meetingLink = "https://www.google.com",
}: ZoomReminderEmailProps) {
return (
<Html>
<Head />
<Preview>Reminder: You have an upcoming {meetingName} meeting</Preview>
<Body style={main}>
<Container style={container}>
<Section style={box}>
<Img
src={imgurLogoImageUrl}
width="128"
height="128"
alt="St. Christopher Truckers Relief Fund"
style={image}
/>
<Text style={paragraph}>
Please click the link below to join the zoom call for{" "}
{meetingName}.
</Text>
<Button style={button} href={meetingLink}>
Join Meeting
</Button>
<Text style={paragraph}>
If you have any questions or concerns, please reach out to us at
our{" "}
<Link style={anchor} href="https://truckersfund.org/contact-us">
contact page.
</Link>
</Text>
<Hr style={hr} />
<Text style={footer}>
St. Christopher Truckers Relief Fund, Phone: (865) 202-9428
</Text>
</Section>
</Container>
</Body>
</Html>
);
}

const main = {
backgroundColor: "#f6f9fc",
fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
};

const container = {
backgroundColor: "#ffffff",
margin: "0 auto",
padding: "20px 0 48px",
marginBottom: "64px",
marginTop: "64px",
};

const box = {
padding: "0 48px",
};

const hr = {
borderColor: "#e6ebf1",
margin: "20px 0",
};

const paragraph = {
color: "#525f7f",
fontSize: "16px",
lineHeight: "24px",
textAlign: "left" as const,
};

const anchor = {
color: "#556cd6",
};

const button = {
backgroundColor: "#183766",
borderRadius: "5px",
color: "#fff",
fontSize: "16px",
fontWeight: "bold",
textDecoration: "none",
textAlign: "center" as const,
display: "block",
width: "100%",
padding: "10px",
};

const footer = {
color: "#8898aa",
fontSize: "12px",
lineHeight: "16px",
};

const image = {
margin: "0 auto",
};
17 changes: 17 additions & 0 deletions src/server/api/emails/private-mutations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import PasswordChangedEmail from "@/components/emails/PasswordChangedEmail";
import RejectionEmail from "@/components/emails/RejectionEmail";
import ResetPasswordEmail from "@/components/emails/ResetPasswordEmail";
import WelcomeEmail from "@/components/emails/WelcomeEmail";
import ZoomReminderEmail from "@/components/emails/ZoomReminderEmail";

import sendEmail from "./helpers";

Expand Down Expand Up @@ -62,3 +63,19 @@ export async function sendEmailVerificationEmail(

await sendEmail(recipientEmail, "Verify your SCF email address", html);
}

export async function sendZoomReminderEmail(
meetingName: string,
meetingLink: string,
recipientEmail: string,
) {
const html = await render(
<ZoomReminderEmail meetingName={meetingName} meetingLink={meetingLink} />,
);

await sendEmail(
recipientEmail,
`Reminder: You have an upcoming meeting for ${meetingName}.`,
html,
);
}
36 changes: 34 additions & 2 deletions src/server/api/users/private-mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ import mongoose from "mongoose";

import { AdminUserRequest } from "@/app/api/users/actions/create-admin-account/route";
import dbConnect from "@/server/dbConnect";
import { UserModel } from "@/server/models";
import { AdminUser, ApiResponse, ClientUser, User } from "@/types";
import { ProgramEnrollmentModel, UserModel } from "@/server/models";
import {
AdminUser,
ApiResponse,
ClientUser,
ProgramEnrollment,
User,
} from "@/types";
import authenticateServerFunction from "@/utils/authenticateServerFunction";
import apiErrors from "@/utils/constants/apiErrors";
import dayjsUtil from "@/utils/dayjsUtil";
Expand Down Expand Up @@ -138,3 +144,29 @@ export async function changePassword(

return [null, null];
}

export async function getUsersByProgram(
programName: string,
): Promise<ApiResponse<User[]>> {
await dbConnect();

try {
// Find program enrollments for the given program and populate the associated users
const enrollments = await ProgramEnrollmentModel.find({
program: programName,
status: "accepted",
})
.populate("user")
.lean()
.exec();

const users = enrollments.map(
(enrollment: ProgramEnrollment) => enrollment.user,
);

return [users, null];
} catch (error) {
console.error(error);
return [null, "Error fetching users by program."];
}
}
Loading