Files
lynkeduppro-crm/src/components/dashboard/charts.tsx
T
Goutam 5ff655d786 Add LynkedUp Pro dashboard (dark + light) from Figma
- Full dashboard at /dashboard matching the Figma Linkedup-Web frame
- Dark/light theme toggle (persisted to localStorage)
- Live animated charts: stat sparkbars, P&L grouped bars, donut, gauge,
  pipeline bars, progress, revenue-potential
- Sidebar, topbar, stat cards, weather, top sales reps
- 29 icons exported from Figma (currentColor) + lucide fallbacks
- Fix: bar charts had zero height (percentage in indefinite flex parent)
- Fix: removed content max-width that left a right-side gap

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 20:12:35 +05:30

181 lines
6.2 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
/* ---- vertical bar chart (HTML/CSS, animated heights) ---- */
export function BarsChart({
labels,
series,
max,
yTicks,
height = 132,
highlight,
barWidth = 7,
}: {
labels: string[];
series: { color: string; values: number[] }[];
max: number;
yTicks?: string[];
height?: number;
highlight?: number; // single-series: index to highlight, others muted
barWidth?: number;
}) {
const [on, setOn] = useState(false);
useEffect(() => {
const t = setTimeout(() => setOn(true), 40);
return () => clearTimeout(t);
}, []);
return (
<div className="chart" style={{ height }}>
{yTicks && (
<div className="chart-y">
{yTicks.map((t) => <span key={t}>{t}</span>)}
</div>
)}
<div className="chart-plot">
{labels.map((lab, i) => (
<div className="bar-group" key={i}>
<div className="bars">
{series.map((s, si) => {
const muted = highlight !== undefined && i !== highlight;
return (
<span
key={si}
className="vbar"
style={{
width: barWidth,
height: on ? `${Math.max(2, (s.values[i] / max) * 100)}%` : "0%",
background: muted ? "var(--track)" : s.color,
transitionDelay: `${i * 35 + si * 20}ms`,
}}
/>
);
})}
</div>
<span className="bar-x">{lab}</span>
</div>
))}
</div>
</div>
);
}
/* ---- mini sparkline bars (stat cards) ---- */
export function Spark({ values, color, height = 40, width = 8 }: { values: number[]; color: string; height?: number; width?: number }) {
const [on, setOn] = useState(false);
useEffect(() => { const t = setTimeout(() => setOn(true), 40); return () => clearTimeout(t); }, []);
const max = Math.max(...values, 1);
return (
<div className="spark" style={{ height }}>
{values.map((v, i) => (
<span key={i} style={{ width, height: on ? `${(v / max) * 100}%` : "0%", background: color, transitionDelay: `${i * 30}ms` }} />
))}
</div>
);
}
/* ---- donut chart ---- */
export function Donut({
segments, total, label, size = 150, thickness = 18,
}: {
segments: { value: number; color: string }[];
total: string;
label: string;
size?: number;
thickness?: number;
}) {
const [on, setOn] = useState(false);
useEffect(() => { const t = setTimeout(() => setOn(true), 60); return () => clearTimeout(t); }, []);
const r = (size - thickness) / 2;
const c = 2 * Math.PI * r;
const sum = segments.reduce((a, s) => a + s.value, 0) || 1;
let offset = 0;
return (
<div className="donut" style={{ width: size, height: size }}>
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
<circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke="var(--track)" strokeWidth={thickness} />
{segments.map((s, i) => {
const frac = s.value / sum;
const len = on ? frac * c : 0;
const dash = `${len} ${c - len}`;
const el = (
<circle
key={i}
cx={size / 2} cy={size / 2} r={r} fill="none"
stroke={s.color} strokeWidth={thickness} strokeLinecap="round"
strokeDasharray={dash}
strokeDashoffset={-offset * c}
transform={`rotate(-90 ${size / 2} ${size / 2})`}
style={{ transition: "stroke-dasharray .9s cubic-bezier(.2,.7,.2,1)" }}
/>
);
offset += frac;
return el;
})}
</svg>
<div className="donut-c">
<div className="dv">{total}</div>
<div className="dl">{label}</div>
</div>
</div>
);
}
/* ---- semicircle gauge ---- */
export function Gauge({
value, max = 100, color, size = 150, label, sub,
}: {
value: number; max?: number; color: string; size?: number; label?: string; sub?: string;
}) {
const [on, setOn] = useState(false);
useEffect(() => { const t = setTimeout(() => setOn(true), 60); return () => clearTimeout(t); }, []);
const sw = 14;
const r = (size - sw) / 2;
const cx = size / 2, cy = size / 2;
const semi = Math.PI * r; // half circumference
const frac = Math.min(1, value / max);
const len = on ? frac * semi : 0;
return (
<div className="gauge" style={{ width: size, height: size / 2 + 16 }}>
<svg width={size} height={size / 2 + 16} viewBox={`0 0 ${size} ${size / 2 + 16}`}>
<path d={`M ${sw / 2} ${cy} A ${r} ${r} 0 0 1 ${size - sw / 2} ${cy}`} fill="none" stroke="var(--track)" strokeWidth={sw} strokeLinecap="round" />
<path
d={`M ${sw / 2} ${cy} A ${r} ${r} 0 0 1 ${size - sw / 2} ${cy}`}
fill="none" stroke={color} strokeWidth={sw} strokeLinecap="round"
strokeDasharray={`${len} ${semi}`}
style={{ transition: "stroke-dasharray 1s cubic-bezier(.2,.7,.2,1)" }}
/>
</svg>
{(label || sub) && (
<div className="gauge-c">
{label && <div className="gv">{label}</div>}
{sub && <div className="gl">{sub}</div>}
</div>
)}
</div>
);
}
/* ---- progress bar ---- */
export function Progress({ value, color, track }: { value: number; color: string; track?: boolean }) {
const [on, setOn] = useState(false);
useEffect(() => { const t = setTimeout(() => setOn(true), 50); return () => clearTimeout(t); }, []);
return (
<div className="bar">
<span style={{ width: on ? `${value}%` : "0%", background: color, transition: "width .9s cubic-bezier(.2,.7,.2,1)" }} />
</div>
);
}
/* ---- horizontal segmented bar (pipeline) ---- */
export function HBar({ value, max, color }: { value: number; max: number; color: string }) {
const [on, setOn] = useState(false);
useEffect(() => { const t = setTimeout(() => setOn(true), 50); return () => clearTimeout(t); }, []);
return (
<div className="bar" style={{ height: 9 }}>
<span style={{ width: on ? `${(value / max) * 100}%` : "0%", background: color, transition: "width .8s cubic-bezier(.2,.7,.2,1)" }} />
</div>
);
}