Files
lms-sb/src/app/(student)/courses/[slug]/lessons/[lessonId]/page.tsx
T
admins 543d5b2d5e Add homework system (admin, student, curator)
Admin:
- HomeworkEditor in lesson page: create/update/delete assignment description

Student:
- HomeworkSection in lesson page: view assignment, submit text + files
- Resubmission allowed until curator gives feedback
- Shows feedback from curator with date and name

Curator:
- New layout with Second Brain dark sidebar (replaces green theme)
- /curator/dashboard: stats cards (pending, total, reviewed this week)
- /curator/homework: list of all submissions, pending highlighted
- /curator/homework/[id]: review submission, write feedback, redirect after send

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 14:13:24 +05:00

185 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { prisma } from "@/lib/prisma";
import { notFound } from "next/navigation";
import Link from "next/link";
import { headers } from "next/headers";
import { auth } from "@/lib/auth";
import { KinescopePlayer } from "@/components/player/kinescope-player";
import { LessonContent } from "@/components/student/lesson-content";
import { LessonCompleteButton } from "@/components/student/lesson-complete-button";
import { HomeworkSection } from "@/components/student/homework-section";
interface Props {
params: Promise<{ slug: string; lessonId: string }>;
}
export default async function LessonPage({ params }: Props) {
const { slug, lessonId } = await params;
const session = await auth.api.getSession({ headers: await headers() });
const isAdmin = session?.user.role === "admin";
const [lesson, progress] = await Promise.all([
prisma.lesson.findUnique({
where: { id: lessonId, ...(isAdmin ? {} : { published: true }) },
include: {
files: { orderBy: { createdAt: "asc" } },
homework: true,
module: {
include: {
course: {
include: {
modules: {
orderBy: { order: "asc" },
include: {
lessons: {
where: { published: true },
orderBy: { order: "asc" },
select: { id: true, title: true },
},
},
},
},
},
},
},
},
}),
session && !isAdmin
? prisma.lessonProgress.findUnique({
where: { userId_lessonId: { userId: session.user.id, lessonId } },
})
: null,
]);
// Fetch homework submission for this student
const homeworkSubmission = lesson?.homework && session && !isAdmin
? await prisma.homeworkSubmission.findFirst({
where: { homeworkId: lesson.homework.id, userId: session.user.id },
include: { feedbacks: { include: { curator: { select: { name: true } } }, orderBy: { createdAt: "desc" } } },
})
: null;
if (!lesson || lesson.module.course.slug !== slug) notFound();
const isCompleted = !!progress;
// Build ordered flat list of all lessons for prev/next
const allLessons = lesson.module.course.modules.flatMap((m) => m.lessons);
const idx = allLessons.findIndex((l) => l.id === lessonId);
const prevLesson = idx > 0 ? allLessons[idx - 1] : null;
const nextLesson = idx < allLessons.length - 1 ? allLessons[idx + 1] : null;
const hasContent = lesson.content && Object.keys(lesson.content as object).length > 0;
function formatSize(bytes: number) {
if (bytes < 1024) return `${bytes} Б`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} КБ`;
return `${(bytes / 1024 / 1024).toFixed(1)} МБ`;
}
return (
<article className="max-w-3xl mx-auto px-6 py-8">
{/* Title */}
<h1 className="text-2xl font-bold mb-6 leading-snug">{lesson.title}</h1>
{/* Video */}
{lesson.kinescopeId && (
<div className="mb-8">
<KinescopePlayer videoId={lesson.kinescopeId} />
</div>
)}
{/* Text content */}
{hasContent && (
<div className="mb-8">
<LessonContent content={lesson.content as object} />
</div>
)}
{/* Files */}
{lesson.files.length > 0 && (
<div className="mb-8">
<p className="text-xs font-bold uppercase tracking-widest mb-3" style={{ color: "var(--muted-foreground)" }}>
Материалы урока
</p>
<div className="space-y-2">
{lesson.files.map((file) => (
<a
key={file.id}
href={file.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 px-4 py-3 text-sm transition-colors"
style={{ border: "2px solid var(--border)", display: "flex" }}
onMouseEnter={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")}
onMouseLeave={(e) => (e.currentTarget.style.borderColor = "var(--border)")}
>
<span className="text-lg">📎</span>
<span className="flex-1 font-medium">{file.name}</span>
<span className="text-xs" style={{ color: "var(--muted-foreground)" }}>
{formatSize(file.size)}
</span>
<span className="text-xs underline" style={{ color: "var(--muted-foreground)" }}>
Скачать
</span>
</a>
))}
</div>
</div>
)}
{/* Homework */}
{lesson.homework && !isAdmin && (
<div className="mb-8">
<p className="text-xs font-bold uppercase tracking-widest mb-3" style={{ color: "var(--muted-foreground)" }}>
Домашнее задание
</p>
<HomeworkSection
homework={lesson.homework}
submission={homeworkSubmission ? {
...homeworkSubmission,
files: (homeworkSubmission.files as { name: string; url: string; size: number }[]) ?? [],
} : null}
slug={slug}
lessonId={lessonId}
/>
</div>
)}
{/* Complete button + Prev/Next navigation */}
<div
className="flex items-center justify-between pt-6 mt-6"
style={{ borderTop: "2px solid var(--border)" }}
>
{prevLesson ? (
<Link
href={`/courses/${slug}/lessons/${prevLesson.id}`}
className="btn-aubade text-sm max-w-[40%]"
>
{prevLesson.title}
</Link>
) : (
<div />
)}
{!isAdmin && (
<LessonCompleteButton lessonId={lessonId} slug={slug} isCompleted={isCompleted} />
)}
{nextLesson ? (
<Link
href={`/courses/${slug}/lessons/${nextLesson.id}`}
className="btn-aubade btn-aubade-accent text-sm max-w-[40%] text-right"
>
{nextLesson.title}
</Link>
) : (
<div className="text-sm" style={{ color: "var(--muted-foreground)" }}>
{isAdmin ? "Последний урок курса" : ""}
</div>
)}
</div>
</article>
);
}