Files
lms-sb/src/components/admin/lesson-files-manager.tsx
T
admins 39d84a3db2 Add labeled file materials with format badge
- Store human-readable label in LessonFile.name via optional label field on upload
- Add PATCH endpoint to rename existing files inline
- Admin: label input before upload, click-to-edit inline rename
- Student: colored format badge (PDF/DOCX/XLSX/ZIP/etc) replaces paperclip emoji

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 11:55:07 +05:00

174 lines
5.8 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, useRef } from "react";
import { FileFormatBadge } from "@/components/shared/file-format-badge";
interface LessonFile {
id: string;
name: string;
url: string;
size: number;
}
export function LessonFilesManager({ lessonId, initialFiles }: { lessonId: string; initialFiles: LessonFile[] }) {
const [files, setFiles] = useState(initialFiles);
const [uploading, setUploading] = useState(false);
const [labelInput, setLabelInput] = useState("");
const [editingId, setEditingId] = useState<string | null>(null);
const [editingLabel, setEditingLabel] = useState("");
const fileInputRef = useRef<HTMLInputElement>(null);
async function handleUpload(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
const fd = new FormData();
fd.append("file", file);
fd.append("lessonId", lessonId);
if (labelInput.trim()) fd.append("label", labelInput.trim());
const res = await fetch("/api/admin/lesson-files", { method: "POST", body: fd });
const created = await res.json();
if (created.id) {
setFiles((prev) => [...prev, created]);
setLabelInput("");
}
setUploading(false);
e.target.value = "";
}
async function handleDelete(fileId: string) {
if (!confirm("Удалить файл?")) return;
await fetch("/api/admin/lesson-files", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ fileId }),
});
setFiles((prev) => prev.filter((f) => f.id !== fileId));
}
async function saveLabel(fileId: string) {
const trimmed = editingLabel.trim();
if (!trimmed) return cancelEdit();
const res = await fetch("/api/admin/lesson-files", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ fileId, label: trimmed }),
});
const updated = await res.json();
if (updated.id) {
setFiles((prev) => prev.map((f) => (f.id === fileId ? { ...f, name: updated.name } : f)));
}
cancelEdit();
}
function startEdit(file: LessonFile) {
setEditingId(file.id);
setEditingLabel(file.name);
}
function cancelEdit() {
setEditingId(null);
setEditingLabel("");
}
function formatSize(bytes: number) {
if (bytes < 1024) return `${bytes} Б`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} КБ`;
return `${(bytes / 1024 / 1024).toFixed(1)} МБ`;
}
return (
<div className="space-y-3">
{files.length > 0 && (
<div className="space-y-2">
{files.map((f) => (
<div
key={f.id}
className="flex items-center gap-3 px-3 py-2.5 text-sm"
style={{ border: "2px solid var(--border)", background: "var(--color-surface)" }}
>
<FileFormatBadge url={f.url} />
{editingId === f.id ? (
<input
autoFocus
value={editingLabel}
onChange={(e) => setEditingLabel(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") saveLabel(f.id);
if (e.key === "Escape") cancelEdit();
}}
onBlur={() => saveLabel(f.id)}
className="flex-1 text-sm px-2 py-0.5"
style={{
border: "1px solid var(--foreground)",
background: "var(--background)",
outline: "none",
fontFamily: "inherit",
}}
/>
) : (
<button
type="button"
onClick={() => startEdit(f)}
className="flex-1 text-left font-medium"
title="Нажмите, чтобы изменить название"
style={{ color: "var(--foreground)" }}
>
{f.name}
</button>
)}
<span className="text-xs" style={{ color: "var(--muted-foreground)" }}>
{formatSize(f.size)}
</span>
<button
type="button"
onClick={() => handleDelete(f.id)}
className="text-xs"
style={{ color: "oklch(0.577 0.245 27.325)" }}
>
Удалить
</button>
</div>
))}
</div>
)}
<div className="flex items-center gap-2">
<input
type="text"
value={labelInput}
onChange={(e) => setLabelInput(e.target.value)}
placeholder="Название (например, Презентация)"
className="flex-1 text-sm px-3 py-2"
style={{
border: "2px solid var(--border)",
background: "var(--background)",
outline: "none",
fontFamily: "inherit",
}}
onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")}
onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")}
/>
<button
type="button"
onClick={() => fileInputRef.current?.click()}
disabled={uploading}
className="btn-aubade text-xs whitespace-nowrap"
style={{ flexShrink: 0, opacity: uploading ? 0.6 : 1 }}
>
{uploading ? "Загрузка..." : "+ Добавить файл"}
</button>
<input
ref={fileInputRef}
type="file"
className="sr-only"
onChange={handleUpload}
disabled={uploading}
accept=".pdf,.zip,.docx,.xlsx,.doc,.xls,.pptx,.ppt,.mp4,.mp3"
/>
</div>
<p className="text-xs" style={{ color: "var(--muted-foreground)" }}>PDF, ZIP, DOCX, XLSX до 100 МБ</p>
</div>
);
}