mirror of
https://github.com/Since-3/since3-portal.git
synced 2026-10-12 19:51:17 +00:00
151 lines
7.9 KiB
TypeScript
151 lines
7.9 KiB
TypeScript
"use client"
|
|
|
|
import { AnimatePresence, motion, useInView } from "motion/react"
|
|
import { useEffect, useEffectEvent, useRef, useState } from "react"
|
|
import { cn } from "@/lib/utils"
|
|
import { TImage } from "../shared/media"
|
|
import { Reveal, ScrollWords, ease } from "../shared/motion"
|
|
import type { StatementData, StoryKapitelData, TextBildData } from "./definitions"
|
|
import type { BlockProps } from "./types"
|
|
import { container, easeInOut, Eyebrow, Heading, InkImage } from "./ui"
|
|
|
|
export function Statement({ data }: BlockProps<StatementData>) {
|
|
return (
|
|
<section className={cn(container, "py-28 md:py-44")}>
|
|
<div className="grid gap-10 md:grid-cols-12 md:gap-6">
|
|
<Reveal className="md:col-span-3">
|
|
<Eyebrow>{data.eyebrow}</Eyebrow>
|
|
</Reveal>
|
|
<div className="md:col-span-9">
|
|
<ScrollWords text={data.text} className="font-t-display text-[clamp(1.9rem,4.2vw,4.25rem)] leading-[1.08] tracking-[-0.02em]" />
|
|
{data.author.trim() && (
|
|
<Reveal className="mt-12">
|
|
<p className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
|
|
<span aria-hidden className="h-px w-10 bg-t-accent" />
|
|
<span className="font-medium">{data.author}</span>
|
|
<span className="text-t-muted">{data.role}</span>
|
|
</p>
|
|
</Reveal>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
type Chapter = StoryKapitelData["items"][number]
|
|
|
|
// Geschichte in Kapiteln: links ein stehendes Bild, das sich mit dem aktiven Kapitel von unten neu aufdeckt
|
|
export function StoryKapitel({ data }: BlockProps<StoryKapitelData>) {
|
|
const [active, setActive] = useState(0)
|
|
const current = Math.min(active, Math.max(0, data.items.length - 1))
|
|
|
|
return (
|
|
<section className={cn(container, "py-24 md:py-36")}>
|
|
<div className="grid gap-8 md:grid-cols-12 md:gap-6">
|
|
<Reveal className="md:col-span-3">
|
|
<Eyebrow>{data.eyebrow}</Eyebrow>
|
|
</Reveal>
|
|
<Heading text={data.title} className="md:col-span-9" />
|
|
</div>
|
|
<div className="mt-12 grid gap-6 md:mt-20 md:grid-cols-12">
|
|
<div className="hidden md:col-span-6 md:block">
|
|
<div className="sticky top-[12vh] h-[76vh] overflow-hidden rounded-[1.5rem] bg-t-surface">
|
|
{data.items.map((item, i) => (
|
|
<motion.div
|
|
key={i}
|
|
className="absolute inset-0"
|
|
style={{ zIndex: i }}
|
|
initial={false}
|
|
animate={{ clipPath: i <= current ? "inset(0% 0% 0% 0%)" : "inset(100% 0% 0% 0%)" }}
|
|
transition={{ duration: 1.1, ease: easeInOut }}
|
|
>
|
|
<motion.div className="absolute inset-0" initial={false} animate={{ scale: i === current ? 1 : 1.12 }} transition={{ duration: 1.6, ease }}>
|
|
<TImage src={item.image} alt={item.title} sizes="50vw" />
|
|
</motion.div>
|
|
</motion.div>
|
|
))}
|
|
<div aria-hidden className="absolute inset-0 z-[40] bg-gradient-to-t from-black/55 via-transparent to-transparent" />
|
|
<div className="absolute bottom-6 left-7 z-[41] overflow-hidden font-t-display text-[clamp(3rem,6vw,6.5rem)] leading-none tracking-[-0.04em] text-white">
|
|
<AnimatePresence mode="popLayout" initial={false}>
|
|
<motion.span
|
|
key={current}
|
|
className="block"
|
|
initial={{ y: "100%" }}
|
|
animate={{ y: "0%" }}
|
|
exit={{ y: "-100%" }}
|
|
transition={{ duration: 0.7, ease: easeInOut }}
|
|
>
|
|
{data.items[current]?.year}
|
|
</motion.span>
|
|
</AnimatePresence>
|
|
</div>
|
|
<div aria-hidden className="absolute right-7 bottom-8 z-[41] flex gap-1.5">
|
|
{data.items.map((_, i) => (
|
|
<span key={i} className={cn("h-1 rounded-full transition-all duration-500", i === current ? "w-8 bg-white" : "w-1.5 bg-white/50")} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col md:col-span-5 md:col-start-8">
|
|
{data.items.map((item, i) => (
|
|
<ChapterText key={i} item={item} active={i === current} onEnter={() => setActive(i)} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
function ChapterText({ item, active, onEnter }: { item: Chapter; active: boolean; onEnter: () => void }) {
|
|
const ref = useRef<HTMLElement>(null)
|
|
// Aktiv, sobald das Kapitel die Bildschirmmitte kreuzt
|
|
const inView = useInView(ref, { margin: "-45% 0px -45% 0px" })
|
|
const enter = useEffectEvent(onEnter)
|
|
|
|
useEffect(() => {
|
|
if (inView) enter()
|
|
}, [inView])
|
|
|
|
return (
|
|
<article
|
|
ref={ref}
|
|
className={cn("flex flex-col justify-center py-10 transition-opacity duration-700 md:min-h-[76vh] md:py-0", active ? "opacity-100" : "md:opacity-30")}
|
|
>
|
|
<div className="relative mb-8 aspect-[4/5] overflow-hidden rounded-[1.25rem] bg-t-surface md:hidden">
|
|
<TImage src={item.image} alt={item.title} sizes="100vw" />
|
|
</div>
|
|
<p className="font-t-display text-5xl leading-none tracking-[-0.03em] text-t-accent">{item.year}</p>
|
|
<h3 className="mt-6 font-t-display text-3xl tracking-[-0.02em] md:text-4xl">{item.title}</h3>
|
|
<p className="mt-4 max-w-md text-lg leading-relaxed text-t-muted">{item.text}</p>
|
|
</article>
|
|
)
|
|
}
|
|
|
|
export function TextBild({ data }: BlockProps<TextBildData>) {
|
|
return (
|
|
<section className={cn(container, "py-24 md:py-36")}>
|
|
<div className="grid items-center gap-12 md:grid-cols-12 md:gap-6">
|
|
<Reveal className="md:col-span-5">
|
|
<figure>
|
|
<InkImage src={data.image} alt={data.caption} sizes="(min-width: 768px) 40vw, 100vw" className="aspect-[3/4] rounded-[1.25rem]" />
|
|
{data.caption.trim() && <figcaption className="mt-3 text-xs text-t-muted">{data.caption}</figcaption>}
|
|
</figure>
|
|
</Reveal>
|
|
<div className="md:col-span-6 md:col-start-7">
|
|
<Eyebrow>{data.eyebrow}</Eyebrow>
|
|
<Heading text={data.title} className="mt-6" />
|
|
<Reveal delay={0.1}>
|
|
<p className="mt-8 text-lg leading-relaxed text-t-fg/80 first-letter:float-left first-letter:mt-1.5 first-letter:mr-3 first-letter:font-t-display first-letter:text-[4.6rem] first-letter:leading-[0.78] first-letter:text-t-accent">
|
|
{data.text}
|
|
</p>
|
|
{data.quote.trim() && (
|
|
<blockquote className="mt-10 border-l-2 border-t-accent pl-6 font-t-display text-2xl leading-snug italic md:text-3xl">„{data.quote}“</blockquote>
|
|
)}
|
|
{data.signature.trim() && <p className="mt-10 font-t-display text-3xl italic">{data.signature}</p>}
|
|
</Reveal>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|