since3-website/modules/landingPage/ui/components/projects/projects-showcase.tsx
2026-09-25 00:45:33 +02:00

148 lines
5.7 KiB
TypeScript

"use client"
import { useEffect, useRef, useState } from "react"
import { motion, useScroll, useTransform } from "motion/react"
import { projects } from "@/modules/landingPage/data/content"
import { Magnetic } from "@/modules/shared/ui/components/magnetic"
import { ProjectVisual } from "./project-visual"
/**
* Vertical scroll drives a horizontal track (md and up). On small screens the
* cards simply stack, because the track then has no horizontal overflow.
*/
export function ProjectsShowcase() {
const sectionRef = useRef<HTMLElement>(null)
const trackRef = useRef<HTMLDivElement>(null)
const [distance, setDistance] = useState(0)
useEffect(() => {
const track = trackRef.current
if (!track) return
const desktop = window.matchMedia("(min-width: 48rem)")
const measure = () => {
setDistance(desktop.matches ? Math.max(0, track.scrollWidth - window.innerWidth) : 0)
}
const observer = new ResizeObserver(measure)
observer.observe(track)
desktop.addEventListener("change", measure)
return () => {
observer.disconnect()
desktop.removeEventListener("change", measure)
}
}, [])
const { scrollYProgress } = useScroll({
target: sectionRef,
offset: ["start start", "end end"],
})
const x = useTransform(scrollYProgress, (value) => -value * distance)
const progress = useTransform(scrollYProgress, (value) => (distance ? value : 0))
return (
<section
ref={sectionRef}
id="projekte"
aria-labelledby="projekte-title"
data-theme="dark"
style={{ height: distance ? `calc(100svh + ${distance}px)` : undefined }}
className="relative"
>
<div className="flex flex-col justify-center overflow-x-clip py-section md:sticky md:top-0 md:h-svh md:py-0">
<motion.div
ref={trackRef}
style={{ x }}
className="flex flex-col gap-6 px-gutter md:w-max md:flex-row md:items-end md:gap-8"
>
{/* Intro panel */}
<div className="flex shrink-0 flex-col justify-end gap-6 pb-4 md:h-[68svh] md:w-[38vw]">
<p className="font-mono text-label text-fg-muted">
(Ausgewählte Arbeiten — {String(projects.length).padStart(2, "0")})
</p>
<h2 id="projekte-title" className="text-display type-poster">
Was wir
<br />
gebaut
<br />
haben.
</h2>
<p className="type-body max-w-[36ch] text-fg-muted">
Websites, interne Tools und Automatisierungen. Scroll weiter.
</p>
</div>
{projects.map((project, index) => (
<article
key={project.slug}
data-cursor={project.url ? "Ansehen" : "Bald online"}
className="group flex w-full shrink-0 flex-col gap-4 md:w-[56vw] lg:w-[46vw]"
>
<div className="relative aspect-4/3 overflow-hidden rounded-glass [container-type:inline-size] md:aspect-auto md:h-[58svh]">
<div className="size-full transition-[scale] duration-1000 ease-out-expo group-hover:scale-[1.04]">
<ProjectVisual project={project} index={index} />
</div>
<span className="absolute top-4 left-4 rounded-full bg-black/60 px-3 py-1 font-mono text-label text-white backdrop-blur">
({String(index + 1).padStart(2, "0")})
</span>
</div>
<div className="flex items-start justify-between gap-6">
<div>
<h3 className="type-title">
{project.url ? (
<a href={project.url} className="hover:underline">
{project.title}
</a>
) : (
project.title
)}
</h3>
<p className="mt-2 text-fg-muted">{project.benefit}</p>
</div>
<dl className="shrink-0 text-right font-mono text-label">
<dt className="sr-only">Jahr</dt>
<dd>{project.year}</dd>
<dt className="sr-only">Kunde</dt>
<dd className="text-fg-muted">{project.client}</dd>
</dl>
</div>
<ul aria-label="Leistungen" className="flex flex-wrap gap-2">
{project.tags.map((tag) => (
<li
key={tag}
className="rounded-full border border-line px-3 py-1 font-mono text-label text-fg-muted"
>
{tag}
</li>
))}
</ul>
</article>
))}
{/* Outro: the next project could be yours */}
<div className="flex shrink-0 items-center justify-center py-12 md:h-[68svh] md:w-[34vw]">
<Magnetic strength={0.25}>
<a
href="#kontakt"
className="grid aspect-square w-64 place-items-center rounded-full bg-white p-8 text-center text-black transition-[scale] duration-700 ease-out-expo hover:scale-105 md:w-[22vw]"
>
<span className="text-statement type-poster">
Dein
<br />
Projekt?
</span>
</a>
</Magnetic>
</div>
</motion.div>
{/* Progress of the horizontal journey */}
<div aria-hidden="true" className="container-site absolute inset-x-0 bottom-8 hidden md:block">
<div className="h-px bg-line">
<motion.div style={{ scaleX: progress }} className="h-full origin-left bg-fg" />
</div>
</div>
</div>
</section>
)
}