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>
This commit is contained in:
2026-04-07 14:13:24 +05:00
parent d0c8c6dd53
commit 543d5b2d5e
13 changed files with 836 additions and 31 deletions
@@ -0,0 +1,46 @@
"use server";
import { prisma } from "@/lib/prisma";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { revalidatePath } from "next/cache";
interface HomeworkFile {
name: string;
url: string;
size: number;
}
export async function submitHomework(
homeworkId: string,
slug: string,
lessonId: string,
text: string,
files: HomeworkFile[]
) {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) throw new Error("Unauthorized");
const existing = await prisma.homeworkSubmission.findFirst({
where: { homeworkId, userId: session.user.id },
include: { feedbacks: true },
});
// Don't allow resubmission if feedback already given
if (existing?.feedbacks && existing.feedbacks.length > 0) {
throw new Error("Работа уже проверена");
}
if (existing) {
await prisma.homeworkSubmission.update({
where: { id: existing.id },
data: { text, files: files as object[], submittedAt: new Date() },
});
} else {
await prisma.homeworkSubmission.create({
data: { homeworkId, userId: session.user.id, text, files: files as object[] },
});
}
revalidatePath(`/courses/${slug}/lessons/${lessonId}`);
}
@@ -6,6 +6,7 @@ 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 }>;
@@ -22,6 +23,7 @@ export default async function LessonPage({ params }: Props) {
where: { id: lessonId, ...(isAdmin ? {} : { published: true }) },
include: {
files: { orderBy: { createdAt: "asc" } },
homework: true,
module: {
include: {
course: {
@@ -49,6 +51,14 @@ export default async function LessonPage({ params }: Props) {
: 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;
@@ -118,6 +128,24 @@ export default async function LessonPage({ params }: Props) {
</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"