Skip to content

Commit d49f68d

Browse files
feat: streamline course enrollment flow with status indicators and CTAs (#849)
Enrollment API Enhancements: - Extended /api/enrollment/enroll to support GET requests - Fetch user enrollments with course details - Order enrollments by most recent first Courses Index Page Updates: - Added enrollment status fetching on page load - Display "Enrolled" badge on enrolled verticals - Dynamic button text ("Continue Learning" vs "View Vertical") - Vertical-to-course mapping for enrollment checking - Support for Software Engineering, Data Engineering, and AI Engineering verticals Individual Course Page Updates (software-engineering.tsx): - Fetch and display enrollment status - Show "You're enrolled" badge when user is enrolled - Display "Enroll in Vertical" CTA when not enrolled - Placeholder enrollment handler (to be configured with actual course IDs) - Real-time enrollment state management Student Benefits: - Clear visibility into enrollment status across all verticals - Streamlined navigation with contextual CTAs - Consistent enrollment experience across course pages - Foundation for future enrollment automation
1 parent bd22d37 commit d49f68d

File tree

4 files changed

+221
-25
lines changed

4 files changed

+221
-25
lines changed

src/pages/api/enrollment/enroll.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,51 @@ import { requireAuth, AuthenticatedRequest } from '@/lib/rbac';
33
import prisma from '@/lib/prisma';
44

55
/**
6-
* POST /api/enrollment/enroll
6+
* GET /api/enrollment/enroll
7+
* Fetch user's enrollments
78
*
9+
* POST /api/enrollment/enroll
810
* Enroll user in a course
911
* Body: { courseId: string }
1012
*/
1113
export default requireAuth(async (req: AuthenticatedRequest, res: NextApiResponse) => {
14+
const userId = req.user!.id;
15+
16+
// GET - Fetch user's enrollments
17+
if (req.method === 'GET') {
18+
try {
19+
const enrollments = await prisma.enrollment.findMany({
20+
where: { userId },
21+
include: {
22+
course: {
23+
select: {
24+
id: true,
25+
title: true,
26+
description: true,
27+
imageUrl: true,
28+
difficulty: true,
29+
category: true,
30+
},
31+
},
32+
},
33+
orderBy: {
34+
enrolledAt: 'desc',
35+
},
36+
});
37+
38+
return res.status(200).json({ enrollments });
39+
} catch (error) {
40+
console.error('Error fetching enrollments:', error);
41+
return res.status(500).json({ error: 'Failed to fetch enrollments' });
42+
}
43+
}
44+
1245
if (req.method !== 'POST') {
1346
return res.status(405).json({ error: 'Method not allowed' });
1447
}
1548

1649
try {
1750
const { courseId } = req.body;
18-
const userId = req.user!.id;
1951

2052
// Validation
2153
if (!courseId) {

src/pages/courses/index.tsx

Lines changed: 108 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from "react";
1+
import React, { useEffect, useState } from "react";
22
import Link from "next/link";
33
import Layout01 from "@layout/layout-01";
44
import type { GetServerSideProps, NextPage } from "next";
@@ -7,6 +7,13 @@ import { options } from "@/pages/api/auth/options";
77
import SEO from "@components/seo/page-seo";
88
import Breadcrumb from "@components/breadcrumb";
99

10+
type Enrollment = {
11+
id: string;
12+
courseId: string;
13+
status: string;
14+
progress: number;
15+
};
16+
1017
type PageProps = {
1118
user: {
1219
id: string;
@@ -27,6 +34,49 @@ type PageWithLayout = NextPage<PageProps> & {
2734
};
2835

2936
const CoursesIndex: PageWithLayout = ({ user }) => {
37+
const [enrollments, setEnrollments] = useState<Enrollment[]>([]);
38+
const [loading, setLoading] = useState(true);
39+
40+
useEffect(() => {
41+
fetchEnrollments();
42+
}, []);
43+
44+
const fetchEnrollments = async () => {
45+
try {
46+
setLoading(true);
47+
const response = await fetch("/api/enrollment/enroll");
48+
const data = await response.json();
49+
50+
if (response.ok) {
51+
setEnrollments(data.enrollments);
52+
}
53+
} catch (error) {
54+
console.error("Error fetching enrollments:", error);
55+
} finally {
56+
setLoading(false);
57+
}
58+
};
59+
60+
// Helper function to check if user is enrolled in a vertical
61+
// Map vertical slugs to course titles for enrollment checking
62+
const verticalCourseMap: Record<string, string[]> = {
63+
"software-engineering": ["Software Engineering", "Full-Stack Development", "Web Development"],
64+
"data-engineering": ["Data Engineering", "Data Science"],
65+
"ai-engineering": ["AI Engineering", "Machine Learning", "Artificial Intelligence"],
66+
"devops": ["DevOps", "Cloud Engineering"],
67+
"web-development": ["Web Development", "Frontend Development"],
68+
};
69+
70+
const isEnrolledInVertical = (verticalSlug: string): boolean => {
71+
const matchingTitles = verticalCourseMap[verticalSlug] || [];
72+
return enrollments.some(
73+
(enrollment) =>
74+
enrollment.status === "ACTIVE" &&
75+
matchingTitles.some((title) =>
76+
enrollment.courseId.toLowerCase().includes(title.toLowerCase())
77+
)
78+
);
79+
};
3080

3181
return (
3282
<>
@@ -100,13 +150,21 @@ const CoursesIndex: PageWithLayout = ({ user }) => {
100150
{/* Software Engineering Vertical */}
101151
<div className="tw-group tw-overflow-hidden tw-rounded-xl tw-border tw-border-gray-100 tw-bg-white tw-shadow-lg tw-transition-all tw-duration-300 hover:tw-scale-105 hover:tw-shadow-2xl">
102152
<div className="tw-bg-gradient-to-br tw-from-primary tw-via-primary tw-to-primary/80 tw-p-8">
103-
<div className="tw-mb-6 tw-flex tw-items-center">
104-
<div className="tw-rounded-lg tw-bg-white/20 tw-p-3">
105-
<i className="fas fa-code tw-text-3xl tw-text-white" />
153+
<div className="tw-mb-6 tw-flex tw-items-center tw-justify-between">
154+
<div className="tw-flex tw-items-center">
155+
<div className="tw-rounded-lg tw-bg-white/20 tw-p-3">
156+
<i className="fas fa-code tw-text-3xl tw-text-white" />
157+
</div>
158+
<h3 className="tw-ml-4 tw-text-2xl tw-font-bold tw-text-white">
159+
Software Engineering
160+
</h3>
106161
</div>
107-
<h3 className="tw-ml-4 tw-text-2xl tw-font-bold tw-text-white">
108-
Software Engineering
109-
</h3>
162+
{!loading && isEnrolledInVertical("software-engineering") && (
163+
<span className="tw-rounded-full tw-bg-white/90 tw-px-3 tw-py-1 tw-text-xs tw-font-semibold tw-text-primary">
164+
<i className="fas fa-check-circle tw-mr-1" />
165+
Enrolled
166+
</span>
167+
)}
110168
</div>
111169
<p className="tw-text-lg tw-leading-relaxed tw-text-white/95">
112170
Master full-stack development, system design, and software
@@ -143,7 +201,11 @@ const CoursesIndex: PageWithLayout = ({ user }) => {
143201
href="/courses/software-engineering"
144202
className="tw-group tw-flex tw-w-full tw-items-center tw-justify-center tw-rounded-lg tw-bg-primary tw-px-6 tw-py-4 tw-font-semibold tw-text-white tw-transition-all tw-duration-200 hover:tw-bg-primary/90 hover:tw-shadow-lg"
145203
>
146-
<span>View Vertical</span>
204+
<span>
205+
{!loading && isEnrolledInVertical("software-engineering")
206+
? "Continue Learning"
207+
: "View Vertical"}
208+
</span>
147209
<i className="fas fa-arrow-right tw-ml-2 tw-transition-transform tw-duration-200 group-hover:tw-translate-x-1" />
148210
</Link>
149211
</div>
@@ -152,13 +214,21 @@ const CoursesIndex: PageWithLayout = ({ user }) => {
152214
{/* Data Engineering Vertical */}
153215
<div className="tw-group tw-overflow-hidden tw-rounded-xl tw-border tw-border-gray-100 tw-bg-white tw-shadow-lg tw-transition-all tw-duration-300 hover:tw-scale-105 hover:tw-shadow-2xl">
154216
<div className="tw-bg-gradient-to-br tw-from-secondary tw-via-secondary tw-to-secondary/80 tw-p-8">
155-
<div className="tw-mb-6 tw-flex tw-items-center">
156-
<div className="tw-rounded-lg tw-bg-white/20 tw-p-3">
157-
<i className="fas fa-database tw-text-3xl tw-text-white" />
217+
<div className="tw-mb-6 tw-flex tw-items-center tw-justify-between">
218+
<div className="tw-flex tw-items-center">
219+
<div className="tw-rounded-lg tw-bg-white/20 tw-p-3">
220+
<i className="fas fa-database tw-text-3xl tw-text-white" />
221+
</div>
222+
<h3 className="tw-ml-4 tw-text-2xl tw-font-bold tw-text-white">
223+
Data Engineering
224+
</h3>
158225
</div>
159-
<h3 className="tw-ml-4 tw-text-2xl tw-font-bold tw-text-white">
160-
Data Engineering
161-
</h3>
226+
{!loading && isEnrolledInVertical("data-engineering") && (
227+
<span className="tw-rounded-full tw-bg-white/90 tw-px-3 tw-py-1 tw-text-xs tw-font-semibold tw-text-secondary">
228+
<i className="fas fa-check-circle tw-mr-1" />
229+
Enrolled
230+
</span>
231+
)}
162232
</div>
163233
<p className="tw-text-lg tw-leading-relaxed tw-text-white/95">
164234
Build data pipelines, work with big data technologies, and create
@@ -195,7 +265,11 @@ const CoursesIndex: PageWithLayout = ({ user }) => {
195265
href="/courses/data-engineering"
196266
className="tw-group tw-flex tw-w-full tw-items-center tw-justify-center tw-rounded-lg tw-bg-secondary tw-px-6 tw-py-4 tw-font-semibold tw-text-white tw-transition-all tw-duration-200 hover:tw-bg-secondary/90 hover:tw-shadow-lg"
197267
>
198-
<span>View Vertical</span>
268+
<span>
269+
{!loading && isEnrolledInVertical("data-engineering")
270+
? "Continue Learning"
271+
: "View Vertical"}
272+
</span>
199273
<i className="fas fa-arrow-right tw-ml-2 tw-transition-transform tw-duration-200 group-hover:tw-translate-x-1" />
200274
</Link>
201275
</div>
@@ -204,13 +278,21 @@ const CoursesIndex: PageWithLayout = ({ user }) => {
204278
{/* AI Engineering Vertical */}
205279
<div className="tw-group tw-overflow-hidden tw-rounded-xl tw-border tw-border-gray-100 tw-bg-white tw-shadow-lg tw-transition-all tw-duration-300 hover:tw-scale-105 hover:tw-shadow-2xl">
206280
<div className="tw-bg-gradient-to-br tw-from-success tw-via-success tw-to-success/80 tw-p-8">
207-
<div className="tw-mb-6 tw-flex tw-items-center">
208-
<div className="tw-rounded-lg tw-bg-white/20 tw-p-3">
209-
<i className="fas fa-brain tw-text-3xl tw-text-white" />
281+
<div className="tw-mb-6 tw-flex tw-items-center tw-justify-between">
282+
<div className="tw-flex tw-items-center">
283+
<div className="tw-rounded-lg tw-bg-white/20 tw-p-3">
284+
<i className="fas fa-brain tw-text-3xl tw-text-white" />
285+
</div>
286+
<h3 className="tw-ml-4 tw-text-2xl tw-font-bold tw-text-white">
287+
AI Engineering
288+
</h3>
210289
</div>
211-
<h3 className="tw-ml-4 tw-text-2xl tw-font-bold tw-text-white">
212-
AI Engineering
213-
</h3>
290+
{!loading && isEnrolledInVertical("ai-engineering") && (
291+
<span className="tw-rounded-full tw-bg-white/90 tw-px-3 tw-py-1 tw-text-xs tw-font-semibold tw-text-success">
292+
<i className="fas fa-check-circle tw-mr-1" />
293+
Enrolled
294+
</span>
295+
)}
214296
</div>
215297
<p className="tw-text-lg tw-leading-relaxed tw-text-white/95">
216298
Develop AI/ML models, work with neural networks, and build
@@ -247,7 +329,11 @@ const CoursesIndex: PageWithLayout = ({ user }) => {
247329
href="/courses/ai-engineering"
248330
className="tw-group tw-flex tw-w-full tw-items-center tw-justify-center tw-rounded-lg tw-bg-success tw-px-6 tw-py-4 tw-font-semibold tw-text-white tw-transition-all tw-duration-200 hover:tw-bg-success/90 hover:tw-shadow-lg"
249331
>
250-
<span>View Vertical</span>
332+
<span>
333+
{!loading && isEnrolledInVertical("ai-engineering")
334+
? "Continue Learning"
335+
: "View Vertical"}
336+
</span>
251337
<i className="fas fa-arrow-right tw-ml-2 tw-transition-transform tw-duration-200 group-hover:tw-translate-x-1" />
252338
</Link>
253339
</div>

src/pages/courses/software-engineering.tsx

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from "react";
1+
import React, { useState, useEffect } from "react";
22
import Link from "next/link";
33
import Layout01 from "@layout/layout-01";
44
import type { GetServerSideProps, NextPage } from "next";
@@ -7,6 +7,13 @@ import { options } from "@/pages/api/auth/options";
77
import SEO from "@components/seo/page-seo";
88
import Breadcrumb from "@components/breadcrumb";
99

10+
type Enrollment = {
11+
id: string;
12+
courseId: string;
13+
status: string;
14+
progress: number;
15+
};
16+
1017
type PageProps = {
1118
user: {
1219
id: string;
@@ -102,6 +109,47 @@ const modules = [
102109

103110
const SoftwareEngineeringCourse: PageWithLayout = ({ user: _user }) => {
104111
const [selectedModule, setSelectedModule] = useState<number | null>(null);
112+
const [enrollments, setEnrollments] = useState<Enrollment[]>([]);
113+
const [loading, setLoading] = useState(true);
114+
const [enrollmentError, setEnrollmentError] = useState<string | null>(null);
115+
116+
useEffect(() => {
117+
fetchEnrollments();
118+
}, []);
119+
120+
const fetchEnrollments = async () => {
121+
try {
122+
setLoading(true);
123+
const response = await fetch("/api/enrollment/enroll");
124+
const data = await response.json();
125+
126+
if (response.ok) {
127+
setEnrollments(data.enrollments);
128+
}
129+
} catch (error) {
130+
console.error("Error fetching enrollments:", error);
131+
} finally {
132+
setLoading(false);
133+
}
134+
};
135+
136+
// Check if enrolled in Software Engineering vertical
137+
const isEnrolled = (): boolean => {
138+
const matchingTitles = ["Software Engineering", "Full-Stack Development", "Web Development"];
139+
return enrollments.some(
140+
(enrollment) =>
141+
enrollment.status === "ACTIVE" &&
142+
matchingTitles.some((title) =>
143+
enrollment.courseId.toLowerCase().includes(title.toLowerCase())
144+
)
145+
);
146+
};
147+
148+
const handleEnroll = async () => {
149+
// For now, this is a placeholder since we don't have a courseId
150+
// In a real implementation, you'd need to map the vertical to an actual course
151+
setEnrollmentError("Enrollment is not yet configured for this vertical. Please contact an administrator.");
152+
};
105153

106154
return (
107155
<>
@@ -134,6 +182,36 @@ const SoftwareEngineeringCourse: PageWithLayout = ({ user: _user }) => {
134182
</p>
135183
</div>
136184

185+
{/* Enrollment Status/CTA */}
186+
{!loading && (
187+
<div className="tw-mb-8 tw-text-center">
188+
{isEnrolled() ? (
189+
<div className="tw-inline-flex tw-items-center tw-rounded-lg tw-bg-success/10 tw-px-6 tw-py-3 tw-text-success">
190+
<i className="fas fa-check-circle tw-mr-2 tw-text-xl" />
191+
<span className="tw-font-semibold">
192+
You&apos;re enrolled in this vertical
193+
</span>
194+
</div>
195+
) : (
196+
<div>
197+
<button
198+
type="button"
199+
onClick={handleEnroll}
200+
className="tw-inline-flex tw-items-center tw-rounded-lg tw-bg-primary tw-px-8 tw-py-4 tw-text-lg tw-font-semibold tw-text-white tw-transition-all hover:tw-bg-primary/90 hover:tw-shadow-lg"
201+
>
202+
<i className="fas fa-user-plus tw-mr-2" />
203+
Enroll in Vertical
204+
</button>
205+
{enrollmentError && (
206+
<p className="tw-mt-3 tw-text-sm tw-text-red-600">
207+
{enrollmentError}
208+
</p>
209+
)}
210+
</div>
211+
)}
212+
</div>
213+
)}
214+
137215
{/* Course Stats */}
138216
<div className="tw-grid tw-grid-cols-2 tw-gap-6 tw-rounded-xl tw-bg-gradient-to-r tw-from-primary tw-to-primary/80 tw-p-8 tw-text-white md:tw-grid-cols-4">
139217
<div className="tw-text-center">

0 commit comments

Comments
 (0)