Download design_analytics_dashboard.html from developerjeremylive/qwopus36-35b-a3b-eval-etheroi: direct link, hf CLI and curl.
- Browser
- Download file 37.5 kB
-
https://huggingface.co/spaces/developerjeremylive/qwopus36-35b-a3b-eval-etheroi/resolve/main/design_analytics_dashboard.html
- Command line
-
hf download hf://spaces/developerjeremylive/qwopus36-35b-a3b-eval-etheroi/design_analytics_dashboard.html
-
curl -L -o design_analytics_dashboard.html https://huggingface.co/spaces/developerjeremylive/qwopus36-35b-a3b-eval-etheroi/resolve/main/design_analytics_dashboard.html
37.5 kB
| <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> |