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>
This commit is contained in:
@@ -14,6 +14,7 @@ interface Course {
|
||||
description: string | null;
|
||||
coverImage: string | null;
|
||||
published: boolean;
|
||||
allowAudio: boolean;
|
||||
categoryId: string | null;
|
||||
}
|
||||
|
||||
@@ -24,6 +25,7 @@ interface Category {
|
||||
|
||||
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);
|
||||
@@ -45,6 +47,7 @@ export function CourseEditForm({ course, categories = [] }: { course: Course; ca
|
||||
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));
|
||||
@@ -99,17 +102,33 @@ export function CourseEditForm({ course, categories = [] }: { course: Course; ca
|
||||
{uploading && <span className="text-sm text-slate-400">Загрузка...</span>}
|
||||
</div>
|
||||
</div>
|
||||
<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 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}>
|
||||
|
||||
Reference in New Issue
Block a user