- Figtree als einzige Schrift, Logo mittig in fester Kopfzeile - Headerbild ueber volle Breite und Hoehe, ohne Preis, mit Terminleiste - "Was dich erwartet" stellt die Villa mit animierter Bilderfolge vor, darunter Tagesablauf als Karussell (Programm folgt als PDF) - Preis: regulaerer Preis durchgestrichen, kein Verkaufsstartdatum, Ueberschrift "Jetzt Platz sichern mit Early Bird" - Team als "Mit dabei", danach drei Reels im Hochformat - Logo, Headerbild, Portraets und Reels ueber feste Dateinamen im Medienverzeichnis (docs/MEDIEN.md), ohne Neustart austauschbar - Widerrufsbelehrung im Admin pflegbar, Seite /widerruf, Live-Verkauf ohne Freigabe gesperrt - Anreise: Abholung am Bahnhof Firenze Santa Maria Novella - Booking-Link entfernt, Gedankenstriche aus Kundentexten entfernt Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
124 lines
5.8 KiB
TypeScript
124 lines
5.8 KiB
TypeScript
import { site, type Media } from "@/content/site";
|
|
import type { Reel } from "@/lib/site-media";
|
|
import { MediaFrame } from "../MediaFrame";
|
|
import { Reveal } from "../Reveal";
|
|
import { VideoSlot } from "../VideoSlot";
|
|
|
|
type TeamMember = { slug: string; name: string; role: string; bio: string | null; portrait: Media };
|
|
|
|
function Bio({ bio, showDrafts }: { bio: string | null; showDrafts: boolean }) {
|
|
if (bio) return <p className="mt-3 max-w-md">{bio}</p>;
|
|
return showDrafts ? <p className="mt-3"><span className="draft-flag">Kurzbeschreibung folgt</span></p> : null;
|
|
}
|
|
|
|
/**
|
|
* Team: Eric Heitz als Head of Modelcamp gross vorangestellt, darunter alle
|
|
* weiteren gleichwertig im Raster. Portraets im Rundbogen, ein Motiv aus
|
|
* der Architektur der Villa. Liegt team/<slug>.jpg im Medienverzeichnis,
|
|
* ersetzt das Foto den Initialen-Platzhalter.
|
|
*/
|
|
export function TeamSection({ showDrafts, portraits }: { showDrafts: boolean; portraits: Record<string, Media> }) {
|
|
const { team } = site;
|
|
const { head } = team;
|
|
const portraitOf = (member: TeamMember) => portraits[member.slug] ?? member.portrait;
|
|
const missing = [head, ...team.people].some((member) => portraitOf(member).kind !== "image");
|
|
return (
|
|
<section id="team" aria-labelledby="team-title" className="bg-paper py-24 md:py-36">
|
|
<div className="container-edge">
|
|
<div className="grid gap-6 md:grid-cols-12 md:gap-10">
|
|
<h2 id="team-title" className="section-title md:col-span-5">{team.title}</h2>
|
|
<p className="max-w-xl text-lg text-ink/85 md:col-span-6 md:col-start-7 md:pt-3">{team.lead}</p>
|
|
</div>
|
|
|
|
<article aria-label={head.name} className="mt-14 grid items-end gap-8 md:mt-20 md:grid-cols-12 md:gap-10">
|
|
<Reveal className="md:col-span-5">
|
|
<MediaFrame media={portraitOf(head)} aspect="aspect-[4/5]" shape="arch" sizes="(min-width: 768px) 40vw, 100vw" showLabel={showDrafts} />
|
|
</Reveal>
|
|
<div className="md:col-span-6 md:col-start-7 md:pb-10">
|
|
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-rosso">{head.title}</p>
|
|
<h3 className="mt-4 text-[3rem] font-extrabold leading-[0.95] tracking-[-0.03em] md:text-[5rem]">{head.name}</h3>
|
|
<p className="mt-5 border-t border-ink pt-4 text-lg">{head.role}</p>
|
|
<Bio bio={head.bio} showDrafts={showDrafts} />
|
|
</div>
|
|
</article>
|
|
|
|
<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) => (
|
|
<li key={person.slug} className={index % 2 === 1 ? "md:mt-16" : ""}>
|
|
<article aria-label={person.name}>
|
|
<Reveal delay={(index % 4) * 0.06}>
|
|
<MediaFrame media={portraitOf(person)} aspect="aspect-[3/4]" shape="arch" sizes="(min-width: 768px) 22vw, 50vw" showLabel={false} />
|
|
</Reveal>
|
|
<h3 className="display mt-4 text-[1.375rem] leading-tight md:text-[1.625rem]">{person.name}</h3>
|
|
<p className="mt-1 text-sm text-stone md:text-[0.9375rem]">{person.role}</p>
|
|
<Bio bio={person.bio} showDrafts={showDrafts} />
|
|
</article>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
{showDrafts && missing && (
|
|
<p className="mt-12">
|
|
<span className="draft-flag">Porträts folgen</span> Platzhalter mit Initialen, bis die Fotos im Medienverzeichnis liegen (docs/MEDIEN.md).
|
|
</p>
|
|
)}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export function AboutSection({ showDrafts }: { showDrafts: boolean }) {
|
|
const { about } = site;
|
|
if (about.paragraphs.length === 0 && !showDrafts) return null;
|
|
return (
|
|
<section id="artista" aria-labelledby="artista-title" className="py-20 md:py-28">
|
|
<div className="container-edge grid gap-6 border-t border-ink pt-10 md:grid-cols-12 md:gap-10 md:pt-14">
|
|
<h2 id="artista-title" className="section-title md:col-span-5">{about.title}</h2>
|
|
<div className="prose-body text-lg md:col-span-6 md:col-start-7 md:pt-2">
|
|
{about.paragraphs.length > 0 ? (
|
|
about.paragraphs.map((p) => <p key={p}>{p}</p>)
|
|
) : (
|
|
<p className="text-stone">
|
|
<span className="draft-flag">Inhalt folgt</span> Grundlage wird die ARTISTA-Stammmodel-Seite (Link folgt).
|
|
</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>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Drei Reels im Hochformat. Leere Slots erscheinen nur in Entwicklung und
|
|
* Staging; oeffentlich nie ein erfundenes Testimonial als Ersatz.
|
|
*/
|
|
export function ReelsSection({ showDrafts, reels }: { showDrafts: boolean; reels: Reel[] }) {
|
|
const { reels: content } = site;
|
|
const slots = content.slots.map((slot, index) => ({ ...slot, ...(reels[index] ?? { src: null, poster: null }) }));
|
|
const visible = showDrafts ? slots : slots.filter((s) => s.src);
|
|
if (visible.length === 0) return null;
|
|
return (
|
|
<section id="reels" aria-labelledby="reels-title" className="on-dark bg-ink py-24 text-paper md:py-32">
|
|
<div className="container-edge">
|
|
<div className="flex flex-wrap items-end justify-between gap-6">
|
|
<h2 id="reels-title" className="section-title">{content.title}</h2>
|
|
<p className="max-w-md text-paper/75">{content.lead}</p>
|
|
</div>
|
|
<ul className="mx-auto mt-12 grid max-w-5xl gap-6 sm:grid-cols-3 md:gap-8">
|
|
{visible.map((slot, index) => (
|
|
<li key={index}>
|
|
<VideoSlot slot={slot} index={index} showDrafts={showDrafts} />
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|