diff --git a/README.md b/README.md index 17c3056..0fa3775 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,9 @@ Testkarte `4242 4242 4242 4242`, für 3-D-Secure-Fälle `4000 0027 6000 3184`. E ## Wo was gepflegt wird -- **Texte, Bilder, Dozentinnen, Videos, FAQ-Grundlagen:** [src/content/site.ts](src/content/site.ts). Einträge mit `confirmed: false` gelten als Entwurf. +- **Texte, Bilder, Team, Videos, FAQ-Grundlagen:** [src/content/site.ts](src/content/site.ts). Einträge mit `confirmed: false` gelten als Entwurf. - **Termin, Ort, Zimmerbeschreibung, Veranstalter, Steuerhinweis, Bedingungen, Kapazität, Freigaben:** Adminbereich → „Einstellungen und Freigaben“. - **Preise und Preisphasen:** fest in [src/lib/pricing.ts](src/lib/pricing.ts), weil sie verbindlich vorgegeben sind. - **Gutscheine (Stammmodel-Codes):** Adminbereich → „Gutscheine“. +- **Feste Bilder:** `public/images/unterkunft/` (Villa, aus dem Booking.com-Eintrag, Nutzung vom Auftraggeber freigegeben) und `public/images/team/` (Porträts, folgen). - **Medien:** Dateien ins Medienverzeichnis (`MEDIA_DIR`) legen und in `site.ts` als `{ kind: "image", src: "/media/…" }` eintragen. diff --git a/docs/BETRIEB.md b/docs/BETRIEB.md index 5a4f0ad..95cfa18 100644 --- a/docs/BETRIEB.md +++ b/docs/BETRIEB.md @@ -106,4 +106,4 @@ Die Software trifft keine Storno-Entscheidung, sie setzt sie nur nachvollziehbar Dateien (jpg, png, webp, avif, mp4, webm) direkt in den SMB-Ordner `modelcamp-media` legen, z. B. über den Windows-Explorer (`\\nas…\artista\modelcamp-media`). Die App sieht sie sofort unter `/media/`. Im Pod ist das Volume schreibgeschützt eingehängt. -Danach in `src/content/site.ts` eintragen. Hotlinking fremder Bilder, etwa von Booking.com, ist nicht vorgesehen: Nur Bilder mit geklärter Nutzungsberechtigung ablegen. +Danach in `src/content/site.ts` eintragen. Hotlinking fremder Bilder ist nicht vorgesehen: Nur Bilder mit geklärter Nutzungsberechtigung ablegen. Die Unterkunftsbilder liegen als lokale Kopie in `public/images/unterkunft/` und sind Teil des Container-Images. diff --git a/docs/OFFENE-PUNKTE.md b/docs/OFFENE-PUNKTE.md index 7a7e58f..9f6004b 100644 --- a/docs/OFFENE-PUNKTE.md +++ b/docs/OFFENE-PUNKTE.md @@ -1,17 +1,20 @@ # Noch benötigte Inhalte und Freigaben +Stand 01.10.2026. Bereits bestätigt und per Migration `20261001120000_camp_2027_bestaetigt` eingetragen (im Admin änderbar): + +- Termin 08.05.–15.05.2027, Villa La Chiesina, Arcidosso (Toskana) +- Unterbringung: eigenes Bett im geteilten Zimmer, keine Einzelzimmer +- 13 verkaufbare Plätze +- Team (Namen und Rollen) sowie Booking-Link und Unterkunftsbilder + Solange die mit **(Freigabe)** markierten Punkte fehlen, bleibt der Live-Verkauf serverseitig gesperrt. Der aktuelle Stand steht im Admin-Dashboard unter „Fehlt für den Live-Verkauf“. ## Inhalte | Punkt | Wo eintragen | |---|---| -| Camp-Termin (Anreise/Abreise) **(Freigabe)** | Admin → Einstellungen | -| Genauer Ort und Region **(Freigabe)** | Admin → Einstellungen | -| Verbindliche Zimmer- und Bettbeschreibung, insbesondere ob ein französisches Doppelbett geteilt wird **(Freigabe)** | Admin → Einstellungen | -| Booking-Link der Unterkunft | `src/content/site.ts` → `accommodation.bookingUrl` | -| Unterkunftsbilder mit geklärter Nutzungsberechtigung | Medienvolume + `site.ts` | -| Namen, Rollen, Kurzbeschreibungen und Porträts der beiden Dozentinnen | `site.ts` → `teachers` | +| Porträts und freigegebene Kurzbeschreibungen der neun Teammitglieder | `public/images/team/` + `site.ts` → `team` | +| Schriftliche Bestätigung des Vermieters, dass die Booking-Bilder genutzt werden dürfen (Urheber ist der Vermieter bzw. Fotograf) | Ablage beim Veranstalter | | Link zur ARTISTA-Stammmodel-Seite und daraus abgeleiteter Text „Über ARTISTA“ | `site.ts` → `about` | | Drei echte, freigegebene Feedback-Videos mit Namen und Begleittext | Medienvolume + `site.ts` → `videos` | | Hero-Leitbild oder -Video | `site.ts` → `hero.media` | @@ -28,7 +31,6 @@ Solange die mit **(Freigabe)** markierten Punkte fehlen, bleibt der Live-Verkauf | Buchungs-, Storno- und Zahlungsbedingungen **(Freigabe)** | Als Dokument oder Seite hinterlegen (Link und Version im Admin). Es wurden keine AGB formuliert | | Reiserechtliche Einordnung (Pauschalreise?) samt gegebenenfalls nötiger Insolvenzabsicherung und Formblätter **(Freigabe)** | Rechtliche Aussagen aus der Kalkulation wurden nicht übernommen | | Vollständiges Impressum und Datenschutzerklärung | Technische Grundlage siehe `/datenschutz` (nur in Entwicklung sichtbar) | -| Verkaufbare Platzzahl (vorläufig 10) **(Freigabe)** | Admin → Einstellungen | | Kontingent Stammmodel-Buchungen (vorläufig 4) **(Freigabe)** | Admin → Einstellungen | | Ratenzahlung: rechtliche Prüfung, Terminregel, Freigabe für den Livemodus | Admin → Einstellungen (zwei Häkchen) | | Teilnahme Minderjähriger | Die Buchung verlangt derzeit die Erklärung „Ich bin volljährig“. Bitte bestätigen oder die Regel vorgeben | diff --git a/e2e/onepage.spec.ts b/e2e/onepage.spec.ts index daa02fe..04f7ca9 100644 --- a/e2e/onepage.spec.ts +++ b/e2e/onepage.spec.ts @@ -10,7 +10,7 @@ import { expect, test } from "@playwright/test"; test("Onepage: Aufbau, kein horizontales Scrollen, Barrierefreiheit", async ({ page }, testInfo) => { await page.goto("/"); await expect(page.getByRole("heading", { level: 1 })).toContainText("ARTISTA"); - for (const id of ["camp", "leistungen", "unterkunft", "dozentinnen", "artista", "preis", "fragen"]) { + for (const id of ["camp", "leistungen", "unterkunft", "team", "artista", "preis", "fragen"]) { await expect(page.locator(`#${id}`)).toBeAttached(); } // Keine oeffentliche Platzzahl und kein Restplatz-Zaehler. diff --git a/next.config.ts b/next.config.ts index 352391c..f65d884 100644 --- a/next.config.ts +++ b/next.config.ts @@ -7,9 +7,9 @@ const nextConfig: NextConfig = { images: { formats: ["image/avif", "image/webp"], qualities: [75, 85], - // Freigegebene Bilder liegen im persistenten Medienverzeichnis und - // werden unter /media/… ausgeliefert (src/app/media/[...path]/route.ts). - localPatterns: [{ pathname: "/media/**" }], + // Feste Bilder (Unterkunft, Team) liegen in public/images/, austauschbare + // im persistenten Medienverzeichnis unter /media/… (src/app/media/[...path]/route.ts). + localPatterns: [{ pathname: "/media/**" }, { pathname: "/images/**" }], dangerouslyAllowSVG: false, }, // Turbopack-Dateicaches kollidieren im OneDrive-synchronisierten diff --git a/prisma/migrations/20261001120000_camp_2027_bestaetigt/migration.sql b/prisma/migrations/20261001120000_camp_2027_bestaetigt/migration.sql new file mode 100644 index 0000000..4eecde2 --- /dev/null +++ b/prisma/migrations/20261001120000_camp_2027_bestaetigt/migration.sql @@ -0,0 +1,40 @@ +-- Vom Auftraggeber am 01.10.2026 bestaetigte Eckdaten: +-- Termin 08.05.–15.05.2027, Villa La Chiesina in Arcidosso (Toskana), +-- eigenes Bett im geteilten Zimmer, 13 verkaufbare Plaetze. +-- Bereits im Adminbereich gepflegte Werte werden nicht ueberschrieben +-- (nur leere bzw. unbestaetigte Felder werden gesetzt). + +ALTER TABLE "Camp" ALTER COLUMN "capacity" SET DEFAULT 13; + +INSERT INTO "Camp" ("id", "updatedAt") VALUES ('modelcamp-2027', CURRENT_TIMESTAMP) +ON CONFLICT ("id") DO NOTHING; + +UPDATE "Camp" SET + "campStartDate" = DATE '2027-05-08', + "campEndDate" = DATE '2027-05-15', + "dateConfirmed" = true, + "updatedAt" = CURRENT_TIMESTAMP, + "updatedBy" = 'migration' +WHERE "id" = 'modelcamp-2027' AND "dateConfirmed" = false; + +UPDATE "Camp" SET + "locationName" = 'Villa La Chiesina, Arcidosso', + "locationRegion" = 'Toskana, Italien', + "locationConfirmed" = true, + "updatedAt" = CURRENT_TIMESTAMP, + "updatedBy" = 'migration' +WHERE "id" = 'modelcamp-2027' AND "locationConfirmed" = false; + +UPDATE "Camp" SET + "accommodationText" = 'Du hast dein eigenes Bett und teilst dir das Zimmer mit anderen Teilnehmerinnen. Einzelzimmer und Zimmer-Upgrades sind nicht buchbar.', + "accommodationConfirmed" = true, + "updatedAt" = CURRENT_TIMESTAMP, + "updatedBy" = 'migration' +WHERE "id" = 'modelcamp-2027' AND "accommodationConfirmed" = false; + +UPDATE "Camp" SET + "capacity" = 13, + "capacityConfirmed" = true, + "updatedAt" = CURRENT_TIMESTAMP, + "updatedBy" = 'migration' +WHERE "id" = 'modelcamp-2027' AND "capacityConfirmed" = false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b925965..1f83b7e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -38,7 +38,7 @@ model Camp { /// vollstaendigen Freigabe-Checkliste (src/lib/sales-gate.ts). salesEnabled Boolean @default(false) - capacity Int @default(10) + capacity Int @default(13) capacityConfirmed Boolean @default(false) stammmodelQuota Int @default(4) stammmodelQuotaConfirmed Boolean @default(false) diff --git a/public/images/unterkunft/anwesen-luftbild.jpg b/public/images/unterkunft/anwesen-luftbild.jpg new file mode 100644 index 0000000..b52b6de Binary files /dev/null and b/public/images/unterkunft/anwesen-luftbild.jpg differ diff --git a/public/images/unterkunft/arcidosso.jpg b/public/images/unterkunft/arcidosso.jpg new file mode 100644 index 0000000..d9bb810 Binary files /dev/null and b/public/images/unterkunft/arcidosso.jpg differ diff --git a/public/images/unterkunft/esszimmer.jpg b/public/images/unterkunft/esszimmer.jpg new file mode 100644 index 0000000..c169582 Binary files /dev/null and b/public/images/unterkunft/esszimmer.jpg differ diff --git a/public/images/unterkunft/pool-zypressen.jpg b/public/images/unterkunft/pool-zypressen.jpg new file mode 100644 index 0000000..60f045b Binary files /dev/null and b/public/images/unterkunft/pool-zypressen.jpg differ diff --git a/public/images/unterkunft/pool.jpg b/public/images/unterkunft/pool.jpg new file mode 100644 index 0000000..24d5f37 Binary files /dev/null and b/public/images/unterkunft/pool.jpg differ diff --git a/public/images/unterkunft/schlafzimmer-einzelbetten.jpg b/public/images/unterkunft/schlafzimmer-einzelbetten.jpg new file mode 100644 index 0000000..364422e Binary files /dev/null and b/public/images/unterkunft/schlafzimmer-einzelbetten.jpg differ diff --git a/public/images/unterkunft/steintreppe.jpg b/public/images/unterkunft/steintreppe.jpg new file mode 100644 index 0000000..71fbde9 Binary files /dev/null and b/public/images/unterkunft/steintreppe.jpg differ diff --git a/public/images/unterkunft/terrasse.jpg b/public/images/unterkunft/terrasse.jpg new file mode 100644 index 0000000..65dcfc9 Binary files /dev/null and b/public/images/unterkunft/terrasse.jpg differ diff --git a/public/images/unterkunft/villa-eingang.jpg b/public/images/unterkunft/villa-eingang.jpg new file mode 100644 index 0000000..8de83c3 Binary files /dev/null and b/public/images/unterkunft/villa-eingang.jpg differ diff --git a/public/images/unterkunft/villa-luftbild.jpg b/public/images/unterkunft/villa-luftbild.jpg new file mode 100644 index 0000000..f1d512f Binary files /dev/null and b/public/images/unterkunft/villa-luftbild.jpg differ diff --git a/public/images/unterkunft/wohnzimmer-kamin.jpg b/public/images/unterkunft/wohnzimmer-kamin.jpg new file mode 100644 index 0000000..39f2ff8 Binary files /dev/null and b/public/images/unterkunft/wohnzimmer-kamin.jpg differ diff --git a/scripts/seed-dev.ts b/scripts/seed-dev.ts index d1db946..76fa966 100644 --- a/scripts/seed-dev.ts +++ b/scripts/seed-dev.ts @@ -2,14 +2,11 @@ * Testdaten NUR fuer Entwicklung und Staging, damit eine Stripe-Testbuchung * sofort moeglich ist: * - Preisphase "Early Bird" simuliert (wirkt nur mit Stripe-Testschluessel) - * - ein TEST-Anreisetag, ausdruecklich NICHT bestaetigt (erscheint deshalb - * nicht oeffentlich, erlaubt aber Ratenplaene im Testmodus) * - ein Test-Gutschein TEST-STAMMMODEL (nicht der Beispielcode aus dem Briefing) * * Verweigert den Lauf mit APP_ENV=production. */ import { getDb, CAMP_ID } from "@/lib/db"; -import { isoToDbDate } from "@/lib/time"; try { process.loadEnvFile(); @@ -26,15 +23,16 @@ const db = getDb(); try { await db.camp.upsert({ where: { id: CAMP_ID }, - create: { id: CAMP_ID, testPhaseOverride: "EARLY_BIRD", campStartDate: isoToDbDate("2027-06-05"), dateConfirmed: false }, - update: { testPhaseOverride: "EARLY_BIRD", campStartDate: isoToDbDate("2027-06-05"), dateConfirmed: false }, + // Termin und Ort setzt die Migration 20261001120000_camp_2027_bestaetigt. + create: { id: CAMP_ID, testPhaseOverride: "EARLY_BIRD" }, + update: { testPhaseOverride: "EARLY_BIRD" }, }); await db.voucher.upsert({ where: { code: "TEST-STAMMMODEL" }, create: { code: "TEST-STAMMMODEL", maxRedemptions: 3, internalNote: "Entwicklung: Testgutschein" }, update: {}, }); - console.log("Entwicklungsdaten gesetzt: Early Bird simuliert, Test-Anreisetag 05.06.2027 (unbestätigt), Gutschein TEST-STAMMMODEL."); + console.log("Entwicklungsdaten gesetzt: Early Bird simuliert, Gutschein TEST-STAMMMODEL."); } finally { await db.$disconnect(); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 853164e..e9f05fc 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -21,7 +21,7 @@ const hanken = Hanken_Grotesk({ const TITLE = "ARTISTA MODELCAMP 2027"; const DESCRIPTION = - "Das ARTISTA Modelcamp 2027 in Italien: lernen, arbeiten und wohnen unter einem Dach. Alle Infos zu Unterkunft, Dozentinnen, Preis und Buchung."; + "Das ARTISTA Modelcamp 2027 in Italien: lernen, arbeiten und wohnen unter einem Dach. Alle Infos zu Unterkunft, Team, Preis und Buchung."; export function generateMetadata(): Metadata { const { APP_URL, APP_ENV } = env(); diff --git a/src/app/page.tsx b/src/app/page.tsx index 873e60f..896bef1 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,7 +4,7 @@ import { MobileBookingBar } from "@/components/booking/MobileBookingBar"; import { Hero, priceLine } from "@/components/Hero"; import { AccommodationSection, CampSection, ServicesSection } from "@/components/sections/CampSections"; import { ClosingSection, FaqSection } from "@/components/sections/FaqSection"; -import { AboutSection, TeachersSection, VoicesSection } from "@/components/sections/PeopleSections"; +import { AboutSection, TeamSection, VoicesSection } from "@/components/sections/PeopleSections"; import { PricingSection } from "@/components/sections/PricingSection"; import { ReviewsSection } from "@/components/sections/ReviewsSection"; import { SiteFooter } from "@/components/SiteFooter"; @@ -31,7 +31,7 @@ export default async function HomePage() { - + diff --git a/src/components/Gallery.tsx b/src/components/Gallery.tsx index 0f8ac86..7e42f0d 100644 --- a/src/components/Gallery.tsx +++ b/src/components/Gallery.tsx @@ -31,17 +31,19 @@ export function Gallery({ images, showLabels }: { images: readonly Media[]; show }; const step = (delta: number) => setCurrent((c) => (c === null ? c : (c + delta + images.length) % images.length)); - // Grosses Bild links ueber zwei Reihen, rechts zwei Bilder, darunter ein Panorama. - const layout = ["md:col-span-7 md:row-span-2", "md:col-span-5", "md:col-span-5", "md:col-span-12"]; + // Grosses Bild links ueber zwei Reihen, rechts zwei Bilder, darunter ein + // Panorama, danach Dreierreihen. Auf dem Smartphone zwei Spalten, damit + // die Galerie nicht endlos lang wird; Bild 1 und das Panorama volle Breite. + const layout = ["col-span-2 md:col-span-7 md:row-span-2", "md:col-span-5", "md:col-span-5", "col-span-2 md:col-span-12"]; const aspect = (index: number) => - index === 0 ? "aspect-[4/3] md:aspect-auto md:h-full" : index === 3 ? "aspect-[3/2] md:aspect-[3/1]" : "aspect-[3/2]"; + index === 0 ? "aspect-[4/3] md:aspect-auto md:h-full" : index === 3 ? "aspect-[3/2] md:aspect-[3/1]" : "aspect-square md:aspect-[3/2]"; return ( <> -
    +
      {images.map((media, index) => (
    • - +