feat: Tagesablauf laut Wochenplan, Termin 03. bis 09. Mai 2027
Docker-Images bauen und veroeffentlichen / build-and-push (push) Successful in 2m51s

- Alle sieben Tage aus dem Wochenplan-PDF mit Uhrzeit, Coach und
  Foto-Kennzeichnung im Karussell
- Migration: Camp-Termin 03.05. bis 09.05.2027 (ersetzt 08. bis 15.05.)
- Leistungen laut PDF: 20 Workshops und Shootings, sechs Naechte,
  Bustransfer ab und bis Florenz, Fruehstueck, Snacks und Abendessen
- FAQ Anreise mit Transferzeiten
- Hinweise "Programmentwurf" und "Laut Beschreibung der Unterkunft" entfernt

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-02 05:49:33 +02:00
1 parent 3f5a2554ee
commit 84c8f2f32b
7 files changed
+133 -47

No files matched your search

+2 -4
View File
@@ -2,11 +2,11 @@
Stand 01.10.2026. Bereits bestätigt und per Migration `20261001120000_camp_2027_bestaetigt` eingetragen (im Admin änderbar): 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) - Termin 03.05. bis 09.05.2027 (laut Wochenplan vom 02.10.2026), Villa La Chiesina, Arcidosso (Toskana)
- Unterbringung: eigenes Bett im geteilten Zimmer, keine Einzelzimmer - Unterbringung: eigenes Bett im geteilten Zimmer, keine Einzelzimmer
- 13 verkaufbare Plätze - 13 verkaufbare Plätze
- Team (Namen und Rollen) und Unterkunftsbilder; die Booking-Seite wird bewusst nicht verlinkt - Team (Namen und Rollen) und Unterkunftsbilder; die Booking-Seite wird bewusst nicht verlinkt
- Abholung am Anreisetag vom Bahnhof Firenze Santa Maria Novella - Tagesablauf aller sieben Tage, Bustransfer ab und bis Florenz, Verpflegung (Frühstück, Snacks, Abendessen)
- Early Bird 01.11. bis 31.12.2026, ohne Anzeige eines Verkaufsstarts - Early Bird 01.11. bis 31.12.2026, ohne Anzeige eines Verkaufsstarts
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“. 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“.
@@ -18,8 +18,6 @@ Solange die mit **(Freigabe)** markierten Punkte fehlen, bleibt der Live-Verkauf
| Logo (`logo.svg`, `logo-hell.svg`) und Headerbild (`header.jpg`) | Medienverzeichnis, Dateinamen in `docs/MEDIEN.md` | | Logo (`logo.svg`, `logo-hell.svg`) und Headerbild (`header.jpg`) | Medienverzeichnis, Dateinamen in `docs/MEDIEN.md` |
| Porträts der neun Teammitglieder | Medienverzeichnis `team/<name>.jpg`, siehe `docs/MEDIEN.md` | | Porträts der neun Teammitglieder | Medienverzeichnis `team/<name>.jpg`, siehe `docs/MEDIEN.md` |
| Freigegebene Kurzbeschreibungen der Teammitglieder | `site.ts` → `team` | | Freigegebene Kurzbeschreibungen der Teammitglieder | `site.ts` → `team` |
| Tagesablauf (Programm als PDF angekündigt) | `site.ts` → `schedule.days` |
| Uhrzeit der Abholung in Florenz | `site.ts` → `schedule.days[0]` |
| Schriftliche Bestätigung des Vermieters, dass die Booking-Bilder genutzt werden dürfen (Urheber ist der Vermieter bzw. Fotograf) | Ablage beim Veranstalter | | 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` | | Link zur ARTISTA-Stammmodel-Seite und daraus abgeleiteter Text „Über ARTISTA“ | `site.ts` → `about` |
| Drei Reels (Video + Vorschaubild) mit Freigabe der gezeigten Personen | Medienverzeichnis `reels/reel-1.mp4` usw., Namen in `site.ts` → `reels` | | Drei Reels (Video + Vorschaubild) mit Freigabe der gezeigten Personen | Medienverzeichnis `reels/reel-1.mp4` usw., Namen in `site.ts` → `reels` |
@@ -0,0 +1,12 @@
-- Termin laut Wochenplan des Auftraggebers (02.10.2026):
-- Montag, 03.05.2027 bis Sonntag, 09.05.2027.
-- Ersetzt nur den per Migration 20261001120000 gesetzten Termin; ein im
-- Adminbereich bereits anders gepflegter Termin bleibt unveraendert.
UPDATE "Camp" SET
"campStartDate" = DATE '2027-05-03',
"campEndDate" = DATE '2027-05-09',
"dateConfirmed" = true,
"updatedAt" = CURRENT_TIMESTAMP,
"updatedBy" = 'migration'
WHERE "id" = 'modelcamp-2027'
AND ("campStartDate" IS NULL OR "campStartDate" = DATE '2027-05-08');
+19 -13
View File
@@ -1,15 +1,16 @@
"use client"; "use client";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import type { ScheduleItem } from "@/content/site";
export type ScheduleCard = { label: string; date: string | null; title: string | null; items: string[] }; export type ScheduleCard = { label: string; date: string | null; title: string; items: ScheduleItem[] };
/** /**
* Tagesablauf als waagerechte Kartenreihe mit Einrasten (scroll-snap). * Tagesablauf als waagerechte Kartenreihe mit Einrasten (scroll-snap).
* Wischen auf dem Smartphone, Pfeile und Punkte auf allen Groessen. Die * Wischen auf dem Smartphone, Pfeile und Punkte auf allen Groessen. Die
* Liste bleibt eine echte Liste: ohne JavaScript scrollt sie trotzdem. * Liste bleibt eine echte Liste: ohne JavaScript scrollt sie trotzdem.
*/ */
export function ScheduleCarousel({ days, emptyText }: { days: ScheduleCard[]; emptyText: string }) { export function ScheduleCarousel({ days }: { days: ScheduleCard[] }) {
const track = useRef<HTMLOListElement>(null); const track = useRef<HTMLOListElement>(null);
const cards = useRef<(HTMLLIElement | null)[]>([]); const cards = useRef<(HTMLLIElement | null)[]>([]);
const [current, setCurrent] = useState(0); const [current, setCurrent] = useState(0);
@@ -52,23 +53,28 @@ export function ScheduleCarousel({ days, emptyText }: { days: ScheduleCard[]; em
cards.current[index] = el; cards.current[index] = el;
}} }}
data-index={index} data-index={index}
className="flex w-[85%] shrink-0 snap-start flex-col rounded-2xl bg-paper p-7 shadow-[0_1px_0_var(--color-line)] sm:w-[60%] md:w-[44%] md:p-9 lg:w-[31%]" className="flex w-[88%] shrink-0 snap-start flex-col rounded-2xl bg-paper p-6 shadow-[0_1px_0_var(--color-line)] sm:w-[62%] md:w-[46%] md:p-8 lg:w-[34%]"
> >
<p className="text-sm font-semibold uppercase tracking-[0.16em] text-rosso">{day.label}</p> <p className="text-sm font-semibold uppercase tracking-[0.16em] text-rosso">
{day.label} · {day.title}
</p>
{day.date && <p className="mt-1 text-xl font-bold md:text-2xl">{day.date}</p>} {day.date && <p className="mt-1 text-xl font-bold md:text-2xl">{day.date}</p>}
{day.title && <p className="mt-1 text-stone">{day.title}</p>} <ul className="mt-5 divide-y divide-line border-t border-line">
{day.items.length > 0 ? (
<ul className="mt-6 space-y-3 border-t border-line pt-5">
{day.items.map((item) => ( {day.items.map((item) => (
<li key={item} className="flex gap-3"> <li key={`${item.time} ${item.text}`} className="grid grid-cols-[5.75rem_1fr] gap-x-3 py-3 text-[0.9375rem] leading-snug">
<span aria-hidden className="mt-[0.6em] h-1.5 w-1.5 shrink-0 rounded-full bg-rosso" /> <span className="font-semibold tabular-nums text-ink/75">{item.time}</span>
{item} <span>
{item.text}
{item.photo && (
<span className="ml-2 inline-block translate-y-[-1px] rounded bg-sand/60 px-1.5 py-px align-middle text-[0.6875rem] font-bold uppercase tracking-[0.08em]">
Foto
</span>
)}
{item.coach && <span className="mt-0.5 block text-sm text-stone">mit {item.coach}</span>}
</span>
</li> </li>
))} ))}
</ul> </ul>
) : (
<p className="mt-6 border-t border-line pt-5 text-stone">{emptyText}</p>
)}
</li> </li>
))} ))}
</ol> </ol>
-1
View File
@@ -29,7 +29,6 @@ export function AccommodationSection({ facts, showDrafts }: { facts: PublicCampF
<li key={feature} className="py-2.5">{feature}</li> <li key={feature} className="py-2.5">{feature}</li>
))} ))}
</ul> </ul>
<p className="mt-2 text-sm text-stone">Laut Beschreibung der Unterkunft.</p>
</div> </div>
</div> </div>
</div> </div>
+2 -4
View File
@@ -52,13 +52,11 @@ export function ScheduleSection({ facts }: { facts: PublicCampFacts }) {
return ( return (
<section id="ablauf" aria-labelledby="ablauf-title" className="bg-sand/35 py-24 md:py-32"> <section id="ablauf" aria-labelledby="ablauf-title" className="bg-sand/35 py-24 md:py-32">
<div className="container-edge"> <div className="container-edge">
<div className="flex flex-wrap items-end justify-between gap-6">
<h2 id="ablauf-title" className="section-title">{schedule.title}</h2> <h2 id="ablauf-title" className="section-title">{schedule.title}</h2>
<p className="max-w-md text-ink/80">{schedule.note}</p>
</div>
<div className="mt-12"> <div className="mt-12">
<ScheduleCarousel days={scheduleCards(facts)} emptyText="Programm folgt." /> <ScheduleCarousel days={scheduleCards(facts)} />
</div> </div>
<p className="mt-8 text-ink/80">{schedule.footnote}</p>
</div> </div>
</section> </section>
); );
+1 -1
View File
@@ -33,7 +33,7 @@ function faqs(facts: PublicCampFacts, pricing: PublicPricing): Faq[] {
}, },
{ {
q: "Wie reise ich an?", q: "Wie reise ich an?",
a: `Du reist selbst nach Florenz an. Am Anreisetag holen wir dich am Bahnhof Firenze Santa Maria Novella ab und bringen dich zur Villa. Kommst du mit dem Auto, fährst du direkt zur Unterkunft. Die Anreise nach Italien und zurück ist nicht im Preis enthalten.${facts.location ? ` Das Camp findet in der ${facts.location} statt.` : ""}`, a: `Du reist selbst nach Florenz an. Am Anreisetag fährt um 15:30 Uhr ein Bus vom Bahnhof Firenze Santa Maria Novella zur Villa. Kommst du mit dem Auto, fährst du direkt zur Unterkunft und bist zwischen 17:30 und 18:00 Uhr da. Am Abreisetag bringt dich der Bus zwischen 09:00 und 12:00 Uhr zurück nach Florenz. Die Anreise nach Italien und zurück ist nicht im Preis enthalten.${facts.location ? ` Das Camp findet in der ${facts.location} statt.` : ""}`,
}, },
{ {
q: "Habe ich ein Widerrufsrecht?", q: "Habe ich ein Widerrufsrecht?",
+97 -24
View File
@@ -30,7 +30,8 @@ export type Media =
export type Confirmable<T> = T & { confirmed: boolean }; export type Confirmable<T> = T & { confirmed: boolean };
export type ScheduleDay = { title: string | null; items: string[] }; export type ScheduleItem = { time: string; text: string; coach?: string; photo?: boolean };
export type ScheduleDay = { title: string; items: ScheduleItem[] };
const portrait = (name: string, initials: string): Media => ({ kind: "placeholder", label: `Porträt ${name}`, tone: "portrait", glyph: initials }); const portrait = (name: string, initials: string): Media => ({ kind: "placeholder", label: `Porträt ${name}`, tone: "portrait", glyph: initials });
/** Lokales Bild aus public/images/unterkunft (1280 x 853 px). */ /** Lokales Bild aus public/images/unterkunft (1280 x 853 px). */
@@ -55,7 +56,7 @@ export const site = {
"Die Villa liegt zwischen Zypressen oberhalb von Arcidosso, mit Blick über die Hügel am Monte Amiata. Hier wohnen, arbeiten und essen wir eine Woche lang zusammen.", "Die Villa liegt zwischen Zypressen oberhalb von Arcidosso, mit Blick über die Hügel am Monte Amiata. Hier wohnen, arbeiten und essen wir eine Woche lang zusammen.",
"Garten, Pool, alte Steintreppen und Räume mit Holzbalkendecken: Die Villa bietet viele Orte zum Fotografieren und zum Durchatmen.", "Garten, Pool, alte Steintreppen und Räume mit Holzbalkendecken: Die Villa bietet viele Orte zum Fotografieren und zum Durchatmen.",
], ],
highlights: ["Eigenes Bett im geteilten Zimmer", "Abholung am Bahnhof in Florenz", "Kleine Gruppe, ein ganzes Team"], highlights: ["Eigenes Bett im geteilten Zimmer", "Bustransfer ab und bis Florenz", "20 Workshops und Shootings mit dem Team"],
slides: [ slides: [
villa("villa-eingang", "Die Villa La Chiesina mit Steintreppe und Rasen im Frühling"), villa("villa-eingang", "Die Villa La Chiesina mit Steintreppe und Rasen im Frühling"),
villa("pool-zypressen", "Pool zwischen hohen Zypressen, im Hintergrund die Hügel der Toskana"), villa("pool-zypressen", "Pool zwischen hohen Zypressen, im Hintergrund die Hügel der Toskana"),
@@ -67,39 +68,111 @@ export const site = {
schedule: { schedule: {
title: "Tagesablauf", title: "Tagesablauf",
note: "Programmentwurf. Das finale Programm kann sich noch ändern.", // Quelle: Modelcamp_2027_Wochenplan.pdf vom Auftraggeber (02.10.2026),
// Ein Eintrag pro Tag ab Anreisetag; das Datum kommt aus dem // Seiten 2 bis 4. Ein Eintrag pro Tag ab Anreisetag; Wochentag und
// Adminbereich. Das Programm liefert der Auftraggeber als PDF nach. // Datum kommen aus dem Camp-Termin im Adminbereich.
// Leere Tage zeigen "Programm folgt". Nichts erfinden.
days: [ days: [
{ {
title: "Anreise", title: "Anreise",
items: [ items: [
"Eigene Anreise nach Florenz", { time: "Vormittag", text: "Eigene Anreise nach Florenz" },
"Abholung am Bahnhof Firenze Santa Maria Novella und gemeinsame Fahrt zur Villa", { time: "15:30", text: "Bustransfer vom Bahnhof Santa Maria Novella zur Unterkunft" },
"Mit dem Auto? Dann fährst du direkt zur Villa", { time: "17:30 bis 18:00", text: "Ankunft der Selbstfahrer" },
{ time: "18:00", text: "Zimmerverteilung" },
{ time: "18:30", text: "Kennenlernen" },
{ time: "19:00", text: "Abendessen" },
{ time: "20:00", text: "Abendausklang am Kamin" },
],
},
{
title: "Fundament",
items: [
{ time: "08:30 bis 09:30", text: "Frühstück, ab 09:00 Einführung und Ziele der Woche" },
{ time: "10:00 bis 11:15", text: "Stretching & Mobility: Yoga und Quick-Routine", coach: "Lola" },
{ time: "11:15 bis 13:00", text: "Catwalk-Grundlagen, inkl. Vorher-Video (gefilmt von Eric und Saskia)", coach: "Eileen" },
{ time: "13:30 bis 14:30", text: "Make-up-Basics: Clean / Natural Look", coach: "Gabi" },
{ time: "15:00 bis 16:30", text: "Social-Media-Grundlagen", coach: "Saskia" },
{ time: "17:30 bis 18:45", text: "Stimmtraining", coach: "Eric" },
{ time: "19:00", text: "Abendessen" },
{ time: "20:00 bis 21:00", text: "Toastmasters Runde 1: Stegreif-Reden, erste Wirkung", coach: "Eric" },
],
},
{
title: "Ausdruck & Kamera",
items: [
{ time: "07:30 bis 08:15", text: "Frühsport (optional)", coach: "Gabi" },
{ time: "08:30 bis 09:30", text: "Frühstück" },
{ time: "10:00 bis 11:30", text: "Mimikkontrolle", coach: "Eric", photo: true },
{ time: "11:30 bis 13:30", text: "Range: Angel or Devil Shoot, Emotion vor der Kamera", coach: "Lola, Eric, Michele", photo: true },
{ time: "14:00 bis 15:00", text: "Commercial-Posing", coach: "Gabi" },
{ time: "15:00 bis 16:00", text: "Testimonial-Look", coach: "Gabi", photo: true },
{ time: "16:15 bis 17:15", text: "Runway-Posen am Laufstegende", coach: "Eileen", photo: true },
{ time: "17:30 bis 18:30", text: "Tennisshoot", coach: "Michele" },
{ time: "19:00", text: "Abendessen" },
{ time: "20:00", text: "Angel-or-Devil-Bildbesprechung und Spieleabend", coach: "Lola" },
],
},
{
title: "Ausflugstag Arcidosso",
items: [
{ time: "08:30 bis 09:30", text: "Frühstück / Sandwichstation" },
{ time: "10:00 bis 11:00", text: "Spaziergang nach Arcidosso" },
{ time: "11:00 bis 12:30", text: "Foto-Content fürs eigene Profil, vor Ort", coach: "Saskia", photo: true },
{ time: "12:30 bis 14:00", text: "Freies Shooten in Arcidosso" },
{ time: "14:00", text: "Treffen und Austausch" },
{ time: "14:30 bis 15:30", text: "Spaziergang zurück zum Camp" },
{ time: "16:30 bis 18:00", text: "Range: Acting & Performance", coach: "Lola" },
{ time: "19:00", text: "Abendessen" },
{ time: "20:00", text: "Lagerfeuer und Musik" },
],
},
{
title: "Walk, Content & Pool",
items: [
{ time: "08:30 bis 09:30", text: "Frühstück" },
{ time: "10:00 bis 13:00", text: "Walk: Runway-Analyse, High-Fashion & Commercial, Fashion Cities, Runway Practice", coach: "Lola" },
{ time: "13:30 bis 14:15", text: "Walk mit Musik & Rhythmus", coach: "Eileen", photo: true },
{ time: "14:15 bis 16:15", text: "Content & Reels produzieren", coach: "Saskia", photo: true },
{ time: "16:30 bis 18:30", text: "Poolparty mit Cocktails und Fingerfood" },
{ time: "18:30", text: "Gruppenfoto" },
{ time: "19:00", text: "Abendessen" },
{ time: "20:00 bis 21:00", text: "Toastmasters Runde 2: Fortschritt sichtbar machen", coach: "Eric" },
{ time: "21:30", text: "Special-Programm für Mutige" },
],
},
{
title: "Editorial, Pitch & Finale",
items: [
{ time: "07:30 bis 08:15", text: "Frühsport (optional)", coach: "Gabi" },
{ time: "08:30 bis 09:30", text: "Frühstück" },
{ time: "10:00 bis 13:00", text: "Editorial Posing inkl. Test-Shoot", coach: "Lola, Michele", photo: true },
{ time: "13:45 bis 14:30", text: "Elevator Pitch: erarbeiten", coach: "Eric" },
{ time: "14:30 bis 15:30", text: "Elevator Pitch: vorstellen, als Vorstellungsvideo (gefilmt von Saskia)", coach: "Eric", photo: true },
{ time: "16:00 bis 17:30", text: "Bildauswahl und Einzelfeedback", coach: "Team" },
{ time: "17:30 bis 19:00", text: "Pause und Styling" },
{ time: "19:00 bis 20:30", text: "Golden-Hour-Finalshooting", coach: "Michele", photo: true },
{ time: "20:30", text: "Abschiedsdinner, Reel- und Bildervorstellung, Abschiedsfeier" },
],
},
{
title: "Abreise",
items: [
{ time: "08:00 bis 08:45", text: "Sandwichstation" },
{ time: "09:00 bis 12:00", text: "Bustransfer nach Florenz" },
], ],
}, },
{ title: null, items: [] },
{ title: null, items: [] },
{ title: null, items: [] },
{ title: null, items: [] },
{ title: null, items: [] },
{ title: null, items: [] },
{ title: "Abreise", items: [] },
] as ScheduleDay[], ] as ScheduleDay[],
footnote: "Frühstück, Snacks über den Tag und Abendessen sind für alle inklusive.",
}, },
services: { services: {
included: [ included: [
// Bestaetigt durch das Buchungsprodukt bzw. den Auftraggeber: // Bestaetigt durch das Buchungsprodukt bzw. den Auftraggeber
{ text: "Teilnahme am Modelcamp 2027", confirmed: true }, // (Wochenplan-PDF vom 02.10.2026):
{ text: "Sieben Nächte in der Villa La Chiesina, eigenes Bett im geteilten Zimmer", confirmed: true }, { text: "Teilnahme am Modelcamp 2027 mit 20 Workshops und Shootings", confirmed: true },
{ text: "Abholung am Anreisetag vom Bahnhof Firenze Santa Maria Novella", confirmed: true }, { text: "Sechs Nächte in der Villa La Chiesina, eigenes Bett im geteilten Zimmer", confirmed: true },
// Entwuerfe: erst nach Bestaetigung (Admin: "Leistungen bestaetigt") { text: "Bustransfer vom Bahnhof Firenze Santa Maria Novella zur Villa und zurück", confirmed: true },
// auf confirmed: true setzen. { text: "Frühstück, Snacks über den Tag und Abendessen", confirmed: true },
{ text: "Workshops und Coachings mit dem ARTISTA-Team", confirmed: false },
{ text: "Verpflegung vor Ort (Umfang folgt)", confirmed: false },
] satisfies Confirmable<{ text: string }>[], ] satisfies Confirmable<{ text: string }>[],
excluded: [ excluded: [
// Laut Vorlage ausdruecklich nicht enthalten. // Laut Vorlage ausdruecklich nicht enthalten.