mirror of
https://github.com/nicetry247/offlineacademy.git
synced 2026-08-09 18:39:23 +00:00
initial commit
This commit is contained in:
Executable
+320
@@ -0,0 +1,320 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { z } from 'zod'
|
||||
|
||||
const progressSchema = z.object({
|
||||
lessonId: z.string(),
|
||||
courseId: z.string(),
|
||||
moduleId: z.string(),
|
||||
position: z.number().min(0),
|
||||
completed: z.boolean().default(false),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const parsed = progressSchema.safeParse(body)
|
||||
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 400 })
|
||||
}
|
||||
|
||||
const { lessonId, courseId, moduleId, position, completed } = parsed.data
|
||||
|
||||
const progress = await prisma.progress.upsert({
|
||||
where: {
|
||||
userId_lessonId: {
|
||||
userId: 'local-user',
|
||||
lessonId,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
position,
|
||||
completed,
|
||||
lastWatched: new Date(),
|
||||
courseId,
|
||||
moduleId,
|
||||
},
|
||||
create: {
|
||||
userId: 'local-user',
|
||||
lessonId,
|
||||
courseId,
|
||||
moduleId,
|
||||
position,
|
||||
completed,
|
||||
lastWatched: new Date(),
|
||||
},
|
||||
})
|
||||
|
||||
// Also maintain a course-level summary row so course pages can resume correctly.
|
||||
// This is stored separately from lesson progress using null lessonId/moduleId.
|
||||
const totalLessons = await prisma.lesson.count({
|
||||
where: { module: { courseId } },
|
||||
})
|
||||
const completedLessons = await prisma.lesson.count({
|
||||
where: {
|
||||
module: { courseId },
|
||||
progress: { some: { userId: 'local-user', completed: true } },
|
||||
},
|
||||
})
|
||||
|
||||
if (totalLessons > 0) {
|
||||
const courseCompleted = completedLessons === totalLessons
|
||||
const courseProgressData = {
|
||||
userId: 'local-user',
|
||||
courseId,
|
||||
lessonId: null,
|
||||
moduleId: null,
|
||||
position,
|
||||
completed: courseCompleted,
|
||||
lastWatched: new Date(),
|
||||
}
|
||||
|
||||
const existingCourseProgress = await prisma.progress.findFirst({
|
||||
where: {
|
||||
userId: 'local-user',
|
||||
courseId,
|
||||
lessonId: null,
|
||||
moduleId: null,
|
||||
},
|
||||
})
|
||||
|
||||
if (existingCourseProgress) {
|
||||
await prisma.progress.update({
|
||||
where: { id: existingCourseProgress.id },
|
||||
data: courseProgressData,
|
||||
})
|
||||
} else {
|
||||
await prisma.progress.create({
|
||||
data: courseProgressData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, progress })
|
||||
} catch (error) {
|
||||
console.error('Progress save error:', error)
|
||||
return NextResponse.json({ error: 'Failed to save progress' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const lessonId = searchParams.get('lessonId')
|
||||
const courseId = searchParams.get('courseId')
|
||||
const type = searchParams.get('type') // 'continue' or 'completed'
|
||||
|
||||
if (type === 'continue') {
|
||||
// Get in-progress lessons
|
||||
const progressRecords = await prisma.progress.findMany({
|
||||
where: {
|
||||
userId: 'local-user',
|
||||
lessonId: { not: null },
|
||||
completed: false,
|
||||
lesson: { module: { course: { hidden: false } } },
|
||||
},
|
||||
orderBy: { lastWatched: 'desc' },
|
||||
take: 20,
|
||||
include: {
|
||||
lesson: {
|
||||
include: {
|
||||
module: {
|
||||
include: {
|
||||
course: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const items = progressRecords
|
||||
.filter(p => p.lesson)
|
||||
.map(p => ({
|
||||
progress: {
|
||||
...p,
|
||||
lastWatched: p.lastWatched.toISOString(),
|
||||
},
|
||||
lesson: p.lesson!,
|
||||
course: p.lesson!.module.course,
|
||||
module: p.lesson!.module,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ items })
|
||||
}
|
||||
|
||||
if (type === 'completed') {
|
||||
// Get completed courses
|
||||
const completedProgress = await prisma.progress.findMany({
|
||||
where: {
|
||||
userId: 'local-user',
|
||||
lessonId: null,
|
||||
moduleId: null,
|
||||
completed: true,
|
||||
course: { hidden: false },
|
||||
},
|
||||
include: {
|
||||
course: true,
|
||||
},
|
||||
})
|
||||
|
||||
const items = completedProgress.map(p => p.course)
|
||||
|
||||
return NextResponse.json({ items })
|
||||
}
|
||||
|
||||
if (type === 'analytics') {
|
||||
// Get analytics data for dashboard
|
||||
const allProgress = await prisma.progress.findMany({
|
||||
where: {
|
||||
userId: 'local-user',
|
||||
course: { hidden: false },
|
||||
},
|
||||
})
|
||||
|
||||
const completedLessons = allProgress.filter(p => p.lessonId && p.completed)
|
||||
const inProgressLessons = allProgress.filter(p => p.lessonId && !p.completed && p.position > 0)
|
||||
const notStartedCount = await prisma.lesson.count({
|
||||
where: {
|
||||
module: { course: { hidden: false } },
|
||||
progress: { none: { userId: 'local-user' } },
|
||||
},
|
||||
})
|
||||
|
||||
const totalCourses = await prisma.course.count({ where: { hidden: false } })
|
||||
const completedCourses = await prisma.progress.count({
|
||||
where: {
|
||||
userId: 'local-user',
|
||||
lessonId: null,
|
||||
moduleId: null,
|
||||
completed: true,
|
||||
course: { hidden: false },
|
||||
},
|
||||
})
|
||||
const inProgressCourses = await prisma.progress.count({
|
||||
where: {
|
||||
userId: 'local-user',
|
||||
lessonId: null,
|
||||
moduleId: null,
|
||||
completed: false,
|
||||
course: { hidden: false },
|
||||
},
|
||||
})
|
||||
|
||||
// Calculate total watched time (in seconds)
|
||||
const totalWatchedSeconds = allProgress.reduce((sum, p) => sum + (p.position || 0), 0)
|
||||
|
||||
// Get weekly activity (last 7 days)
|
||||
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
||||
const recentProgress = allProgress.filter(p => new Date(p.lastWatched) >= sevenDaysAgo)
|
||||
const weeklyWatchedSeconds = recentProgress.reduce((sum, p) => sum + (p.position || 0), 0)
|
||||
const weeklyCompleted = recentProgress.filter(p => p.lessonId && p.completed).length
|
||||
|
||||
// Lessons by type
|
||||
const lessonsByType = await prisma.lesson.groupBy({
|
||||
by: ['type'],
|
||||
_count: true,
|
||||
where: { module: { course: { hidden: false } } },
|
||||
})
|
||||
|
||||
// Completed lessons by type
|
||||
const completedByType: Record<string, number> = {}
|
||||
for (const p of completedLessons) {
|
||||
if (!p.lessonId) continue
|
||||
const lesson = await prisma.lesson.findUnique({ where: { id: p.lessonId }, select: { type: true } })
|
||||
if (lesson) {
|
||||
completedByType[lesson.type] = (completedByType[lesson.type] || 0) + 1
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
totalCourses,
|
||||
completedCourses,
|
||||
inProgressCourses,
|
||||
totalLessons: completedLessons.length + inProgressLessons.length + notStartedCount,
|
||||
completedLessons: completedLessons.length,
|
||||
inProgressLessons: inProgressLessons.length,
|
||||
notStartedLessons: notStartedCount,
|
||||
totalWatchedHours: Math.round(totalWatchedSeconds / 3600 * 10) / 10,
|
||||
totalWatchedMinutes: Math.round(totalWatchedSeconds / 60),
|
||||
weeklyWatchedHours: Math.round(weeklyWatchedSeconds / 3600 * 10) / 10,
|
||||
weeklyWatchedMinutes: Math.round(weeklyWatchedSeconds / 60),
|
||||
weeklyCompletedLessons: weeklyCompleted,
|
||||
lessonsByType,
|
||||
completedByType,
|
||||
completionRate: totalCourses > 0 ? Math.round((completedCourses / totalCourses) * 100) : 0,
|
||||
lessonCompletionRate: (completedLessons.length + inProgressLessons.length + notStartedCount) > 0
|
||||
? Math.round((completedLessons.length / (completedLessons.length + inProgressLessons.length + notStartedCount)) * 100)
|
||||
: 0,
|
||||
})
|
||||
}
|
||||
|
||||
if (lessonId) {
|
||||
const progress = await prisma.progress.findUnique({
|
||||
where: {
|
||||
userId_lessonId: {
|
||||
userId: 'local-user',
|
||||
lessonId,
|
||||
},
|
||||
},
|
||||
})
|
||||
if (progress) {
|
||||
return NextResponse.json({
|
||||
progress: {
|
||||
...progress,
|
||||
lastWatched: progress.lastWatched.toISOString()
|
||||
}
|
||||
})
|
||||
}
|
||||
return NextResponse.json({ progress: null })
|
||||
}
|
||||
|
||||
if (courseId) {
|
||||
const progress = await prisma.progress.findFirst({
|
||||
where: {
|
||||
userId: 'local-user',
|
||||
courseId,
|
||||
lessonId: null,
|
||||
moduleId: null,
|
||||
course: { hidden: false },
|
||||
},
|
||||
})
|
||||
|
||||
if (progress) {
|
||||
return NextResponse.json({
|
||||
progress: {
|
||||
...progress,
|
||||
lastWatched: progress.lastWatched.toISOString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const latestLessonProgress = await prisma.progress.findFirst({
|
||||
where: {
|
||||
userId: 'local-user',
|
||||
courseId,
|
||||
lessonId: { not: null },
|
||||
course: { hidden: false },
|
||||
},
|
||||
orderBy: { lastWatched: 'desc' },
|
||||
})
|
||||
|
||||
if (latestLessonProgress) {
|
||||
return NextResponse.json({
|
||||
progress: {
|
||||
...latestLessonProgress,
|
||||
lastWatched: latestLessonProgress.lastWatched.toISOString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({ progress: null })
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'lessonId, courseId or type required' }, { status: 400 })
|
||||
} catch (error) {
|
||||
console.error('Progress fetch error:', error)
|
||||
return NextResponse.json({ error: 'Failed to fetch progress' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user