| <!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> |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
| background-color: #f5f5f5; |
| color: #1f2937; |
| } |
| |
| .container { |
| display: flex; |
| min-height: 100vh; |
| } |
| |
| |
| .sidebar { |
| width: 250px; |
| background-color: #fff; |
| border-right: 1px solid #e5e7eb; |
| display: flex; |
| flex-direction: column; |
| position: fixed; |
| height: 100vh; |
| z-index: 100; |
| } |
| |
| .workspace-switcher { |
| padding: 16px; |
| border-bottom: 1px solid #e5e7eb; |
| } |
| |
| .workspace-select { |
| width: 100%; |
| padding: 8px 12px; |
| border: 1px solid #d1d5db; |
| border-radius: 6px; |
| background-color: #fff; |
| font-size: 14px; |
| cursor: pointer; |
| } |
| |
| .nav-menu { |
| padding: 16px; |
| flex: 1; |
| } |
| |
| .nav-item { |
| display: flex; |
| align-items: center; |
| padding: 10px 12px; |
| margin-bottom: 4px; |
| border-radius: 6px; |
| cursor: pointer; |
| transition: background-color 0.2s; |
| } |
| |
| .nav-item:hover { |
| background-color: #f3f4f6; |
| } |
| |
| .nav-item.active { |
| background-color: #ecfdf5; |
| color: #059669; |
| } |
| |
| .nav-icon { |
| width: 20px; |
| height: 20px; |
| margin-right: 12px; |
| } |
| |
| .nav-text { |
| font-size: 14px; |
| } |
| |
| .user-profile { |
| padding: 16px; |
| border-top: 1px solid #e5e7eb; |
| display: flex; |
| align-items: center; |
| } |
| |
| .avatar { |
| width: 32px; |
| height: 32px; |
| border-radius: 50%; |
| background-color: #10b981; |
| color: white; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-weight: bold; |
| margin-right: 12px; |
| } |
| |
| .user-info { |
| font-size: 14px; |
| } |
| |
| .user-name { |
| font-weight: 500; |
| } |
| |
| .user-email { |
| color: #6b7280; |
| font-size: 12px; |
| } |
| |
| |
| .main-content { |
| flex: 1; |
| margin-left: 250px; |
| } |
| |
| .topbar { |
| background-color: #fff; |
| border-bottom: 1px solid #e5e7eb; |
| padding: 16px 24px; |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| position: sticky; |
| top: 0; |
| z-index: 90; |
| } |
| |
| .search-container { |
| position: relative; |
| } |
| |
| .search-input { |
| padding: 8px 12px 8px 36px; |
| border: 1px solid #d1d5db; |
| border-radius: 6px; |
| width: 300px; |
| font-size: 14px; |
| } |
| |
| .search-icon { |
| position: absolute; |
| left: 12px; |
| top: 50%; |
| transform: translateY(-50%); |
| color: #6b7280; |
| } |
| |
| .date-range { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| } |
| |
| .date-input { |
| padding: 8px 12px; |
| border: 1px solid #d1d5db; |
| border-radius: 6px; |
| font-size: 14px; |
| } |
| |
| .notifications { |
| position: relative; |
| cursor: pointer; |
| } |
| |
| .notification-dot { |
| position: absolute; |
| top: -2px; |
| right: -2px; |
| width: 8px; |
| height: 8px; |
| background-color: #ef4444; |
| border-radius: 50%; |
| } |
| |
| .content { |
| padding: 24px; |
| } |
| |
| .kpi-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); |
| gap: 24px; |
| margin-bottom: 32px; |
| } |
| |
| .kpi-card { |
| background-color: #fff; |
| border-radius: 8px; |
| padding: 20px; |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); |
| } |
| |
| .kpi-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 8px; |
| } |
| |
| .kpi-title { |
| font-size: 14px; |
| color: #6b7280; |
| } |
| |
| .kpi-value { |
| font-size: 24px; |
| font-weight: 600; |
| } |
| |
| .kpi-change { |
| font-size: 12px; |
| margin-top: 4px; |
| } |
| |
| .kpi-change.positive { |
| color: #10b981; |
| } |
| |
| .kpi-change.negative { |
| color: #ef4444; |
| } |
| |
| .charts-container { |
| display: grid; |
| grid-template-columns: 2fr 1fr; |
| gap: 24px; |
| margin-bottom: 32px; |
| } |
| |
| .chart-card { |
| background-color: #fff; |
| border-radius: 8px; |
| padding: 24px; |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); |
| } |
| |
| .table-card { |
| background-color: #fff; |
| border-radius: 8px; |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); |
| overflow: hidden; |
| } |
| |
| .table-header { |
| padding: 16px 24px; |
| border-bottom: 1px solid #e5e7eb; |
| } |
| |
| .table-title { |
| font-size: 16px; |
| font-weight: 500; |
| } |
| |
| .data-table { |
| width: 100%; |
| border-collapse: collapse; |
| } |
| |
| .data-table th, |
| .data-table td { |
| padding: 12px 24px; |
| text-align: left; |
| border-bottom: 1px solid #e5e7eb; |
| } |
| |
| .data-table th { |
| background-color: #f9fafb; |
| font-weight: 500; |
| cursor: pointer; |
| } |
| |
| .data-table tr:hover { |
| background-color: #f9fafb; |
| } |
| |
| .status { |
| padding: 4px 8px; |
| border-radius: 4px; |
| font-size: 12px; |
| font-weight: 500; |
| } |
| |
| .status.completed { |
| background-color: #ecfdf5; |
| color: #059669; |
| } |
| |
| .status.pending { |
| background-color: #fef3c7; |
| color: #d97706; |
| } |
| |
| .status.failed { |
| background-color: #fef2f2; |
| color: #dc2626; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <aside class="sidebar"> |
| <div class="workspace-switcher"> |
| <select class="workspace-select" id="workspaceSelect"> |
| <option value="acme">Acme Corp</option> |
| <option value="globex">Globex Inc</option> |
| <option value="initech">Initech</option> |
| </select> |
| </div> |
| <nav class="nav-menu"> |
| <div class="nav-item active"> |
| <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/> |
| </svg> |
| <span class="nav-text">Dashboard</span> |
| </div> |
| <div class="nav-item"> |
| <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/> |
| </svg> |
| <span class="nav-text">Analytics</span> |
| </div> |
| <div class="nav-item"> |
| <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"></path> |
| <circle cx="9" cy="7" r="4"></circle> |
| <path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"></path> |
| </svg> |
| <span class="nav-text">Customers</span> |
| </div> |
| <div class="nav-item"> |
| <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/> |
| </svg> |
| <span class="nav-text">Reports</span> |
| </div> |
| <div class="nav-item"> |
| <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/> |
| </svg> |
| <span class="nav-text">Settings</span> |
| </div> |
| <div class="nav-item"> |
| <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/> |
| </svg> |
| <span class="nav-text">Integrations</span> |
| </div> |
| </nav> |
| <div class="user-profile"> |
| <div class="avatar">JS</div> |
| <div class="user-info"> |
| <div class="user-name">Jane Smith</div> |
| <div class="user-email">jane.smith@acmecorp.com</div> |
| </div> |
| </div> |
| </aside> |
|
|
| <main class="main-content"> |
| <header class="topbar"> |
| <div class="search-container"> |
| <svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <circle cx="11" cy="11" r="8"></circle> |
| <line x1="21" y1="21" x2="16.65" y2="16.65"></line> |
| </svg> |
| <input type="text" class="search-input" placeholder="Search anything..."> |
| </div> |
| <div class="date-range"> |
| <input type="date" class="date-input" value="2024-01-01"> |
| <span>to</span> |
| <input type="date" class="date-input" value="2024-01-31"> |
| </div> |
| <div class="notifications"> |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"></path> |
| <path d="M13.73 21a2 2 0 01-3.46 0"></path> |
| </svg> |
| <span class="notification-dot"></span> |
| </div> |
| </header> |
|
|
| <div class="content"> |
| <div class="kpi-grid"> |
| <div class="kpi-card"> |
| <div class="kpi-header"> |
| <span class="kpi-title">Revenue</span> |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2"> |
| <polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline> |
| <polyline points="17 6 23 6 23 12"></polyline> |
| </svg> |
| </div> |
| <div class="kpi-value">$124,500</div> |
| <div class="kpi-change positive">+12% from last month</div> |
| </div> |
| <div class="kpi-card"> |
| <div class="kpi-header"> |
| <span class="kpi-title">Active Users</span> |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2"> |
| <polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline> |
| <polyline points="17 6 23 6 23 12"></polyline> |
| </svg> |
| </div> |
| <div class="kpi-value">8,234</div> |
| <div class="kpi-change positive">+5% from last month</div> |
| </div> |
| <div class="kpi-card"> |
| <div class="kpi-header"> |
| <span class="kpi-title">Conversion Rate</span> |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2"> |
| <polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline> |
| <polyline points="17 6 23 6 23 12"></polyline> |
| </svg> |
| </div> |
| <div class="kpi-value">3.2%</div> |
| <div class="kpi-change positive">+0.4% from last month</div> |
| </div> |
| <div class="kpi-card"> |
| <div class="kpi-header"> |
| <span class="kpi-title">Churn Rate</span> |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2"> |
| <polyline points="23 18 13.5 8.5 8.5 13.5 1 6"></polyline> |
| <polyline points="17 18 23 18 23 12"></polyline> |
| </svg> |
| </div> |
| <div class="kpi-value">2.1%</div> |
| <div class="kpi-change negative">-0.3% from last month</div> |
| </div> |
| </div> |
|
|
| <div class="charts-container"> |
| <div class="chart-card"> |
| <h3>Weekly Active Users</h3> |
| <svg width="100%" height="200" id="lineChart"></svg> |
| </div> |
| <div class="chart-card"> |
| <h3>Traffic Sources</h3> |
| <svg width="200" height="200" id="donutChart"></svg> |
| </div> |
| </div> |
|
|
| <div class="table-card"> |
| <div class="table-header"> |
| <h3 class="table-title">Recent Transactions</h3> |
| </div> |
| <table class="data-table" id="transactionsTable"> |
| <thead> |
| <tr> |
| <th>User</th> |
| <th>Amount</th> |
| <th>Date</th> |
| <th>Status</th> |
| </tr> |
| </thead> |
| <tbody> |
| <tr> |
| <td>John Doe</td> |
| <td>$1,250.00</td> |
| <td>2024-01-28</td> |
| <td><span class="status completed">Completed</span></td> |
| </tr> |
| <tr> |
| <td>Jane Smith</td> |
| <td>$875.00</td> |
| <td>2024-01-27</td> |
| <td><span class="status completed">Completed</span></td> |
| </tr> |
| <tr> |
| <td>Mike Johnson</td> |
| <td>$2,100.00</td> |
| <td>2024-01-26</td> |
| <td><span class="status pending">Pending</span></td> |
| </tr> |
| <tr> |
| <td>Sarah Wilson</td> |
| <td>$450.00</td> |
| <td>2024-01-25</td> |
| <td><span class="status completed">Completed</span></td> |
| </tr> |
| <tr> |
| <td>David Brown</td> |
| <td>$1,800.00</td> |
| <td>2024-01-24</td> |
| <td><span class="status failed">Failed</span></td> |
| </tr> |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </main> |
| </div> |
|
|
| <script> |
| |
| const revenueData = [100, 120, 115, 130, 125, 140, 135, 150, 145, 160, 155, 170]; |
| const usersData = [80, 85, 82, 88, 86, 90, 87, 92, 89, 95, 91, 96]; |
| const conversionData = [2.5, 2.8, 3.1, 3.0, 3.2, 3.1, 3.3, 3.2, 3.4, 3.3, 3.5, 3.2]; |
| const churnData = [2.5, 2.4, 2.3, 2.2, 2.1, 2.0, 2.1, 2.0, 1.9, 2.0, 1.9, 2.1]; |
| |
| |
| function createSparkline(data, color) { |
| const width = 120; |
| const height = 40; |
| const padding = 5; |
| const maxValue = Math.max(...data); |
| const minValue = Math.min(...data); |
| const range = maxValue - minValue || 1; |
| |
| const points = data.map((value, index) => { |
| const x = padding + (index / (data.length - 1)) * (width - 2 * padding); |
| const y = height - padding - ((value - minValue) / range) * (height - 2 * padding); |
| return `${x},${y}`; |
| }); |
| |
| const path = `M${points.join(' L')}`; |
| |
| return ` |
| <svg width="${width}" height="${height}"> |
| <path d="${path}" fill="none" stroke="${color}" stroke-width="2"/> |
| </svg> |
| `; |
| } |
| |
| |
| document.querySelectorAll('.kpi-card').forEach((card, index) => { |
| const data = [revenueData, usersData, conversionData, churnData][index]; |
| const color = index === 3 ? '#ef4444' : '#10b981'; |
| card.innerHTML += createSparkline(data, color); |
| }); |
| |
| |
| const weeklyActiveUsers = [ |
| { week: 'Week 1', users: 1200 }, |
| { week: 'Week 2', users: 1350 }, |
| { week: 'Week 3', users: 1100 }, |
| { week: 'Week 4', users: 1450 }, |
| { week: 'Week 5', users: 1300 }, |
| { week: 'Week 6', users: 1500 }, |
| { week: 'Week 7', users: 1250 }, |
| { week: 'Week 8', users: 1400 }, |
| { week: 'Week 9', users: 1350 }, |
| { week: 'Week 10', users: 1550 }, |
| { week: 'Week 11', users: 1200 }, |
| { week: 'Week 12', users: 1600 } |
| ]; |
| |
| |
| function createLineChart() { |
| const svg = document.getElementById('lineChart'); |
| const width = 600; |
| const height = 200; |
| const padding = { top: 20, right: 20, bottom: 30, left: 40 }; |
| |
| const maxValue = Math.max(...weeklyActiveUsers.map(d => d.users)); |
| const minValue = Math.min(...weeklyActiveUsers.map(d => d.users)); |
| const range = maxValue - minValue || 1; |
| |
| const points = weeklyActiveUsers.map((data, index) => { |
| const x = padding.left + (index / (weeklyActiveUsers.length - 1)) * (width - padding.left - padding.right); |
| const y = height - padding.bottom - ((data.users - minValue) / range) * (height - padding.top - padding.bottom); |
| return { x, y, data }; |
| }); |
| |
| const path = `M${points.map(p => `${p.x},${p.y}`).join(' L')}`; |
| |
| svg.innerHTML = ` |
| <path d="${path}" fill="none" stroke="#10b981" stroke-width="2"/> |
| ${points.map(p => ` |
| <circle cx="${p.x}" cy="${p.y}" r="3" fill="#10b981"/> |
| <text x="${p.x}" y="${p.y + 20}" text-anchor="middle" font-size="10">${p.data.week}</text> |
| `).join('')} |
| `; |
| } |
| |
| createLineChart(); |
| |
| |
| const trafficSources = [ |
| { name: 'Organic', percentage: 45 }, |
| { name: 'Paid', percentage: 30 }, |
| { name: 'Social', percentage: 15 }, |
| { name: 'Direct', percentage: 10 } |
| ]; |
| |
| |
| function createDonutChart() { |
| const svg = document.getElementById('donutChart'); |
| const width = 200; |
| const height = 200; |
| const radius = 80; |
| const innerRadius = 60; |
| |
| let cumulativePercentage = 0; |
| |
| svg.innerHTML = trafficSources.map((source, index) => { |
| const startAngle = (cumulativePercentage * 3.6) * Math.PI / 180; |
| const endAngle = (cumulativePercentage + source.percentage) * 3.6 * Math.PI / 180; |
| cumulativePercentage += source.percentage; |
| |
| const x1 = width / 2 + radius * Math.cos(startAngle); |
| const y1 = height / 2 + radius * Math.sin(startAngle); |
| const x2 = width / 2 + radius * Math.cos(endAngle); |
| const y2 = height / 2 + radius * Math.sin(endAngle); |
| |
| const largeArc = source.percentage > 50 ? 1 : 0; |
| |
| return ` |
| <path d="M ${width / 2} ${height / 2} L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} Z" |
| fill="#10b981" fill-opacity="0.7"/> |
| <text x="${(width / 2 + x1 + x2) / 2}" y="${(height / 2 + y1 + y2) / 2}" |
| text-anchor="middle" font-size="12" fill="white">${source.name}</text> |
| `; |
| }).join(''); |
| } |
| |
| createDonutChart(); |
| |
| |
| const table = document.getElementById('transactionsTable'); |
| const headers = table.querySelectorAll('th'); |
| |
| headers.forEach((header, index) => { |
| header.addEventListener('click', () => { |
| const rows = Array.from(table.querySelectorAll('tbody tr')); |
| const isAscending = header.classList.contains('ascending'); |
| const sortIndex = index; |
| |
| rows.sort((a, b) => { |
| const aValue = a.cells[sortIndex].textContent.trim(); |
| const bValue = b.cells[sortIndex].textContent.trim(); |
| const numericA = parseFloat(aValue.replace(/[^0-9.-]/g, '')); |
| const numericB = parseFloat(bValue.replace(/[^0-9.-]/g, '')); |
| |
| if (!isNaN(numericA) && !isNaN(numericB)) { |
| return isAscending ? numericA - numericB : numericB - numericA; |
| } else { |
| return isAscending ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue); |
| } |
| }); |
| |
| rows.forEach(row => table.querySelector('tbody').appendChild(row)); |
| header.classList.toggle('ascending'); |
| }); |
| }); |
| </script> |
| </body> |
| </html> |