qwopus36-35b-a3b-eval-etheroi / design_analytics_dashboard.html
KyleHessling1's picture
Initial benchmark: Qwopus3.6-35B-A3B-v1 Q5_K_M, 162 tok/s, 17 runs
bac03df verified
Raw History Blame Contribute Delete
37.5 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Analytics Dashboard</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--emerald:#10b981;
--emerald-light:#d1fae5;
--emerald-dark:#059669;
--gray-50:#f9fafb;
--gray-100:#f3f4f6;
--gray-200:#e5e7eb;
--gray-300:#d1d5db;
--gray-400:#9ca3af;
--gray-500:#6b7280;
--gray-600:#4b5563;
--gray-700:#374151;
--gray-800:#1f2937;
--gray-900:#111827;
--sidebar-w:240px;
--sidebar-collapsed-w:68px;
--topbar-h:56px;
--radius:8px;
--radius-lg:12px;
}
body{
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
background:var(--gray-50);
color:var(--gray-800);
line-height:1.5;
overflow:hidden;
height:100vh;
}
:focus-visible{outline:2px solid var(--emerald);outline-offset:2px;border-radius:4px}
/* ===== SIDEBAR ===== */
.sidebar{
position:fixed;left:0;top:0;bottom:0;
width:var(--sidebar-w);
background:var(--gray-900);
color:var(--gray-300);
display:flex;flex-direction:column;
transition:width .3s cubic-bezier(.4,0,.2,1);
z-index:100;
overflow:hidden;
}
.sidebar.collapsed{width:var(--sidebar-collapsed-w)}
.sidebar-header{
display:flex;align-items:center;justify-content:space-between;
padding:16px 12px;min-height:var(--topbar-h);
border-bottom:1px solid var(--gray-700);
}
.sidebar-logo{
display:flex;align-items:center;gap:10px;
font-weight:700;font-size:16px;color:#fff;
white-space:nowrap;overflow:hidden;
}
.sidebar-logo svg{flex-shrink:0}
.sidebar-toggle{
background:none;border:none;color:var(--gray-400);
cursor:pointer;padding:4px;border-radius:4px;
display:flex;align-items:center;justify-content:center;
}
.sidebar-toggle:hover{color:#fff;background:var(--gray-700)}
.workspace-switcher{
padding:12px 12px 8px;
border-bottom:1px solid var(--gray-700);
}
.workspace-btn{
display:flex;align-items:center;gap:10px;
width:100%;padding:8px 10px;border:none;
background:var(--gray-800);color:var(--gray-200);
border-radius:var(--radius);cursor:pointer;
font-size:13px;font-weight:600;text-align:left;
white-space:nowrap;overflow:hidden;
transition:background .15s;
}
.workspace-btn:hover{background:var(--gray-700)}
.workspace-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.nav-section{padding:8px 8px 4px;flex:1;overflow-y:auto}
.nav-label{
font-size:10px;font-weight:600;text-transform:uppercase;
letter-spacing:.08em;color:var(--gray-500);
padding:8px 10px 4px;white-space:nowrap;overflow:hidden;
}
.sidebar.collapsed .nav-label{opacity:0;height:0;padding:0}
.nav-item{
display:flex;align-items:center;gap:10px;
padding:8px 10px;border:none;background:none;
color:var(--gray-400);cursor:pointer;border-radius:var(--radius);
font-size:13px;font-weight:500;width:100%;text-align:left;
white-space:nowrap;overflow:hidden;
transition:all .15s;
}
.nav-item:hover{background:var(--gray-800);color:var(--gray-200)}
.nav-item.active{background:rgba(16,185,129,.12);color:var(--emerald)}
.nav-item svg{flex-shrink:0}
.nav-badge{
margin-left:auto;background:var(--emerald);color:#fff;
font-size:10px;font-weight:700;padding:1px 6px;border-radius:10px;
}
.sidebar.collapsed .nav-badge{display:none}
.sidebar-footer{
padding:12px;border-top:1px solid var(--gray-700);
display:flex;align-items:center;gap:10px;
}
.avatar{
width:32px;height:32px;border-radius:50%;
background:linear-gradient(135deg,var(--emerald),var(--emerald-dark));
display:flex;align-items:center;justify-content:center;
color:#fff;font-weight:700;font-size:13px;flex-shrink:0;
}
.user-info{overflow:hidden;white-space:nowrap}
.user-name{font-size:13px;font-weight:600;color:var(--gray-200)}
.user-role{font-size:11px;color:var(--gray-500)}
.sidebar.collapsed .user-info{opacity:0;height:0}
/* ===== MAIN LAYOUT ===== */
.main-wrap{
margin-left:var(--sidebar-w);
transition:margin-left .3s cubic-bezier(.4,0,.2,1);
height:100vh;display:flex;flex-direction:column;
overflow:hidden;
}
.sidebar.collapsed ~ .main-wrap{margin-left:var(--sidebar-collapsed-w)}
/* ===== TOPBAR ===== */
.topbar{
height:var(--topbar-h);background:#fff;
border-bottom:1px solid var(--gray-200);
display:flex;align-items:center;padding:0 24px;gap:16px;
flex-shrink:0;
}
.search-box{
display:flex;align-items:center;gap:8px;
background:var(--gray-100);border:1px solid var(--gray-200);
border-radius:var(--radius);padding:6px 12px;
width:280px;transition:border-color .15s;
}
.search-box:focus-within{border-color:var(--emerald)}
.search-box svg{color:var(--gray-400);flex-shrink:0}
.search-box input{
border:none;background:none;outline:none;
font-size:13px;color:var(--gray-700);width:100%;
}
.search-box input::placeholder{color:var(--gray-400)}
.search-box input:focus{color:var(--gray-800)}
.date-picker{
display:flex;align-items:center;gap:8px;
padding:6px 12px;border:1px solid var(--gray-200);
border-radius:var(--radius);cursor:pointer;
font-size:13px;color:var(--gray-600);
transition:border-color .15s,background .15s;
white-space:nowrap;
}
.date-picker:hover{border-color:var(--gray-300);background:var(--gray-50)}
.date-picker:focus-visible{border-color:var(--emerald)}
.date-divider{color:var(--gray-400)}
.topbar-right{margin-left:auto;display:flex;align-items:center;gap:8px}
.notif-btn{
position:relative;background:none;border:none;
padding:8px;border-radius:var(--radius);cursor:pointer;
color:var(--gray-500);transition:color .15s,background .15s;
}
.notif-btn:hover{color:var(--gray-700);background:var(--gray-100)}
.notif-btn:focus-visible{border-color:var(--emerald)}
.notif-dot{
position:absolute;top:6px;right:6px;
width:8px;height:8px;background:#ef4444;
border-radius:50%;border:2px solid #fff;
}
/* ===== CONTENT ===== */
.content{
flex:1;overflow-y:auto;padding:24px;
}
.content-header{
display:flex;align-items:center;justify-content:space-between;
margin-bottom:24px;
}
.content-title{font-size:22px;font-weight:700;color:var(--gray-900)}
.content-subtitle{font-size:13px;color:var(--gray-500);margin-top:2px}
.export-btn{
display:flex;align-items:center;gap:6px;
padding:8px 16px;border:1px solid var(--gray-200);
background:#fff;border-radius:var(--radius);
font-size:13px;font-weight:500;color:var(--gray-600);
cursor:pointer;transition:all .15s;
}
.export-btn:hover{border-color:var(--gray-300);color:var(--gray-800)}
.export-btn:focus-visible{border-color:var(--emerald)}
/* ===== KPI CARDS ===== */
.kpi-grid{
display:grid;grid-template-columns:repeat(4,1fr);gap:16px;
margin-bottom:24px;
}
.kpi-card{
background:#fff;border:1px solid var(--gray-200);
border-radius:var(--radius-lg);padding:20px;
transition:box-shadow .15s,border-color .15s;
cursor:default;
}
.kpi-card:hover{box-shadow:0 2px 8px rgba(0,0,0,.06);border-color:var(--gray-300)}
.kpi-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
.kpi-label{font-size:13px;font-weight:500;color:var(--gray-500)}
.kpi-icon{
width:32px;height:32px;border-radius:var(--radius);
display:flex;align-items:center;justify-content:center;
}
.kpi-value{font-size:28px;font-weight:700;color:var(--gray-900);line-height:1}
.kpi-trend{
display:flex;align-items:center;gap:4px;
font-size:12px;font-weight:600;margin-top:8px;
}
.kpi-trend.up{color:var(--emerald)}
.kpi-trend.down{color:#ef4444}
.kpi-sparkline{margin-top:12px}
/* ===== CHARTS ROW ===== */
.charts-row{
display:grid;grid-template-columns:2fr 1fr;gap:16px;
margin-bottom:24px;
}
.chart-card{
background:#fff;border:1px solid var(--gray-200);
border-radius:var(--radius-lg);padding:20px;
transition:box-shadow .15s,border-color .15s;
}
.chart-card:hover{box-shadow:0 2px 8px rgba(0,0,0,.06)}
.chart-title{font-size:15px;font-weight:600;color:var(--gray-800);margin-bottom:4px}
.chart-subtitle{font-size:12px;color:var(--gray-500);margin-bottom:16px}
.chart-container{position:relative}
.chart-legend{
display:flex;gap:16px;margin-top:12px;flex-wrap:wrap;
}
.legend-item{
display:flex;align-items:center;gap:6px;
font-size:12px;color:var(--gray-600);cursor:pointer;
transition:color .15s;
}
.legend-item:hover{color:var(--gray-800)}
.legend-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
/* ===== LINE CHART ===== */
.line-chart-area{position:relative;height:240px}
.line-chart-area svg{width:100%;height:100%}
.chart-line{
stroke:var(--emerald);stroke-width:2.5;
fill:none;stroke-linecap:round;stroke-linejoin:round;
}
.chart-area-fill{
fill:url(#lineGrad);opacity:.15;
}
.chart-grid-line{stroke:var(--gray-100);stroke-width:1}
.chart-label{fill:var(--gray-400);font-size:11px;font-weight:500}
.chart-dot{
fill:var(--emerald);stroke:#fff;stroke-width:2;
transition:r .15s;cursor:pointer;
}
.chart-dot:hover{r:6}
.chart-tooltip{
position:absolute;background:var(--gray-800);color:#fff;
padding:6px 10px;border-radius:6px;font-size:12px;
pointer-events:none;opacity:0;transition:opacity .15s;
white-space:nowrap;z-index:10;
}
.chart-tooltip.visible{opacity:1}
/* ===== DONUT CHART ===== */
.donut-wrapper{
display:flex;flex-direction:column;align-items:center;
gap:16px;
}
.donut-chart-area{position:relative;height:200px}
.donut-chart-area svg{width:100%;height:100%}
.donut-segment{
transition:opacity .15s,filter .15s;cursor:pointer;
}
.donut-segment:hover{opacity:.8;filter:brightness(1.1)}
.donut-center{fill:#fff}
.donut-center-value{font-size:22px;font-weight:700;fill:var(--gray-800)}
.donut-center-label{font-size:11px;fill:var(--gray-400)}
.donut-legend{
display:grid;grid-template-columns:1fr 1fr;gap:8px 16px;
width:100%;
}
.donut-legend-item{
display:flex;align-items:center;gap:8px;
font-size:12px;color:var(--gray-600);cursor:pointer;
padding:4px 6px;border-radius:4px;
transition:background .15s;
}
.donut-legend-item:hover{background:var(--gray-100)}
.donut-legend-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.donut-legend-value{margin-left:auto;font-weight:600;color:var(--gray-700)}
/* ===== TABLE ===== */
.table-card{
background:#fff;border:1px solid var(--gray-200);
border-radius:var(--radius-lg);overflow:hidden;
transition:box-shadow .15s,border-color .15s;
}
.table-card:hover{box-shadow:0 2px 8px rgba(0,0,0,.06)}
.table-header{
display:flex;align-items:center;justify-content:space-between;
padding:16px 20px;border-bottom:1px solid var(--gray-200);
}
.table-title{font-size:15px;font-weight:600;color:var(--gray-800)}
.table-actions{display:flex;gap:8px}
.table-filter-btn{
display:flex;align-items:center;gap:4px;
padding:6px 12px;border:1px solid var(--gray-200);
background:#fff;border-radius:var(--radius);
font-size:12px;color:var(--gray-500);cursor:pointer;
transition:all .15s;
}
.table-filter-btn:hover{border-color:var(--gray-300);color:var(--gray-700)}
.table-wrap{overflow-x:auto}
table{width:100%;border-collapse:collapse}
th{
text-align:left;padding:12px 20px;
font-size:11px;font-weight:600;text-transform:uppercase;
letter-spacing:.05em;color:var(--gray-500);
border-bottom:1px solid var(--gray-200);
white-space:nowrap;
cursor:pointer;user-select:none;
transition:color .15s;
}
th:hover{color:var(--gray-700)}
th .sort-icon{margin-left:4px;opacity:.4;font-size:10px}
th.sorted .sort-icon{opacity:1;color:var(--emerald)}
td{
padding:12px 20px;font-size:13px;color:var(--gray-700);
border-bottom:1px solid var(--gray-100);
white-space:nowrap;
}
tr:hover td{background:var(--gray-50)}
.status-badge{
display:inline-flex;align-items:center;gap:4px;
padding:2px 8px;border-radius:10px;
font-size:11px;font-weight:600;
}
.status-badge.completed{background:var(--emerald-light);color:var(--emerald-dark)}
.status-badge.pending{background:#fef3c7;color:#92400e}
.status-badge.failed{background:#fee2e2;color:#991b1b}
.status-dot{width:6px;height:6px;border-radius:50%}
.status-badge.completed .status-dot{background:var(--emerald-dark)}
.status-badge.pending .status-dot{background:#92400e}
.status-badge.failed .status-dot{background:#991b1b}
.amount{font-weight:600;color:var(--gray-800);font-variant-numeric:tabular-nums}
.customer-cell{display:flex;align-items:center;gap:8px}
.customer-avatar{
width:24px;height:24px;border-radius:50%;
display:flex;align-items:center;justify-content:center;
color:#fff;font-weight:700;font-size:10px;flex-shrink:0;
}
.table-footer{
display:flex;align-items:center;justify-content:space-between;
padding:12px 20px;border-top:1px solid var(--gray-200);
font-size:12px;color:var(--gray-500);
}
.pagination{display:flex;gap:4px}
.page-btn{
width:32px;height:32px;display:flex;align-items:center;justify-content:center;
border:1px solid var(--gray-200);background:#fff;border-radius:6px;
font-size:13px;color:var(--gray-600);cursor:pointer;
transition:all .15s;
}
.page-btn:hover{border-color:var(--gray-300);color:var(--gray-800)}
.page-btn.active{background:var(--emerald);color:#fff;border-color:var(--emerald)}
.page-btn:disabled{opacity:.4;cursor:default}
/* ===== RESPONSIVE ===== */
@media(max-width:1200px){
.kpi-grid{grid-template-columns:repeat(2,1fr)}
}
@media(max-width:900px){
.sidebar{width:var(--sidebar-collapsed-w)}
.sidebar .nav-label,.sidebar .workspace-btn span,.sidebar .user-info{opacity:0;height:0;padding:0}
.main-wrap{margin-left:var(--sidebar-collapsed-w)}
.charts-row{grid-template-columns:1fr}
.search-box{width:200px}
.date-picker span{display:none}
}
@media(max-width:600px){
.kpi-grid{grid-template-columns:1fr}
.search-box{width:140px}
.topbar{padding:0 12px}
.content{padding:16px}
}
</style>
</head>
<body>
<!-- SIDEBAR -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="sidebar-logo">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<rect x="2" y="2" width="8" height="8" rx="2" fill="#10b981"/>
<rect x="14" y="2" width="8" height="8" rx="2" fill="#10b981" opacity=".6"/>
<rect x="2" y="14" width="8" height="8" rx="2" fill="#10b981" opacity=".4"/>
<rect x="14" y="14" width="8" height="8" rx="2" fill="#10b981" opacity=".2"/>
</svg>
<span>MetricFlow</span>
</div>
<button class="sidebar-toggle" id="sidebarToggle" aria-label="Toggle sidebar">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
</svg>
</button>
</div>
<div class="workspace-switcher">
<button class="workspace-btn" id="workspaceBtn">
<span class="workspace-dot" style="background:#10b981"></span>
<span>Acme Corp</span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="6 9 12 15 18 9"/></svg>
</button>
</div>
<nav class="nav-section">
<div class="nav-label">Main</div>
<button class="nav-item active">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
<span>Dashboard</span>
</button>
<button class="nav-item">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
<span>Analytics</span>
<span class="nav-badge">3</span>
</button>
<button class="nav-item">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
<span>Customers</span>
</button>
<button class="nav-item">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="4" width="22" height="16" rx="2" ry="2"/><line x1="1" y1="10" x2="23" y2="10"/></svg>
<span>Transactions</span>
</button>
<div class="nav-label">Management</div>
<button class="nav-item">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
<span>Integrations</span>
</button>
<button class="nav-item">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
<span>Settings</span>
</button>
</nav>
<div class="sidebar-footer">
<div class="avatar">JD</div>
<div class="user-info">
<div class="user-name">Jane Doe</div>
<div class="user-role">Admin</div>
</div>
</div>
</aside>
<!-- MAIN -->
<div class="main-wrap" id="mainWrap">
<!-- TOPBAR -->
<header class="topbar">
<div class="search-box">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="text" placeholder="Search anything…" aria-label="Global search">
</div>
<div class="date-picker" tabindex="0" aria-label="Date range picker">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
<span>Jan 1, 2024</span>
<span class="date-divider">—</span>
<span>Jun 30, 2024</span>
</div>
<div class="topbar-right">
<button class="notif-btn" aria-label="Notifications">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
<span class="notif-dot"></span>
</button>
</div>
</header>
<!-- CONTENT -->
<main class="content">
<div class="content-header">
<div>
<h1 class="content-title">Dashboard Overview</h1>
<p class="content-subtitle">Performance metrics for the last 6 months</p>
</div>
<button class="export-btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Export
</button>
</div>
<!-- KPI CARDS -->
<div class="kpi-grid" id="kpiGrid"></div>
<!-- CHARTS -->
<div class="charts-row">
<div class="chart-card">
<div class="chart-title">Weekly Active Users</div>
<div class="chart-subtitle">Tracking user engagement over the past 12 weeks</div>
<div class="chart-container">
<div class="line-chart-area" id="lineChartArea">
<svg id="lineChart"></svg>
<div class="chart-tooltip" id="lineTooltip"></div>
</div>
</div>
</div>
<div class="chart-card">
<div class="chart-title">Traffic Sources</div>
<div class="chart-subtitle">Distribution of user acquisition channels</div>
<div class="donut-wrapper">
<div class="donut-chart-area">
<svg id="donutChart"></svg>
</div>
<div class="donut-legend" id="donutLegend"></div>
</div>
</div>
</div>
<!-- TABLE -->
<div class="table-card">
<div class="table-header">
<div class="table-title">Recent Transactions</div>
<div class="table-actions">
<button class="table-filter-btn">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>
Filter
</button>
<button class="table-filter-btn">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
Export
</button>
</div>
</div>
<div class="table-wrap">
<table id="table">
<thead>
<tr>
<th data-sort="id" class="sorted">ID <span class="sort-icon">↑</span></th>
<th data-sort="customer">Customer <span class="sort-icon">↕</span></th>
<th data-sort="amount">Amount <span class="sort-icon">↕</span></th>
<th data-sort="status">Status <span class="sort-icon">↕</span></th>
<th data-sort="date">Date <span class="sort-icon">↕</span></th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
<div class="table-footer">
<span>Showing 1-10 of 48 transactions</span>
<div class="pagination">
<button class="page-btn" disabled>‹</button>
<button class="page-btn active">1</button>
<button class="page-btn">2</button>
<button class="page-btn">3</button>
<button class="page-btn">4</button>
<button class="page-btn">5</button>
<button class="page-btn">›</button>
</div>
</div>
</div>
</main>
</div>
<script>
// ===== DATA =====
const kpiData = [
{ label: "Revenue", value: "$847,320", change: "+12.5%", up: true, sparkline: [52,58,45,62,55,68,72,65,78,82,75,84], icon: "$", color: "#10b981", bgColor: "#d1fae5" },
{ label: "Active Users", value: "24,891", change: "+8.2%", up: true, sparkline: [30,35,32,38,42,39,45,48,44,50,52,55], icon: "👤", color: "#3b82f6", bgColor: "#dbeafe" },
{ label: "Conversion Rate", value: "3.24%", change: "-0.4%", up: false, sparkline: [4.2,3.9,3.8,3.5,3.7,3.6,3.4,3.5,3.3,3.4,3.3,3.24], icon: "📊", color: "#f59e0b", bgColor: "#fef3c7" },
{ label: "Churn Rate", value: "1.8%", change: "-0.3%", up: false, sparkline: [3.2,3.0,2.8,2.9,2.7,2.5,2.6,2.4,2.2,2.0,1.9,1.8], icon: "📉", color: "#ef4444", bgColor: "#fee2e2" }
];
const wauData = {
labels: ["W1","W2","W3","W4","W5","W6","W7","W8","W9","W10","W11","W12"],
values: [12400,13100,12800,14200,15100,14800,16200,17100,16800,18400,19200,20100],
values2: [8200,8600,9100,9400,9800,10200,10600,11100,11500,12000,12600,13200]
};
const donutData = [
{ label: "Organic Search", value: 38, color: "#10b981" },
{ label: "Direct", value: 24, color: "#3b82f6" },
{ label: "Social Media", value: 18, color: "#f59e0b" },
{ label: "Referral", value: 12, color: "#8b5cf6" },
{ label: "Email", value: 8, color: "#ec4899" }
];
const transactions = [
{ id: "TXN-4821", customer: "Sarah Chen", email: "sarah@acme.io", amount: 12450, status: "completed", date: "2024-06-28" },
{ id: "TXN-4820", customer: "Marcus Johnson", email: "marcus@nexus.com", amount: 8920, status: "completed", date: "2024-06-28" },
{ id: "TXN-4819", customer: "Elena Rodriguez", email: "elena@orbit.co", amount: 15780, status: "pending", date: "2024-06-27" },
{ id: "TXN-4818", customer: "David Kim", email: "david@pulse.io", amount: 6340, status: "completed", date: "2024-06-27" },
{ id: "TXN-4817", customer: "Priya Patel", email: "priya@zenith.com", amount: 21090, status: "failed", date: "2024-06-26" },
{ id: "TXN-4816", customer: "Alex Thompson", email: "alex@forge.dev", amount: 4560, status: "completed", date: "2024-06-26" },
{ id: "TXN-4815", customer: "Yuki Tanaka", email: "yuki@nova.io", amount: 9870, status: "completed", date: "2024-06-25" },
{ id: "TXN-4814", customer: "James Wilson", email: "james@atlas.com", amount: 17230, status: "pending", date: "2024-06-25" },
{ id: "TXN-4813", customer: "Fatima Al-Hassan", email: "fatima@luna.co", amount: 5430, status: "completed", date: "2024-06-24" },
{ id: "TXN-4812", customer: "Ryan O'Brien", email: "ryan@delta.io", amount: 11650, status: "completed", date: "2024-06-24" }
];
// ===== SIDEBAR TOGGLE =====
const sidebar = document.getElementById('sidebar');
const sidebarToggle = document.getElementById('sidebarToggle');
sidebarToggle.addEventListener('click', () => sidebar.classList.toggle('collapsed'));
// ===== NAV ITEMS =====
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', () => {
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
item.classList.add('active');
});
});
// ===== KPI CARDS =====
function buildKPIs() {
const grid = document.getElementById('kpiGrid');
kpiData.forEach(kpi => {
const card = document.createElement('div');
card.className = 'kpi-card';
card.innerHTML = `
<div class="kpi-header">
<span class="kpi-label">${kpi.label}</span>
<div class="kpi-icon" style="background:${kpi.bgColor};color:${kpi.color}">
<span style="font-size:16px">${kpi.icon}</span>
</div>
</div>
<div class="kpi-value">${kpi.value}</div>
<div class="kpi-trend ${kpi.up ? 'up' : 'down'}">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
${kpi.up ? '<polyline points="18 15 12 9 6 15"/>' : '<polyline points="6 9 12 15 18 9"/>'}
</svg>
${kpi.change}
</div>
<div class="kpi-sparkline">${drawSparkline(kpi.sparkline, kpi.color, kpi.up)}</div>
`;
grid.appendChild(card);
});
}
function drawSparkline(data, color, up) {
const w = 120, h = 32, pad = 2;
const min = Math.min(...data), max = Math.max(...data);
const range = max - min || 1;
const points = data.map((v, i) => {
const x = pad + (i / (data.length - 1)) * (w - pad * 2);
const y = h - pad - ((v - min) / range) * (h - pad * 2);
return `${x},${y}`;
});
const lastPt = points[points.length - 1].split(',');
const firstPt = points[0].split(',');
const areaPts = [...points, `${lastPt[0]},${h}`, `${firstPt[0]},${h}`];
const areaFill = up ? color + '20' : '#ef444420';
return `<svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
<defs>
<linearGradient id="sparkGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="${color}" stop-opacity="0.25"/>
<stop offset="100%" stop-color="${color}" stop-opacity="0.02"/>
</linearGradient>
</defs>
<polygon points="${areaPts.join(' ')}" fill="url(#sparkGrad)"/>
<polyline points="${points.join(' ')}" fill="none" stroke="${color}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="${lastPt[0]}" cy="${lastPt[1]}" r="2.5" fill="${color}"/>
</svg>`;
}
// ===== LINE CHART =====
function buildLineChart() {
const svg = document.getElementById('lineChart');
const container = document.getElementById('lineChartArea');
const w = container.clientWidth;
const h = container.clientHeight;
const pad = { top: 20, right: 20, bottom: 30, left: 50 };
const cw = w - pad.left - pad.right;
const ch = h - pad.top - pad.bottom;
const allVals = [...wauData.values, ...wauData.values2];
const minVal = Math.min(...allVals);
const maxVal = Math.max(...allVals);
const rangeVal = maxVal - minVal;
const yMin = Math.floor(minVal - rangeVal * 0.1);
const yMax = Math.ceil(maxVal + rangeVal * 0.05);
function xPos(i) { return pad.left + (i / (wauData.values.length - 1)) * cw; }
function yPos(v) { return pad.top + ch - ((v - yMin) / (yMax - yMin)) * ch; }
// Grid lines
const gridSteps = 5;
let gridLines = '';
for (let i = 0; i <= gridSteps; i++) {
const val = yMin + (i / gridSteps) * (yMax - yMin);
const y = yPos(val);
gridLines += `<line x1="${pad.left}" y1="${y}" x2="${w - pad.right}" y2="${y}" class="chart-grid-line"/>`;
gridLines += `<text x="${pad.left - 8}" y="${y + 4}" class="chart-label" text-anchor="end">${formatNum(val)}</text>`;
}
// X labels
let xLabels = '';
wauData.labels.forEach((label, i) => {
xLabels += `<text x="${xPos(i)}" y="${h - 4}" class="chart-label" text-anchor="middle">${label}</text>`;
});
// Lines
function makeLine(values, color, dash) {
const pts = values.map((v, i) => `${xPos(i)},${yPos(v)}`).join(' ');
const areaPts = [...values.map((v, i) => `${xPos(i)},${yPos(v)}`)];
const lastX = xPos(values.length - 1);
const firstX = xPos(0);
const areaPath = `${areaPts.join(' ')} L${lastX},${pad.top + ch} L${firstX},${pad.top + ch} Z`;
return { line: `<polyline points="${pts}" class="chart-line" stroke="${color}" stroke-width="2.5" stroke-dasharray="${dash}"/>`, area: `<path d="${areaPath}" fill="${color}" opacity="0.08"/>`, dots: values.map((v, i) => `<circle cx="${xPos(i)}" cy="${yPos(v)}" r="3" fill="${color}" stroke="#fff" stroke-width="2" class="chart-dot" data-x="${i}" data-val="${v}" data-label="${wauData.labels[i]}"/>`).join('') };
}
const series1 = makeLine(wauData.values, '#10b981', 'none');
const series2 = makeLine(wauData.values2, '#3b82f6', '6,4');
// Gradient for area
const defs = `<defs>
<linearGradient id="lineGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#10b981" stop-opacity="0.3"/>
<stop offset="100%" stop-color="#10b981" stop-opacity="0"/>
</linearGradient>
</defs>`;
svg.innerHTML = defs + gridLines + xLabels + series1.area + series2.area + series1.line + series2.line + series1.dots + series2.dots;
svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
// Tooltip
const tooltip = document.getElementById('lineTooltip');
svg.querySelectorAll('.chart-dot').forEach(dot => {
dot.addEventListener('mouseenter', (e) => {
const x = parseFloat(dot.getAttribute('data-x'));
const val = parseFloat(dot.getAttribute('data-val'));
const label = dot.getAttribute('data-label');
tooltip.innerHTML = `<strong>${label}</strong><br>${formatNum(val)} users`;
tooltip.classList.add('visible');
const rect = svg.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
tooltip.style.left = (rect.left - containerRect.left + xPos(x) + 10) + 'px';
tooltip.style.top = (rect.top - containerRect.top + yPos(val) - 20) + 'px';
});
dot.addEventListener('mouseleave', () => {
tooltip.classList.remove('visible');
});
});
}
// ===== DONUT CHART =====
function buildDonutChart() {
const svg = document.getElementById('donutChart');
const container = svg.parentElement;
const size = container.clientWidth || 200;
const cx = size / 2, cy = size / 2;
const outerR = size * 0.42, innerR = outerR * 0.6;
const total = donutData.reduce((s, d) => s + d.value, 0);
let startAngle = -90;
let paths = '';
let legend = '';
donutData.forEach((d, i) => {
const angle = (d.value / total) * 360;
const startRad = (startAngle * Math.PI) / 180;
const endRad = ((startAngle + angle) * Math.PI) / 180;
const largeArc = angle > 180 ? 1 : 0;
const x1o = cx + outerR * Math.cos(startRad);
const y1o = cy + outerR * Math.sin(startRad);
const x2o = cx + outerR * Math.cos(endRad);
const y2o = cy + outerR * Math.sin(endRad);
const x1i = cx + innerR * Math.cos(endRad);
const y1i = cy + innerR * Math.sin(endRad);
const x2i = cx + innerR * Math.cos(startRad);
const y2i = cy + innerR * Math.sin(startRad);
const path = `M${x1o},${y1o} A${outerR},${outerR} 0 ${largeArc},1 ${x2o},${y2o} L${x1i},${y1i} A${innerR},${innerR} 0 ${largeArc},0 ${x2i},${y2i} Z`;
paths += `<path d="${path}" fill="${d.color}" class="donut-segment"/>`;
// Label position
const midAngle = startAngle + angle / 2;
const midRad = (midAngle * Math.PI) / 180;
const labelR = (outerR + innerR) / 2;
const lx = cx + labelR * Math.cos(midRad);
const ly = cy + labelR * Math.sin(midRad);
legend += `<div class="donut-legend-item">
<span class="donut-legend-dot" style="background:${d.color}"></span>
<span>${d.label}</span>
<span class="donut-legend-value">${d.value}%</span>
</div>`;
startAngle += angle;
});
svg.innerHTML = paths + `
<circle cx="${cx}" cy="${cy}" r="${innerR - 4}" class="donut-center"/>
<text x="${cx}" y="${cy - 6}" text-anchor="middle" class="donut-center-value">${total}%</text>
<text x="${cx}" y="${cy + 12}" text-anchor="middle" class="donut-center-label">Total Traffic</text>
`;
svg.setAttribute('viewBox', `0 0 ${size} ${size}`);
document.getElementById('donutLegend').innerHTML = legend;
}
// ===== TABLE =====
let sortCol = 'id', sortDir = 'asc';
function buildTable() {
const tbody = document.getElementById('tableBody');
const sorted = [...transactions].sort((a, b) => {
let va = a[sortCol], vb = b[sortCol];
if (sortCol === 'amount') { va = Number(va); vb = Number(vb); }
else if (sortCol === 'date') { va = new Date(va); vb = new Date(vb); }
else { va = String(va).toLowerCase(); vb = String(vb).toLowerCase(); }
if (va < vb) return sortDir === 'asc' ? -1 : 1;
if (va > vb) return sortDir === 'asc' ? 1 : -1;
return 0;
});
tbody.innerHTML = sorted.map(t => {
const initials = t.customer.split(' ').map(n => n[0]).join('');
const colors = ['#10b981','#3b82f6','#f59e0b','#8b5cf6','#ec4899','#ef4444'];
const color = colors[Math.abs(initials.charCodeAt(0) + initials.charCodeAt(1)) % colors.length];
return `<tr>
<td style="font-weight:600;color:var(--gray-700);font-size:12px">${t.id}</td>
<td><div class="customer-cell">
<div class="customer-avatar" style="background:${color}">${initials}</div>
<div><div style="font-weight:500">${t.customer}</div><div style="font-size:11px;color:var(--gray-400)">${t.email}</div></div>
</div></td>
<td class="amount">$${t.amount.toLocaleString()}</td>
<td><span class="status-badge ${t.status}"><span class="status-dot"></span>${t.status.charAt(0).toUpperCase() + t.status.slice(1)}</span></td>
<td style="color:var(--gray-500)">${formatDate(t.date)}</td>
</tr>`;
}).join('');
}
function formatDate(dateStr) {
const d = new Date(dateStr);
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}
function formatNum(n) {
if (n >= 10000) return (n / 1000).toFixed(1) + 'k';
if (n >= 1000) return n.toLocaleString();
return n.toString();
}
// Sort headers
document.querySelectorAll('th[data-sort]').forEach(th => {
th.addEventListener('click', () => {
const col = th.dataset.sort;
if (sortCol === col) sortDir = sortDir === 'asc' ? 'desc' : 'asc';
else { sortCol = col; sortDir = 'asc'; }
document.querySelectorAll('th').forEach(h => { h.classList.remove('sorted'); h.querySelector('.sort-icon').textContent = '↕'; });
th.classList.add('sorted');
th.querySelector('.sort-icon').textContent = sortDir === 'asc' ? '↑' : '↓';
buildTable();
});
});
// ===== INIT =====
buildKPIs();
buildLineChart();
buildDonutChart();
buildTable();
// Rebuild charts on resize
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
buildLineChart();
buildDonutChart();
}, 200);
});
</script>
</body>
</html>