mirror of
https://github.com/Since-3/since3-website.git
synced 2026-10-12 21:41:16 +00:00
148 lines
5.7 KiB
TypeScript
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>
|
|
)
|
|
}
|