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
+94
View File
@@ -0,0 +1,94 @@
"use client";
import { useState, useTransition } from "react";
import { saveHomework, deleteHomework } from "@/app/admin/courses/[courseId]/modules/[moduleId]/lessons/[lessonId]/homework-actions";
interface Props {
lessonId: string;
initial: { id: string; description: string } | null;
}
export function HomeworkEditor({ lessonId, initial }: Props) {
const [editing, setEditing] = useState(!initial);
const [text, setText] = useState(initial?.description ?? "");
const [saved, setSaved] = useState(false);
const [pending, startTransition] = useTransition();
const inputStyle = {
border: "2px solid var(--border)",
background: "var(--background)",
outline: "none",
width: "100%",
padding: "0.5rem 0.75rem",
fontSize: "0.875rem",
fontFamily: "inherit",
resize: "vertical" as const,
minHeight: "120px",
};
function handleSave() {
if (!text.trim()) return;
startTransition(async () => {
await saveHomework(lessonId, text.trim());
setSaved(true);
setEditing(false);
setTimeout(() => setSaved(false), 2000);
});
}
function handleDelete() {
if (!confirm("Удалить домашнее задание? Все сданные работы будут удалены.")) return;
startTransition(async () => {
await deleteHomework(lessonId);
setText("");
setEditing(true);
});
}
if (!editing && initial) {
return (
<div className="space-y-3">
<div className="px-4 py-3 text-sm whitespace-pre-wrap" style={{ border: "2px solid var(--border)", background: "var(--color-surface)" }}>
{text || initial.description}
</div>
<div className="flex gap-2">
<button onClick={() => setEditing(true)} className="btn-aubade text-xs px-3 py-1.5">
Редактировать
</button>
<button onClick={handleDelete} disabled={pending} className="text-xs px-3 py-1.5" style={{ color: "oklch(0.577 0.245 27.325)" }}>
Удалить ДЗ
</button>
{saved && <span className="text-xs self-center" style={{ color: "var(--muted-foreground)" }}> Сохранено</span>}
</div>
</div>
);
}
return (
<div className="space-y-3">
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
style={inputStyle}
placeholder="Опишите задание для студентов..."
onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")}
onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")}
/>
<div className="flex items-center gap-2">
<button
onClick={handleSave}
disabled={pending || !text.trim()}
className="btn-aubade btn-aubade-accent text-xs px-4 py-1.5"
style={{ opacity: pending || !text.trim() ? 0.6 : 1 }}
>
{pending ? "Сохранение..." : "Сохранить задание"}
</button>
{initial && (
<button onClick={() => { setEditing(false); setText(initial.description); }} className="text-xs" style={{ color: "var(--muted-foreground)" }}>
Отмена
</button>
)}
</div>
</div>
);
}