fix: remove fallback content, fix video upload and positioning

- Remove hardcoded fallback data — DB is sole content source
- Sections render conditionally when data exists
- Hero video slots save after each upload (not only when all 3 filled)
- Video positions preserved (left/center/right) with empty string slots
- Client-side 10MB hard limit on video uploads with clear error
- Server-side upload error handling for body size limits
- Guard Team section against empty members array
- Clean up old uploaded images and videos
This commit is contained in:
2026-03-29 22:17:11 +03:00
parent 77ad2a6b68
commit e56a6a1608
66 changed files with 75 additions and 66 deletions
+19 -15
View File
@@ -6,7 +6,7 @@ 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 = 8;
const MAX_VIDEO_SIZE_MB = 10;
const MAX_VIDEO_SIZE_BYTES = MAX_VIDEO_SIZE_MB * 1024 * 1024;
function formatFileSize(bytes: number): string {
@@ -209,10 +209,8 @@ function VideoManager({
const syncToParent = useCallback(
(updated: (string | null)[]) => {
setSlots(updated);
// Only propagate when all 3 are filled
if (updated.every((s) => s !== null)) {
onChange(updated as string[]);
}
// Save all 3 slots (empty string for unfilled) to preserve positions
onChange(updated.map((s) => s || ""));
},
[onChange]
);
@@ -246,10 +244,10 @@ function VideoManager({
async function handleUpload(idx: number, file: File) {
if (file.size > MAX_VIDEO_SIZE_BYTES) {
const sizeMb = (file.size / (1024 * 1024)).toFixed(1);
setSizeWarning(`Видео ${sizeMb} МБ — рекомендуем до ${MAX_VIDEO_SIZE_MB} МБ для быстрой загрузки`);
} else {
setSizeWarning(null);
alert(`Видео ${sizeMb} МБ — максимум ${MAX_VIDEO_SIZE_MB} МБ. Сожмите видео и попробуйте снова.`);
return;
}
setSizeWarning(null);
setUploadingIdx(idx);
try {
const form = new FormData();
@@ -260,14 +258,21 @@ function VideoManager({
body: form,
});
if (!res.ok) {
const err = await res.json();
alert(err.error || "Ошибка загрузки");
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);
}
@@ -276,8 +281,7 @@ function VideoManager({
function handleRemove(idx: number) {
const updated = [...slots];
updated[idx] = null;
setSlots(updated);
// Don't propagate incomplete state — keep old saved videos in DB
syncToParent(updated);
}
const allFilled = slots.every((s) => s !== null);
@@ -351,17 +355,17 @@ export default function HeroEditorPage() {
<InputField
label="Заголовок"
value={data.headline}
value={data.headline || ""}
onChange={(v) => update({ ...data, headline: v })}
/>
<InputField
label="Подзаголовок"
value={data.subheadline}
value={data.subheadline || ""}
onChange={(v) => update({ ...data, subheadline: v })}
/>
<InputField
label="Текст кнопки"
value={data.ctaText}
value={data.ctaText || ""}
onChange={(v) => update({ ...data, ctaText: v })}
/>
</>