Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>User Analytics Dashboard</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet"> | |
| <script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> | |
| <style> | |
| .dashboard-card { | |
| transition: all 0.3s ease; | |
| } | |
| .dashboard-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 10px 20px rgba(0,0,0,0.1); | |
| } | |
| .file-upload { | |
| border: 2px dashed #cbd5e0; | |
| transition: all 0.3s ease; | |
| } | |
| .file-upload:hover { | |
| border-color: #4f46e5; | |
| } | |
| .data-table { | |
| max-height: 400px; | |
| overflow-y: auto; | |
| } | |
| .heatmap-container { | |
| height: 400px; | |
| } | |
| @media (max-width: 768px) { | |
| .dashboard-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50"> | |
| <div id="app" class="min-h-screen"> | |
| <!-- Navigation --> | |
| <nav class="bg-indigo-600 text-white shadow-lg"> | |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | |
| <div class="flex justify-between h-16 items-center"> | |
| <div class="flex items-center"> | |
| <i data-feather="bar-chart-2" class="h-6 w-6 mr-2"></i> | |
| <span class="text-xl font-semibold">User Analytics</span> | |
| </div> | |
| <div class="hidden md:block"> | |
| <div class="ml-10 flex items-baseline space-x-4"> | |
| <a href="#" class="px-3 py-2 rounded-md text-sm font-medium bg-indigo-700">Dashboard</a> | |
| <a href="#" class="px-3 py-2 rounded-md text-sm font-medium hover:bg-indigo-500">Reports</a> | |
| <a href="#" class="px-3 py-2 rounded-md text-sm font-medium hover:bg-indigo-500">Settings</a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </nav> | |
| <!-- Main Content --> | |
| <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> | |
| <!-- Upload Section --> | |
| <div class="mb-8 bg-white rounded-lg shadow p-6" data-aos="fade-up"> | |
| <div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6"> | |
| <h2 class="text-2xl font-bold text-gray-800">Upload User Data</h2> | |
| <div class="mt-4 md:mt-0"> | |
| <button id="sampleDataBtn" class="mr-3 px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50"> | |
| Load Sample Data | |
| </button> | |
| <button id="exportBtn" class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700"> | |
| Export Data | |
| </button> | |
| </div> | |
| </div> | |
| <div class="file-upload p-8 rounded-lg text-center"> | |
| <div class="flex flex-col items-center justify-center"> | |
| <i data-feather="upload-cloud" class="h-12 w-12 text-indigo-500 mb-3"></i> | |
| <p class="text-lg font-medium text-gray-700">Drag & drop your CSV file here</p> | |
| <p class="text-sm text-gray-500 mb-4">or</p> | |
| <label for="file-upload" class="cursor-pointer px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700"> | |
| Browse Files | |
| </label> | |
| <input id="file-upload" type="file" accept=".csv" class="hidden"> | |
| </div> | |
| </div> | |
| <div id="uploadPreview" class="mt-6 hidden"> | |
| <h3 class="text-lg font-medium text-gray-900 mb-4">Preview</h3> | |
| <div class="data-table overflow-x-auto"> | |
| <table class="min-w-full divide-y divide-gray-200"> | |
| <thead class="bg-gray-50"> | |
| <tr> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Email</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Verified</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Country</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Birth Date</th> | |
| </tr> | |
| </thead> | |
| <tbody id="previewTableBody" class="bg-white divide-y divide-gray-200"> | |
| <!-- Data will be inserted here --> | |
| </tbody> | |
| </table> | |
| </div> | |
| <div class="mt-4 flex justify-end"> | |
| <button id="confirmUpload" class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700"> | |
| Confirm Upload | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Dashboard Cards --> | |
| <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"> | |
| <!-- Total Users Card --> | |
| <div class="dashboard-card bg-white rounded-lg shadow p-6" data-aos="fade-up"> | |
| <div class="flex items-center"> | |
| <div class="p-3 rounded-full bg-indigo-100 text-indigo-600 mr-4"> | |
| <i data-feather="users" class="h-6 w-6"></i> | |
| </div> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">Total Users</p> | |
| <p id="totalUsers" class="text-2xl font-semibold text-gray-800">0</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- New Users (7 days) Card --> | |
| <div class="dashboard-card bg-white rounded-lg shadow p-6" data-aos="fade-up" data-aos-delay="100"> | |
| <div class="flex items-center"> | |
| <div class="p-3 rounded-full bg-green-100 text-green-600 mr-4"> | |
| <i data-feather="user-plus" class="h-6 w-6"></i> | |
| </div> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">New Users (7d)</p> | |
| <div class="flex items-baseline"> | |
| <p id="newUsers7d" class="text-2xl font-semibold text-gray-800">0</p> | |
| <span id="growth7d" class="ml-2 text-sm font-medium text-green-600 flex items-center"> | |
| <i data-feather="trending-up" class="h-4 w-4 mr-1"></i> | |
| <span>0%</span> | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Top Country Card --> | |
| <div class="dashboard-card bg-white rounded-lg shadow p-6" data-aos="fade-up" data-aos-delay="200"> | |
| <div class="flex items-center"> | |
| <div class="p-3 rounded-full bg-blue-100 text-blue-600 mr-4"> | |
| <i data-feather="globe" class="h-6 w-6"></i> | |
| </div> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">Top Country</p> | |
| <p id="topCountry" class="text-2xl font-semibold text-gray-800">-</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Verified Users Card --> | |
| <div class="dashboard-card bg-white rounded-lg shadow p-6" data-aos="fade-up" data-aos-delay="300"> | |
| <div class="flex items-center"> | |
| <div class="p-3 rounded-full bg-purple-100 text-purple-600 mr-4"> | |
| <i data-feather="check-circle" class="h-6 w-6"></i> | |
| </div> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">Verified Users</p> | |
| <p id="verifiedUsers" class="text-2xl font-semibold text-gray-800">0%</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Date Range Filter --> | |
| <div class="bg-white rounded-lg shadow p-6 mb-8" data-aos="fade-up"> | |
| <h2 class="text-xl font-bold text-gray-800 mb-6">Date Range Analysis</h2> | |
| <div class="flex flex-col md:flex-row md:items-center md:space-x-4 space-y-4 md:space-y-0"> | |
| <div class="flex-1"> | |
| <label for="startDate" class="block text-sm font-medium text-gray-700 mb-1">Start Date</label> | |
| <input type="date" id="startDate" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"> | |
| </div> | |
| <div class="flex-1"> | |
| <label for="endDate" class="block text-sm font-medium text-gray-700 mb-1">End Date</label> | |
| <input type="date" id="endDate" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"> | |
| </div> | |
| <div class="flex items-end"> | |
| <button id="applyDateFilter" class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700"> | |
| Apply Filter | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Charts Section --> | |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8"> | |
| <!-- Growth Chart --> | |
| <div class="bg-white rounded-lg shadow p-6" data-aos="fade-up"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h2 class="text-xl font-bold text-gray-800">User Growth</h2> | |
| <div class="relative"> | |
| <select id="growthPeriod" class="appearance-none bg-white border border-gray-300 rounded-md pl-3 pr-8 py-2 text-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"> | |
| <option value="7">Last 7 days</option> | |
| <option value="14">Last 14 days</option> | |
| <option value="30">Last 30 days</option> | |
| <option value="60">Last 60 days</option> | |
| </select> | |
| <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"> | |
| <i data-feather="chevron-down" class="h-4 w-4"></i> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="h-80"> | |
| <canvas id="growthChart"></canvas> | |
| </div> | |
| </div> | |
| <!-- Country Distribution Chart --> | |
| <div class="bg-white rounded-lg shadow p-6" data-aos="fade-up" data-aos-delay="100"> | |
| <h2 class="text-xl font-bold text-gray-800 mb-6">Country Distribution</h2> | |
| <div class="h-80"> | |
| <canvas id="countryChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Data Tables Section --> | |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8"> | |
| <!-- User Data Table --> | |
| <div class="bg-white rounded-lg shadow overflow-hidden" data-aos="fade-up"> | |
| <div class="px-6 py-4 border-b border-gray-200"> | |
| <h2 class="text-xl font-bold text-gray-800">User Data</h2> | |
| </div> | |
| <div class="p-4"> | |
| <div class="mb-4 flex justify-between items-center"> | |
| <div class="relative w-64"> | |
| <input type="text" id="searchUsers" placeholder="Search users..." class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"> | |
| <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> | |
| <i data-feather="search" class="h-4 w-4 text-gray-400"></i> | |
| </div> | |
| </div> | |
| <div class="relative"> | |
| <select id="filterCountry" class="appearance-none bg-white border border-gray-300 rounded-md pl-3 pr-8 py-2 text-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"> | |
| <option value="">All Countries</option> | |
| <!-- Countries will be added dynamically --> | |
| </select> | |
| <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"> | |
| <i data-feather="chevron-down" class="h-4 w-4"></i> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="data-table overflow-x-auto"> | |
| <table class="min-w-full divide-y divide-gray-200"> | |
| <thead class="bg-gray-50"> | |
| <tr> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Email</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Country</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Verified</th> | |
| </tr> | |
| </thead> | |
| <tbody id="userTableBody" class="bg-white divide-y divide-gray-200"> | |
| <!-- User data will be inserted here --> | |
| </tbody> | |
| </table> | |
| </div> | |
| <div class="mt-4 flex justify-between items-center"> | |
| <div class="text-sm text-gray-500"> | |
| Showing <span id="startItem">1</span> to <span id="endItem">10</span> of <span id="totalItems">0</span> users | |
| </div> | |
| <div class="flex space-x-2"> | |
| <button id="prevPage" class="px-3 py-1 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"> | |
| Previous | |
| </button> | |
| <button id="nextPage" class="px-3 py-1 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"> | |
| Next | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Country Comparison Table --> | |
| <div class="bg-white rounded-lg shadow overflow-hidden" data-aos="fade-up" data-aos-delay="100"> | |
| <div class="px-6 py-4 border-b border-gray-200"> | |
| <h2 class="text-xl font-bold text-gray-800">Country Comparison</h2> | |
| </div> | |
| <div class="p-4"> | |
| <div class="data-table overflow-x-auto"> | |
| <table class="min-w-full divide-y divide-gray-200"> | |
| <thead class="bg-gray-50"> | |
| <tr> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Country</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total Users</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last 7d</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Growth</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Verified %</th> | |
| </tr> | |
| </thead> | |
| <tbody id="countryTableBody" class="bg-white divide-y divide-gray-200"> | |
| <!-- Country data will be inserted here --> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Heatmap Section --> | |
| <div class="bg-white rounded-lg shadow p-6 mb-8" data-aos="fade-up"> | |
| <h2 class="text-xl font-bold text-gray-800 mb-6">User Density Heatmap</h2> | |
| <div class="heatmap-container bg-gray-100 rounded-lg flex items-center justify-center"> | |
| <p class="text-gray-500">World map visualization would appear here with actual implementation</p> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Footer --> | |
| <footer class="bg-gray-100 border-t border-gray-200"> | |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> | |
| <div class="flex flex-col md:flex-row justify-between items-center"> | |
| <p class="text-sm text-gray-500">© 2023 User Analytics Dashboard. All rights reserved.</p> | |
| <div class="mt-4 md:mt-0 flex space-x-6"> | |
| <a href="#" class="text-gray-400 hover:text-gray-500"> | |
| <i data-feather="github" class="h-5 w-5"></i> | |
| </a> | |
| <a href="#" class="text-gray-400 hover:text-gray-500"> | |
| <i data-feather="twitter" class="h-5 w-5"></i> | |
| </a> | |
| <a href="#" class="text-gray-400 hover:text-gray-500"> | |
| <i data-feather="linkedin" class="h-5 w-5"></i> | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| </footer> | |
| </div> | |
| <script> | |
| // Initialize AOS animations | |
| AOS.init({ | |
| duration: 800, | |
| easing: 'ease-in-out', | |
| once: true | |
| }); | |
| // Initialize feather icons | |
| feather.replace(); | |
| // Sample data for demonstration | |
| const sampleData = [ | |
| { | |
| "Date": "Wednesday, September 24, 2025 at 3:13:40 AM UTC", | |
| "Name": "BGC", | |
| "Email": "2640770072937555@facebook.com", | |
| "Email verified": "✓", | |
| "Country": "Philippines", | |
| "Birth date": "25-Sep-03" | |
| }, | |
| { | |
| "Date": "Wednesday, September 24, 2025 at 3:13:37 AM UTC", | |
| "Name": "Khairul12", | |
| "Email": "shsnjzhzhshdb@gmail.com", | |
| "Email verified": "✓", | |
| "Country": "Andorra", | |
| "Birth date": "12-Jun-08" | |
| }, | |
| { | |
| "Date": "Wednesday, September 24, 2025 at 2:35:53 AM UTC", | |
| "Name": "yo_farah", | |
| "Email": "arzadonjaiii09@gmail.com", | |
| "Email verified": "X", | |
| "Country": "Philippines", | |
| "Birth date": "24-Sep-07" | |
| }, | |
| { | |
| "Date": "Wednesday, September 24, 2025 at 2:24:28 AM UTC", | |
| "Name": "Trespuertes", | |
| "Email": "1330019208758711@facebook.com", | |
| "Email verified": "✓", | |
| "Country": "Andorra", | |
| "Birth date": "24-Sep-25" | |
| }, | |
| { | |
| "Date": "Wednesday, September 24, 2025 at 2:18:26 AM UTC", | |
| "Name": "M4nghsu", | |
| "Email": "seanjohndelacruz211@gmail.com", | |
| "Email verified": "✓", | |
| "Country": "Andorra", | |
| "Birth date": "22-Jan-02" | |
| }, | |
| { | |
| "Date": "Wednesday, September 24, 2025 at 2:15:54 AM UTC", | |
| "Name": "ê§â˜†â˜¬â„œifaτ☬☆꧂", | |
| "Email": "omiethasanripat@gmail.com", | |
| "Email verified": "✓", | |
| "Country": "Bahamas", | |
| "Birth date": "24-Sep-25" | |
| }, | |
| { | |
| "Date": "Wednesday, September 24, 2025 at 1:46:38 AM UTC", | |
| "Name": "go minsi", | |
| "Email": "jamireponce15@gmail.com", | |
| "Email verified": "✓", | |
| "Country": "Philippines", | |
| "Birth date": "18-Oct-09" | |
| }, | |
| { | |
| "Date": "Wednesday, September 24, 2025 at 1:29:18 AM UTC", | |
| "Name": "kusakabe", | |
| "Email": "puertollanocarl@gmail.com", | |
| "Email verified": "X", | |
| "Country": "Philippines", | |
| "Birth date": "15-Jul-07" | |
| }, | |
| { | |
| "Date": "Wednesday, September 24, 2025 at 1:27:23 AM UTC", | |
| "Name": "shoooo", | |
| "Email": "kkazekazekaze@gmail.com", | |
| "Email verified": "✓", | |
| "Country": "Philippines", | |
| "Birth date": "15-Jul-07" | |
| }, | |
| { | |
| "Date": "Wednesday, September 24, 2025 at 1:22:57 AM UTC", | |
| "Name": "EveresT", | |
| "Email": "linfordlora11@gmail.com", | |
| "Email verified": "✓", | |
| "Country": "Andorra", | |
| "Birth date": "11-Aug-07" | |
| } | |
| ]; | |
| // Global variables | |
| let userData = []; | |
| let currentPage = 1; | |
| const itemsPerPage = 10; | |
| // DOM elements | |
| const fileUploadInput = document.getElementById('file-upload'); | |
| const uploadPreview = document.getElementById('uploadPreview'); | |
| const previewTableBody = document.getElementById('previewTableBody'); | |
| const confirmUploadBtn = document.getElementById('confirmUpload'); | |
| const sampleDataBtn = document.getElementById('sampleDataBtn'); | |
| const exportBtn = document.getElementById('exportBtn'); | |
| const totalUsersEl = document.getElementById('totalUsers'); | |
| const newUsers7dEl = document.getElementById('newUsers7d'); | |
| const growth7dEl = document.getElementById('growth7d'); | |
| const topCountryEl = document.getElementById('topCountry'); | |
| const verifiedUsersEl = document.getElementById('verifiedUsers'); | |
| const startDateInput = document.getElementById('startDate'); | |
| const endDateInput = document.getElementById('endDate'); | |
| const applyDateFilterBtn = document.getElementById('applyDateFilter'); | |
| const growthPeriodSelect = document.getElementById('growthPeriod'); | |
| const searchUsersInput = document.getElementById('searchUsers'); | |
| const filterCountrySelect = document.getElementById('filterCountry'); | |
| const userTableBody = document.getElementById('userTableBody'); | |
| const countryTableBody = document.getElementById('countryTableBody'); | |
| const prevPageBtn = document.getElementById('prevPage'); | |
| const nextPageBtn = document.getElementById('nextPage'); | |
| const startItemEl = document.getElementById('startItem'); | |
| const endItemEl = document.getElementById('endItem'); | |
| const totalItemsEl = document.getElementById('totalItems'); | |
| const growthChartCtx = document.getElementById('growthChart').getContext('2d'); | |
| const countryChartCtx = document.getElementById('countryChart').getContext('2d'); | |
| // Charts | |
| let growthChart, countryChart; | |
| // Event listeners | |
| fileUploadInput.addEventListener('change', handleFileUpload); | |
| confirmUploadBtn.addEventListener('click', confirmUpload); | |
| sampleDataBtn.addEventListener('click', loadSampleData); | |
| exportBtn.addEventListener('click', exportData); | |
| applyDateFilterBtn.addEventListener('click', applyDateFilter); | |
| growthPeriodSelect.addEventListener('change', updateGrowthChart); | |
| searchUsersInput.addEventListener('input', filterUsers); | |
| filterCountrySelect.addEventListener('change', filterUsers); | |
| prevPageBtn.addEventListener('click', goToPrevPage); | |
| nextPageBtn.addEventListener('click', goToNextPage); | |
| // Initialize date inputs with default values | |
| function initDateInputs() { | |
| const today = new Date(); | |
| const sevenDaysAgo = new Date(); | |
| sevenDaysAgo.setDate(today.getDate() - 7); | |
| startDateInput.valueAsDate = sevenDaysAgo; | |
| endDateInput.valueAsDate = today; | |
| } | |
| // Handle file upload | |
| function handleFileUpload(event) { | |
| const file = event.target.files[0]; | |
| if (!file) return; | |
| Papa.parse(file, { | |
| header: true, | |
| complete: function(results) { | |
| if (results.data.length > 0) { | |
| userData = results.data; | |
| showUploadPreview(userData.slice(0, 5)); // Show first 5 rows as preview | |
| } | |
| }, | |
| error: function(error) { | |
| console.error("Error parsing CSV:", error); | |
| alert("Error parsing CSV file. Please check the file format."); | |
| } | |
| }); | |
| } | |
| // Show upload preview | |
| function showUploadPreview(data) { | |
| previewTableBody.innerHTML = ''; | |
| data.forEach(row => { | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = ` | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${row.Date || '-'}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${row.Name || '-'}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${row.Email || '-'}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${row['Email verified'] || '-'}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${row.Country || '-'}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${row['Birth date'] || '-'}</td> | |
| `; | |
| previewTableBody.appendChild(tr); | |
| }); | |
| uploadPreview.classList.remove('hidden'); | |
| } | |
| // Confirm upload and update dashboard | |
| function confirmUpload() { | |
| updateDashboard(); | |
| uploadPreview.classList.add('hidden'); | |
| fileUploadInput.value = ''; | |
| } | |
| // Load sample data | |
| function loadSampleData() { | |
| userData = sampleData; | |
| updateDashboard(); | |
| } | |
| // Export data | |
| function exportData() { | |
| if (userData.length === 0) { | |
| alert("No data to export"); | |
| return; | |
| } | |
| // In a real implementation, this would export the filtered data | |
| alert("Export functionality would be implemented here"); | |
| } | |
| // Apply date filter | |
| function applyDateFilter() { | |
| updateDashboard(); | |
| } | |
| // Update growth chart | |
| function updateGrowthChart() { | |
| // In a real implementation, this would update the chart based on selected period | |
| renderGrowthChart(); | |
| } | |
| // Filter users | |
| function filterUsers() { | |
| currentPage = 1; | |
| renderUserTable(); | |
| } | |
| // Pagination functions | |
| function goToPrevPage() { | |
| if (currentPage > 1) { | |
| currentPage--; | |
| renderUserTable(); | |
| } | |
| } | |
| function goToNextPage() { | |
| const filteredData = getFilteredData(); | |
| const totalPages = Math.ceil(filteredData.length / itemsPerPage); | |
| if (currentPage < totalPages) { | |
| currentPage++; | |
| renderUserTable(); | |
| } | |
| } | |
| // Get filtered data based on search and country filter | |
| function getFilteredData() { | |
| const searchTerm = searchUsersInput.value.toLowerCase(); | |
| const countryFilter = filterCountrySelect.value; | |
| return userData.filter(user => { | |
| const matchesSearch = | |
| (user.Name && user.Name.toLowerCase().includes(searchTerm)) || | |
| (user.Email && user.Email.toLowerCase().includes(searchTerm)); | |
| const matchesCountry = !countryFilter || user.Country === countryFilter; | |
| return matchesSearch && matchesCountry; | |
| }); | |
| } | |
| // Update the entire dashboard | |
| function updateDashboard() { | |
| updateSummaryCards(); | |
| renderUserTable(); | |
| renderCountryTable(); | |
| renderGrowthChart(); | |
| renderCountryChart(); | |
| updateCountryFilterOptions(); | |
| } | |
| // Update summary cards | |
| function updateSummaryCards() { | |
| // Total users | |
| totalUsersEl.textContent = userData.length; | |
| // New users in last 7 days | |
| const sevenDaysAgo = new Date(); | |
| sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); | |
| const newUsersLast7d = userData.filter(user => { | |
| const userDate = new Date(user.Date); | |
| return userDate >= sevenDaysAgo; | |
| }).length; | |
| newUsers7dEl.textContent = newUsersLast7d; | |
| // Growth percentage (mock data) | |
| const growthPercentage = Math.floor(Math.random() * 30) - 5; // Random between -5% and 25% | |
| growth7dEl.querySelector('span').textContent = `${growthPercentage}%`; | |
| if (growthPercentage >= 0) { | |
| growth7dEl.className = 'ml-2 text-sm font-medium text-green-600 flex items-center'; | |
| growth7dEl.innerHTML = '<i data-feather="trending-up" class="h-4 w-4 mr-1"></i>' + `<span>${growthPercentage}%</span>`; | |
| } else { | |
| growth7dEl.className = 'ml-2 text-sm font-medium text-red-600 flex items-center'; | |
| growth7dEl.innerHTML = '<i data-feather="trending-down" class="h-4 w-4 mr-1"></i>' + `<span>${Math.abs(growthPercentage)}%</span>`; | |
| } | |
| // Top country | |
| const countryCounts = {}; | |
| userData.forEach(user => { | |
| if (user.Country) { | |
| countryCounts[user.Country] = (countryCounts[user.Country] || 0) + 1; | |
| } | |
| }); | |
| const topCountry = Object.entries(countryCounts).sort((a, b) => b[1] - a[1])[0]; | |
| topCountryEl.textContent = topCountry ? `${topCountry[0]} (${topCountry[1]})` : '-'; | |
| // Verified users percentage | |
| const verifiedCount = userData.filter(user => user['Email verified'] === '✓').length; | |
| const verifiedPercentage = userData.length > 0 ? Math.round((verifiedCount / userData.length) * 100) : 0; | |
| verifiedUsersEl.textContent = `${verifiedPercentage}%`; | |
| feather.replace(); | |
| } | |
| // Render user table | |
| function renderUserTable() { | |
| const filteredData = getFilteredData(); | |
| const startIndex = (currentPage - 1) * itemsPerPage; | |
| const endIndex = Math.min(startIndex + itemsPerPage, filteredData.length); | |
| const paginatedData = filteredData.slice(startIndex, endIndex); | |
| userTableBody.innerHTML = ''; | |
| paginatedData.forEach(user => { | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = ` | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="flex items-center"> | |
| <div class="flex-shrink-0 h-10 w-10 rounded-full bg-gray-200 flex items-center justify-center"> | |
| <i data-feather="user" class="h-5 w-5 text-gray-500"></i> | |
| </div> | |
| <div class="ml-4"> | |
| <div class="text-sm font-medium text-gray-900">${user.Name || 'Unknown'}</div> | |
| </div> | |
| </div> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="text-sm text-gray-900">${user.Email || '-'}</div> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"> | |
| ${user.Country || '-'} | |
| </span> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> | |
| ${user.Date ? new Date(user.Date).toLocaleDateString() : '-'} | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${user['Email verified'] === '✓' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}"> | |
| ${user['Email verified'] === '✓' ? 'Verified' : 'Not Verified'} | |
| </span> | |
| </td> | |
| `; | |
| userTableBody.appendChild(tr); | |
| }); | |
| // Update pagination info | |
| startItemEl.textContent = startIndex + 1; | |
| endItemEl.textContent = endIndex; | |
| totalItemsEl.textContent = filteredData.length; | |
| // Update pagination buttons | |
| prevPageBtn.disabled = currentPage === 1; | |
| nextPageBtn.disabled = endIndex >= filteredData.length; | |
| feather.replace(); | |
| } | |
| // Render country table | |
| function renderCountryTable() { | |
| const countryStats = {}; | |
| userData.forEach(user => { | |
| if (!user.Country) return; | |
| if (!countryStats[user.Country]) { | |
| countryStats[user.Country] = { | |
| total: 0, | |
| verified: 0, | |
| last7d: 0 | |
| }; | |
| } | |
| countryStats[user.Country].total++; | |
| if (user['Email verified'] === '✓') { | |
| countryStats[user.Country].verified++; | |
| } | |
| // Check if user was created in last 7 days | |
| const sevenDaysAgo = new Date(); | |
| sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); | |
| const userDate = new Date(user.Date); | |
| if (userDate >= sevenDaysAgo) { | |
| countryStats[user.Country].last7d++; | |
| } | |
| }); | |
| countryTableBody.innerHTML = ''; | |
| Object.entries(countryStats).forEach(([country, stats]) => { | |
| const growthPercentage = Math.floor(Math.random() * 30) - 5; // Random between -5% and 25% | |
| const verifiedPercentage = Math.round((stats.verified / stats.total) * 100); | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = ` | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="flex items-center"> | |
| <div class="flex-shrink-0 h-10 w-10 rounded-full bg-gray-200 flex items-center justify-center"> | |
| <i data-feather="flag" class="h-5 w-5 text-gray-500"></i> | |
| </div> | |
| <div class="ml-4"> | |
| <div class="text-sm font-medium text-gray-900">${country}</div> | |
| </div> | |
| </div> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> | |
| ${stats.total} | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> | |
| ${stats.last7d} | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${growthPercentage >= 0 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}"> | |
| ${growthPercentage >= 0 ? '+' : ''}${growthPercentage}% | |
| </span> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="w-full bg-gray-200 rounded-full h-2.5"> | |
| <div class="bg-blue-600 h-2.5 rounded-full" style="width: ${verifiedPercentage}%"></div> | |
| </div> | |
| <span class="text-xs text-gray-500">${verifiedPercentage}%</span> | |
| </td> | |
| `; | |
| countryTableBody.appendChild(tr); | |
| }); | |
| feather.replace(); | |
| } | |
| // Render growth chart | |
| function renderGrowthChart() { | |
| if (growthChart) { | |
| growthChart.destroy(); | |
| } | |
| // Mock data for demonstration | |
| const period = parseInt(growthPeriodSelect.value); | |
| const labels = []; | |
| const data = []; | |
| for (let i = period - 1; i >= 0; i--) { | |
| const date = new Date(); | |
| date.setDate(date.getDate() - i); | |
| labels.push(date.toLocaleDateString()); | |
| data.push(Math.floor(Math.random() * 20) + 5); // Random between 5 and 25 | |
| } | |
| growthChart = new Chart(growthChartCtx, { | |
| type: 'line', | |
| data: { | |
| labels: labels, | |
| datasets: [{ | |
| label: 'New Users', | |
| data: data, | |
| backgroundColor: 'rgba(79, 70, 229, 0.1)', | |
| borderColor: 'rgba(79, 70, 229, 1)', | |
| borderWidth: 2, | |
| tension: 0.3, | |
| fill: true | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| display: false | |
| }, | |
| tooltip: { | |
| mode: 'index', | |
| intersect: false | |
| } | |
| }, | |
| scales: { | |
| y: { | |
| beginAtZero: true, | |
| ticks: { | |
| precision: 0 | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Render country chart | |
| function renderCountryChart() { | |
| if (countryChart) { | |
| countryChart.destroy(); | |
| } | |
| // Get country counts | |
| const countryCounts = {}; | |
| userData.forEach(user => { | |
| if (user.Country) { | |
| countryCounts[user.Country] = (countryCounts[user.Country] || 0) + 1; | |
| } | |
| }); | |
| // Sort countries by count and take top 5 | |
| const sortedCountries = Object.entries(countryCounts) | |
| .sort((a, b) => b[1] - a[1]) | |
| .slice(0, 5); | |
| const labels = sortedCountries.map(item => item[0]); | |
| const data = sortedCountries.map(item => item[1]); | |
| countryChart = new Chart(countryChartCtx, { | |
| type: 'bar', | |
| data: { | |
| labels: labels, | |
| datasets: [{ | |
| label: 'Users', | |
| data: data, | |
| backgroundColor: [ | |
| 'rgba(79, 70, 229, 0.7)', | |
| 'rgba(99, 102, 241, 0.7)', | |
| 'rgba(129, 140, 248, 0.7)', | |
| 'rgba(165, 180, 252, 0.7)', | |
| 'rgba(199, 210, 254, 0.7)' | |
| ], | |
| borderColor: [ | |
| 'rgba(79, 70, 229, 1)', | |
| 'rgba(99, 102, 241, 1)', | |
| 'rgba(129, 140, 248, 1)', | |
| 'rgba(165, 180, 252, 1)', | |
| 'rgba(199, 210, 254, 1)' | |
| ], | |
| borderWidth: 1 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| display: false | |
| } | |
| }, | |
| scales: { | |
| y: { | |
| beginAtZero: true, | |
| ticks: { | |
| precision: 0 | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Update country filter options | |
| function updateCountryFilterOptions() { | |
| const countries = new Set(); | |
| userData.forEach(user => { | |
| if (user.Country) { | |
| countries.add(user.Country); | |
| } | |
| }); | |
| filterCountrySelect.innerHTML = ` | |
| <option value="">All Countries</option> | |
| ${Array.from(countries).map(country => `<option value="${country}">${country}</option>`).join('')} | |
| `; | |
| feather.replace(); | |
| } | |
| // Initialize the dashboard | |
| initDateInputs(); | |
| updateDashboard(); | |
| </script> | |
| </body> | |
| </html> | |