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:
2026-06-27 20:12:35 +05:30
parent 8c5df83d63
commit 5ff655d786
38 changed files with 1223 additions and 48 deletions
+212
View File
@@ -0,0 +1,212 @@
/* =========================================================================
LynkedUp Pro — Dashboard (matches Figma "Linkedup-Web")
Dark + light themes via [data-theme] on .dash-root.
========================================================================= */
.dash-root {
/* accents (shared) */
--orange: #fda913;
--orange-2: #fd6d13;
--yellow: #ffd60a;
--cyan: #09b9c6;
--green: #14bc83;
--blue: #285ef0;
--purple: #9036e9;
--red: #f0563f;
--radius: 16px;
--radius-sm: 12px;
--font: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
min-height: 100vh;
display: flex;
background: var(--bg);
color: var(--text);
font-family: var(--font);
-webkit-font-smoothing: antialiased;
}
/* ---- dark (default) ---- */
.dash-root,
.dash-root[data-theme="dark"] {
--bg: #060608;
--panel: #0e0e13;
--panel-2: #15151b;
--panel-3: #1b1b22;
--border: rgba(255, 255, 255, 0.07);
--border-2: rgba(255, 255, 255, 0.12);
--text: #ffffff;
--text-2: #eaeaea;
--muted: #8c8c8c;
--faint: #686868;
--track: rgba(255, 255, 255, 0.08);
--sidebar: #08080b;
--shadow: 0 18px 40px -24px rgba(0, 0, 0, 0.8);
}
/* ---- light ---- */
.dash-root[data-theme="light"] {
--bg: #eef1f7;
--panel: #ffffff;
--panel-2: #f5f7fb;
--panel-3: #eef1f7;
--border: rgba(15, 23, 42, 0.08);
--border-2: rgba(15, 23, 42, 0.14);
--text: #0c0e14;
--text-2: #1f2430;
--muted: #5b6472;
--faint: #97a0b0;
--track: rgba(15, 23, 42, 0.07);
--sidebar: #ffffff;
--shadow: 0 18px 40px -26px rgba(15, 23, 42, 0.25);
}
.dash-root * { box-sizing: border-box; }
.dash-root h1, .dash-root h2, .dash-root h3, .dash-root h4 { margin: 0; }
/* ---- sidebar ---- */
.dash-sidebar {
width: 214px;
flex: 0 0 214px;
background: var(--sidebar);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
height: 100vh;
position: sticky;
top: 0;
}
.dash-brand { display: flex; align-items: center; gap: 9px; padding: 18px 18px 14px; }
.dash-brand img { height: 26px; width: auto; }
.dash-brand .bk { font-size: 15px; font-weight: 700; letter-spacing: -0.01em; }
.dash-nav { flex: 1; overflow-y: auto; padding: 6px 12px 12px; }
.dash-nav::-webkit-scrollbar { width: 6px; }
.dash-nav::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 99px; }
.nav-group { font-size: 10px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--faint); padding: 14px 10px 6px; }
.nav-item {
display: flex; align-items: center; gap: 11px;
width: 100%; padding: 8.5px 10px; border-radius: 9px;
border: 0; background: none; cursor: pointer;
color: var(--muted); font-family: inherit; font-size: 13px; font-weight: 500;
text-align: left; transition: background 0.14s, color 0.14s;
}
.nav-item:hover { background: var(--panel-2); color: var(--text-2); }
.nav-item.active { background: var(--orange); color: #1a1206; font-weight: 600; }
.nav-item.active svg { color: #1a1206; }
.nav-item svg { color: var(--faint); flex: 0 0 auto; }
.nav-item:hover svg { color: var(--text-2); }
.fig-ic { display: inline-flex; flex: 0 0 auto; color: inherit; }
.fig-ic svg { width: 100%; height: 100%; display: block; }
.dash-sidebar .sb-foot { border-top: 1px solid var(--border); padding: 10px 12px; }
.sb-user { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 10px; }
.sb-user:hover { background: var(--panel-2); }
.sb-user .av { width: 30px; height: 30px; border-radius: 8px; object-fit: cover; flex: 0 0 30px; }
.sb-user .nm { font-size: 12.5px; font-weight: 600; }
.sb-user .rl { font-size: 11px; color: var(--faint); }
/* ---- main ---- */
.dash-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.dash-topbar {
height: 64px; display: flex; align-items: center; justify-content: space-between;
padding: 0 24px; border-bottom: 1px solid var(--border);
position: sticky; top: 0; z-index: 20;
background: color-mix(in srgb, var(--bg) 80%, transparent);
backdrop-filter: blur(10px);
}
.dash-title h1 { font-size: 20px; font-weight: 700; letter-spacing: -0.01em; }
.dash-title p { font-size: 12.5px; color: var(--muted); margin-top: 2px; }
.top-actions { display: flex; align-items: center; gap: 8px; }
.ic-btn { width: 38px; height: 38px; border-radius: 10px; border: 1px solid var(--border); background: var(--panel); color: var(--muted); display: grid; place-items: center; cursor: pointer; transition: 0.14s; }
.ic-btn:hover { color: var(--text); border-color: var(--border-2); }
.top-user { display: flex; align-items: center; gap: 9px; padding: 5px 8px 5px 5px; border: 1px solid var(--border); border-radius: 99px; background: var(--panel); cursor: pointer; }
.top-user .av { width: 30px; height: 30px; border-radius: 99px; object-fit: cover; }
.top-user .nm { font-size: 12.5px; font-weight: 600; }
.top-user .rl { font-size: 10.5px; color: var(--faint); }
.dash-content { padding: 22px 28px 40px; width: 100%; }
.sec-title { font-size: 15px; font-weight: 700; margin: 6px 0 14px; }
/* ---- grid helpers ---- */
.grid { display: grid; gap: 16px; }
.row { display: flex; align-items: center; }
.between { justify-content: space-between; }
.gap-2 { gap: 8px; } .gap-3 { gap: 12px; }
.muted { color: var(--muted); } .faint { color: var(--faint); }
/* ---- card ---- */
.dcard { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 18px; }
.dcard-pad-0 { padding: 0; }
.card-h { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.card-h h3 { font-size: 14px; font-weight: 600; }
.card-sub { font-size: 11.5px; color: var(--faint); }
/* ---- stat bits ---- */
.stat-ic { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center; }
.big-val { font-size: 26px; font-weight: 800; letter-spacing: -0.02em; }
.mid-val { font-size: 21px; font-weight: 800; letter-spacing: -0.02em; }
.lbl { font-size: 12px; color: var(--muted); }
.pill { display: inline-flex; align-items: center; gap: 5px; height: 22px; padding: 0 9px; border-radius: 99px; font-size: 11px; font-weight: 600; }
.pill-orange { background: color-mix(in srgb, var(--orange) 18%, transparent); color: var(--orange); }
.pill-green { background: color-mix(in srgb, var(--green) 18%, transparent); color: var(--green); }
.pill-blue { background: color-mix(in srgb, var(--blue) 20%, transparent); color: #6f9bff; }
.pill-cyan { background: color-mix(in srgb, var(--cyan) 18%, transparent); color: var(--cyan); }
.pill-red { background: color-mix(in srgb, var(--red) 18%, transparent); color: var(--red); }
/* small stat card (right 2x2) */
.mini-card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 14px 15px; }
.mini-card .top { display: flex; justify-content: space-between; align-items: flex-start; }
/* progress */
.bar { height: 8px; border-radius: 99px; background: var(--track); overflow: hidden; }
.bar > span { display: block; height: 100%; border-radius: 99px; }
/* legend dot */
.ldot { width: 8px; height: 8px; border-radius: 99px; display: inline-block; }
/* pipeline rows */
.pl-row { display: grid; grid-template-columns: 92px 1fr 22px; align-items: center; gap: 10px; margin-bottom: 10px; }
.pl-row .lab { font-size: 12px; color: var(--muted); }
.pl-row .n { font-size: 12px; font-weight: 700; text-align: right; }
/* weather */
.wx-hours { display: flex; justify-content: space-between; gap: 6px; margin-top: 14px; }
.wx-hr { text-align: center; flex: 1; }
.wx-hr .t { font-size: 11px; color: var(--faint); }
.wx-hr .te { font-size: 13px; font-weight: 700; margin-top: 6px; }
/* reps */
.rep-row { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--border); }
.rep-row:last-child { border-bottom: 0; }
.rep-row .rk { width: 22px; font-size: 12px; font-weight: 700; color: var(--faint); }
.rep-row .av { width: 34px; height: 34px; border-radius: 99px; object-fit: cover; }
.rep-row .nm { font-size: 13px; font-weight: 600; }
.rep-row .em { font-size: 11px; color: var(--faint); }
.rep-row .amt { font-size: 13px; font-weight: 700; color: var(--green); }
.link-btn { font-size: 12px; font-weight: 600; color: var(--orange); background: none; border: 0; cursor: pointer; }
/* ---- charts ---- */
.chart { display: flex; gap: 8px; width: 100%; }
.chart-y { display: flex; flex-direction: column; justify-content: space-between; font-size: 9px; color: var(--faint); padding-bottom: 16px; text-align: right; min-width: 18px; }
.chart-plot { flex: 1; display: flex; align-items: flex-end; gap: 2px; }
.bar-group { flex: 1; display: flex; flex-direction: column; align-items: center; height: 100%; }
.bar-group .bars { height: calc(100% - 20px); width: 100%; display: flex; align-items: flex-end; justify-content: center; gap: 3px; }
.vbar { border-radius: 3px; align-self: flex-end; transition: height 0.7s cubic-bezier(0.2, 0.7, 0.2, 1); }
.bar-x { font-size: 10px; color: var(--faint); margin-top: 7px; }
.spark { display: flex; align-items: flex-end; gap: 4px; }
.spark > span { border-radius: 3px; transition: height 0.7s cubic-bezier(0.2, 0.7, 0.2, 1); }
.donut { position: relative; display: grid; place-items: center; }
.donut-c { position: absolute; inset: 0; display: grid; place-items: center; text-align: center; }
.donut-c .dv { font-size: 30px; font-weight: 800; letter-spacing: -0.02em; }
.donut-c .dl { font-size: 11px; color: var(--faint); }
.gauge { position: relative; }
.gauge-c { position: absolute; bottom: 0; left: 0; right: 0; text-align: center; }
.gauge-c .gv { font-size: 18px; font-weight: 800; }
.gauge-c .gl { font-size: 10px; color: var(--faint); }
/* responsive */
@media (max-width: 1100px) { .dash-sidebar { display: none; } }
@media (max-width: 760px) { .dash-content { padding: 16px; } }
+15 -10
View File
@@ -1,14 +1,19 @@
import { Sidebar } from "@/components/layout/sidebar";
import type { Metadata } from "next";
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
export const metadata: Metadata = {
title: "Dashboard — LynkedUp Pro",
};
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex min-h-screen">
<Sidebar />
<div className="flex-1 flex flex-col">{children}</div>
</div>
<>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
{children}
</>
);
}
+2 -38
View File
@@ -1,41 +1,5 @@
import type { Metadata } from "next";
import { Header } from "@/components/layout/header";
export const metadata: Metadata = {
title: "Dashboard",
};
const stats = [
{ label: "Total Leads", value: "0" },
{ label: "Open Deals", value: "0" },
{ label: "Contacts", value: "0" },
{ label: "Revenue", value: "$0" },
];
import { Dashboard } from "@/components/dashboard/dashboard";
export default function DashboardPage() {
return (
<>
<Header title="Dashboard" />
<main className="flex-1 p-6 bg-neutral-50 dark:bg-neutral-900">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{stats.map((stat) => (
<div
key={stat.label}
className="rounded-xl border border-black/10 dark:border-white/10 bg-white dark:bg-neutral-950 p-5"
>
<p className="text-sm text-neutral-500">{stat.label}</p>
<p className="mt-2 text-2xl font-semibold">{stat.value}</p>
</div>
))}
</div>
<div className="mt-6 rounded-xl border border-black/10 dark:border-white/10 bg-white dark:bg-neutral-950 p-6">
<p className="text-neutral-500 text-sm">
Yahan pages add karenge Leads, Contacts, Deals, Tasks. Aap batao
kaunsa page pehle banana hai.
</p>
</div>
</main>
</>
);
return <Dashboard />;
}
+180
View File
@@ -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>
);
}
+258
View File
@@ -0,0 +1,258 @@
"use client";
import { useEffect, useState } from "react";
import { TrendingUp, Cloud, MoreHorizontal, ArrowUpRight } from "lucide-react";
import { Sidebar } from "./sidebar";
import { Topbar } from "./topbar";
import { FigIcon } from "./figicon";
import { BarsChart, Spark, Donut, Gauge, Progress, HBar } from "./charts";
import "../../app/dashboard/dashboard.css";
const C = {
orange: "#fda913", orange2: "#fd6d13", yellow: "#ffd60a",
cyan: "#09b9c6", green: "#14bc83", blue: "#285ef0", purple: "#9036e9", red: "#f0563f",
};
const rnd = (v: number, p = 0.12) => Math.max(1, Math.round(v * (1 + (Math.random() * 2 - 1) * p)));
const arr = (base: number[], p = 0.14) => base.map((v) => rnd(v, p));
export function Dashboard() {
const [theme, setTheme] = useState<"dark" | "light">("dark");
const [active, setActive] = useState("dashboard");
useEffect(() => {
try { const t = localStorage.getItem("lup_dash_theme"); if (t === "light" || t === "dark") setTheme(t); } catch {}
}, []);
function toggle() {
setTheme((t) => { const n = t === "dark" ? "light" : "dark"; try { localStorage.setItem("lup_dash_theme", n); } catch {} return n; });
}
// ---- live data ----
const [pnl, setPnl] = useState({
revenue: [4, 6, 3, 7, 5, 6, 4], gross: [5, 3, 6, 4, 7, 5, 6],
comm: [3, 5, 4, 6, 5, 7, 4], net: [4, 6, 5, 3, 6, 4, 7],
});
const [rev, setRev] = useState([5, 7, 4, 8, 6, 9, 16, 7, 5, 8]);
const [sparks, setSparks] = useState({
revenue: [4, 7, 5, 9, 6, 10, 8], leads: [6, 4, 8, 5, 9, 6, 10], meets: [5, 8, 6, 9, 7, 10, 8],
});
useEffect(() => {
const id = setInterval(() => {
setPnl((p) => ({ revenue: arr(p.revenue), gross: arr(p.gross), comm: arr(p.comm), net: arr(p.net) }));
setRev((r) => r.map((v, i) => (i === 6 ? 16 : rnd(v, 0.18))));
setSparks((s) => ({ revenue: arr(s.revenue), leads: arr(s.leads), meets: arr(s.meets) }));
}, 2600);
return () => clearInterval(id);
}, []);
const days = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
const yk = ["8k", "6k", "4k", "2k", "0"];
return (
<div className="dash-root" data-theme={theme}>
<Sidebar active={active} onSelect={setActive} />
<div className="dash-main">
<Topbar theme={theme} onToggle={toggle} />
<div className="dash-content">
{/* ===== Statistics ===== */}
<h2 className="sec-title">Statistics</h2>
<div className="grid" style={{ gridTemplateColumns: "1.55fr 1fr", marginBottom: 16 }}>
<div className="grid" style={{ gridTemplateColumns: "1fr 1fr 1fr" }}>
<BigStat icon={<FigIcon name="i_revenue" size={18} />} ic={C.orange} tag="GROSS PROFIT $114K" value="$626K" label="Revenue" spark={sparks.revenue} sc={C.orange} />
<BigStat icon={<FigIcon name="i_hotleads" size={18} />} ic={C.red} tag="REQUIRES ACTION" value="20k" label="Hot Leads" spark={sparks.leads} sc={C.purple} />
<BigStat icon={<FigIcon name="i_meets" size={18} />} ic={C.cyan} tag="NEXT 7 DAYS" value="70k" label="Upcoming Meets" spark={sparks.meets} sc={C.cyan} />
</div>
<div className="grid" style={{ gridTemplateColumns: "1fr 1fr" }}>
<MiniStat icon={<FigIcon name="i_collected" size={16} />} ic={C.cyan} value="$311.3k" label="Collected to Date" sub="32 payments" pill="pill-cyan" />
<MiniStat icon={<FigIcon name="i_ar" size={16} />} ic={C.orange} value="$325.9k" label="Outstanding AR" sub="25 pending" pill="pill-orange" />
<MiniStat icon={<FigIcon name="i_payouts" size={16} />} ic={C.purple} value="$203.7k" label="Pending Payouts" sub="20 pending" pill="pill-blue" />
<MiniStat icon={<FigIcon name="i_vendor" size={16} />} ic={C.blue} value="$432.4k" label="YTD Vendor Spend" sub="57 invoices" pill="pill-blue" />
</div>
</div>
{/* ===== Pipeline / Win / Storm ===== */}
<div className="grid" style={{ gridTemplateColumns: "1fr 1fr 1fr", marginBottom: 16 }}>
<div className="dcard">
<div className="card-h"><h3>Open Pipeline</h3><span className="pill pill-orange">27 leads</span></div>
<div className="big-val" style={{ marginBottom: 14 }}>$396K</div>
{[["New", 6], ["Contacted", 7], ["Appointment", 7], ["Estimate", 3]].map(([l, n]) => (
<div className="pl-row" key={l as string}>
<span className="lab">{l}</span>
<HBar value={n as number} max={8} color={C.orange} />
<span className="n">{n}</span>
</div>
))}
</div>
<div className="dcard">
<div className="card-h"><h3>Win Rate</h3><TrendingUp size={16} color={C.green} /></div>
<div className="big-val" style={{ marginBottom: 14 }}>94%</div>
<Progress value={94} color={C.cyan} />
<div className="row" style={{ gap: 16, marginTop: 14, flexWrap: "wrap" }}>
<Legend dot={C.cyan} text="17 Won" />
<Legend dot="var(--muted)" text="18 Decided" />
<Legend dot={C.red} text="1 Lost" />
</div>
</div>
<div className="dcard">
<div className="card-h"><h3>Storm-Attributed Revenue</h3><span className="pill pill-blue">19 Storm Leads</span></div>
<div className="big-val" style={{ marginBottom: 14 }}>24%</div>
<Progress value={24} color={C.blue} />
<div className="lbl" style={{ marginTop: 12 }}>$56K of $626K</div>
</div>
</div>
{/* ===== Roof / Risk / Weather ===== */}
<div className="grid" style={{ gridTemplateColumns: "1fr 1.15fr 1.25fr", marginBottom: 16 }}>
<div className="dcard">
<div className="card-h"><div><h3>Roof Condition</h3><div className="card-sub">Territory health snapshot</div></div></div>
<div className="row" style={{ justifyContent: "center", margin: "6px 0 10px" }}>
<Donut segments={[{ value: 30, color: C.orange }, { value: 14, color: C.cyan }]} total="44" label="Total" />
</div>
<div className="row" style={{ gap: 18, justifyContent: "center" }}>
<Legend dot={C.orange} text="Good" />
<Legend dot={C.cyan} text="Bad" />
</div>
</div>
<div className="dcard">
<div className="card-h"><div><h3>Risk Index</h3><div className="card-sub">Storm Impact Probability</div></div></div>
<div className="row" style={{ justifyContent: "center", margin: "4px 0" }}>
<Gauge value={51} max={100} color={C.blue} size={170} label="51" sub="of 100" />
</div>
<div className="row between" style={{ marginTop: 6 }}>
<Readout big="10 mph" small="Wind Exp." />
<Readout big="52%" small="Vulnerable" color={C.orange} />
</div>
</div>
<div className="dcard">
<div className="row between">
<div>
<div className="row gap-2"><Cloud size={20} color={C.cyan} /><span className="big-val">80&deg;F</span></div>
<div className="lbl">Plano, US</div>
</div>
<div style={{ textAlign: "right" }}>
<div className="card-sub">Updated: 02:48 PM</div>
<div className="row gap-3" style={{ marginTop: 8, justifyContent: "flex-end" }}>
<span className="row gap-2" style={{ fontSize: 12 }}><span style={{ color: "var(--muted)", display: "inline-flex" }}><FigIcon name="i_wind" size={14} /></span> 10 mph</span>
<span className="row gap-2" style={{ fontSize: 12 }}><span style={{ color: C.blue, display: "inline-flex" }}><FigIcon name="i_humidity" size={14} /></span> 82%</span>
</div>
</div>
</div>
<div className="wx-hours">
{[["12 PM", 94], ["5 PM", 79], ["8 PM", 82], ["10 PM", 90], ["1 AM", 92], ["5 AM", 79]].map(([t, te]) => (
<div className="wx-hr" key={t as string}>
<div className="t">{t}</div>
<Cloud size={16} color="var(--faint)" style={{ display: "block", margin: "6px auto 0" }} />
<div className="te">{te}&deg;</div>
</div>
))}
</div>
</div>
</div>
{/* ===== P&L Summary ===== */}
<h2 className="sec-title">P&amp;L Summary</h2>
<div className="grid" style={{ gridTemplateColumns: "1fr 1fr 1fr 1fr", marginBottom: 16 }}>
<PnlCard title="Revenue" value="$626K" labels={days} yk={yk}
series={[{ color: C.orange, values: pnl.revenue }, { color: C.orange2, values: pnl.gross }]} />
<PnlCard title="Gross Profit" value="$114K" labels={days} yk={yk}
series={[{ color: C.cyan, values: pnl.gross }, { color: C.orange, values: pnl.revenue }]} />
<PnlCard title="Commissions" value="$63K" labels={days} yk={yk}
series={[{ color: C.blue, values: pnl.comm }, { color: "#6f9bff", values: pnl.net }]} />
<PnlCard title="Net Profit" value="$51K" labels={days} yk={yk}
series={[{ color: C.orange, values: pnl.net }, { color: C.blue, values: pnl.comm }]} />
</div>
{/* ===== Revenue Potential / Top Reps ===== */}
<div className="grid" style={{ gridTemplateColumns: "1.7fr 1fr" }}>
<div className="dcard">
<div className="card-h">
<div><h3>Revenue Potential</h3><div className="card-sub">By neighborhood rating</div></div>
<span className="pill pill-orange"><ArrowUpRight size={13} /> live</span>
</div>
<BarsChart
labels={["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}
series={[{ color: C.orange, values: rev }]}
max={18} highlight={6} height={210} barWidth={26}
yTicks={["$240k", "$180k", "$120k", "$60k", "0"]}
/>
</div>
<div className="dcard">
<div className="card-h"><h3>Top Sales Reps</h3><button className="link-btn">View All</button></div>
{[["Hannah Reyes", "hannah@lynkeduppro.com", C.orange], ["Cody Tatum", "cody@lynkeduppro.com", C.cyan], ["Travis Boone", "travis@lynkeduppro.com", C.blue]].map(([nm, em, col], i) => (
<div className="rep-row" key={nm as string}>
<span className="rk">0{i + 1}</span>
<span className="av" style={{ background: col as string, display: "grid", placeItems: "center", color: "#fff", fontWeight: 700, fontSize: 12 }}>{(nm as string).split(" ").map((x) => x[0]).join("")}</span>
<div style={{ flex: 1 }}>
<div className="nm">{nm}</div>
<div className="em">{em}</div>
</div>
<span className="amt">$112,500</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
}
/* ---- sub components ---- */
function BigStat({ icon, ic, tag, value, label, spark, sc }: { icon: React.ReactNode; ic: string; tag: string; value: string; label: string; spark: number[]; sc: string }) {
return (
<div className="dcard">
<div className="row between" style={{ marginBottom: 14 }}>
<span className="stat-ic" style={{ background: `color-mix(in srgb, ${ic} 18%, transparent)`, color: ic }}>{icon}</span>
<span className="card-sub" style={{ fontWeight: 600, letterSpacing: "0.04em" }}>{tag}</span>
</div>
<div className="row between" style={{ alignItems: "flex-end" }}>
<div>
<div className="big-val">{value}</div>
<div className="lbl">{label}</div>
</div>
<Spark values={spark} color={sc} />
</div>
</div>
);
}
function MiniStat({ icon, ic, value, label, sub, pill }: { icon: React.ReactNode; ic: string; value: string; label: string; sub: string; pill: string }) {
return (
<div className="mini-card">
<div className="top">
<span className="stat-ic" style={{ width: 30, height: 30, background: `color-mix(in srgb, ${ic} 18%, transparent)`, color: ic }}>{icon}</span>
<span className={`pill ${pill}`}>{sub}</span>
</div>
<div className="mid-val" style={{ marginTop: 12 }}>{value}</div>
<div className="lbl">{label}</div>
</div>
);
}
function PnlCard({ title, value, labels, yk, series }: { title: string; value: string; labels: string[]; yk: string[]; series: { color: string; values: number[] }[] }) {
return (
<div className="dcard">
<div className="card-h"><h3>{title}</h3><MoreHorizontal size={16} color="var(--faint)" /></div>
<div className="mid-val" style={{ marginBottom: 14 }}>{value}</div>
<BarsChart labels={labels} series={series} max={9} yTicks={yk} height={120} barWidth={5} />
</div>
);
}
function Legend({ dot, text }: { dot: string; text: string }) {
return <span className="row gap-2" style={{ fontSize: 12, color: "var(--muted)" }}><span className="ldot" style={{ background: dot }} /> {text}</span>;
}
function Readout({ big, small, color }: { big: string; small: string; color?: string }) {
return (
<div>
<div style={{ fontSize: 16, fontWeight: 800, color: color || "var(--text)" }}>{big}</div>
<div className="lbl">{small}</div>
</div>
);
}
+17
View File
@@ -0,0 +1,17 @@
"use client";
import { Sparkles } from "lucide-react";
import { figmaIcons } from "./figma-icons";
/** Renders a Figma-exported icon (recolored to currentColor) or a fallback. */
export function FigIcon({ name, size = 18 }: { name: string; size?: number }) {
const svg = figmaIcons[name];
if (!svg) return <Sparkles size={size} />;
return (
<span
className="fig-ic"
style={{ width: size, height: size }}
dangerouslySetInnerHTML={{ __html: svg }}
/>
);
}
File diff suppressed because one or more lines are too long
+88
View File
@@ -0,0 +1,88 @@
"use client";
import { ChevronsUpDown } from "lucide-react";
import { FigIcon } from "./figicon";
type Item = { key: string; label: string };
const GROUPS: { title: string; items: Item[] }[] = [
{
title: "Workspace",
items: [
{ key: "dashboard", label: "Dashboard" },
{ key: "owners", label: "Owners Box" },
{ key: "projects", label: "Projects" },
{ key: "leads", label: "Leads" },
{ key: "verify", label: "Lead Verification" },
{ key: "pipeline", label: "Pipeline" },
],
},
{
title: "Workspace",
items: [
{ key: "dispatch", label: "LynkDispatch" },
{ key: "storm", label: "Storm Intel" },
{ key: "territory", label: "Territory Map" },
{ key: "procanvas", label: "ProCanvas" },
{ key: "estimates", label: "Estimates" },
],
},
{
title: "Team",
items: [
{ key: "schedule", label: "Team Schedule" },
{ key: "leaderboard", label: "Leaderboard" },
{ key: "subtasks", label: "Subcontractor Tasks" },
{ key: "people", label: "People" },
],
},
];
const EXTRA: Item[] = [
{ key: "settings", label: "Org Settings" },
{ key: "ai", label: "AI Assistant" },
{ key: "home", label: "Home" },
];
export function Sidebar({ active, onSelect }: { active: string; onSelect: (k: string) => void }) {
return (
<aside className="dash-sidebar">
<div className="dash-brand">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src="/image/logo.png" alt="LynkedUp Pro" />
<span className="bk">LynkedUp Pro</span>
</div>
<nav className="dash-nav">
{GROUPS.map((g, gi) => (
<div key={gi}>
<div className="nav-group">{g.title}</div>
{g.items.map((it) => <NavBtn key={it.key} it={it} active={active} onSelect={onSelect} />)}
</div>
))}
<div style={{ height: 8 }} />
{EXTRA.map((it) => <NavBtn key={it.key} it={it} active={active} onSelect={onSelect} />)}
</nav>
<div className="sb-foot">
<div className="sb-user">
<span className="av" style={{ background: "linear-gradient(135deg,#285ef0,#09b9c6)", display: "grid", placeItems: "center", color: "#fff", fontWeight: 700, fontSize: 12 }}>JJ</span>
<div style={{ flex: 1 }}>
<div className="nm">Justin Johnson</div>
<div className="rl">Owner</div>
</div>
<ChevronsUpDown size={15} />
</div>
</div>
</aside>
);
}
function NavBtn({ it, active, onSelect }: { it: Item; active: string; onSelect: (k: string) => void }) {
return (
<button className={`nav-item ${active === it.key ? "active" : ""}`} onClick={() => onSelect(it.key)}>
<FigIcon name={it.key} size={18} />
{it.label}
</button>
);
}
+33
View File
@@ -0,0 +1,33 @@
"use client";
import { Sun, ChevronDown } from "lucide-react";
import { FigIcon } from "./figicon";
export function Topbar({ theme, onToggle }: { theme: "dark" | "light"; onToggle: () => void }) {
return (
<header className="dash-topbar">
<div className="dash-title">
<h1>Dashboard Overview</h1>
<p>Welcome back, here&apos;s what&apos;s happening with your territory</p>
</div>
<div className="top-actions">
<button className="ic-btn" aria-label="Search"><FigIcon name="i_search" size={18} /></button>
<button className="ic-btn" aria-label="Toggle theme" onClick={onToggle}>
{theme === "dark" ? <FigIcon name="i_theme" size={18} /> : <Sun size={18} />}
</button>
<button className="ic-btn" aria-label="Notifications" style={{ position: "relative" }}>
<FigIcon name="i_bell" size={18} />
<span style={{ position: "absolute", top: 9, right: 10, width: 7, height: 7, borderRadius: 99, background: "var(--orange)", border: "2px solid var(--panel)" }} />
</button>
<button className="top-user">
<span className="av" style={{ background: "linear-gradient(135deg,#fda913,#fd6d13)", display: "grid", placeItems: "center", color: "#fff", fontWeight: 700, fontSize: 12 }}>JJ</span>
<div style={{ textAlign: "left" }}>
<div className="nm">Justin Johnson</div>
<div className="rl">Owner</div>
</div>
<ChevronDown size={16} />
</button>
</div>
</header>
);
}