diff --git a/src/app/(student)/courses/[slug]/lessons/[lessonId]/homework-actions.ts b/src/app/(student)/courses/[slug]/lessons/[lessonId]/homework-actions.ts new file mode 100644 index 0000000..67b44e0 --- /dev/null +++ b/src/app/(student)/courses/[slug]/lessons/[lessonId]/homework-actions.ts @@ -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}`); +} diff --git a/src/app/(student)/courses/[slug]/lessons/[lessonId]/page.tsx b/src/app/(student)/courses/[slug]/lessons/[lessonId]/page.tsx index 2060012..64575a1 100644 --- a/src/app/(student)/courses/[slug]/lessons/[lessonId]/page.tsx +++ b/src/app/(student)/courses/[slug]/lessons/[lessonId]/page.tsx @@ -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) { )} + {/* Homework */} + {lesson.homework && !isAdmin && ( +
+

+ Домашнее задание +

+ +
+ )} + {/* Complete button + Prev/Next navigation */}
; @@ -15,6 +16,7 @@ export default async function LessonEditorPage({ params }: Props) { where: { id: lessonId }, include: { files: { orderBy: { createdAt: "asc" } }, + homework: true, module: { include: { course: { select: { title: true, slug: true } } }, }, @@ -52,12 +54,20 @@ export default async function LessonEditorPage({ params }: Props) {
{/* Files section */} -
+

Файлы и материалы

+ + {/* Homework section */} +
+

+ Домашнее задание +

+ +
); } diff --git a/src/app/api/student/homework-upload/route.ts b/src/app/api/student/homework-upload/route.ts new file mode 100644 index 0000000..644c1d6 --- /dev/null +++ b/src/app/api/student/homework-upload/route.ts @@ -0,0 +1,21 @@ +import { NextRequest, NextResponse } from "next/server"; +import { headers } from "next/headers"; +import { auth } from "@/lib/auth"; +import { uploadFile } from "@/lib/s3"; +import { randomUUID } from "crypto"; + +export async function POST(req: NextRequest) { + const session = await auth.api.getSession({ headers: await headers() }); + if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + + const form = await req.formData(); + const file = form.get("file") as File | null; + if (!file) return NextResponse.json({ error: "Missing file" }, { status: 400 }); + + const ext = file.name.split(".").pop() ?? "bin"; + const key = `homework/${session.user.id}/${randomUUID()}.${ext}`; + const buffer = Buffer.from(await file.arrayBuffer()); + const url = await uploadFile(key, buffer, file.type); + + return NextResponse.json({ name: file.name, url, size: file.size }); +} diff --git a/src/app/curator/dashboard/page.tsx b/src/app/curator/dashboard/page.tsx index f807f27..e5c0031 100644 --- a/src/app/curator/dashboard/page.tsx +++ b/src/app/curator/dashboard/page.tsx @@ -1,43 +1,57 @@ import { headers } from "next/headers"; import { auth } from "@/lib/auth"; import { redirect } from "next/navigation"; -import { LogoutButton } from "@/components/layout/logout-button"; +import { prisma } from "@/lib/prisma"; +import Link from "next/link"; export default async function CuratorDashboard() { const session = await auth.api.getSession({ headers: await headers() }); - if (!session) redirect("/login"); - if (session.user.role !== "curator" && session.user.role !== "admin") { - redirect("/dashboard"); - } + + const [pending, total, recentFeedbacks] = await Promise.all([ + prisma.homeworkSubmission.count({ where: { feedbacks: { none: {} } } }), + prisma.homeworkSubmission.count(), + prisma.homeworkFeedback.count({ + where: { + createdAt: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }, + curatorId: session.user.id, + }, + }), + ]); return ( -
-
-

Second Brain — Куратор

-
- {session.user.name} - +
+

Обзор

+

Панель куратора

+ +
+ 0} /> + + +
+ + {pending > 0 ? ( + + Перейти к проверке ({pending}) → + + ) : ( +
+

+

Все работы проверены

+

Новых заданий нет

-
-
-

- Панель куратора -

-

Здесь будут домашние задания на проверку.

-
-
-

📝

-

Домашние задания

-

Новых заданий нет

-
-
-

👥

-

Мои ученики

-

Откроется в Этапе 3

-
-
-
+ )} +
+ ); +} + +function StatCard({ label, value, accent }: { label: string; value: number; accent?: boolean }) { + return ( +
+

+ {value} +

+

{label}

); } diff --git a/src/app/curator/homework/[submissionId]/actions.ts b/src/app/curator/homework/[submissionId]/actions.ts new file mode 100644 index 0000000..078d518 --- /dev/null +++ b/src/app/curator/homework/[submissionId]/actions.ts @@ -0,0 +1,20 @@ +"use server"; + +import { prisma } from "@/lib/prisma"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; +import { revalidatePath } from "next/cache"; + +export async function submitFeedback(submissionId: string, text: string) { + const session = await auth.api.getSession({ headers: await headers() }); + if (!session || (session.user.role !== "curator" && session.user.role !== "admin")) { + throw new Error("Forbidden"); + } + + await prisma.homeworkFeedback.create({ + data: { submissionId, curatorId: session.user.id, text }, + }); + + revalidatePath("/curator/homework"); + revalidatePath(`/curator/homework/${submissionId}`); +} diff --git a/src/app/curator/homework/[submissionId]/feedback-form.tsx b/src/app/curator/homework/[submissionId]/feedback-form.tsx new file mode 100644 index 0000000..a0f6042 --- /dev/null +++ b/src/app/curator/homework/[submissionId]/feedback-form.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; +import { submitFeedback } from "./actions"; + +export function FeedbackForm({ submissionId }: { submissionId: string }) { + const [text, setText] = useState(""); + const [pending, startTransition] = useTransition(); + const router = useRouter(); + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!text.trim()) return; + startTransition(async () => { + await submitFeedback(submissionId, text.trim()); + router.push("/curator/homework"); + }); + } + + return ( +
+

+ Написать фидбек +

+