feat: hero video management — diagonal split, mobile single video, admin editor
- Hero: diagonal 3-video split on desktop, single center video on mobile (CSS breakpoint) - Videos render client-side only to avoid hydration mismatch - Loading overlay (z-5) hides buffering without blocking text content - Admin hero editor: 3 fixed slots (left/center/right) with upload, preview, remove - Center slot marked as main (used on mobile), save blocked until all 3 filled - Upload API: supports MP4/WebM (50MB) in hero folder alongside existing image uploads - Added videos field to hero type and fallback data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,38 +2,48 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { writeFile, mkdir } from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp", "image/avif"];
|
||||
const ALLOWED_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".avif"];
|
||||
const ALLOWED_FOLDERS = ["team", "master-classes", "news", "classes"];
|
||||
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
const IMAGE_TYPES = ["image/jpeg", "image/png", "image/webp", "image/avif"];
|
||||
const VIDEO_TYPES = ["video/mp4", "video/webm"];
|
||||
const IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".avif"];
|
||||
const VIDEO_EXTENSIONS = [".mp4", ".webm"];
|
||||
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
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get("file") as File | null;
|
||||
const rawFolder = (formData.get("folder") as string) || "team";
|
||||
const folder = ALLOWED_FOLDERS.includes(rawFolder) ? rawFolder : "team";
|
||||
const folder = ALL_FOLDERS.includes(rawFolder) ? rawFolder : "team";
|
||||
const isVideoFolder = VIDEO_FOLDERS.includes(folder);
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: "No file provided" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!ALLOWED_TYPES.includes(file.type)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Only JPEG, PNG, WebP, and AVIF are allowed" },
|
||||
{ status: 400 }
|
||||
);
|
||||
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";
|
||||
return NextResponse.json({ error: msg }, { status: 400 });
|
||||
}
|
||||
|
||||
if (file.size > MAX_SIZE) {
|
||||
const maxSize = isVideoFolder ? VIDEO_MAX_SIZE : IMAGE_MAX_SIZE;
|
||||
if (file.size > maxSize) {
|
||||
const label = isVideoFolder ? "50MB" : "5MB";
|
||||
return NextResponse.json(
|
||||
{ error: "File too large (max 5MB)" },
|
||||
{ error: `File too large (max ${label})` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate and sanitize filename
|
||||
const ext = path.extname(file.name).toLowerCase() || ".webp";
|
||||
if (!ALLOWED_EXTENSIONS.includes(ext)) {
|
||||
const ext = path.extname(file.name).toLowerCase() || (isVideoFolder ? ".mp4" : ".webp");
|
||||
const allowedExts = isVideoFolder ? VIDEO_EXTENSIONS : IMAGE_EXTENSIONS;
|
||||
if (!allowedExts.includes(ext)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid file extension" },
|
||||
{ status: 400 }
|
||||
@@ -47,13 +57,14 @@ export async function POST(request: NextRequest) {
|
||||
.slice(0, 50);
|
||||
const fileName = `${baseName}-${Date.now()}${ext}`;
|
||||
|
||||
const dir = path.join(process.cwd(), "public", "images", folder);
|
||||
const subDir = isVideoFolder ? path.join("video") : path.join("images", folder);
|
||||
const dir = path.join(process.cwd(), "public", subDir);
|
||||
await mkdir(dir, { recursive: true });
|
||||
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
const filePath = path.join(dir, fileName);
|
||||
await writeFile(filePath, buffer);
|
||||
|
||||
const publicPath = `/images/${folder}/${fileName}`;
|
||||
const publicPath = `/${subDir.replace(/\\/g, "/")}/${fileName}`;
|
||||
return NextResponse.json({ path: publicPath });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user