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
217 changes: 217 additions & 0 deletions src/pages/api/lms/progress/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import { NextApiResponse } from 'next';
import { requireAuth, AuthenticatedRequest } from '@/lib/rbac';
import prisma from '@/lib/prisma';

/**
* GET /api/lms/progress
* Fetch user's lesson progress (optionally filtered by courseId or moduleId)
*
* Query params:
* - courseId?: string - Filter by course
* - moduleId?: string - Filter by module
* - lessonId?: string - Get specific lesson progress
*
* POST /api/lms/progress
* Update lesson progress (mark as started, completed, or update time spent)
*
* Request body:
* {
* lessonId: string,
* completed?: boolean,
* timeSpent?: number (in minutes)
* }
*/
export default requireAuth(async (req: AuthenticatedRequest, res: NextApiResponse) => {
const userId = req.user!.id;

// GET - Fetch user's progress
if (req.method === 'GET') {
try {
const { courseId, moduleId, lessonId } = req.query;

// Build where clause for filtering
const where: any = { userId };
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.

Using any type defeats TypeScript's type safety. Consider using Prisma.ProgressWhereInput or defining a proper interface for the where clause to maintain type safety.

Copilot uses AI. Check for mistakes.

if (lessonId) {
where.lessonId = lessonId as string;
} else if (moduleId) {
where.lesson = {
moduleId: moduleId as string,
};
} else if (courseId) {
where.lesson = {
module: {
courseId: courseId as string,
},
};
}

const progress = await prisma.progress.findMany({
where,
include: {
lesson: {
select: {
id: true,
title: true,
order: true,
duration: true,
moduleId: true,
module: {
select: {
id: true,
title: true,
order: true,
courseId: true,
},
},
},
},
},
orderBy: [
{ lesson: { module: { order: 'asc' } } },
{ lesson: { order: 'asc' } },
],
});

return res.status(200).json({ progress });
} catch (error) {
console.error('Error fetching progress:', error);
return res.status(500).json({ error: 'Failed to fetch progress' });
}
}

// POST - Update lesson progress
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}

try {
const { lessonId, completed, timeSpent } = req.body;

// Validation
if (!lessonId) {
return res.status(400).json({ error: 'Lesson ID is required' });
}

// Verify lesson exists
const lesson = await prisma.lesson.findUnique({
where: { id: lessonId },
include: {
module: {
include: {
course: {
include: {
enrollments: {
where: {
userId,
status: 'ACTIVE',
},
},
},
},
},
},
},
});

if (!lesson) {
return res.status(404).json({ error: 'Lesson not found' });
}

// Verify user is enrolled in the course
if (lesson.module.course.enrollments.length === 0) {
return res.status(403).json({
error: 'You must be enrolled in the course to track progress',
});
}

// Check for existing progress record
const existingProgress = await prisma.progress.findUnique({
where: {
userId_lessonId: {
userId,
lessonId,
},
},
});

let progressRecord;

if (existingProgress) {
// Update existing progress
const updateData: any = {};
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.

Using any type bypasses TypeScript's type checking. Use Prisma.ProgressUpdateInput or a properly typed object to ensure type safety for update operations.

Copilot uses AI. Check for mistakes.

if (typeof completed === 'boolean') {
updateData.completed = completed;
if (completed && !existingProgress.completedAt) {
updateData.completedAt = new Date();
} else if (!completed) {
updateData.completedAt = null;
}
}

if (typeof timeSpent === 'number' && timeSpent >= 0) {
updateData.timeSpent = timeSpent;
}

progressRecord = await prisma.progress.update({
where: {
id: existingProgress.id,
},
data: updateData,
include: {
lesson: {
select: {
id: true,
title: true,
order: true,
moduleId: true,
},
},
},
});
} else {
// Create new progress record
progressRecord = await prisma.progress.create({
data: {
userId,
lessonId,
completed: completed || false,
timeSpent: timeSpent || 0,
completedAt: completed ? new Date() : null,
},
include: {
lesson: {
select: {
id: true,
title: true,
order: true,
moduleId: true,
},
},
},
});
}

// Update enrollment lastActivity timestamp
await prisma.enrollment.updateMany({
where: {
userId,
courseId: lesson.module.courseId,
},
data: {
lastActivity: new Date(),
},
});

res.status(existingProgress ? 200 : 201).json({
progress: progressRecord,
message: existingProgress
? 'Progress updated successfully'
: 'Progress tracking started',
});
} catch (error) {
console.error('Error updating progress:', error);
res.status(500).json({ error: 'Failed to update progress' });
}
});
116 changes: 97 additions & 19 deletions src/pages/courses/web-development/[moduleId]/[lessonId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,41 @@ const LessonPage: PageWithLayout = ({ lesson, module }) => {
const [completed, setCompleted] = useState(false);
const [showAssignment, setShowAssignment] = useState(false);
const [isAIAssistantOpen, setIsAIAssistantOpen] = useState(false);
const [updating, setUpdating] = useState(false);
const [moduleProgress, setModuleProgress] = useState({ completed: 0, total: module.totalLessons });

// Fetch lesson progress and module progress on mount
useEffect(() => {
// TODO: Check if lesson is completed from database
// For now, check localStorage for demo
const lessonKey = `lesson_${lesson.id}_completed`;
setCompleted(localStorage.getItem(lessonKey) === "true");
}, [lesson.id]);
const fetchProgress = async () => {
try {
// Fetch current lesson progress
const lessonResponse = await fetch(`/api/lms/progress?lessonId=${lesson.id}`);
const lessonData = await lessonResponse.json();

if (lessonResponse.ok && lessonData.progress.length > 0) {
setCompleted(lessonData.progress[0].completed);
}

// Fetch module progress
const moduleResponse = await fetch(`/api/lms/progress?moduleId=${module.id}`);
const moduleData = await moduleResponse.json();

if (moduleResponse.ok) {
const completedCount = moduleData.progress.filter(
(p: { completed: boolean }) => p.completed
).length;
setModuleProgress({
completed: completedCount,
total: module.totalLessons,
});
}
} catch (error) {
console.error("Error fetching progress:", error);
}
};

fetchProgress();
}, [lesson.id, module.id, module.totalLessons]);

// Keyboard shortcut for AI Assistant ('A' key)
useEffect(() => {
Expand All @@ -143,12 +171,36 @@ const LessonPage: PageWithLayout = ({ lesson, module }) => {
return () => window.removeEventListener('keydown', handleKeyPress);
}, []);

const markAsCompleted = () => {
// TODO: Update database with lesson completion
// For now, use localStorage for demo
const lessonKey = `lesson_${lesson.id}_completed`;
localStorage.setItem(lessonKey, "true");
setCompleted(true);
const markAsCompleted = async () => {
try {
setUpdating(true);
const response = await fetch("/api/lms/progress", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
lessonId: lesson.id,
completed: true,
}),
});

if (response.ok) {
setCompleted(true);
// Refresh module progress
setModuleProgress((prev) => ({
...prev,
completed: prev.completed + 1,
}));
Comment on lines +191 to +194
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 progress counter increments even if the lesson was already completed, causing incorrect progress counts. Check if completed is currently false before incrementing, or refetch the module progress from the API to ensure accuracy.

Copilot uses AI. Check for mistakes.
} else {
const data = await response.json();
console.error("Failed to mark as completed:", data.error);
}
} catch (error) {
console.error("Error marking lesson as completed:", error);
} finally {
setUpdating(false);
}
};

return (
Expand Down Expand Up @@ -243,10 +295,11 @@ const LessonPage: PageWithLayout = ({ lesson, module }) => {
<button
type="button"
onClick={markAsCompleted}
className="tw-rounded-md tw-bg-gold-rich tw-px-6 tw-py-2 tw-font-medium tw-text-white tw-transition-colors hover:tw-bg-green-700"
disabled={updating}
className="tw-rounded-md tw-bg-gold-rich tw-px-6 tw-py-2 tw-font-medium tw-text-white tw-transition-colors hover:tw-bg-green-700 disabled:tw-cursor-not-allowed disabled:tw-opacity-50"
>
<i className="fas fa-check tw-mr-2" />
Mark as Complete
<i className={`fas ${updating ? "fa-spinner fa-spin" : "fa-check"} tw-mr-2`} />
{updating ? "Saving..." : "Mark as Complete"}
</button>
)}

Expand All @@ -272,18 +325,43 @@ const LessonPage: PageWithLayout = ({ lesson, module }) => {
</h3>
<div className="tw-mb-2 tw-flex tw-justify-between tw-text-sm tw-text-gray-300">
<span>Module Progress</span>
<span>3/12 lessons</span>
<span>
{moduleProgress.completed}/{moduleProgress.total} lessons
</span>
</div>
<div className="tw-mb-4 tw-h-2 tw-w-full tw-rounded-full tw-bg-gray-50">
<div className="tw-h-2 tw-w-[25%] tw-rounded-full tw-bg-navy-royal" />
<div
className="tw-h-2 tw-rounded-full tw-bg-navy-royal tw-transition-all"
style={{
width: `${
moduleProgress.total > 0
? (moduleProgress.completed / moduleProgress.total) * 100
: 0
}%`,
}}
/>
</div>

<div className="tw-mb-2 tw-flex tw-justify-between tw-text-sm tw-text-gray-300">
<span>Overall Progress</span>
<span>15%</span>
<span>Module Completion</span>
<span>
{moduleProgress.total > 0
? Math.round((moduleProgress.completed / moduleProgress.total) * 100)
: 0}
%
</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-gold-rich" />
<div
className="tw-h-2 tw-rounded-full tw-bg-gold-rich tw-transition-all"
style={{
width: `${
moduleProgress.total > 0
? (moduleProgress.completed / moduleProgress.total) * 100
: 0
}%`,
}}
/>
</div>
</div>

Expand Down