103 lines
3.6 KiB
TypeScript
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>
|
|
);
|
|
}
|