543d5b2d5e
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>
185 lines
6.3 KiB
TypeScript
185 lines
6.3 KiB
TypeScript
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>
|
||
);
|
||
}
|