agency-web/app/components/Scoreboard.tsx

94 lines
2.9 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 80%", once: true },
});
tl.from(".score-bar", {
scaleY: 0,
transformOrigin: "bottom",
duration: 0.9,
ease: "expo.out",
stagger: 0.08,
}).from(
".score__baseline",
{ drawSVG: "0%", duration: 0.9, ease: "power2.out" },
"-=0.7"
);
}, 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}%` }} />
))}
</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>
</div>
</section>
);
}