agency-web/app/components/BeforeAfter.tsx

103 lines
3.6 KiB
TypeScript

"use client";
import { useRef, useState } from "react";
/**
* Interactive "before / after" revenue slider — the visual proof-of-value
* pattern. A draggable divider wipes a muted "before" chart to reveal a
* surging acid "after" chart. Fully keyboard operable via a real range
* input (arrow keys). The headline result is always visible as text, so
* meaning never depends on the interaction.
*/
export default function BeforeAfter({
before,
after,
caption,
}: {
before: string;
after: string;
caption: string;
}) {
const [pos, setPos] = useState(38);
const wrap = useRef<HTMLDivElement>(null);
const id = `ba-${caption.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}`;
const drag = (clientX: number) => {
const r = wrap.current?.getBoundingClientRect();
if (!r) return;
const p = ((clientX - r.left) / r.width) * 100;
setPos(Math.max(2, Math.min(98, p)));
};
return (
<figure className="ba">
<div
className="ba__stage"
ref={wrap}
onPointerMove={(e) => e.buttons === 1 && drag(e.clientX)}
onPointerDown={(e) => drag(e.clientX)}
>
{/* AFTER (full) — surging line */}
<div className="ba__layer ba__after" aria-hidden="true">
<Chart variant="after" />
<span className="ba__tag ba__tag--after">{after}</span>
</div>
{/* BEFORE (clipped to slider) — flat, muted */}
<div
className="ba__layer ba__before"
style={{ clipPath: `inset(0 ${100 - pos}% 0 0)` }}
aria-hidden="true"
>
<Chart variant="before" />
<span className="ba__tag ba__tag--before">{before}</span>
</div>
{/* divider + accessible control */}
<div className="ba__divider" style={{ left: `${pos}%` }} aria-hidden="true">
<span className="ba__handle">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M9 6 4 12l5 6M15 6l5 6-5 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</span>
</div>
<label className="sr-only" htmlFor={id}>
Reveal results for {caption}: drag to compare before and after
</label>
<input
id={id}
className="ba__range"
type="range"
min={2}
max={98}
value={pos}
onChange={(e) => setPos(Number(e.target.value))}
/>
</div>
<figcaption className="ba__cap">{caption}</figcaption>
</figure>
);
}
/** SVG sparkline. "before" = flat/jagged & muted, "after" = steep climb + acid. */
function Chart({ variant }: { variant: "before" | "after" }) {
const after = variant === "after";
const path = after
? "M0 86 C 40 84, 70 80, 110 70 S 190 30, 240 10"
: "M0 70 C 40 72, 70 66, 110 70 S 190 64, 240 62";
return (
<svg
className={`ba__chart ba__chart--${variant}`}
viewBox="0 0 240 100"
preserveAspectRatio="none"
>
<defs>
<linearGradient id={`g-${variant}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor={after ? "var(--c-acid)" : "currentColor"} stopOpacity={after ? 0.35 : 0.12} />
<stop offset="1" stopColor={after ? "var(--c-acid)" : "currentColor"} stopOpacity="0" />
</linearGradient>
</defs>
<path d={`${path} L 240 100 L 0 100 Z`} fill={`url(#g-${variant})`} stroke="none" />
<path d={path} fill="none" stroke={after ? "var(--c-acid)" : "currentColor"} strokeWidth={after ? 2.5 : 1.5} strokeLinecap="round" vectorEffect="non-scaling-stroke" />
</svg>
);
}