Compare commits

...
3 Commits
Author SHA1 Message Date
Nick SzaunigandClaude Opus 5.5 5c2878875b feat: Buchung sofort möglich, sobald alle Freigaben erteilt sind
Docker-Images bauen und veroeffentlichen / build-and-push (push) Successful in 2m54s
Vor dem 01.11.2026 gilt dann der Early-Bird-Preis. Fehlt eine Freigabe, bleibt die Vorschau.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-06 23:07:39 +02:00
Nick SzaunigandClaude Opus 5.5 a76a9cf616 fix: Teambilder in gerader Reihe, Tagesablauf-Karte auf dem Smartphone mittig
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-06 23:07:38 +02:00
Nick SzaunigandClaude Opus 5.5 2be1de6a63 fix: Intro-Video passt sich dem Format an, keine schwarzen Balken bei Hochformat
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-06 23:00:24 +02:00
6 changed files with 26 additions and 10 deletions

No files matched your search

+1 -1
View File
@@ -85,7 +85,7 @@ Der gehostete Checkout bildet den Ablauf vollständig ab: Betrag, Ablaufzeit, da
`src/lib/sales-gate.ts` wird von jedem Checkout-Endpunkt aufgerufen. `src/lib/sales-gate.ts` wird von jedem Checkout-Endpunkt aufgerufen.
- **Livemodus (`sk_live_`):** Verkauf nur mit laufender echter Preisphase, eingeschaltetem Live-Verkauf und allen elf Bestätigungen. - **Livemodus (`sk_live_`):** Verkauf nur mit eingeschaltetem Live-Verkauf und allen elf Bestätigungen. Sind alle erteilt, startet der Verkauf sofort, auch vor dem 01.11.2026; bis dahin gilt der Early-Bird-Preis (seit 06.10.2026).
- **Testmodus:** Checkout ist ohne Freigaben möglich, die Preisphase lässt sich simulieren. - **Testmodus:** Checkout ist ohne Freigaben möglich, die Preisphase lässt sich simulieren.
- **Schutz vor Fehlkonfiguration:** Ein Live-Schlüssel außerhalb von `APP_ENV=production` bricht mit einem Fehler ab. - **Schutz vor Fehlkonfiguration:** Ein Live-Schlüssel außerhalb von `APP_ENV=production` bricht mit einem Fehler ab.
+4 -3
View File
@@ -109,10 +109,11 @@ export function ScheduleCarousel({ days }: { days: ScheduleCard[] }) {
aria-label="Tage im Camp" aria-label="Tage im Camp"
// Per Tastatur scrollbar (Pfeiltasten), WCAG 2.1.1. // Per Tastatur scrollbar (Pfeiltasten), WCAG 2.1.1.
tabIndex={0} tabIndex={0}
// Smartphone: Karten ragen bis an den Rand, die naechste lugt hervor. // Smartphone: Karte 85vw breit und mittig (7,5vw Rand links und
// rechts), Nachbarkarten lugen an beiden Seiten hervor.
// Ab md: Karten passen vollstaendig in den Inhaltsbereich (2 bzw. 3 // Ab md: Karten passen vollstaendig in den Inhaltsbereich (2 bzw. 3
// nebeneinander), nichts wird am Rand abgeschnitten. // nebeneinander), nichts wird am Rand abgeschnitten.
className="-mx-4 flex snap-x snap-mandatory scroll-px-4 gap-4 overflow-x-auto px-4 pb-4 [scrollbar-width:none] md:mx-0 md:scroll-px-0 md:gap-6 md:px-0 [&::-webkit-scrollbar]:hidden" className="-mx-4 flex snap-x snap-mandatory scroll-px-[7.5vw] gap-4 overflow-x-auto px-[7.5vw] pb-4 [scrollbar-width:none] sm:scroll-px-4 sm:px-4 md:mx-0 md:scroll-px-0 md:gap-6 md:px-0 [&::-webkit-scrollbar]:hidden"
> >
{days.map((day, index) => ( {days.map((day, index) => (
<li <li
@@ -120,7 +121,7 @@ export function ScheduleCarousel({ days }: { days: ScheduleCard[] }) {
ref={(el) => { ref={(el) => {
cards.current[index] = el; cards.current[index] = el;
}} }}
className="flex w-[85%] shrink-0 snap-start flex-col rounded-2xl bg-paper p-6 shadow-[0_1px_0_var(--color-line)] sm:w-[62%] md:w-[calc((100%-1.5rem)/2)] md:p-8 lg:w-[calc((100%-3rem)/3)]" className="flex w-[85vw] shrink-0 snap-start flex-col rounded-2xl bg-paper p-6 shadow-[0_1px_0_var(--color-line)] sm:w-[62%] md:w-[calc((100%-1.5rem)/2)] md:p-8 lg:w-[calc((100%-3rem)/3)]"
> >
<p className="text-sm font-semibold uppercase tracking-[0.16em] text-rosso"> <p className="text-sm font-semibold uppercase tracking-[0.16em] text-rosso">
{day.label} · {day.title} {day.label} · {day.title}
@@ -33,8 +33,9 @@ export function IntroVideoSection({ video, showDrafts }: { video: IntroVideo; sh
playsInline playsInline
preload={poster ? "none" : "metadata"} preload={poster ? "none" : "metadata"}
aria-label="Video ARTISTA Modelcamp" aria-label="Video ARTISTA Modelcamp"
// Eigenes Seitenverhaeltnis (quer oder hoch), wie bei den Reels. // Groesse folgt dem Video (quer oder hoch): ohne volle Breite keine
className="mx-auto block max-h-[85vh] w-full rounded-2xl bg-black object-contain" // schwarzen Balken neben einem Hochformat-Video.
className="mx-auto block h-auto max-h-[85vh] w-auto max-w-full rounded-2xl bg-black"
> >
<track kind="captions" /> <track kind="captions" />
</video> </video>
+1 -1
View File
@@ -44,7 +44,7 @@ export function TeamSection({ showDrafts, portraits }: { showDrafts: boolean; po
<ul className="mt-20 grid grid-cols-2 gap-x-4 gap-y-12 md:mt-28 md:grid-cols-4 md:gap-x-8 md:gap-y-16"> <ul className="mt-20 grid grid-cols-2 gap-x-4 gap-y-12 md:mt-28 md:grid-cols-4 md:gap-x-8 md:gap-y-16">
{team.people.map((person: TeamMember, index) => ( {team.people.map((person: TeamMember, index) => (
<li key={person.slug} className={index % 2 === 1 ? "md:mt-16" : ""}> <li key={person.slug}>
<article aria-label={person.name}> <article aria-label={person.name}>
<Reveal delay={(index % 4) * 0.06}> <Reveal delay={(index % 4) * 0.06}>
<MediaFrame media={portraitOf(person)} aspect="aspect-[3/4]" shape="arch" sizes="(min-width: 768px) 22vw, 50vw" showLabel={false} /> <MediaFrame media={portraitOf(person)} aspect="aspect-[3/4]" shape="arch" sizes="(min-width: 768px) 22vw, 50vw" showLabel={false} />
+4 -1
View File
@@ -107,8 +107,11 @@ export function releaseBlockers(camp: CampGateFields): ReleaseBlocker[] {
export function evaluateSales(now: Date, camp: CampGateFields, mode: StripeMode): SalesState { export function evaluateSales(now: Date, camp: CampGateFields, mode: StripeMode): SalesState {
const phase = pricePhaseAt(now); const phase = pricePhaseAt(now);
const simulate = mode === "test" && camp.testPhaseOverride !== null && camp.testPhaseOverride !== "PREVIEW"; const simulate = mode === "test" && camp.testPhaseOverride !== null && camp.testPhaseOverride !== "PREVIEW";
const effectivePhase = simulate ? camp.testPhaseOverride! : phase;
const blockers = releaseBlockers(camp); const blockers = releaseBlockers(camp);
// Sind alle Freigaben bestaetigt, startet der Verkauf sofort, auch vor dem
// 01.11.2026: Bis dahin gilt der Early-Bird-Preis.
const releasedEarly = phase === "PREVIEW" && blockers.length === 0;
const effectivePhase = simulate ? camp.testPhaseOverride! : releasedEarly ? "EARLY_BIRD" : phase;
const campStarted = camp.campStartDate !== null && now.getTime() >= berlinStartOfDay(camp.campStartDate).getTime(); const campStarted = camp.campStartDate !== null && now.getTime() >= berlinStartOfDay(camp.campStartDate).getTime();
let closedReason: SalesState["closedReason"] = null; let closedReason: SalesState["closedReason"] = null;
+13 -2
View File
@@ -73,8 +73,19 @@ describe("Verkaufsfreigabe", () => {
expect(evaluateSales(earlyBird, released, "live").checkoutOpen).toBe(true); expect(evaluateSales(earlyBird, released, "live").checkoutOpen).toBe(true);
}); });
test("Livemodus vor dem 01.11.2026: Vorschau ohne Kaufmoeglichkeit, Override wirkungslos", () => { test("Livemodus vor dem 01.11.2026 mit allen Freigaben: offen zum Early-Bird-Preis", () => {
const state = evaluateSales(new Date("2026-10-01T12:00:00Z"), { ...released, testPhaseOverride: "EARLY_BIRD" }, "live"); const state = evaluateSales(new Date("2026-10-01T12:00:00Z"), released, "live");
expect(state.checkoutOpen).toBe(true);
expect(state.effectivePhase).toBe("EARLY_BIRD");
expect(state.phaseSimulated).toBe(false);
});
test("Livemodus vor dem 01.11.2026 mit fehlender Freigabe: Vorschau, Override wirkungslos", () => {
const state = evaluateSales(
new Date("2026-10-01T12:00:00Z"),
{ ...released, travelLawChecked: false, testPhaseOverride: "REGULAR" },
"live",
);
expect(state.checkoutOpen).toBe(false); expect(state.checkoutOpen).toBe(false);
expect(state.closedReason).toBe("PREVIEW"); expect(state.closedReason).toBe("PREVIEW");
}); });