- 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]>
70 lines
2.6 KiB
TypeScript
70 lines
2.6 KiB
TypeScript
"use client";
|
|
|
|
import { AnimatePresence, LazyMotion, domAnimation, m } from "motion/react";
|
|
import { useEffect, useState } from "react";
|
|
import { BookButton, useBooking } from "./BookingProvider";
|
|
|
|
/**
|
|
* Buchungsleiste fuer kleine Bildschirme. Erscheint erst, wenn der Hero
|
|
* verlassen ist, und verschwindet im Preisbereich und am Seitenende, damit
|
|
* sie dort keine Inhalte oder Bedienelemente verdeckt.
|
|
*/
|
|
export function MobileBookingBar({ priceLabel }: { priceLabel: string }) {
|
|
const { pricing } = useBooking();
|
|
const [heroVisible, setHeroVisible] = useState(true);
|
|
const [blocked, setBlocked] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const hero = document.getElementById("top");
|
|
const blockers = ["preis", "site-footer"].map((id) => document.getElementById(id)).filter(Boolean) as HTMLElement[];
|
|
const visible = new Set<string>();
|
|
const heroObserver = new IntersectionObserver(([entry]) => setHeroVisible(entry?.isIntersecting ?? false));
|
|
const blockObserver = new IntersectionObserver((entries) => {
|
|
for (const entry of entries) {
|
|
if (entry.isIntersecting) visible.add(entry.target.id);
|
|
else visible.delete(entry.target.id);
|
|
}
|
|
setBlocked(visible.size > 0);
|
|
});
|
|
if (hero) heroObserver.observe(hero);
|
|
blockers.forEach((b) => blockObserver.observe(b));
|
|
return () => {
|
|
heroObserver.disconnect();
|
|
blockObserver.disconnect();
|
|
};
|
|
}, []);
|
|
|
|
const show = !heroVisible && !blocked;
|
|
|
|
return (
|
|
<LazyMotion features={domAnimation}>
|
|
<AnimatePresence>
|
|
{show && (
|
|
<m.div
|
|
className="mobile-book-bar fixed inset-x-0 bottom-0 z-30 border-t border-line bg-plaster/95 backdrop-blur lg:hidden"
|
|
data-visible="true"
|
|
initial={{ y: "100%" }}
|
|
animate={{ y: 0 }}
|
|
exit={{ y: "100%" }}
|
|
transition={{ duration: 0.3, ease: [0.22, 1, 0.36, 1] }}
|
|
>
|
|
<div className="container-edge flex items-center justify-between gap-4 py-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]">
|
|
<p className="text-sm leading-tight">
|
|
<span className="block font-semibold">Modelcamp 2027</span>
|
|
<span className="text-stone">{priceLabel}</span>
|
|
</p>
|
|
{pricing.checkoutOpen ? (
|
|
<BookButton className="btn btn-primary min-h-11 px-5 py-2 text-sm">Platz sichern</BookButton>
|
|
) : (
|
|
<a href="#preis" className="btn btn-quiet min-h-11 px-5 py-2 text-sm">
|
|
Zum Preis
|
|
</a>
|
|
)}
|
|
</div>
|
|
</m.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</LazyMotion>
|
|
);
|
|
}
|