Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c2878875b | ||
|
|
a76a9cf616 | ||
|
|
2be1de6a63 |
No files matched your search
+1
-1
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user