94 lines
2.9 KiB
TypeScript
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>
|
|
);
|
|
}
|