feat: Hero-Hintergrundvideo im Admin, ScrollReveal + Bild-Perf auf Unterseiten
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m31s

Neues admin-editierbares Hintergrundvideo fuer den Startseiten-Hero
(home.hero.video, MP4/WebM) mit eigenem, magic-byte-validiertem
Upload-Endpunkt (app/api/admin/upload-video) analog zur bestehenden
Bild-Upload-Pipeline. Ausserdem: ScrollReveal-Animation von der
Startseite auf Imkerei, Herkunft, Unternehmen, Ernten, Wissen, Kontakt,
Shop sowie die Produkt-/Ernte-/Chargen-Detailseiten ausgerollt (bisher
nur Startseite), priority auf bislang lazy geladene Above-the-fold-Bilder
(Herkunft/Unternehmen) gesetzt, und passende sizes-Hints auf Grid-Bilder
(Produktkarten, Origin/Regional/Business-Teaser, Ernten-Liste) ergaenzt
statt voller Viewport-Breite.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Sonnet 5 committed 2026-09-11 14:53:14 +02:00
1 parent 398db5c5ec
commit 6814e9e2c8
25 files changed
+524 -119

No files matched your search

+6 -1
View File
@@ -5,7 +5,7 @@ import { revalidatePath } from "next/cache";
import { prisma } from "@/lib/prisma";
import { logAdminAction } from "@/lib/audit-log";
import { CONTENT_REGISTRY } from "@/lib/content-registry";
import { isValidUploadPath } from "@/lib/uploads";
import { isValidUploadPath, isValidVideoUploadPath } from "@/lib/uploads";
import { requireAdmin } from "./_shared";
/**
@@ -43,6 +43,11 @@ export async function updateSiteContentAction(formData: FormData): Promise<void>
if (def.type === "image" && valueDe !== "" && !isValidUploadPath(valueDe)) {
continue;
}
// Analog zum Bild-Feld: ein Video-Feld darf nur einen Pfad enthalten,
// den app/api/admin/upload-video/route.ts tatsaechlich erzeugt hat.
if (def.type === "video" && valueDe !== "" && !isValidVideoUploadPath(valueDe)) {
continue;
}
updates.push({ key: def.key, valueDe, valueEn: null });
}
}
+15 -3
View File
@@ -1,6 +1,7 @@
import { prisma } from "@/lib/prisma";
import { BackLink } from "@/components/back-link";
import { AdminImageField } from "@/components/admin-image-field";
import { AdminVideoField } from "@/components/admin-video-field";
import { CONTENT_REGISTRY, type ContentFieldDef } from "@/lib/content-registry";
import { updateSiteContentAction } from "../actions";
import { getLocale } from "@/i18n/locale";
@@ -13,10 +14,11 @@ const ROWS_BY_TYPE: Record<ContentFieldDef["type"], number> = {
"list-titled": 5,
"list-numbered": 6,
"richtext-sections": 14,
// Bild-Feld hat kein <textarea>, nutzt ROWS_BY_TYPE also nicht — Wert nur
// vorhanden, damit dieses Record<ContentFieldDef["type"], number> weiter
// vollstaendig ist.
// Bild-/Video-Felder haben kein <textarea>, nutzen ROWS_BY_TYPE also nicht
// — Werte nur vorhanden, damit dieses Record<ContentFieldDef["type"], number>
// weiter vollstaendig ist.
image: 0,
video: 0,
};
async function loadCurrentValues(): Promise<Map<string, { valueDe: string; valueEn: string | null }>> {
@@ -77,6 +79,16 @@ export default async function AdminInhaltePage(props: PageProps<"/admin/inhalte"
/>
);
}
if (def.type === "video") {
return (
<AdminVideoField
key={def.key}
name={def.key}
label={label}
initialValue={row?.valueDe ?? ""}
/>
);
}
return (
<label key={def.key} className="block text-sm">
<span className="text-ink/70">{label}</span>