feat: Onepage nach Filmcamp-Gliederung umgebaut

- Figtree als einzige Schrift, Logo mittig in fester Kopfzeile
- Headerbild ueber volle Breite und Hoehe, ohne Preis, mit Terminleiste
- "Was dich erwartet" stellt die Villa mit animierter Bilderfolge vor,
  darunter Tagesablauf als Karussell (Programm folgt als PDF)
- Preis: regulaerer Preis durchgestrichen, kein Verkaufsstartdatum,
  Ueberschrift "Jetzt Platz sichern mit Early Bird"
- Team als "Mit dabei", danach drei Reels im Hochformat
- Logo, Headerbild, Portraets und Reels ueber feste Dateinamen im
  Medienverzeichnis (docs/MEDIEN.md), ohne Neustart austauschbar
- Widerrufsbelehrung im Admin pflegbar, Seite /widerruf, Live-Verkauf
  ohne Freigabe gesperrt
- Anreise: Abholung am Bahnhof Firenze Santa Maria Novella
- Booking-Link entfernt, Gedankenstriche aus Kundentexten entfernt

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-01 23:53:18 +02:00
1 parent 3cba3e2341
commit 7203cd1e6e
37 files changed
+962 -517

No files matched your search

+20 -56
View File
@@ -4,50 +4,32 @@ import { useState } from "react";
import type { Media } from "@/content/site";
import { MediaFrame } from "./MediaFrame";
type Slot = { name: string | null; caption: string | null; poster: Media | null; src: string | null; externalId: string | null };
const EXTERNAL = {
"youtube-nocookie": { label: "YouTube", url: (id: string) => `https://www.youtube-nocookie.com/embed/${encodeURIComponent(id)}?autoplay=1&rel=0` },
vimeo: { label: "Vimeo", url: (id: string) => `https://player.vimeo.com/video/${encodeURIComponent(id)}?autoplay=1&dnt=1` },
} as const;
type Slot = { name: string | null; caption: string | null; poster: Media | null; src: string | null };
/**
* Ein Feedback-Video. Geladen wird erst nach Klick (preload="none"); es
* startet nie von selbst mit Ton. Externe Player (YouTube/Vimeo) werden nur
* geladen, wenn das Datenschutzkonzept sie erlaubt (EXTERNAL_VIDEO_PROVIDER)
* und die Besucherin dem Laden ausdruecklich zustimmt.
* Ein Reel im Hochformat (9:16) aus dem eigenen Medienverzeichnis. Das
* Video laedt erst nach Klick (preload="none") und startet nie von selbst
* mit Ton. Kein externer Anbieter, keine Datenuebertragung an Dritte.
*/
export function VideoSlot({ slot, index, provider, showDrafts }: { slot: Slot; index: number; provider: "none" | "youtube-nocookie" | "vimeo"; showDrafts: boolean }) {
const [stage, setStage] = useState<"poster" | "consent" | "playing">("poster");
const label = slot.name ? `Video von ${slot.name} abspielen` : `Feedback-Video ${index + 1} abspielen`;
const external = slot.externalId && provider !== "none" ? EXTERNAL[provider] : null;
const playable = Boolean(slot.src) || Boolean(external);
const poster: Media = slot.poster ?? { kind: "placeholder", label: `Feedback-Video ${index + 1} folgt`, tone: "studio" };
export function VideoSlot({ slot, index, showDrafts }: { slot: Slot; index: number; showDrafts: boolean }) {
const [playing, setPlaying] = useState(false);
const label = slot.name ? `Reel von ${slot.name} abspielen` : `Reel ${index + 1} abspielen`;
const poster: Media = slot.poster ?? { kind: "placeholder", label: `Reel ${index + 1} folgt`, tone: "studio" };
return (
<figure>
<div className="relative aspect-[4/5] overflow-hidden bg-ink">
{stage === "playing" && slot.src && (
<div className="relative aspect-[9/16] overflow-hidden rounded-2xl bg-ink">
{playing && slot.src ? (
<video src={slot.src} controls autoPlay playsInline preload="none" className="h-full w-full object-cover">
<track kind="captions" />
</video>
)}
{stage === "playing" && !slot.src && external && slot.externalId && (
<iframe
src={external.url(slot.externalId)}
title={slot.name ? `Video von ${slot.name}` : `Feedback-Video ${index + 1}`}
allow="autoplay; fullscreen; picture-in-picture"
allowFullScreen
className="h-full w-full"
/>
)}
{stage !== "playing" && (
) : (
<>
<MediaFrame media={poster} aspect="h-full w-full" sizes="(min-width: 768px) 33vw, 100vw" showLabel={showDrafts} />
{playable && stage === "poster" && (
<MediaFrame media={poster} aspect="h-full w-full" sizes="(min-width: 640px) 33vw, 100vw" showLabel={showDrafts} />
{slot.src && (
<button
type="button"
onClick={() => setStage(external && !slot.src ? "consent" : "playing")}
onClick={() => setPlaying(true)}
aria-label={label}
className="absolute inset-0 m-auto flex h-20 w-20 items-center justify-center rounded-full bg-paper/90 text-ink transition-transform hover:scale-105"
>
@@ -56,33 +38,15 @@ export function VideoSlot({ slot, index, provider, showDrafts }: { slot: Slot; i
</svg>
</button>
)}
{stage === "consent" && external && (
<div className="absolute inset-0 flex flex-col justify-end gap-3 bg-ink/85 p-5 text-paper">
<p className="text-[0.9375rem]">
Beim Abspielen wird eine Verbindung zu {external.label} hergestellt und Daten werden dorthin übertragen. Details in der{" "}
<a href="/datenschutz" className="underline">Datenschutzerklärung</a>.
</p>
<div className="flex gap-2">
<button type="button" className="btn bg-paper text-ink" onClick={() => setStage("playing")}>
Video laden
</button>
<button type="button" className="btn btn-quiet text-paper" onClick={() => setStage("poster")}>
Abbrechen
</button>
</div>
</div>
)}
</>
)}
</div>
<figcaption className="mt-4">
{slot.name ? (
<span className="block font-semibold">{slot.name}</span>
) : (
showDrafts && <span className="draft-flag">Name und Freigabe folgen</span>
)}
{slot.caption && <span className="mt-1 block text-ink/80">{slot.caption}</span>}
</figcaption>
{(slot.name || slot.caption || showDrafts) && (
<figcaption className="mt-4">
{slot.name ? <span className="block font-semibold">{slot.name}</span> : showDrafts && <span className="draft-flag">Name und Freigabe folgen</span>}
{slot.caption && <span className="mt-1 block opacity-80">{slot.caption}</span>}
</figcaption>
)}
</figure>
);
}