agency-web/app/components/Scoreboard.tsx

119 lines
3.7 KiB
TypeScript

"use client";
/**
* METRICS scoreboard.
* - CountUp numbers animate on view (Motion).
* - A second ANIMATED-SVG moment: a bar chart whose bars DRAW/grow + a baseline
* that draws itself (GSAP DrawSVG) when the section scrolls in.
* Reduced-motion: numbers jump to final, bars render at full height (CSS).
*/
import { useEffect, useRef } from "react";
import { gsap } from "./gsap";
import CountUp from "./CountUp";
import { metrics } from "../content";
const BARS = [38, 56, 72, 64, 88, 96]; // decorative growth bars (0..100)
export default function Scoreboard() {
const root = useRef<HTMLElement>(null);
useEffect(() => {
const el = root.current;
if (!el) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) return;
const ctx = gsap.context(() => {
const tl = gsap.timeline({
scrollTrigger: { trigger: ".score__chart", start: "top 82%", once: true },
});
// Baseline draws first, then bars grow up off it with a tight stagger and
// a soft landing (back ease => a hair of overshoot so they feel physical).
tl.from(".score__baseline", {
drawSVG: "0%",
duration: 0.8,
ease: "power2.out",
})
.from(
".score-bar",
{
scaleY: 0,
transformOrigin: "bottom",
duration: 1,
ease: "back.out(1.4)",
stagger: 0.07,
},
"-=0.45"
)
.from(
".score-bar__cap",
{
scale: 0,
autoAlpha: 0,
transformOrigin: "center",
duration: 0.5,
ease: "back.out(2.4)",
stagger: 0.07,
},
"-=0.9"
);
}, el);
return () => ctx.revert();
}, []);
return (
<section ref={root} data-invert className="score frame" aria-labelledby="score-h">
<div className="wrap">
<header className="sec-head sec-head--center">
<p className="kicker">
<span className="kicker__dot" />
The scoreboard sample data
</p>
<h2 id="score-h" className="display sec-head__title">
Numbers we report on
</h2>
</header>
<dl className="score__grid">
{metrics.map((m) => (
<div className="score__cell" key={m.label}>
<dt className="sr-only">{m.label}</dt>
<dd
className={`score__num display ${"accent" in m && m.accent ? "is-accent" : ""}`}
>
<CountUp
to={m.value}
prefix={"prefix" in m ? m.prefix : ""}
suffix={m.suffix}
decimals={"decimals" in m ? m.decimals : 0}
/>
</dd>
<p className="score__lab" aria-hidden="true">
{m.label}
</p>
</div>
))}
</dl>
{/* decorative animated bar chart */}
<div className="score__chart" aria-hidden="true">
<div className="score__bars">
{BARS.map((h, i) => (
<span key={i} className="score-bar" style={{ height: `${h}%` }}>
<span className="score-bar__cap" />
</span>
))}
</div>
<svg className="score__axis" viewBox="0 0 600 8" preserveAspectRatio="none">
<line className="score__baseline" x1="0" y1="4" x2="600" y2="4" />
</svg>
</div>
{/* anchors the section + sets honest expectation; kills the empty gap
above the gradient divider */}
<p className="score__foot">Sample data your numbers, reported monthly.</p>
</div>
</section>
);
}