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>
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user