Das hochgeladene logo.png ist die helle Fassung und war auf der hellen Kopfzeile und im Fuß bis auf das goldene A unsichtbar. Die Seite misst jetzt die Helligkeit (sharp) und ordnet die Datei selbst zu; fehlt eine passende Fassung, wird per CSS abgedunkelt bzw. aufgehellt. Über dem Foto bis 176 px hoch mit weichem Schatten, nach dem Scrollen 80 px. Raster-Logos laufen über die Bildoptimierung (Original 5378 px, 313 KB). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
98 lines
4.7 KiB
TypeScript
98 lines
4.7 KiB
TypeScript
import Image from "next/image";
|
|
import { site, type Media } from "@/content/site";
|
|
import type { PublicCampFacts } from "@/lib/camp";
|
|
import { formatEuroShort } from "@/lib/money";
|
|
import type { PublicPricing } from "@/lib/public-page";
|
|
import { formatIsoDateLong } from "@/lib/time";
|
|
import { MediaFrame } from "./MediaFrame";
|
|
import { BookButton } from "./booking/BookingProvider";
|
|
|
|
export function dateLine(facts: PublicCampFacts): string | null {
|
|
if (!facts.startDate) return null;
|
|
if (!facts.endDate) return `ab ${formatIsoDateLong(facts.startDate)}`;
|
|
// Gleicher Monat: "8. bis 15. Mai 2027" statt zweimal Monat und Jahr.
|
|
if (facts.startDate.slice(0, 7) === facts.endDate.slice(0, 7)) {
|
|
return `${Number(facts.startDate.slice(8, 10))}. bis ${formatIsoDateLong(facts.endDate)}`;
|
|
}
|
|
return `${formatIsoDateLong(facts.startDate)} bis ${formatIsoDateLong(facts.endDate)}`;
|
|
}
|
|
|
|
/** Kurzform fuer die mobile Buchungsleiste. Ohne Startdatum des Verkaufs. */
|
|
export function priceLine(pricing: PublicPricing): string {
|
|
if (pricing.phase === "REGULAR" && pricing.currentCents !== null) return `${formatEuroShort(pricing.currentCents)} pro Person`;
|
|
return `Early Bird ${formatEuroShort(pricing.earlyBirdCents)} statt ${formatEuroShort(pricing.regularCents)}`;
|
|
}
|
|
|
|
function Backdrop({ media, video }: { media: Media; video: string | null }) {
|
|
if (media.kind !== "image") {
|
|
return <MediaFrame media={media} aspect="h-full w-full" sizes="100vw" showLabel={false} />;
|
|
}
|
|
return (
|
|
<>
|
|
<Image src={media.src} alt={media.alt} fill priority sizes="100vw" className="hero-zoom object-cover" />
|
|
{video && (
|
|
// Stumm und ohne Ton-Autoplay; bei reduzierter Bewegung blendet
|
|
// globals.css das Video aus und das Standbild bleibt.
|
|
<video src={video} poster={media.src} autoPlay muted loop playsInline preload="metadata" aria-hidden className="hero-video absolute inset-0 h-full w-full object-cover" />
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Headerbild ueber die volle Breite und Hoehe, Titel und Buchung mittig,
|
|
* wie bei der Filmcamp-Referenz. Bewusst ohne Preis: der steht im
|
|
* Preisabschnitt.
|
|
*/
|
|
export function Hero({ pricing, media, video }: { pricing: PublicPricing; media: Media; video: string | null }) {
|
|
const { hero } = site;
|
|
return (
|
|
<section id="top" aria-labelledby="hero-title" className="on-dark relative isolate flex min-h-[100svh] items-center overflow-hidden bg-ink text-paper">
|
|
<div className="absolute inset-0 -z-10">
|
|
<Backdrop media={media} video={video} />
|
|
<div aria-hidden className="absolute inset-0 bg-[linear-gradient(180deg,rgb(35_33_31/0.6)_0%,rgb(35_33_31/0.45)_45%,rgb(35_33_31/0.75)_100%)]" />
|
|
</div>
|
|
|
|
<div className="container-edge flex flex-col items-center pb-20 pt-44 text-center md:pt-60">
|
|
<h1 id="hero-title" className="hero-fade max-w-4xl text-[clamp(2.75rem,8vw,6.25rem)] font-extrabold leading-[0.98] tracking-[-0.03em]">
|
|
<span className="sr-only">ARTISTA </span>
|
|
{hero.title}
|
|
</h1>
|
|
<p className="hero-fade hero-fade-late mt-7 max-w-2xl text-lg text-paper/90 md:text-xl">{hero.subline}</p>
|
|
<div className="hero-fade hero-fade-later mt-10 flex flex-wrap justify-center gap-3">
|
|
{pricing.checkoutOpen ? (
|
|
<BookButton className="btn btn-primary px-8 text-lg">{hero.cta}</BookButton>
|
|
) : (
|
|
<a href="#preis" className="btn btn-primary px-8 text-lg">
|
|
{hero.cta}
|
|
</a>
|
|
)}
|
|
<a href="#erwartet" className="btn btn-quiet px-8 text-lg">
|
|
Was dich erwartet
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<a href="#termin" aria-label="Weiter zum Termin" className="hero-fade hero-fade-later absolute bottom-6 left-1/2 -translate-x-1/2 p-2 text-paper/80 hover:text-paper">
|
|
<svg viewBox="0 0 24 24" className="hero-bounce h-7 w-7" aria-hidden>
|
|
<path d="M6 9l6 6 6-6" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
|
</svg>
|
|
</a>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
/** Dunkle Leiste unter dem Header mit dem Termin, wie bei der Referenz. */
|
|
export function DateBar({ facts }: { facts: PublicCampFacts }) {
|
|
const dates = dateLine(facts);
|
|
if (!dates) return null;
|
|
return (
|
|
<section id="termin" aria-label="Termin" className="on-dark bg-ink py-10 text-center text-paper md:py-14">
|
|
<p className="container-edge text-xl font-semibold md:text-2xl">
|
|
Verbringe eine unvergessliche Woche mit uns vom <span className="text-sand">{dates}</span>
|
|
{facts.location && <span className="mt-2 block text-base font-normal text-paper/75 md:text-lg">{facts.location}{facts.region ? `, ${facts.region}` : ""}</span>}
|
|
</p>
|
|
</section>
|
|
);
|
|
}
|