Skip to content

Commit 2c0be87

Browse files
committed
feat: cron created, enrolment amends, schema amends and email created
1 parent 2e7183a commit 2c0be87

File tree

14 files changed

+406
-96
lines changed

14 files changed

+406
-96
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AddForeignKey
2+
ALTER TABLE "StudyPathGoal" ADD CONSTRAINT "StudyPathGoal_userUid_studyPathUid_fkey" FOREIGN KEY ("userUid", "studyPathUid") REFERENCES "UserStudyPath"("userUid", "studyPathUid") ON DELETE CASCADE ON UPDATE CASCADE;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "StudyPathGoal" ADD COLUMN "userStudyPathUid" TEXT;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- DropForeignKey
2+
ALTER TABLE "StudyPathGoal" DROP CONSTRAINT "StudyPathGoal_userUid_studyPathUid_fkey";
3+
4+
-- AddForeignKey
5+
ALTER TABLE "StudyPathGoal" ADD CONSTRAINT "StudyPathGoal_userStudyPathUid_fkey" FOREIGN KEY ("userStudyPathUid") REFERENCES "UserStudyPath"("uid") ON DELETE CASCADE ON UPDATE CASCADE;

prisma/schema/study-path.prisma

+5
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ model UserStudyPath {
2828
enrolledAt DateTime @default(now())
2929
completedAt DateTime?
3030
progress Float @default(0)
31+
3132
studyPath StudyPath @relation(fields: [studyPathUid], references: [uid], onDelete: Cascade)
3233
user Users @relation(fields: [userUid], references: [uid], onDelete: Cascade)
34+
StudyPathGoal StudyPathGoal[]
3335
3436
@@unique([userUid, studyPathUid])
3537
}
@@ -53,11 +55,14 @@ model StudyPathGoal {
5355
5456
userUid String
5557
studyPathUid String
58+
userStudyPathUid String?
5659
5760
// the study path the goal is for
5861
studyPath StudyPath @relation(fields: [studyPathUid], references: [uid], onDelete: Cascade)
5962
// the user the goal is for
6063
user Users @relation(fields: [userUid], references: [uid], onDelete: Cascade)
64+
// the users study path enrollment
65+
userStudyPath UserStudyPath? @relation(fields: [userStudyPathUid], references: [uid], onDelete: Cascade)
6166
6267
@@unique([userUid, studyPathUid])
6368
}

src/actions/misc/send-invite.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { resend } from '@/lib/resend';
44
import { getUser } from '../user/authed/get-user';
5-
import ReferralEmail from '@/components/templates/referral';
5+
import ReferralEmail from '@/components/emails/referral';
66
import { renderAsync } from '@react-email/components';
77
import React from 'react';
88
import { getUserMissionRecords } from '@/utils/data/missions/get-user-mission-record';

src/actions/study-paths/set-goal.ts

+12-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
'use server';
22
import { prisma } from '@/lib/prisma';
33
import { getUser } from '../user/authed/get-user';
4-
import { isUserEnrolledInStudyPath } from '@/utils/data/study-paths/get';
4+
import { getUserStudyPaths, isUserEnrolledInStudyPath } from '@/utils/data/study-paths/get';
55
import { enrollInStudyPath } from './enroll';
6+
import { UserStudyPath } from '@prisma/client';
67

78
export const setUserStudyPathGoal = async (studyPathUid: string, goal: Date) => {
89
const user = await getUser();
@@ -18,36 +19,40 @@ export const setUserStudyPathGoal = async (studyPathUid: string, goal: Date) =>
1819
// check if the user has already set a goal for this study path
1920
const existingGoal = await prisma.studyPathGoal.findFirst({
2021
where: {
21-
studyPathUid,
2222
userUid: user.uid,
23+
studyPathUid: studyPathUid,
24+
AND: {
25+
completed: false,
26+
},
2327
},
2428
});
2529

2630
if (existingGoal) {
2731
return {
2832
success: false,
29-
error: 'Goal already set',
33+
error: 'You already have a goal set. Complete it before setting a new one.',
3034
};
3135
}
3236

3337
// now check if the users is enrolled in the study path
34-
const existingEnrollment = await isUserEnrolledInStudyPath(studyPathUid);
38+
let existingEnrollment = await isUserEnrolledInStudyPath(studyPathUid);
3539

3640
// if the user is not enrolled in the study path, enroll them
3741
if (!existingEnrollment) {
38-
await enrollInStudyPath(studyPathUid);
42+
existingEnrollment = await enrollInStudyPath(studyPathUid);
3943
}
4044

4145
// now set the goal
4246
await prisma.studyPathGoal.create({
4347
data: {
44-
studyPathUid,
45-
userUid: user.uid,
4648
dateSet: new Date(),
4749
targetDate: goal,
4850
completed: false,
4951
completedAt: null,
5052
isActive: true,
53+
userUid: user.uid,
54+
studyPathUid: studyPathUid,
55+
userStudyPathUid: existingEnrollment?.uid,
5156
},
5257
});
5358

src/app/api/cron/challenges/send-suggested-challenge/route.ts

+25-21
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SuggestedChallengeEmailTemplate } from '@/components/templates/daily-challenge';
1+
import { SuggestedChallengeEmailTemplate } from '@/components/emails/daily-challenge';
22
import { prisma } from '@/lib/prisma';
33
import { resend } from '@/lib/resend';
44
import { QuestionWithTags } from '@/types/Questions';
@@ -14,6 +14,8 @@ import { type NextRequest, NextResponse } from 'next/server';
1414
import React from 'react';
1515

1616
export const dynamic = 'force-dynamic';
17+
export const runtime = 'nodejs';
18+
export const maxDuration = 300; // 5 minutes
1719

1820
const getRandomElement = (arr: string[]) => arr[Math.floor(Math.random() * arr.length)];
1921

@@ -53,7 +55,7 @@ async function sendEmail(user: UserRecord, challenge: QuestionWithTags) {
5355
});
5456
}
5557

56-
export async function GET(request: NextRequest) {
58+
export async function POST(request: NextRequest) {
5759
const authHeader = request.headers.get('authorization');
5860
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
5961
return new Response('Unauthorized', {
@@ -64,25 +66,26 @@ export async function GET(request: NextRequest) {
6466
console.log('Sending daily challenge email');
6567

6668
try {
67-
const users = await prisma.users.findMany({
68-
where: {
69-
sendPushNotifications: true,
70-
},
71-
});
72-
73-
if (!users.length) {
74-
return NextResponse.json({ message: 'No users found' }, { status: 404 });
75-
}
76-
77-
// Process users in batches of 5 to avoid connection pool exhaustion
78-
const batchSize = 5;
69+
// Process users in larger batches with pagination
70+
const batchSize = 50; // Increased from 5 to 50
7971
const results = [];
80-
81-
for (let i = 0; i < users.length; i += batchSize) {
82-
const batch = users.slice(i, i + batchSize);
72+
let skip = 0;
73+
74+
while (true) {
75+
const users = await prisma.users.findMany({
76+
where: {
77+
sendPushNotifications: true,
78+
},
79+
take: batchSize,
80+
skip,
81+
});
82+
83+
if (!users.length) {
84+
break;
85+
}
8386

8487
const batchResults = await Promise.allSettled(
85-
batch.map(async (user) => {
88+
users.map(async (user) => {
8689
try {
8790
const challenge = await getChallenge(user.uid);
8891
if (!challenge) {
@@ -103,10 +106,11 @@ export async function GET(request: NextRequest) {
103106
);
104107

105108
results.push(...batchResults);
109+
skip += batchSize;
106110

107-
// Add a small delay between batches to allow connections to be released
108-
if (i + batchSize < users.length) {
109-
await new Promise((resolve) => setTimeout(resolve, 1000));
111+
// Add a smaller delay between batches to prevent rate limiting
112+
if (skip < (await prisma.users.count({ where: { sendPushNotifications: true } }))) {
113+
await new Promise((resolve) => setTimeout(resolve, 100));
110114
}
111115
}
112116

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { prisma } from '@/lib/prisma';
2+
import { isAuthorized } from '@/utils/cron';
3+
import { NextRequest, NextResponse } from 'next/server';
4+
import { resend } from '@/lib/resend';
5+
import { renderAsync } from '@react-email/components';
6+
import StudyReminderEmail from '@/components/emails/study-reminder';
7+
import React from 'react';
8+
import { UserRecord } from '@/types/User';
9+
import { StudyPath, StudyPathGoal, UserStudyPath } from '@prisma/client';
10+
import { getUserDisplayName } from '@/utils/user';
11+
12+
export const dynamic = 'force-dynamic';
13+
export const runtime = 'nodejs';
14+
export const maxDuration = 300; // 5 minutes
15+
16+
type GoalWithRelations = StudyPathGoal & {
17+
studyPath: StudyPath;
18+
user: UserRecord;
19+
userStudyPath: UserStudyPath;
20+
};
21+
22+
async function SendEmail(goal: GoalWithRelations) {
23+
const displayName = getUserDisplayName(goal.user);
24+
const subject = `TechBlitz - Daily Goal Reminder`;
25+
const link = `${process.env.NEXT_PUBLIC_URL}/roadmap/${goal.studyPathUid}`;
26+
27+
// calculate the days remaining by comparing the target date to the current date
28+
const daysRemaining = Math.ceil(
29+
(goal.targetDate.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)
30+
);
31+
32+
const html = await renderAsync(
33+
React.createElement(StudyReminderEmail, {
34+
userName: displayName,
35+
studyPathTitle: goal.studyPath.title,
36+
goalDate: goal.targetDate.toLocaleDateString(),
37+
progressPercentage: goal.userStudyPath.progress.toFixed(2),
38+
daysRemaining,
39+
link,
40+
})
41+
);
42+
43+
await resend.emails.send({
44+
from: 'TechBlitz <team@techblitz.dev>',
45+
to: goal.user.email,
46+
subject,
47+
html,
48+
replyTo: 'team@techblitz.dev',
49+
});
50+
}
51+
52+
export async function GET(request: NextRequest) {
53+
if (!isAuthorized(request)) {
54+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
55+
}
56+
57+
// get all users who have a goal
58+
const goals = await prisma.studyPathGoal.findMany({
59+
where: {
60+
targetDate: {
61+
gt: new Date(),
62+
},
63+
AND: {
64+
completed: false,
65+
},
66+
},
67+
include: {
68+
user: true,
69+
studyPath: true,
70+
userStudyPath: true,
71+
},
72+
});
73+
74+
// loop through all users and send a reminder email
75+
for (const goal of goals) {
76+
await SendEmail(goal);
77+
}
78+
79+
return NextResponse.json({ message: 'Reminders sent' }, { status: 200 });
80+
}
File renamed without changes.

0 commit comments

Comments
 (0)