Skip to content
Merged
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
46 changes: 44 additions & 2 deletions src/pages/api/lms/progress/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,19 +193,61 @@ export default requireAuth(async (req: AuthenticatedRequest, res: NextApiRespons
});
}

// Update enrollment lastActivity timestamp
// Calculate and update course progress
const courseId = lesson.module.courseId;

// Count total lessons in course
const totalLessons = await prisma.lesson.count({
where: {
module: {
courseId,
},
},
});

// Count completed lessons for this user
const completedLessons = await prisma.progress.count({
where: {
userId,
completed: true,
lesson: {
module: {
courseId,
},
},
},
});

// Calculate progress percentage
const progressPercentage = totalLessons > 0
? Math.round((completedLessons / totalLessons) * 100)
: 0;

// Check if course is complete
const isComplete = progressPercentage === 100;

// Update enrollment with progress and completion status
await prisma.enrollment.updateMany({
where: {
userId,
courseId: lesson.module.courseId,
courseId,
},
data: {
progress: progressPercentage,
lastActivity: new Date(),
status: isComplete ? 'COMPLETED' : 'ACTIVE',
completedAt: isComplete ? new Date() : null,
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting completedAt to null when a course is not complete will overwrite any existing completion date if a user uncompletes a lesson after finishing a course. Consider only setting completedAt when isComplete is true, without explicitly setting it to null.

Suggested change
completedAt: isComplete ? new Date() : null,
...(isComplete && { completedAt: new Date() }),

Copilot uses AI. Check for mistakes.
},
});

res.status(existingProgress ? 200 : 201).json({
progress: progressRecord,
courseProgress: {
completed: completedLessons,
total: totalLessons,
percentage: progressPercentage,
isComplete,
},
message: existingProgress
? 'Progress updated successfully'
: 'Progress tracking started',
Expand Down
239 changes: 174 additions & 65 deletions src/pages/dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
import React from "react";
import React, { useEffect, useState } from "react";
import Link from "next/link";
import { useSession } from "next-auth/react";
import Layout01 from "@layout/layout-01";
import type { GetStaticProps, NextPage } from "next";
import SEO from "@components/seo/page-seo";
import Breadcrumb from "@components/breadcrumb";

type Enrollment = {
id: string;
progress: number;
status: string;
completedAt: string | null;
course: {
id: string;
title: string;
estimatedHours: number | null;
};
};

type PageProps = {
layout?: {
headerShadow: boolean;
Expand All @@ -20,8 +32,42 @@ type PageWithLayout = NextPage<PageProps> & {

const Dashboard: PageWithLayout = () => {
const { data: session, status } = useSession();
const [enrollments, setEnrollments] = useState<Enrollment[]>([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
if (status === "authenticated") {
fetchEnrollments();
}
}, [status]);

const fetchEnrollments = async () => {
try {
setLoading(true);
const response = await fetch("/api/enrollment/enroll");
const data = await response.json();

if (response.ok) {
setEnrollments(data.enrollments);
}
} catch (error) {
console.error("Error fetching enrollments:", error);
} finally {
setLoading(false);
}
};

if (status === "loading") {
// Calculate stats from enrollments
const stats = {
enrolled: enrollments.filter((e) => e.status === "ACTIVE" || e.status === "COMPLETED").length,
Comment on lines +60 to +62
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filter condition e.status === \"ACTIVE\" || e.status === \"COMPLETED\" is duplicated multiple times (lines 62, 158, 161, 262). Consider extracting this into a helper function or constant to reduce duplication and improve maintainability.

Suggested change
// Calculate stats from enrollments
const stats = {
enrolled: enrollments.filter((e) => e.status === "ACTIVE" || e.status === "COMPLETED").length,
const isActiveOrCompleted = (e: Enrollment) =>
e.status === "ACTIVE" || e.status === "COMPLETED";
// Calculate stats from enrollments
const stats = {
enrolled: enrollments.filter(isActiveOrCompleted).length,

Copilot uses AI. Check for mistakes.
completed: enrollments.filter((e) => e.status === "COMPLETED").length,
totalHours: enrollments.reduce((sum, e) => sum + (e.course.estimatedHours || 0), 0),
averageProgress: enrollments.length > 0
? Math.round(enrollments.reduce((sum, e) => sum + e.progress, 0) / enrollments.length)
: 0,
};

if (status === "loading" || loading) {
return (
<div className="tw-container tw-py-16">
<div className="tw-text-center">
Expand Down Expand Up @@ -76,22 +122,26 @@ const Dashboard: PageWithLayout = () => {
{/* Quick Stats */}
<div className="tw-mb-12 tw-grid tw-grid-cols-1 tw-gap-6 md:tw-grid-cols-4">
<div className="tw-rounded-lg tw-bg-white tw-p-6 tw-text-center tw-shadow-md">
<div className="tw-mb-2 tw-text-3xl tw-font-bold tw-text-navy-royal">1</div>
<div className="tw-mb-2 tw-text-3xl tw-font-bold tw-text-navy-royal">
{stats.enrolled}
</div>
<div className="tw-text-gray-300">Courses Enrolled</div>
</div>
<div className="tw-rounded-lg tw-bg-white tw-p-6 tw-text-center tw-shadow-md">
<div className="tw-mb-2 tw-text-3xl tw-font-bold tw-text-gold">0</div>
<div className="tw-mb-2 tw-text-3xl tw-font-bold tw-text-gold">
{stats.completed}
</div>
<div className="tw-text-gray-300">Courses Completed</div>
</div>
<div className="tw-rounded-lg tw-bg-white tw-p-6 tw-text-center tw-shadow-md">
<div className="tw-mb-2 tw-text-3xl tw-font-bold tw-text-navy">
12
{stats.totalHours}
</div>
<div className="tw-text-gray-300">Hours Studied</div>
<div className="tw-text-gray-300">Total Course Hours</div>
</div>
<div className="tw-rounded-lg tw-bg-white tw-p-6 tw-text-center tw-shadow-md">
<div className="tw-mb-2 tw-text-3xl tw-font-bold tw-text-red">
85%
{stats.averageProgress}%
</div>
<div className="tw-text-gray-300">Average Progress</div>
</div>
Expand All @@ -105,68 +155,127 @@ const Dashboard: PageWithLayout = () => {
</h2>

<div className="tw-space-y-6">
{/* Sample enrolled course */}
<div className="tw-overflow-hidden tw-rounded-lg tw-bg-white tw-shadow-md">
<div className="tw-p-6">
<div className="tw-mb-4 tw-flex tw-items-start tw-justify-between">
<div className="tw-flex tw-items-center tw-space-x-4">
<div className="tw-flex tw-h-16 tw-w-16 tw-items-center tw-justify-center tw-rounded-lg tw-bg-gradient-to-r tw-from-blue-500 tw-to-blue-600">
<i className="fab fa-html5 tw-text-2xl tw-text-white" />
</div>
<div>
<h3 className="tw-text-xl tw-font-semibold tw-text-ink">
Web Development
</h3>
<p className="tw-text-gray-300">
Build modern web applications
</p>
</div>
</div>
<span className="tw-rounded-full tw-bg-gold-light/30 tw-px-3 tw-py-1 tw-text-sm tw-font-medium tw-text-gold-deep">
Active
</span>
</div>
{enrollments.filter((e) => e.status === "ACTIVE" || e.status === "COMPLETED").length > 0 ? (
<>
{enrollments
.filter((e) => e.status === "ACTIVE" || e.status === "COMPLETED")
.map((enrollment) => (
<div
key={enrollment.id}
className="tw-overflow-hidden tw-rounded-lg tw-bg-white tw-shadow-md"
>
<div className="tw-p-6">
<div className="tw-mb-4 tw-flex tw-items-start tw-justify-between">
<div className="tw-flex tw-items-center tw-space-x-4">
<div className="tw-flex tw-h-16 tw-w-16 tw-items-center tw-justify-center tw-rounded-lg tw-bg-gradient-to-r tw-from-blue-500 tw-to-blue-600">
<i className="fas fa-code tw-text-2xl tw-text-white" />
</div>
<div>
<h3 className="tw-text-xl tw-font-semibold tw-text-ink">
{enrollment.course.title}
</h3>
{enrollment.course.estimatedHours && (
<p className="tw-text-gray-300">
{enrollment.course.estimatedHours} hours
</p>
)}
</div>
</div>
<span
className={`tw-rounded-full tw-px-3 tw-py-1 tw-text-sm tw-font-medium ${
enrollment.status === "COMPLETED"
? "tw-bg-green-100 tw-text-green-800"
: "tw-bg-gold-light/30 tw-text-gold-deep"
}`}
>
{enrollment.status === "COMPLETED"
? "Completed"
: "Active"}
</span>
</div>

<div className="tw-mb-4">
<div className="tw-mb-2 tw-flex tw-justify-between tw-text-sm tw-text-gray-300">
<span>Progress</span>
<span>15%</span>
</div>
<div className="tw-h-2 tw-w-full tw-rounded-full tw-bg-gray-50">
<div className="tw-h-2 tw-w-[15%] tw-rounded-full tw-bg-navy-royal" />
</div>
</div>
<div className="tw-mb-4">
<div className="tw-mb-2 tw-flex tw-justify-between tw-text-sm tw-text-gray-300">
<span>Progress</span>
<span>{enrollment.progress}%</span>
</div>
<div className="tw-h-2 tw-w-full tw-rounded-full tw-bg-gray-50">
<div
className={`tw-h-2 tw-rounded-full tw-transition-all ${
enrollment.status === "COMPLETED"
? "tw-bg-green-600"
: "tw-bg-navy-royal"
}`}
style={{ width: `${enrollment.progress}%` }}
/>
</div>
</div>

<div className="tw-flex tw-items-center tw-justify-between">
<div className="tw-text-sm tw-text-gray-300">
Next: CSS Styling & Layout
</div>
<Link
href="/courses/web-development"
className="hover:tw-bg-primary-dark tw-rounded-md tw-bg-primary tw-px-4 tw-py-2 tw-text-sm tw-font-medium tw-text-white tw-transition-colors"
>
Continue Learning
</Link>
</div>
{enrollment.status !== "COMPLETED" && (
<div className="tw-flex tw-items-center tw-justify-end">
<Link
href={`/courses/${enrollment.course.id}`}
className="hover:tw-bg-primary-dark tw-rounded-md tw-bg-primary tw-px-4 tw-py-2 tw-text-sm tw-font-medium tw-text-white tw-transition-colors"
>
Continue Learning
</Link>
</div>
)}

{enrollment.status === "COMPLETED" && enrollment.completedAt && (
<div className="tw-flex tw-items-center tw-justify-between">
<div className="tw-text-sm tw-text-gray-500">
Completed on{" "}
{new Date(enrollment.completedAt).toLocaleDateString()}
</div>
<Link
href={`/courses/${enrollment.course.id}`}
className="tw-text-sm tw-text-primary hover:tw-underline"
>
View Course
</Link>
</div>
)}
</div>
</div>
))}
</>
) : (
<div className="tw-rounded-lg tw-border-2 tw-border-dashed tw-border-gray-300 tw-bg-gray-50 tw-p-8 tw-text-center">
<i className="fas fa-book tw-mb-4 tw-text-4xl tw-text-gray-400" />
<h3 className="tw-mb-2 tw-text-lg tw-font-semibold tw-text-ink">
No Courses Yet
</h3>
<p className="tw-mb-4 tw-text-gray-300">
Start your learning journey by enrolling in a course
</p>
<Link
href="/courses"
className="hover:tw-bg-primary-dark tw-rounded-md tw-bg-primary tw-px-6 tw-py-2 tw-font-medium tw-text-white tw-transition-colors"
>
Browse Courses
</Link>
</div>
</div>
)}

{/* Empty state for additional courses */}
<div className="tw-rounded-lg tw-border-2 tw-border-dashed tw-border-gray-300 tw-bg-gray-50 tw-p-8 tw-text-center">
<i className="fas fa-plus-circle tw-mb-4 tw-text-4xl tw-text-gray-400" />
<h3 className="tw-mb-2 tw-text-lg tw-font-semibold tw-text-ink">
Enroll in More Courses
</h3>
<p className="tw-mb-4 tw-text-gray-300">
Expand your skills with additional courses
</p>
<Link
href="/courses"
className="hover:tw-bg-primary-dark tw-rounded-md tw-bg-primary tw-px-6 tw-py-2 tw-font-medium tw-text-white tw-transition-colors"
>
Browse Courses
</Link>
</div>
{/* Enroll in more courses CTA */}
{enrollments.filter((e) => e.status === "ACTIVE" || e.status === "COMPLETED").length > 0 && (
<div className="tw-rounded-lg tw-border-2 tw-border-dashed tw-border-gray-300 tw-bg-gray-50 tw-p-8 tw-text-center">
<i className="fas fa-plus-circle tw-mb-4 tw-text-4xl tw-text-gray-400" />
<h3 className="tw-mb-2 tw-text-lg tw-font-semibold tw-text-ink">
Enroll in More Courses
</h3>
<p className="tw-mb-4 tw-text-gray-300">
Expand your skills with additional courses
</p>
<Link
href="/courses"
className="hover:tw-bg-primary-dark tw-rounded-md tw-bg-primary tw-px-6 tw-py-2 tw-font-medium tw-text-white tw-transition-colors"
>
Browse Courses
</Link>
</div>
)}
</div>
</div>

Expand Down