since3-portal/modules/templates/catalog/agentur/template.tsx
2026-10-10 02:38:50 +02:00

255 lines
14 KiB
TypeScript

"use client"
import { AnimatePresence, motion, useMotionValue, useSpring } from "motion/react"
import { useEffect, useRef, useState } from "react"
import { ArrowUpRightIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import type { TemplateProps } from "../../lib/types"
import { TImage } from "../shared/media"
import { Counter, Marquee, Reveal, RevealWords, ScrollWords, ease } from "../shared/motion"
import { TemplateShell } from "../shared/shell"
import { agentur, type AgenturData } from "./definition"
import { fonts } from "./fonts"
const em = "italic text-t-accent"
const container = "mx-auto w-full max-w-[1500px] px-5 md:px-10"
const display = "font-t-display font-extrabold tracking-[-0.05em]"
const GLYPHS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ#%&*+"
// Buchstaben würfeln kurz durcheinander und rasten dann ein
function Scramble({ text, className }: { text: string; className?: string }) {
const [output, setOutput] = useState(text)
const timer = useRef<ReturnType<typeof setInterval> | null>(null)
useEffect(() => () => {
if (timer.current) clearInterval(timer.current)
}, [])
function run() {
if (timer.current) clearInterval(timer.current)
let frame = 0
timer.current = setInterval(() => {
frame += 1
setOutput(
text
.split("")
.map((char, i) => (char === " " || i < frame / 2 ? char : GLYPHS[(i * 7 + frame * 3) % GLYPHS.length]))
.join("")
)
if (frame / 2 >= text.length && timer.current) clearInterval(timer.current)
}, 30)
}
return (
<span onMouseEnter={run} className={className}>
{output}
</span>
)
}
export function AgenturTemplate({ content }: TemplateProps<AgenturData>) {
const { brand, hero, clients, work, manifest, services, numbers, team, contact } = content.data
const show = (key: string) => !content.hidden.includes(key)
const [cursor, setCursor] = useState<string | null>(null)
const x = useMotionValue(-100)
const y = useMotionValue(-100)
const springX = useSpring(x, { stiffness: 500, damping: 40 })
const springY = useSpring(y, { stiffness: 500, damping: 40 })
useEffect(() => {
const move = (event: PointerEvent) => {
x.set(event.clientX)
y.set(event.clientY)
}
window.addEventListener("pointermove", move)
return () => window.removeEventListener("pointermove", move)
}, [x, y])
return (
<TemplateShell def={agentur} content={content} fonts={fonts[content.fonts] ?? fonts.brutal} className="[@media(hover:hover)]:cursor-none [@media(hover:hover)]:[&_a]:cursor-none [@media(hover:hover)]:[&_button]:cursor-none">
<motion.div
aria-hidden
style={{ x: springX, y: springY }}
className="pointer-events-none fixed top-0 left-0 z-[100] hidden [@media(hover:hover)]:block"
>
<motion.div
animate={{ width: cursor ? 110 : 16, height: cursor ? 110 : 16 }}
transition={{ type: "spring", stiffness: 400, damping: 30 }}
className="grid -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full bg-t-accent text-xs font-bold tracking-wider text-t-accent-fg uppercase"
>
{cursor}
</motion.div>
</motion.div>
<header className="fixed inset-x-0 top-0 z-50 mix-blend-difference">
<nav className={cn(container, "flex h-20 items-center justify-between text-white")}>
<a href="#top" className={cn(display, "text-2xl")}>{brand.name}</a>
<div className="hidden gap-8 text-sm font-semibold uppercase md:flex">
<a href="#arbeiten"><Scramble text="Arbeiten" /></a>
{show("services") && <a href="#leistungen"><Scramble text="Leistungen" /></a>}
{show("team") && <a href="#team"><Scramble text="Team" /></a>}
</div>
<a href="#kontakt" className="text-sm font-semibold uppercase underline underline-offset-4"><Scramble text={hero.cta} /></a>
</nav>
</header>
<section id="top" className={cn(container, "relative flex min-h-svh flex-col justify-end pt-32 pb-12")}>
<motion.div
initial={{ scale: 0, rotate: -40 }}
animate={{ scale: 1, rotate: 0 }}
transition={{ duration: 1.4, ease, delay: 0.4 }}
className="absolute top-28 right-5 size-40 overflow-hidden rounded-full md:right-10 md:size-72"
>
<motion.div className="absolute inset-[-20%]" animate={{ rotate: 360 }} transition={{ duration: 30, repeat: Infinity, ease: "linear" }}>
<TImage src={hero.image} eager sizes="300px" />
</motion.div>
</motion.div>
<RevealWords as="h1" onMount delay={0.2} text={hero.title} emClassName={em} className={cn(display, "relative text-[clamp(4rem,14vw,15rem)] leading-[0.82]")} />
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.8, ease, delay: 1 }} className="mt-12 grid gap-8 md:grid-cols-[1fr_auto] md:items-end">
<p className="max-w-md text-xl leading-snug">{hero.text}</p>
<p className="text-sm font-semibold uppercase text-t-muted">{brand.tagline} · {brand.city}</p>
</motion.div>
</section>
{show("clients") && (
<section data-order="clients" className="border-y border-t-fg/15 py-8">
<Marquee duration={22}>
{clients.items.map((client, i) => (
<span key={i} className={cn(display, "px-10 text-5xl md:text-7xl", i % 2 ? "text-t-fg" : "text-transparent [-webkit-text-stroke:1.5px_var(--t-fg)]")}>
{client.name}
</span>
))}
</Marquee>
</section>
)}
<section id="arbeiten" className={cn(container, "py-24 md:py-32")}>
<RevealWords text={work.title} emClassName={em} className={cn(display, "text-[clamp(3rem,8vw,8rem)] leading-[0.9]")} />
<div className="mt-16 space-y-6">
{work.items.map((project, i) => (
<article
key={i}
onMouseEnter={() => setCursor("Ansehen")}
onMouseLeave={() => setCursor(null)}
className="sticky overflow-hidden rounded-[2rem] p-6 text-white md:p-10"
style={{ top: `${6 + i * 1.5}rem`, background: project.color || "var(--t-surface)" }}
>
<div className="grid gap-8 md:grid-cols-[1fr_1.3fr] md:items-end">
<div>
<p className="text-sm font-semibold uppercase opacity-80">{project.category} · {project.year}</p>
<h3 className={cn(display, "mt-3 text-[clamp(3rem,7vw,7rem)] leading-[0.9]")}>{project.title}</h3>
<p className="mt-5 max-w-sm text-lg opacity-90">{project.text}</p>
<span className="mt-6 inline-flex items-center gap-2 rounded-full bg-white px-5 py-2.5 text-sm font-bold text-zinc-900">
Case ansehen <ArrowUpRightIcon className="size-4" />
</span>
</div>
<div className="relative aspect-[4/3] overflow-hidden rounded-2xl">
<TImage src={project.image} alt={project.title} sizes="(min-width: 768px) 55vw, 100vw" />
</div>
</div>
</article>
))}
</div>
</section>
{show("manifest") && (
<section data-order="manifest" className={cn(container, "py-24 md:py-40")}>
<ScrollWords text={manifest.text} className={cn(display, "text-[clamp(2.25rem,5.5vw,5.5rem)] leading-[1] tracking-[-0.04em]")} />
</section>
)}
{show("services") && <Services data-order="services" services={services} />}
{show("numbers") && (
<section data-order="numbers" className="bg-t-accent py-16 text-t-accent-fg">
<dl className={cn(container, "grid grid-cols-2 gap-8 md:grid-cols-4")}>
{numbers.items.map((item, i) => (
<div key={i}>
<dd className={cn(display, "text-6xl md:text-8xl")}>
<Counter value={item.value} />
{item.suffix}
</dd>
<dt className="mt-1 font-semibold uppercase">{item.label}</dt>
</div>
))}
</dl>
</section>
)}
{show("team") && (
<section data-order="team" id="team" className={cn(container, "py-24 md:py-32")}>
<RevealWords text={team.title} emClassName={em} className={cn(display, "text-[clamp(3rem,8vw,8rem)] leading-[0.9]")} />
<div className="mt-14 grid grid-cols-2 gap-4 md:grid-cols-4">
{team.members.map((member, i) => (
<Reveal key={i} delay={i * 0.08}>
<div className="group" onMouseEnter={() => setCursor("Hallo!")} onMouseLeave={() => setCursor(null)}>
<div className="relative aspect-[3/4] overflow-hidden rounded-2xl">
<TImage src={member.image} alt={member.name} sizes="(min-width: 768px) 25vw, 50vw" className="grayscale transition duration-500 group-hover:scale-105 group-hover:grayscale-0" />
</div>
<p className={cn(display, "mt-4 text-2xl")}>{member.name}</p>
<p className="text-sm uppercase text-t-muted">{member.role}</p>
</div>
</Reveal>
))}
</div>
</section>
)}
<section id="kontakt" className={cn(container, "pt-24 pb-16 md:pt-32")}>
<RevealWords text={contact.title} emClassName={em} className={cn(display, "text-[clamp(3.5rem,10vw,11rem)] leading-[0.85]")} />
<Reveal delay={0.1}>
<p className="mt-10 max-w-md text-xl">{contact.text}</p>
<a
href={`mailto:${brand.email}`}
onMouseEnter={() => setCursor("Schreiben")}
onMouseLeave={() => setCursor(null)}
className={cn(display, "group relative mt-10 inline-block text-[clamp(2rem,6vw,5.5rem)] text-t-accent")}
>
{brand.email}
<span className="absolute -bottom-1 left-0 h-1.5 w-full origin-left scale-x-0 bg-t-accent transition-transform duration-500 group-hover:scale-x-100" />
</a>
</Reveal>
<div className="mt-24 flex flex-col gap-4 border-t border-t-fg/15 pt-8 text-sm uppercase text-t-muted md:flex-row md:justify-between">
<p>{brand.name} · {brand.city}</p>
<div className="flex gap-6">
<a href="#" className="hover:text-t-fg">Impressum</a>
<a href="#" className="hover:text-t-fg">Datenschutz</a>
</div>
</div>
</section>
</TemplateShell>
)
}
function Services({ services }: { services: AgenturData["services"] }) {
const [active, setActive] = useState<number | null>(null)
return (
<section id="leistungen" className={cn(container, "py-24 md:py-32")}>
<RevealWords text={services.title} emClassName={em} className={cn(display, "text-[clamp(3rem,8vw,8rem)] leading-[0.9]")} />
<ul className="mt-14 border-t border-t-fg/15" onMouseLeave={() => setActive(null)}>
{services.items.map((item, i) => (
<li key={i} onMouseEnter={() => setActive(i)} className="border-b border-t-fg/15">
<div className="flex flex-col gap-2 py-6 md:flex-row md:items-center md:gap-10 md:py-8">
<span className="text-sm font-semibold text-t-muted">0{i + 1}</span>
<motion.span
animate={{ x: active === i ? 24 : 0, opacity: active === null || active === i ? 1 : 0.3 }}
transition={{ duration: 0.4, ease }}
className={cn(display, "flex-1 text-[clamp(2.5rem,6vw,6rem)] leading-none")}
>
{item.title}
</motion.span>
<AnimatePresence>
{(active === i || active === null) && (
<motion.p initial={{ opacity: 0 }} animate={{ opacity: active === i ? 1 : 0.6 }} exit={{ opacity: 0 }} className="max-w-xs text-t-muted md:text-right">
{item.text}
</motion.p>
)}
</AnimatePresence>
</div>
</li>
))}
</ul>
</section>
)
}