feat: Onepage nach Filmcamp-Gliederung umgebaut
- 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]>
This commit is contained in:
1 parent
3cba3e2341
commit
7203cd1e6e
37 files changed
+962
-517
No files matched your search
@@ -1,9 +1,10 @@
|
||||
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 = { name: string; role: string; bio: string | null; portrait: Media };
|
||||
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>;
|
||||
@@ -12,13 +13,15 @@ function Bio({ bio, showDrafts }: { bio: string | null; showDrafts: boolean }) {
|
||||
|
||||
/**
|
||||
* 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. Die Spalten sind auf breiten Bildschirmen
|
||||
* leicht versetzt, damit das Raster wie eine Magazinseite wirkt.
|
||||
* 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 }: { showDrafts: boolean }) {
|
||||
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">
|
||||
@@ -29,11 +32,11 @@ export function TeamSection({ showDrafts }: { showDrafts: boolean }) {
|
||||
|
||||
<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={head.portrait} aspect="aspect-[4/5]" shape="arch" sizes="(min-width: 768px) 40vw, 100vw" showLabel={showDrafts} />
|
||||
<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="display mt-4 text-[3rem] leading-[0.95] md:text-[5.5rem]">{head.name}</h3>
|
||||
<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>
|
||||
@@ -41,27 +44,21 @@ export function TeamSection({ showDrafts }: { showDrafts: boolean }) {
|
||||
|
||||
<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.name} className={index % 2 === 1 ? "md:mt-16" : ""}>
|
||||
<li key={person.slug} className={index % 2 === 1 ? "md:mt-16" : ""}>
|
||||
<article aria-label={person.name}>
|
||||
<Reveal delay={(index % 4) * 0.06}>
|
||||
<MediaFrame
|
||||
media={person.portrait}
|
||||
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} />
|
||||
</Reveal>
|
||||
<h3 className="display mt-4 text-[1.375rem] leading-tight md:text-[1.75rem]">{person.name}</h3>
|
||||
<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 && (
|
||||
{showDrafts && missing && (
|
||||
<p className="mt-12">
|
||||
<span className="draft-flag">Porträts folgen</span> Platzhalter mit Initialen, bis freigegebene Fotos vorliegen.
|
||||
<span className="draft-flag">Porträts folgen</span> Platzhalter mit Initialen, bis die Fotos im Medienverzeichnis liegen (docs/MEDIEN.md).
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -71,22 +68,17 @@ export function TeamSection({ showDrafts }: { showDrafts: boolean }) {
|
||||
|
||||
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="display text-[2.25rem] leading-none md:col-span-5 md:text-[3rem]">{about.title}</h2>
|
||||
<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">
|
||||
{showDrafts ? (
|
||||
<>
|
||||
<span className="draft-flag">Inhalt folgt</span> Grundlage wird die ARTISTA-Stammmodel-Seite (Link folgt).
|
||||
</>
|
||||
) : (
|
||||
"Mehr über ARTISTA erfährst du in Kürze hier."
|
||||
)}
|
||||
<span className="draft-flag">Inhalt folgt</span> Grundlage wird die ARTISTA-Stammmodel-Seite (Link folgt).
|
||||
</p>
|
||||
)}
|
||||
{about.sourceUrl && (
|
||||
@@ -102,21 +94,26 @@ export function AboutSection({ showDrafts }: { showDrafts: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function VoicesSection({ showDrafts, provider }: { showDrafts: boolean; provider: "none" | "youtube-nocookie" | "vimeo" }) {
|
||||
const { videos } = site;
|
||||
const filled = videos.slots.filter((s) => s.src || s.externalId);
|
||||
// Leere Slots nur in Entwicklung/Staging zeigen; oeffentlich nie ein
|
||||
// erfundenes Testimonial als Ersatz.
|
||||
const slots = showDrafts ? videos.slots : filled;
|
||||
if (slots.length === 0) return null;
|
||||
/**
|
||||
* 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="stimmen" aria-labelledby="stimmen-title" className="border-t border-line py-20 md:py-28">
|
||||
<section id="reels" aria-labelledby="reels-title" className="on-dark bg-ink py-24 text-paper md:py-32">
|
||||
<div className="container-edge">
|
||||
<h2 id="stimmen-title" className="section-title">{videos.title}</h2>
|
||||
<ul className="mt-12 grid gap-10 md:grid-cols-3">
|
||||
{slots.map((slot, index) => (
|
||||
<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} provider={provider} showDrafts={showDrafts} />
|
||||
<VideoSlot slot={slot} index={index} showDrafts={showDrafts} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
Reference in new issue
Block a user