first commit
Docker-Images bauen und veroeffentlichen / build-and-push (push) Failing after 1m32s

This commit is contained in:
Nick Szaunig committed 2026-09-27 06:06:56 +02:00
commit 59bb5a38a3
132 files changed
+19435

No files matched your search

+139
View File
@@ -0,0 +1,139 @@
import { site } from "@/content/site";
import type { PublicCampFacts } from "@/lib/camp";
import { Gallery } from "../Gallery";
function DraftFlag({ show }: { show: boolean }) {
return show ? <span className="draft-flag ml-2 align-middle">Entwurf, nicht bestätigt</span> : null;
}
export function CampSection({ showDrafts }: { showDrafts: boolean }) {
const { camp } = site;
const showAudience = camp.audience.confirmed || showDrafts;
return (
<section id="camp" aria-labelledby="camp-title" className="py-20 md:py-32">
<div className="container-edge grid gap-10 md:grid-cols-12">
<div className="md:col-span-6 lg:col-span-5">
<h2 id="camp-title" className="section-title">{camp.title}</h2>
<p className="display mt-8 text-[1.75rem] leading-[1.2] text-ink md:text-[2.125rem]">{camp.lead}</p>
</div>
<div className="md:col-span-6 md:col-start-7 lg:col-span-5 lg:col-start-8 md:pt-24">
<div className="prose-body text-ink/90">
{camp.paragraphs.map((p) => (
<p key={p}>{p}</p>
))}
</div>
{showAudience && (
<div className="mt-12 border-t border-ink pt-6">
<h3 className="text-lg font-semibold">
{camp.audience.title}
<DraftFlag show={!camp.audience.confirmed} />
</h3>
<ul className="mt-4 space-y-3">
{camp.audience.items.map((item) => (
<li key={item} className="flex gap-3">
<span aria-hidden className="mt-[0.7em] h-px w-5 shrink-0 bg-rosso" />
{item}
</li>
))}
</ul>
</div>
)}
</div>
</div>
</section>
);
}
export function ServicesSection({ showDrafts }: { showDrafts: boolean }) {
const { services } = site;
const included = services.included.filter((s) => s.confirmed || showDrafts);
return (
<section id="leistungen" aria-labelledby="leistungen-title" className="on-dark bg-ink py-20 text-paper md:py-32">
<div className="container-edge">
<h2 id="leistungen-title" className="section-title max-w-3xl">{services.title}</h2>
<div className="mt-14 grid gap-12 md:grid-cols-2 md:gap-16">
<div>
<h3 className="border-b border-paper/30 pb-3 text-lg font-semibold">Im Preis enthalten</h3>
<ul className="mt-2 divide-y divide-paper/15">
{included.map((item) => (
<li key={item.text} className="py-4 text-lg">
{item.text}
{!item.confirmed && <DraftFlag show />}
</li>
))}
</ul>
{!showDrafts && included.length < services.included.length && (
<p className="mt-4 text-paper/75">Weitere Leistungen veröffentlichen wir, sobald sie verbindlich feststehen.</p>
)}
</div>
<div>
<h3 className="border-b border-paper/30 pb-3 text-lg font-semibold">Nicht enthalten</h3>
<ul className="mt-2 divide-y divide-paper/15">
{services.excluded.map((item) => (
<li key={item} className="py-4 text-lg text-paper/85">
{item}
</li>
))}
</ul>
</div>
</div>
<div className="mt-20 border-t border-paper/30 pt-10">
<div className="flex flex-wrap items-baseline justify-between gap-4">
<h3 className="display text-3xl">So könnte ein Tag im Camp aussehen</h3>
<p className="max-w-md text-[0.9375rem] text-paper/75">{services.exampleSchedule.note}</p>
</div>
{/* Nummeriert, weil es eine Abfolge ist — ausdruecklich als Beispiel gekennzeichnet. */}
<ol className="mt-10 grid gap-8 md:grid-cols-3">
{services.exampleSchedule.steps.map((step, index) => (
<li key={step.title} className="border-t border-paper/30 pt-5">
<span className="display text-5xl text-paper/40">{index + 1}</span>
<p className="mt-3 text-lg font-semibold">{step.title}</p>
<p className="mt-2 text-paper/80">{step.text}</p>
</li>
))}
</ol>
</div>
</div>
</section>
);
}
export function AccommodationSection({ facts, showDrafts }: { facts: PublicCampFacts; showDrafts: boolean }) {
const { accommodation } = site;
return (
<section id="unterkunft" aria-labelledby="unterkunft-title" className="py-20 md:py-32">
<div className="container-edge">
<div className="grid gap-8 md:grid-cols-12">
<h2 id="unterkunft-title" className="section-title md:col-span-6">{accommodation.title}</h2>
<p className="text-lg text-ink/85 md:col-span-5 md:col-start-8 md:self-end">{accommodation.lead}</p>
</div>
<Gallery images={accommodation.images} showLabels={showDrafts} />
<div className="mt-12 grid gap-8 md:grid-cols-12">
<div className="md:col-span-7">
<h3 className="text-lg font-semibold">Zimmer und Unterbringung</h3>
<p className="mt-3 max-w-2xl">
Gebucht wird ein Platz pro Person mit geteilter Unterbringung. Einzelbelegung und Zimmer-Upgrades sind nicht buchbar.
</p>
{facts.accommodationText ? (
<p className="mt-3 max-w-2xl whitespace-pre-line">{facts.accommodationText}</p>
) : (
<p className="mt-3 max-w-2xl text-stone">
Die genaue Zimmer- und Bettaufteilung veröffentlichen wir, sobald sie verbindlich feststeht – in jedem Fall bevor du buchst.
</p>
)}
</div>
<div className="md:col-span-4 md:col-start-9">
{accommodation.bookingUrl ? (
<a href={accommodation.bookingUrl} target="_blank" rel="noopener noreferrer" className="btn btn-quiet">
Unterkunft ansehen (externe Seite)
</a>
) : (
showDrafts && <span className="draft-flag">Link zur Unterkunft folgt</span>
)}
</div>
</div>
</div>
</section>
);
}
+108
View File
@@ -0,0 +1,108 @@
import { site } from "@/content/site";
import type { PublicCampFacts } from "@/lib/camp";
import type { PublicPricing } from "@/lib/public-page";
import { BookButton } from "../booking/BookingProvider";
type Faq = { q: string; a: React.ReactNode };
function faqs(facts: PublicCampFacts, pricing: PublicPricing): Faq[] {
return [
{
q: "Wie läuft die Buchung ab?",
a: "Du gibst deine Daten ein, wählst die Zahlungsweise und prüfst alles in einer Übersicht. Bezahlt wird auf einer sicheren Zahlungsseite unseres Zahlungsdienstleisters Stripe. Verbindlich bestätigt ist deine Buchung, sobald die Zahlung bei uns eingegangen ist – dann bekommst du eine E-Mail.",
},
{
q: "Wie lange gilt der angezeigte Preis?",
a: `Es gilt der Preis zum Zeitpunkt, an dem du die Buchung startest. Er bleibt ${pricing.reservationMinutes} Minuten für dich reserviert. Schließt du die Zahlung in dieser Zeit nicht ab, verfällt die Reservierung, und ein neuer Buchungsversuch wird zum dann gültigen Preis berechnet. Der Early-Bird-Preis endet am 31. Dezember 2026 um 24 Uhr und wird nicht verlängert.`,
},
{
q: "Kann ich in Raten zahlen?",
a: pricing.installmentsAllowed
? "Ja, in zwei oder drei Raten. Die erste Rate zahlst du bei der Buchung, die weiteren werden zu festen Terminen über deine hinterlegte Zahlungsmethode eingezogen – die letzte spätestens 30 Tage vor Anreise. Alle Beträge und Termine siehst du vor der Zahlung. Nach der letzten Rate finden keine weiteren Abbuchungen statt."
: "Ob eine Ratenzahlung möglich ist, geben wir bekannt, sobald der Camp-Termin feststeht.",
},
{
q: "Welche Zahlungsarten gibt es?",
a: "Die verfügbaren Zahlungsarten siehst du auf der Zahlungsseite von Stripe. Kartendaten gibst du nur dort ein; wir speichern sie nicht.",
},
{
q: "Wie bin ich untergebracht?",
a: facts.accommodationText
? `Du buchst einen Platz pro Person mit geteilter Unterbringung. ${facts.accommodationText}`
: "Du buchst einen Platz pro Person mit geteilter Unterbringung. Die genaue Zimmer- und Bettaufteilung veröffentlichen wir, sobald sie verbindlich feststeht – in jedem Fall bevor du buchst.",
},
{
q: "Wie reise ich an?",
a: `Die Anreise nach Italien und zurück organisierst du selbst; sie ist nicht im Preis enthalten.${facts.location ? ` Das Camp findet in ${facts.location} statt.` : " Den genauen Ort geben wir bekannt, sobald er feststeht."}`,
},
{
q: "Was ist nicht im Preis enthalten?",
a: `${site.services.excluded.join(", ")}.`,
},
{
q: "Was gilt, wenn ich nicht teilnehmen kann?",
a: facts.termsUrl ? (
<>
Das regeln unsere{" "}
<a href={facts.termsUrl} className="underline">
Buchungs-, Storno- und Zahlungsbedingungen
</a>
. Eine Reiserücktrittsversicherung ist nicht enthalten.
</>
) : (
"Die Buchungs-, Storno- und Zahlungsbedingungen veröffentlichen wir vor dem Verkaufsstart. Eine Reiserücktrittsversicherung ist nicht enthalten."
),
},
{
q: "Ich habe einen Gutscheincode. Wo gebe ich ihn ein?",
a: "Im zweiten Schritt der Buchung. Pro Buchung lässt sich ein Gutschein einlösen.",
},
];
}
export function FaqSection({ facts, pricing }: { facts: PublicCampFacts; pricing: PublicPricing }) {
return (
<section id="fragen" aria-labelledby="fragen-title" className="py-20 md:py-32">
<div className="container-edge grid gap-10 md:grid-cols-12">
<h2 id="fragen-title" className="section-title md:col-span-4">{site.faq.title}</h2>
<div className="md:col-span-8">
{faqs(facts, pricing).map((item) => (
<details key={item.q} className="group border-b border-line py-1 first:border-t">
<summary className="flex cursor-pointer list-none items-center justify-between gap-6 py-4 text-lg font-semibold [&::-webkit-details-marker]:hidden">
{item.q}
<span aria-hidden className="text-2xl font-normal text-rosso transition-transform duration-200 group-open:rotate-45">
+
</span>
</summary>
<div className="max-w-2xl pb-5 text-ink/85">{item.a}</div>
</details>
))}
</div>
</div>
</section>
);
}
export function ClosingSection({ pricing }: { pricing: PublicPricing }) {
return (
<section aria-labelledby="schluss-title" className="on-dark bg-ink py-24 text-paper md:py-36">
<div className="container-edge">
<h2 id="schluss-title" className="display max-w-4xl text-[clamp(2.75rem,7vw,6rem)] leading-[0.98]">
Wir freuen uns auf dich in Italien.
</h2>
<div className="mt-10 flex flex-wrap gap-3">
{pricing.checkoutOpen ? (
<BookButton className="btn bg-paper px-8 text-lg text-ink hover:bg-plaster">Platz buchen</BookButton>
) : (
<a href="#preis" className="btn bg-paper px-8 text-lg text-ink hover:bg-plaster">
Zum Buchungsstart
</a>
)}
<a href="#fragen" className="btn btn-quiet text-paper">
Fragen ansehen
</a>
</div>
</div>
</section>
);
}
@@ -0,0 +1,93 @@
import { site } from "@/content/site";
import { MediaFrame } from "../MediaFrame";
import { Reveal } from "../Reveal";
import { VideoSlot } from "../VideoSlot";
export function TeachersSection({ showDrafts }: { showDrafts: boolean }) {
const { teachers } = site;
return (
<section id="dozentinnen" aria-labelledby="dozentinnen-title" className="bg-paper py-20 md:py-32">
<div className="container-edge">
<h2 id="dozentinnen-title" className="section-title">{teachers.title}</h2>
<div className="mt-14 grid gap-16 md:grid-cols-12 md:gap-10">
{teachers.people.map((person, index) => (
<article
key={index}
aria-label={person.name ?? `Dozentin ${index + 1}`}
className={index === 0 ? "md:col-span-5" : "md:col-span-5 md:col-start-8 md:mt-40"}
>
<Reveal>
<MediaFrame media={person.portrait} aspect="aspect-[3/4]" sizes="(min-width: 768px) 40vw, 100vw" showLabel={showDrafts} />
</Reveal>
<h3 className="display mt-6 text-3xl">
{person.name ?? (showDrafts ? `Dozentin ${index + 1}: Name folgt` : "Wird in Kürze vorgestellt")}
</h3>
{person.role && <p className="mt-1 text-stone">{person.role}</p>}
{person.bio ? (
<p className="mt-4 max-w-md">{person.bio}</p>
) : (
showDrafts && <p className="mt-4"><span className="draft-flag">Kurzbeschreibung folgt</span></p>
)}
</article>
))}
</div>
</div>
</section>
);
}
export function AboutSection({ showDrafts }: { showDrafts: boolean }) {
const { about } = site;
return (
<section id="artista" aria-labelledby="artista-title" className="py-20 md:py-32">
<div className="container-edge grid gap-8 md:grid-cols-12">
<h2 id="artista-title" className="section-title md:col-span-5">{about.title}</h2>
<div className="prose-body md:col-span-6 md:col-start-7">
{about.paragraphs.length > 0 ? (
about.paragraphs.map((p) => <p key={p}>{p}</p>)
) : (
<p className="text-stone">
{showDrafts ? (
<>
<span className="draft-flag">Inhalt folgt</span> Grundlage wird die ARTISTA-Stammmodel-Seite (Link folgt).
</>
) : (
"Mehr über ARTISTA erfährst du in Kürze hier."
)}
</p>
)}
{about.sourceUrl && (
<p className="mt-6">
<a href={about.sourceUrl} target="_blank" rel="noopener noreferrer" className="font-semibold underline underline-offset-4">
Zur ARTISTA-Stammmodel-Seite
</a>
</p>
)}
</div>
</div>
</section>
);
}
export function VoicesSection({ showDrafts, provider }: { showDrafts: boolean; provider: "none" | "youtube-nocookie" | "vimeo" }) {
const { videos } = site;
const filled = videos.slots.filter((s) => s.src || s.externalId);
// Leere Slots nur in Entwicklung/Staging zeigen; oeffentlich nie ein
// erfundenes Testimonial als Ersatz.
const slots = showDrafts ? videos.slots : filled;
if (slots.length === 0) return null;
return (
<section id="stimmen" aria-labelledby="stimmen-title" className="border-t border-line py-20 md:py-28">
<div className="container-edge">
<h2 id="stimmen-title" className="section-title">{videos.title}</h2>
<ul className="mt-12 grid gap-10 md:grid-cols-3">
{slots.map((slot, index) => (
<li key={index}>
<VideoSlot slot={slot} index={index} provider={provider} showDrafts={showDrafts} />
</li>
))}
</ul>
</div>
</section>
);
}
+104
View File
@@ -0,0 +1,104 @@
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";
const CLOSED_TEXT: Record<Exclude<PublicPricing["closedReason"], null>, string> = {
PREVIEW: "Die Buchung öffnet am 1. November 2026.",
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.",
};
export function PricingSection({ pricing, facts, showDrafts }: { pricing: PublicPricing; facts: PublicCampFacts; showDrafts: boolean }) {
const included = site.services.included.filter((s) => s.confirmed);
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">Preis und Buchung</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">
{pricing.phase === "EARLY_BIRD" && pricing.currentCents !== null && (
<>
<p className="text-stone">
Regulärer Preis <s className="tabular-nums">{formatEuroShort(pricing.regularCents)}</s>
</p>
<p className="display mt-2 text-[clamp(4rem,12vw,8rem)] leading-none tabular-nums">{formatEuroShort(pricing.currentCents)}</p>
<p className="mt-3 text-lg">Early-Bird-Preis pro Person, gültig bis einschließlich 31. Dezember 2026.</p>
</>
)}
{pricing.phase === "REGULAR" && pricing.currentCents !== null && (
<>
<p className="display text-[clamp(4rem,12vw,8rem)] leading-none tabular-nums">{formatEuroShort(pricing.currentCents)}</p>
<p className="mt-3 text-lg">pro Person</p>
</>
)}
{pricing.phase === "PREVIEW" && (
<dl className="grid gap-6 sm:grid-cols-2">
<div className="border-t border-ink pt-4">
<dt className="text-stone">Early Bird, 1. November bis 31. Dezember 2026</dt>
<dd className="display mt-2 text-6xl tabular-nums">{formatEuroShort(pricing.earlyBirdCents)}</dd>
</div>
<div className="border-t border-line pt-4">
<dt className="text-stone">Regulär, ab 1. Januar 2027</dt>
<dd className="display mt-2 text-6xl tabular-nums">{formatEuroShort(pricing.regularCents)}</dd>
</div>
</dl>
)}
</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">Platz buchen</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>
</aside>
</div>
</section>
);
}