"use client"; import { useState, useRef, useCallback, useEffect } from "react"; import { SectionEditor } from "../_components/SectionEditor"; import { InputField } from "../_components/FormField"; import { adminFetch } from "@/lib/csrf"; import { Upload, X, Loader2, Smartphone, Monitor, Star } from "lucide-react"; const MAX_VIDEO_SIZE_MB = 10; const MAX_VIDEO_SIZE_BYTES = MAX_VIDEO_SIZE_MB * 1024 * 1024; function formatFileSize(bytes: number): string { if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} КБ`; return `${(bytes / (1024 * 1024)).toFixed(1)} МБ`; } interface HeroData { headline: string; subheadline: string; ctaText: string; videos?: string[]; } const SLOTS = [ { key: "left", label: "Левое", sublabel: "Диагональ слева" }, { key: "center", label: "Центр", sublabel: "Главное видео" }, { key: "right", label: "Правое", sublabel: "Диагональ справа" }, ] as const; function VideoSlot({ label, sublabel, src, isCenter, onUpload, onRemove, uploading, }: { label: string; sublabel: string; src: string | null; isCenter: boolean; onUpload: (file: File) => void; onRemove: () => void; uploading: boolean; }) { const fileRef = useRef(null); const videoRef = useRef(null); const [fileSize, setFileSize] = useState(null); // Fetch file size via HEAD request useEffect(() => { if (!src) { setFileSize(null); return; } fetch(src, { method: "HEAD" }) .then((r) => { const len = r.headers.get("content-length"); if (len) setFileSize(parseInt(len, 10)); }) .catch(() => {}); }, [src]); const isLarge = fileSize !== null && fileSize > MAX_VIDEO_SIZE_BYTES; return (
{/* Label */}
{label} {isCenter && ( мобильная версия )}

{sublabel}

{/* Slot */} {src ? (
videoRef.current?.play()} onMouseLeave={() => { videoRef.current?.pause(); }} >
) : ( )} { const file = e.target.files?.[0]; if (file) onUpload(file); if (fileRef.current) fileRef.current.value = ""; }} />
); } function VideoSizeInfo({ totalSize, totalMb, rating }: { totalSize: number; totalMb: number; rating: { label: string; color: string } }) { const [open, setOpen] = useState(false); return ( ); } function VideoManager({ videos, onChange, }: { videos: string[]; onChange: (videos: string[]) => void; }) { const [slots, setSlots] = useState<(string | null)[]>(() => [ videos[0] || null, videos[1] || null, videos[2] || null, ]); const [uploadingIdx, setUploadingIdx] = useState(null); const syncToParent = useCallback( (updated: (string | null)[]) => { setSlots(updated); // Save all 3 slots (empty string for unfilled) to preserve positions onChange(updated.map((s) => s || "")); }, [onChange] ); const [sizeWarning, setSizeWarning] = useState(null); const [fileSizes, setFileSizes] = useState<(number | null)[]>([null, null, null]); // Fetch file sizes for all slots useEffect(() => { slots.forEach((src, i) => { if (!src) { setFileSizes((p) => { const n = [...p]; n[i] = null; return n; }); return; } fetch(src, { method: "HEAD" }) .then((r) => { const len = r.headers.get("content-length"); if (len) setFileSizes((p) => { const n = [...p]; n[i] = parseInt(len, 10); return n; }); }) .catch(() => {}); }); }, [slots]); const totalSize = fileSizes.reduce((sum: number, s) => sum + (s || 0), 0); const totalMb = totalSize / (1024 * 1024); function getLoadRating(mb: number): { label: string; color: string } { if (mb <= 15) return { label: "Быстрая загрузка", color: "text-emerald-400" }; if (mb <= 24) return { label: "Нормальная загрузка", color: "text-blue-400" }; if (mb <= 40) return { label: "Медленная загрузка", color: "text-amber-400" }; return { label: "Очень медленная загрузка", color: "text-red-400" }; } async function handleUpload(idx: number, file: File) { if (file.size > MAX_VIDEO_SIZE_BYTES) { const sizeMb = (file.size / (1024 * 1024)).toFixed(1); alert(`Видео ${sizeMb} МБ — максимум ${MAX_VIDEO_SIZE_MB} МБ. Сожмите видео и попробуйте снова.`); return; } setSizeWarning(null); setUploadingIdx(idx); try { const form = new FormData(); form.append("file", file); form.append("folder", "hero"); const res = await adminFetch("/api/admin/upload", { method: "POST", body: form, }); if (!res.ok) { const text = await res.text(); let msg = "Ошибка загрузки"; try { const err = JSON.parse(text); msg = err.error || msg; } catch { /* empty response */ } alert(`${msg} (${res.status})`); return; } const { path } = await res.json(); const updated = [...slots]; updated[idx] = path; syncToParent(updated); } catch (e) { alert(`Ошибка сети: ${e instanceof Error ? e.message : "попробуйте снова"}`); } finally { setUploadingIdx(null); } } function handleRemove(idx: number) { const updated = [...slots]; updated[idx] = null; syncToParent(updated); } const allFilled = slots.every((s) => s !== null); const filledCount = slots.filter((s) => s !== null).length; return (
{!allFilled && ( Загружено {filledCount}/3 — загрузите все для сохранения )} {allFilled && ( ✓ Все видео загружены )}
{SLOTS.map((slot, i) => ( handleUpload(i, file)} onRemove={() => handleRemove(i)} /> ))}
ПК — диагональный сплит из 3 видео
Телефон — только центральное видео
{sizeWarning && (
⚠ {sizeWarning}
)} {/* Total size — collapsible */} {totalSize > 0 && }
); } export default function HeroEditorPage() { return ( sectionKey="hero" title="Главный экран"> {(data, update) => ( <> update({ ...data, videos })} /> update({ ...data, headline: v })} /> update({ ...data, subheadline: v })} /> update({ ...data, ctaText: v })} /> )} ); }