Files
vibn-frontend/design-templates/VIBN (2)/page-dashboard.jsx

356 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// page-dashboard.jsx — KPI strip + time-series chart +
// pipeline funnel + recent activity + team leaderboard.
// Theme-aware so it adapts to dark rail chrome.
// ============================================================
const DashboardBody = ({ theme = "light" }) => {
const dark = theme === "dark";
const c = dark ? {
bg: "#0f0f14", panel: "#13131a", border: "#ffffff10",
text: "#e8e8ee", subtext: "#9a9aa6", muted: "#6a6a78",
grid: "#ffffff08", accent: "#7a78ff", up: "#22c55e", down: "#ff4d5e",
} : {
bg: "#fafaf9", panel: "#ffffff", border: "#ebebe6",
text: "#111", subtext: "#5a5a5e", muted: "#8a8a90",
grid: "#eeeee9", accent: "#5e5cff", up: "#22c55e", down: "#ff4d5e",
};
// Synthetic but consistent daily series, weekday-shaped
const days = ["M","T","W","T","F","S","S","M","T","W","T","F","S","S"];
const series = [42,58,71,64,79,32,28, 51,68,82,75,90,38,33];
const max = Math.max(...series);
// Funnel data
const funnel = [
{ stage: "New", n: 184, v: "€2.1m" },
{ stage: "Qualified", n: 96, v: "€1.4m" },
{ stage: "Proposal", n: 42, v: "€780k" },
{ stage: "Negotiation", n: 19, v: "€420k" },
{ stage: "Closed-won", n: 11, v: "€286k" },
];
const fmax = funnel[0].n;
const Avatar = ({ name, color = "#d4b8a8", size = 22 }) => (
<div style={{
width: size, height: size, borderRadius: "50%", background: color,
fontSize: size * 0.42, fontWeight: 600, color: "#3a2820",
display: "flex", alignItems: "center", justifyContent: "center",
flexShrink: 0,
}}>{name}</div>
);
return (
<div style={{
height: "100%", background: c.bg, color: c.text, fontFamily: SANS,
display: "flex", flexDirection: "column", overflow: "hidden",
}}>
{/* Header */}
<div style={{
padding: "20px 28px 16px", borderBottom: `1px solid ${c.border}`,
display: "flex", alignItems: "flex-end", justifyContent: "space-between",
}}>
<div>
<div style={{
fontSize: 11, color: c.muted, letterSpacing: "0.06em",
textTransform: "uppercase", marginBottom: 4, fontWeight: 500,
}}>Workspace dashboard</div>
<h1 style={{
fontSize: 26, fontWeight: 600, margin: 0, letterSpacing: "-0.02em",
}}>Good afternoon, Mira</h1>
<div style={{ fontSize: 13, color: c.subtext, marginTop: 4 }}>
3 deals moved stage today · 12 unread in Inbox · 1 task overdue
</div>
</div>
<div style={{ display: "flex", gap: 8 }}>
<div style={{
display: "flex", alignItems: "center", padding: "6px 10px",
borderRadius: 6, background: c.panel, border: `1px solid ${c.border}`,
fontSize: 12, color: c.subtext, gap: 8,
}}>
<span style={{ fontWeight: 500, color: c.text }}>Last 14 days</span>
<Icon d={P.chevron} size={12} />
</div>
<button style={{
padding: "7px 12px", borderRadius: 6, fontSize: 12, fontFamily: SANS,
background: c.panel, border: `1px solid ${c.border}`, color: c.text,
cursor: "pointer",
}}>Export</button>
<button style={{
padding: "7px 14px", borderRadius: 6, fontSize: 12, fontFamily: SANS,
background: dark ? "#fff" : "#111", color: dark ? "#111" : "#fff",
border: "none", cursor: "pointer", fontWeight: 500,
display: "flex", alignItems: "center", gap: 6,
}}><Icon d={P.plus} size={12}/> New report</button>
</div>
</div>
<div style={{
flex: 1, overflowY: "auto", padding: "20px 28px 28px",
display: "flex", flexDirection: "column", gap: 20,
}}>
{/* KPI strip */}
<div style={{
display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 12,
}}>
{[
{ l: "Revenue · MTD", v: "€286,420", d: "+18.4%", up: true,
spark: [20,28,24,36,30,42,52,48,58,62,70,82] },
{ l: "Active deals", v: "168", d: "+12", up: true,
spark: [40,42,45,46,49,52,54,56,58,60,62,65] },
{ l: "Win rate · 30d", v: "34.2%", d: "1.1%", up: false,
spark: [60,58,55,52,54,50,48,45,46,42,38,36] },
{ l: "Pipeline ratio", v: "4.8×", d: "healthy", up: true,
spark: [50,48,52,55,53,58,56,60,62,65,63,68] },
].map(k => {
const sm = Math.max(...k.spark), sn = Math.min(...k.spark);
const pts = k.spark.map((v, i) => {
const x = (i / (k.spark.length - 1)) * 100;
const y = 30 - ((v - sn) / (sm - sn || 1)) * 26 - 2;
return `${x},${y}`;
}).join(" ");
return (
<div key={k.l} style={{
padding: "16px 18px", borderRadius: 10,
background: c.panel, border: `1px solid ${c.border}`,
}}>
<div style={{
display: "flex", justifyContent: "space-between", alignItems: "baseline",
marginBottom: 8,
}}>
<span style={{ fontSize: 12, color: c.muted }}>{k.l}</span>
<span style={{
fontSize: 11, color: k.up ? c.up : c.down, fontWeight: 500,
}}>{k.d}</span>
</div>
<div style={{
fontSize: 26, fontWeight: 600, letterSpacing: "-0.02em",
marginBottom: 6, fontVariantNumeric: "tabular-nums",
}}>{k.v}</div>
<svg viewBox="0 0 100 30" style={{
width: "100%", height: 26, display: "block",
}} preserveAspectRatio="none">
<polyline points={pts} fill="none"
stroke={k.up ? c.up : c.down} strokeWidth="1.5"
vectorEffect="non-scaling-stroke" />
</svg>
</div>
);
})}
</div>
{/* Chart + funnel */}
<div style={{
display: "grid", gridTemplateColumns: "1.5fr 1fr", gap: 16,
}}>
{/* Time-series */}
<div style={{
padding: "18px 20px", borderRadius: 12,
background: c.panel, border: `1px solid ${c.border}`,
}}>
<div style={{
display: "flex", justifyContent: "space-between", alignItems: "center",
marginBottom: 14,
}}>
<div>
<div style={{ fontSize: 13, fontWeight: 600 }}>Revenue, daily</div>
<div style={{ fontSize: 11, color: c.muted, marginTop: 2 }}>
Bookings · GBP closed-won
</div>
</div>
<div style={{ display: "flex", gap: 4 }}>
{["Day", "Week", "Month"].map((t, i) => (
<span key={t} style={{
padding: "4px 10px", borderRadius: 5, fontSize: 11, fontWeight: 500,
background: i === 0 ? (dark ? "#ffffff10" : "#f1f0eb") : "transparent",
color: i === 0 ? c.text : c.muted, cursor: "pointer",
}}>{t}</span>
))}
</div>
</div>
<div style={{ position: "relative", height: 180 }}>
{/* Gridlines */}
{[0, 0.25, 0.5, 0.75, 1].map(p => (
<div key={p} style={{
position: "absolute", left: 0, right: 0,
bottom: `${p * 100}%`, height: 1, background: c.grid,
}}></div>
))}
{/* Bars */}
<div style={{
position: "absolute", inset: 0, display: "flex",
alignItems: "flex-end", gap: 6, paddingRight: 6,
}}>
{series.map((v, i) => (
<div key={i} style={{ flex: 1, position: "relative",
display: "flex", flexDirection: "column",
alignItems: "center", justifyContent: "flex-end",
height: "100%",
}}>
<div style={{
width: "100%", height: `${(v / max) * 100}%`,
background: i === 11
? `linear-gradient(180deg, ${c.accent}, ${dark ? "#3a38c0" : "#bfbeff"})`
: (dark ? "#ffffff14" : "#e8e7e0"),
borderRadius: 3,
}}></div>
</div>
))}
</div>
{/* Annotation */}
<div style={{
position: "absolute", right: 6, top: -6,
background: c.text, color: c.bg,
padding: "3px 8px", borderRadius: 4, fontSize: 11, fontWeight: 500,
}}>42k · today</div>
</div>
<div style={{
display: "flex", justifyContent: "space-between", marginTop: 6,
fontSize: 10, color: c.muted, fontFamily: "ui-monospace, monospace",
}}>
{days.map((d, i) => (
<span key={i} style={{ flex: 1, textAlign: "center" }}>{d}</span>
))}
</div>
</div>
{/* Funnel */}
<div style={{
padding: "18px 20px", borderRadius: 12,
background: c.panel, border: `1px solid ${c.border}`,
}}>
<div style={{
display: "flex", justifyContent: "space-between", alignItems: "baseline",
marginBottom: 14,
}}>
<div style={{ fontSize: 13, fontWeight: 600 }}>Pipeline funnel</div>
<span style={{ fontSize: 11, color: c.muted }}>Q2 · 168 deals</span>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
{funnel.map((f, i) => {
const w = (f.n / fmax) * 100;
const colors = ["#5e5cff", "#7a78ff", "#9b99ff", "#bcb9ff", "#22c55e"];
return (
<div key={f.stage} style={{ position: "relative" }}>
<div style={{
width: `${w}%`, height: 30, borderRadius: 5,
background: colors[i], display: "flex",
alignItems: "center", paddingLeft: 12, color: "#fff",
fontSize: 12, fontWeight: 500,
}}>{f.stage}</div>
<div style={{
position: "absolute", right: 0, top: 0, height: 30,
display: "flex", alignItems: "center", gap: 10,
fontSize: 12, color: c.muted,
}}>
<span style={{
fontFamily: "ui-monospace, monospace", color: c.text,
}}>{f.n}</span>
<span style={{ fontSize: 11 }}>{f.v}</span>
</div>
</div>
);
})}
</div>
</div>
</div>
{/* Activity + leaderboard */}
<div style={{
display: "grid", gridTemplateColumns: "1.5fr 1fr", gap: 16,
}}>
<div style={{
padding: "18px 20px", borderRadius: 12,
background: c.panel, border: `1px solid ${c.border}`,
}}>
<div style={{
display: "flex", justifyContent: "space-between", alignItems: "baseline",
marginBottom: 14,
}}>
<div style={{ fontSize: 13, fontWeight: 600 }}>Recent activity</div>
<span style={{ fontSize: 11, color: c.accent, cursor: "pointer" }}>View all </span>
</div>
{[
{ who: "MR", c: "#d4b8a8", n: "Mira Reyes", v: "moved",
w: <><b>Q3 Carrier API</b> to <span style={{ color: "#22c55e" }}>Negotiation</span></>,
t: "2m ago" },
{ who: "TR", c: "#c8e8a8", n: "Theo Roux", v: "logged a call with",
w: <><b>Sun Kim · Northstar</b></>, t: "14m" },
{ who: "DP", c: "#a8c8e8", n: "Devi Patel", v: "closed",
w: <><b>Halcyon · Pro renewal</b> · €24,000</>, t: "1h" },
{ who: "MR", c: "#d4b8a8", n: "Mira Reyes", v: "created a deal",
w: <><b>Brooke Foods Q3 pilot</b></>, t: "2h" },
{ who: "SK", c: "#e8a87c", n: "Sun Kim", v: "added 4 contacts to",
w: <><b>Kestrel</b></>, t: "3h" },
].map((a, i) => (
<div key={i} style={{
display: "flex", alignItems: "center", gap: 10,
padding: "8px 0",
borderTop: i === 0 ? "none" : `1px solid ${c.border}`,
}}>
<Avatar name={a.who} color={a.c} size={26} />
<div style={{ flex: 1, fontSize: 13 }}>
<span style={{ fontWeight: 500 }}>{a.n}</span>
<span style={{ color: c.muted }}> {a.v} </span>
<span>{a.w}</span>
</div>
<span style={{ fontSize: 11, color: c.muted, whiteSpace: "nowrap" }}>{a.t}</span>
</div>
))}
</div>
<div style={{
padding: "18px 20px", borderRadius: 12,
background: c.panel, border: `1px solid ${c.border}`,
}}>
<div style={{
display: "flex", justifyContent: "space-between", alignItems: "baseline",
marginBottom: 14,
}}>
<div style={{ fontSize: 13, fontWeight: 600 }}>Team · this month</div>
<span style={{ fontSize: 11, color: c.muted }}>By bookings</span>
</div>
{[
{ i: "MR", c: "#d4b8a8", n: "Mira Reyes", v: 124, d: "€124k", p: 100 },
{ i: "DP", c: "#a8c8e8", n: "Devi Patel", v: 86, d: "€86k", p: 70 },
{ i: "TR", c: "#c8e8a8", n: "Theo Roux", v: 62, d: "€62k", p: 50 },
{ i: "SK", c: "#e8a87c", n: "Sun Kim", v: 48, d: "€48k", p: 39 },
].map(t => (
<div key={t.i} style={{
display: "grid", gridTemplateColumns: "26px 1fr auto", gap: 10,
alignItems: "center", padding: "8px 0",
}}>
<Avatar name={t.i} color={t.c} size={26} />
<div style={{ minWidth: 0 }}>
<div style={{
display: "flex", justifyContent: "space-between",
fontSize: 12, marginBottom: 4,
}}>
<span style={{ fontWeight: 500 }}>{t.n}</span>
<span style={{
color: c.subtext, fontVariantNumeric: "tabular-nums",
}}>{t.d}</span>
</div>
<div style={{
height: 3, borderRadius: 2,
background: dark ? "#ffffff10" : "#eeeee9", overflow: "hidden",
}}>
<div style={{
width: `${t.p}%`, height: "100%",
background: `linear-gradient(90deg, ${c.accent}, ${dark ? "#9b99ff" : "#b15bff"})`,
}}></div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
};
window.DashboardBody = DashboardBody;