Files
lms-sb/src/components/admin/course-edit-form.tsx
T
admins 48a9398905 Add optional audio response for students in homework submissions
- Course: add allowAudio toggle (per-course setting, off by default)
- HomeworkSubmission: add audioUrl field
- Student: AudioRecorder in homework form when allowAudio is enabled
- Student: show audio player in submission view and curator feedback view
- Curator: show student audio on submission detail page
- AudioRecorder: accept uploadUrl prop (reused for student/curator)
- API: /api/student/audio-upload route for S3 upload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 14:17:46 +05:00

144 lines
5.7 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.
"use client";
import { useState, useTransition } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { updateCourse, deleteCourse } from "@/app/admin/courses/actions";
interface Course {
id: string;
title: string;
slug: string;
description: string | null;
coverImage: string | null;
published: boolean;
allowAudio: boolean;
categoryId: string | null;
}
interface Category {
id: string;
title: string;
}
export function CourseEditForm({ course, categories = [] }: { course: Course; categories?: Category[] }) {
const [published, setPublished] = useState(course.published);
const [allowAudio, setAllowAudio] = useState(course.allowAudio);
const [coverImage, setCoverImage] = useState(course.coverImage ?? "");
const [categoryId, setCategoryId] = useState(course.categoryId ?? "");
const [uploading, setUploading] = useState(false);
const [pending, startTransition] = useTransition();
async function handleImageUpload(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
const fd = new FormData();
fd.append("file", file);
const res = await fetch("/api/admin/upload", { method: "POST", body: fd });
const data = await res.json();
if (data.url) setCoverImage(data.url);
setUploading(false);
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const fd = new FormData(e.currentTarget);
fd.set("published", String(published));
fd.set("allowAudio", String(allowAudio));
fd.set("coverImage", coverImage);
fd.set("categoryId", categoryId);
startTransition(() => updateCourse(course.id, fd));
}
function handleDelete() {
if (!confirm("Удалить курс? Это действие нельзя отменить.")) return;
startTransition(() => deleteCourse(course.id));
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label htmlFor="title">Название</Label>
<Input id="title" name="title" defaultValue={course.title} required />
</div>
<div className="space-y-1.5">
<Label htmlFor="slug">Slug</Label>
<Input id="slug" name="slug" defaultValue={course.slug} required />
</div>
</div>
<div className="space-y-1.5">
<Label htmlFor="description">Описание</Label>
<Textarea id="description" name="description" defaultValue={course.description ?? ""} rows={3} />
</div>
{categories.length > 0 && (
<div className="space-y-1.5">
<Label htmlFor="categoryId">Категория</Label>
<select
id="categoryId"
value={categoryId}
onChange={(e) => setCategoryId(e.target.value)}
className="w-full px-3 py-2 text-sm bg-transparent"
style={{ border: "2px solid var(--border)", color: "var(--foreground)", fontFamily: "var(--font-sans)" }}
>
<option value="">Без категории</option>
{categories.map((c) => (
<option key={c.id} value={c.id}>{c.title}</option>
))}
</select>
</div>
)}
<div className="space-y-1.5">
<Label>Обложка</Label>
<div className="flex items-center gap-3">
{coverImage && (
// eslint-disable-next-line @next/next/no-img-element
<img src={coverImage} alt="cover" className="w-16 h-10 object-cover rounded-md border" />
)}
<Input type="file" accept="image/*" onChange={handleImageUpload} disabled={uploading} className="max-w-xs" />
{uploading && <span className="text-sm text-slate-400">Загрузка...</span>}
</div>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<button
type="button"
role="switch"
aria-checked={published}
onClick={() => setPublished(!published)}
className={`relative w-10 h-6 rounded-full transition-colors ${published ? "bg-green-500" : "bg-slate-300"}`}
>
<span className={`absolute top-1 left-1 w-4 h-4 bg-white rounded-full shadow transition-transform ${published ? "translate-x-4" : ""}`} />
</button>
<span className="text-sm text-slate-600">{published ? "Опубликован" : "Черновик"}</span>
</div>
<div className="flex items-center gap-2">
<button
type="button"
role="switch"
aria-checked={allowAudio}
onClick={() => setAllowAudio(!allowAudio)}
className={`relative w-10 h-6 rounded-full transition-colors ${allowAudio ? "bg-blue-500" : "bg-slate-300"}`}
>
<span className={`absolute top-1 left-1 w-4 h-4 bg-white rounded-full shadow transition-transform ${allowAudio ? "translate-x-4" : ""}`} />
</button>
<span className="text-sm text-slate-600">
{allowAudio ? "🎤 Аудио-ответ в ДЗ включён" : "Аудио-ответ в ДЗ выключен"}
</span>
</div>
</div>
<div className="flex justify-between pt-2">
<Button type="button" variant="destructive" onClick={handleDelete} disabled={pending}>
Удалить курс
</Button>
<Button type="submit" disabled={pending || uploading}>
{pending ? "Сохранение..." : "Сохранить"}
</Button>
</div>
</form>
);
}