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

+99
View File
@@ -0,0 +1,99 @@
"use client";
import { AnimatePresence, LazyMotion, domAnimation, m, useReducedMotion } from "motion/react";
import Image from "next/image";
import { useEffect, useState } from "react";
import type { Media } from "@/content/site";
const INTERVAL_MS = 5500;
type ImageMedia = Extract<Media, { kind: "image" }>;
/**
* Bilderfolge der Villa: weiche Ueberblendung mit langsamem Zoom.
* Laeuft von selbst, laesst sich aber jederzeit anhalten (WCAG 2.2.2) und
* pausiert bei Maus oder Tastaturfokus. Bei reduzierter Bewegung kein
* automatischer Wechsel und kein Zoom, nur die Punkte zum Umschalten.
*/
export function Slideshow({ images, className = "" }: { images: readonly Media[]; className?: string }) {
const slides = images.filter((m): m is ImageMedia => m.kind === "image");
const reduce = useReducedMotion();
const [index, setIndex] = useState(0);
const [paused, setPaused] = useState(false);
const [hovered, setHovered] = useState(false);
const running = !reduce && !paused && !hovered && slides.length > 1;
useEffect(() => {
if (!running) return;
const timer = window.setInterval(() => setIndex((i) => (i + 1) % slides.length), INTERVAL_MS);
return () => window.clearInterval(timer);
}, [running, slides.length]);
if (slides.length === 0) return null;
const current = slides[index]!;
return (
<LazyMotion features={domAnimation}>
<figure
className={className}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onFocus={() => setHovered(true)}
onBlur={() => setHovered(false)}
>
<div className="relative aspect-[4/5] overflow-hidden rounded-2xl bg-sand md:aspect-[5/6]">
<AnimatePresence initial={false}>
<m.div
key={current.src}
className="absolute inset-0"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: reduce ? 0 : 1.2, ease: "easeInOut" }}
>
<m.div
className="absolute inset-0"
initial={{ scale: reduce ? 1 : 1.08 }}
animate={{ scale: 1 }}
transition={{ duration: reduce ? 0 : INTERVAL_MS / 1000 + 1.2, ease: "linear" }}
>
<Image src={current.src} alt={current.alt} fill sizes="(min-width: 768px) 50vw, 100vw" className="object-cover" />
</m.div>
</m.div>
</AnimatePresence>
</div>
<div className="mt-4 flex items-center justify-between gap-4">
<figcaption aria-live={running ? "off" : "polite"} className="text-sm text-stone">
{current.alt}
</figcaption>
<div className="flex shrink-0 items-center gap-1">
{slides.map((slide, i) => (
<button
key={slide.src}
type="button"
onClick={() => setIndex(i)}
aria-label={`Bild ${i + 1} von ${slides.length} zeigen`}
aria-current={i === index ? "true" : undefined}
className="grid h-8 w-6 place-items-center"
>
<span className={`block h-1.5 rounded-full transition-all duration-300 ${i === index ? "w-5 bg-ink" : "w-1.5 bg-ink/30"}`} />
</button>
))}
{!reduce && slides.length > 1 && (
<button
type="button"
onClick={() => setPaused((p) => !p)}
aria-label={paused ? "Bilderfolge abspielen" : "Bilderfolge anhalten"}
className="ml-2 grid h-9 w-9 place-items-center rounded-full border border-ink/25 hover:border-ink"
>
<svg viewBox="0 0 24 24" className="h-4 w-4" aria-hidden>
{paused ? <path d="M8 5v14l11-7z" fill="currentColor" /> : <path d="M7 5h3v14H7zM14 5h3v14h-3z" fill="currentColor" />}
</svg>
</button>
)}
</div>
</div>
</figure>
</LazyMotion>
);
}