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:
+19
-15
@@ -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 })}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getSection, setSection, SECTION_KEYS } from "@/lib/db";
|
||||
import { siteContent } from "@/data/content";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { invalidateContentCache } from "@/lib/content";
|
||||
|
||||
@@ -12,16 +11,10 @@ export async function GET(_request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ error: "Invalid section key" }, { status: 400 });
|
||||
}
|
||||
|
||||
let data = getSection(key);
|
||||
const data = getSection(key);
|
||||
if (!data) {
|
||||
// Auto-seed from fallback content if section doesn't exist yet
|
||||
const fallback = (siteContent as unknown as Record<string, unknown>)[key];
|
||||
if (fallback) {
|
||||
setSection(key, fallback);
|
||||
data = fallback;
|
||||
} else {
|
||||
return NextResponse.json({ error: "Section not found" }, { status: 404 });
|
||||
}
|
||||
// Return empty object so admin editor can initialize fields
|
||||
return NextResponse.json({});
|
||||
}
|
||||
|
||||
return NextResponse.json(data, {
|
||||
|
||||
@@ -10,10 +10,19 @@ const IMAGE_FOLDERS = ["team", "master-classes", "news", "classes"];
|
||||
const VIDEO_FOLDERS = ["hero"];
|
||||
const ALL_FOLDERS = [...IMAGE_FOLDERS, ...VIDEO_FOLDERS];
|
||||
const IMAGE_MAX_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
const VIDEO_MAX_SIZE = 50 * 1024 * 1024; // 50MB
|
||||
const VIDEO_MAX_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const formData = await request.formData();
|
||||
let formData: FormData;
|
||||
try {
|
||||
formData = await request.formData();
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : "Upload failed";
|
||||
return NextResponse.json(
|
||||
{ error: msg.includes("size") || msg.includes("multipart") ? "Файл слишком большой (макс. 10 МБ)" : `Ошибка загрузки: ${msg}` },
|
||||
{ status: 413 }
|
||||
);
|
||||
}
|
||||
const file = formData.get("file") as File | null;
|
||||
const rawFolder = (formData.get("folder") as string) || "team";
|
||||
const folder = ALL_FOLDERS.includes(rawFolder) ? rawFolder : "team";
|
||||
@@ -26,16 +35,16 @@ export async function POST(request: NextRequest) {
|
||||
const allowedTypes = isVideoFolder ? VIDEO_TYPES : IMAGE_TYPES;
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
const msg = isVideoFolder
|
||||
? "Only MP4 and WebM videos are allowed"
|
||||
: "Only JPEG, PNG, WebP, and AVIF are allowed";
|
||||
? "Допустимы только MP4 и WebM"
|
||||
: "Допустимы только JPEG, PNG, WebP и AVIF";
|
||||
return NextResponse.json({ error: msg }, { status: 400 });
|
||||
}
|
||||
|
||||
const maxSize = isVideoFolder ? VIDEO_MAX_SIZE : IMAGE_MAX_SIZE;
|
||||
if (file.size > maxSize) {
|
||||
const label = isVideoFolder ? "50MB" : "5MB";
|
||||
const label = isVideoFolder ? "10 МБ" : "5 МБ";
|
||||
return NextResponse.json(
|
||||
{ error: `File too large (max ${label})` },
|
||||
{ error: `Файл слишком большой (макс. ${label})` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
@@ -45,7 +54,7 @@ export async function POST(request: NextRequest) {
|
||||
const allowedExts = isVideoFolder ? VIDEO_EXTENSIONS : IMAGE_EXTENSIONS;
|
||||
if (!allowedExts.includes(ext)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid file extension" },
|
||||
{ error: "Недопустимое расширение файла" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
+7
-5
@@ -22,13 +22,15 @@ const oswald = localFont({
|
||||
});
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
const { meta } = getContent();
|
||||
const content = getContent();
|
||||
const title = content?.meta?.title || "BLACK HEART DANCE HOUSE";
|
||||
const description = content?.meta?.description || "";
|
||||
return {
|
||||
title: meta.title,
|
||||
description: meta.description,
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title: meta.title,
|
||||
description: meta.description,
|
||||
title,
|
||||
description,
|
||||
locale: "ru_RU",
|
||||
type: "website",
|
||||
},
|
||||
|
||||
+20
-18
@@ -33,24 +33,26 @@ export default function HomePage() {
|
||||
<ClientShell>
|
||||
<Header />
|
||||
<main id="main-content">
|
||||
<Hero data={content.hero} />
|
||||
<About
|
||||
data={content.about}
|
||||
stats={{
|
||||
trainers: content.team.members.length,
|
||||
classes: content.classes.items.length,
|
||||
locations: content.schedule.locations.length,
|
||||
}}
|
||||
/>
|
||||
<Classes data={content.classes} />
|
||||
<Team data={content.team} schedule={content.schedule.locations} />
|
||||
{openDayData && <OpenDay data={openDayData} popups={content.popups} teamMembers={content.team.members} />}
|
||||
<Schedule data={content.schedule} scheduleConfig={content.scheduleConfig} classItems={content.classes.items} teamMembers={content.team.members} />
|
||||
<Pricing data={content.pricing} />
|
||||
<MasterClasses data={content.masterClasses} regCounts={mcRegCounts} popups={content.popups} />
|
||||
<News data={content.news} />
|
||||
<FAQ data={content.faq} />
|
||||
<Contact data={content.contact} />
|
||||
{content?.hero && <Hero data={content.hero} />}
|
||||
{content?.about && (
|
||||
<About
|
||||
data={content.about}
|
||||
stats={{
|
||||
trainers: content.team?.members?.length ?? 0,
|
||||
classes: content.classes?.items?.length ?? 0,
|
||||
locations: content.schedule?.locations?.length ?? 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{content?.classes && <Classes data={content.classes} />}
|
||||
{content?.team && <Team data={content.team} schedule={content.schedule?.locations} />}
|
||||
{openDayData && content?.popups && <OpenDay data={openDayData} popups={content.popups} teamMembers={content.team?.members ?? []} />}
|
||||
{content?.schedule && <Schedule data={content.schedule} scheduleConfig={content.scheduleConfig} classItems={content.classes?.items ?? []} teamMembers={content.team?.members ?? []} />}
|
||||
{content?.pricing && <Pricing data={content.pricing} />}
|
||||
{content?.masterClasses && <MasterClasses data={content.masterClasses} regCounts={mcRegCounts} popups={content.popups} />}
|
||||
{content?.news && <News data={content.news} />}
|
||||
{content?.faq && <FAQ data={content.faq} />}
|
||||
{content?.contact && <Contact data={content.contact} />}
|
||||
<BackToTop />
|
||||
<FloatingContact />
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user