Session 28: Parlay builder, line movement tracker, book comparison — 3 features, zero credits (1623 tests)

This commit is contained in:
Kev
2026-06-13 12:37:08 -04:00
parent 66fafd8429
commit c48aecd510
23 changed files with 1567 additions and 1 deletions
+57
View File
@@ -0,0 +1,57 @@
'use client';
/**
* LineMovementChart (Session 28).
*
* Dependency-free SVG sparkline of a prop's line through the day. Green
* when the line rose, red when it dropped. Renders open + current labels.
* Degrades to a flat midline for <2 points.
*/
export interface Snapshot {
time?: number;
line: number;
}
export interface LineMovementChartProps {
snapshots: Snapshot[];
width?: number;
height?: number;
}
export default function LineMovementChart({ snapshots, width = 120, height = 32 }: LineMovementChartProps) {
const pts = (snapshots || []).map((s) => Number(s.line)).filter((n) => Number.isFinite(n));
if (pts.length === 0) return null;
const opening = pts[0];
const current = pts[pts.length - 1];
const delta = current - opening;
const stroke = Math.abs(delta) < 0.5 ? 'var(--text-tertiary, #6B6B7B)' : delta > 0 ? '#00D4A0' : '#FF4D4D';
const min = Math.min(...pts);
const max = Math.max(...pts);
const range = max - min || 1;
const pad = 4;
const innerH = height - pad * 2;
const stepX = pts.length > 1 ? width / (pts.length - 1) : 0;
const yScale = (v: number) => pad + innerH - ((v - min) / range) * innerH;
const polyline =
pts.length > 1
? pts.map((v, i) => `${(i * stepX).toFixed(1)},${yScale(v).toFixed(1)}`).join(' ')
: `0,${(height / 2).toFixed(1)} ${width},${(height / 2).toFixed(1)}`;
return (
<svg
viewBox={`0 0 ${width} ${height}`}
width={width}
height={height}
role="img"
aria-label={`Line moved from ${opening} to ${current}`}
style={{ display: 'block' }}
>
<polyline points={polyline} fill="none" stroke={stroke} strokeWidth={2} strokeLinejoin="round" strokeLinecap="round" />
{pts.length > 1 && <circle cx={width} cy={yScale(current)} r={2.5} fill={stroke} />}
</svg>
);
}