- 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]>
107 lines
5.1 KiB
TypeScript
107 lines
5.1 KiB
TypeScript
import { site } from "@/content/site";
|
|
import type { PublicCampFacts } from "@/lib/camp";
|
|
import { formatEuroShort } from "@/lib/money";
|
|
import type { PublicPricing } from "@/lib/public-page";
|
|
import { BookButton } from "../booking/BookingProvider";
|
|
|
|
// Bewusst ohne Startdatum des Verkaufs (Wunsch des Auftraggebers).
|
|
const CLOSED_TEXT: Record<Exclude<PublicPricing["closedReason"], null>, string> = {
|
|
PREVIEW: "Bald buchbar.",
|
|
NOT_RELEASED: "Die Buchung ist noch nicht freigeschaltet. Wir veröffentlichen alle verbindlichen Angaben vor dem Verkaufsstart.",
|
|
CAMP_STARTED: "Die Buchung ist geschlossen.",
|
|
NOT_CONFIGURED: "Die Buchung ist derzeit nicht verfügbar.",
|
|
};
|
|
|
|
function PriceDisplay({ pricing }: { pricing: PublicPricing }) {
|
|
if (pricing.phase === "REGULAR" && pricing.currentCents !== null) {
|
|
return (
|
|
<>
|
|
<p className="text-[clamp(4rem,12vw,7.5rem)] font-extrabold leading-none tracking-[-0.03em] tabular-nums">{formatEuroShort(pricing.currentCents)}</p>
|
|
<p className="mt-3 text-lg">pro Person</p>
|
|
</>
|
|
);
|
|
}
|
|
// Vorschau und Early Bird: regulaerer Preis durchgestrichen, Early Bird gross.
|
|
const earlyBird = pricing.currentCents ?? pricing.earlyBirdCents;
|
|
return (
|
|
<>
|
|
<p className="text-lg text-stone">
|
|
Regulärer Preis <s className="tabular-nums">{formatEuroShort(pricing.regularCents)}</s>
|
|
</p>
|
|
<p className="mt-2 flex flex-wrap items-baseline gap-x-4 gap-y-2">
|
|
<span className="text-[clamp(4rem,12vw,7.5rem)] font-extrabold leading-none tracking-[-0.03em] tabular-nums">{formatEuroShort(earlyBird)}</span>
|
|
<span className="rounded-full bg-rosso px-3 py-1 text-sm font-bold uppercase tracking-[0.12em] text-paper">Early Bird</span>
|
|
</p>
|
|
<p className="mt-3 text-lg">Early-Bird-Preis pro Person, gültig bis einschließlich 31. Dezember 2026.</p>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function PricingSection({ pricing, facts, showDrafts }: { pricing: PublicPricing; facts: PublicCampFacts; showDrafts: boolean }) {
|
|
const included = site.services.included.filter((s) => s.confirmed);
|
|
const title = pricing.phase === "REGULAR" ? "Jetzt Platz sichern" : "Jetzt Platz sichern mit Early Bird";
|
|
return (
|
|
<section id="preis" aria-labelledby="preis-title" className="bg-paper py-20 md:py-32">
|
|
<div className="container-edge grid gap-14 md:grid-cols-12">
|
|
<div className="md:col-span-7">
|
|
<h2 id="preis-title" className="section-title">{title}</h2>
|
|
|
|
{pricing.mode === "test" && (
|
|
<p className="mt-6 rounded-lg border border-line bg-plaster p-4 text-[0.9375rem]">
|
|
<span className="draft-flag">Testmodus</span> Zahlungen laufen über Stripe-Testdaten, es wird kein Geld eingezogen.
|
|
{pricing.phaseSimulated && " Die Preisphase ist für Tests simuliert."}
|
|
</p>
|
|
)}
|
|
|
|
<div className="mt-10">
|
|
<PriceDisplay pricing={pricing} />
|
|
</div>
|
|
|
|
<ul className="mt-8 max-w-xl space-y-2 text-[0.9375rem] text-ink/85">
|
|
<li>Gesamtpreis pro Person für die Teilnahme mit geteilter Unterbringung.</li>
|
|
<li>
|
|
{pricing.installmentsAllowed
|
|
? "Bezahlen kannst du auf einmal oder in bis zu drei Raten. Die erste Rate ist bei Buchung fällig, die letzte spätestens 30 Tage vor Anreise."
|
|
: "Bezahlt wird bei Buchung. Ob zusätzlich Ratenzahlung möglich ist, geben wir bekannt, sobald der Termin feststeht."}
|
|
</li>
|
|
<li>Einen Gutscheincode kannst du im Buchungsschritt eingeben.</li>
|
|
{facts.taxNote ? (
|
|
<li>{facts.taxNote}</li>
|
|
) : (
|
|
showDrafts && (
|
|
<li>
|
|
<span className="draft-flag">Steuerlicher Preishinweis folgt vor Verkaufsstart</span>
|
|
</li>
|
|
)
|
|
)}
|
|
</ul>
|
|
|
|
<div className="mt-10">
|
|
{pricing.checkoutOpen ? (
|
|
<BookButton className="btn btn-primary px-8 text-lg">Jetzt Platz sichern</BookButton>
|
|
) : (
|
|
<p className="max-w-xl text-lg font-semibold">{CLOSED_TEXT[pricing.closedReason ?? "NOT_CONFIGURED"]}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<aside aria-labelledby="preis-umfang" className="md:col-span-4 md:col-start-9 md:pt-24">
|
|
<h3 id="preis-umfang" className="text-lg font-semibold">Was du buchst</h3>
|
|
<ul className="mt-4 divide-y divide-line border-y border-line">
|
|
{included.map((item) => (
|
|
<li key={item.text} className="py-3">{item.text}</li>
|
|
))}
|
|
</ul>
|
|
<p className="mt-6 text-[0.9375rem] text-stone">Nicht enthalten: {site.services.excluded.join(", ")}.</p>
|
|
<p className="mt-4 text-[0.9375rem] text-stone">
|
|
Vor der Zahlung siehst du Gesamtpreis, heute fälligen Betrag, alle weiteren Raten mit Terminen und die Beschreibung der Unterbringung auf einen Blick.
|
|
</p>
|
|
<p className="mt-4 text-[0.9375rem] text-stone">
|
|
Hinweise zu deinem <a href="/widerruf" className="link-quiet">Widerrufsrecht</a>.
|
|
</p>
|
|
</aside>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|