diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
index 44e6169..c61209d 100644
--- a/app/dashboard/page.tsx
+++ b/app/dashboard/page.tsx
@@ -1,21 +1,40 @@
'use client';
-import { useState, useEffect, useMemo } from 'react';
-import { Game, User, fetchGames, fetchUsers, truncateAddress } from '../utils/api';
-import Pagination from '../components/Pagination';
-import SearchInput from '../components/SearchInput';
+import { useState, useEffect } from 'react';
+import { Game, User, fetchGames, fetchUsers, getDisplayGameName } from '../utils/api';
+import Link from 'next/link';
+import { Line, Bar } from 'react-chartjs-2';
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ BarElement,
+ Title,
+ Tooltip,
+ Legend
+} from 'chart.js';
-const ITEMS_PER_PAGE = 20;
+ChartJS.register(
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ BarElement,
+ Title,
+ Tooltip,
+ Legend
+);
+
+type TimeRange = '7d' | '30d' | '90d';
export default function Dashboard() {
const [users, setUsers] = useState
([]);
const [games, setGames] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
- const [currentUserPage, setCurrentUserPage] = useState(1);
- const [currentGamePage, setCurrentGamePage] = useState(1);
- const [userSearch, setUserSearch] = useState('');
- const [gameSearch, setGameSearch] = useState('');
+ const [gameTimeRange, setGameTimeRange] = useState('30d');
useEffect(() => {
const loadData = async () => {
@@ -37,77 +56,172 @@ export default function Dashboard() {
loadData();
}, []);
- useEffect(() => {
- // Reset to first page when search changes
- setCurrentUserPage(1);
- }, [userSearch]);
+ const getDaysFromTimeRange = (range: TimeRange): number => {
+ switch (range) {
+ case '7d': return 7;
+ case '30d': return 30;
+ case '90d': return 90;
+ }
+ };
- useEffect(() => {
- // Reset to first page when search changes
- setCurrentGamePage(1);
- }, [gameSearch]);
+ const getGameCountData = () => {
+ const days = getDaysFromTimeRange(gameTimeRange);
+ const lastDays = Array.from({ length: days }, (_, i) => {
+ const date = new Date();
+ date.setDate(date.getDate() - i);
+ return date.toISOString().split('T')[0];
+ }).reverse();
- const formatDate = (dateStr: string) => {
- const date = new Date(dateStr);
- return date.toLocaleString('en-US', {
- year: 'numeric',
- month: 'short',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit'
+ const gameCounts = lastDays.map(date => {
+ return games.filter(game => {
+ try {
+ const gameDate = new Date(game.ended_time).toISOString().split('T')[0];
+ return gameDate === date;
+ } catch (err) {
+ console.error('Error parsing date:', err);
+ return false;
+ }
+ }).length;
});
+
+ return {
+ labels: lastDays.map(date => new Date(date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })),
+ datasets: [
+ {
+ label: 'Games per Day',
+ data: gameCounts,
+ borderColor: '#FFA500',
+ backgroundColor: 'rgba(255, 165, 0, 0.2)',
+ tension: 0.4,
+ fill: true
+ }
+ ]
+ };
};
- const getGameResult = (game: Game) => {
- const masterScore = parseInt(game.master_score);
- const clientScore = parseInt(game.client_score);
- if (masterScore === clientScore) return 'Draw';
- return game.winner === 'master' ? 'Master Won' : 'Client Won';
+ const getGamePopularityData = () => {
+ const gameTypes = Array.from(new Set(games.map(game => game.game)));
+ const gameCounts = gameTypes.map(type => ({
+ type,
+ displayName: getDisplayGameName(type),
+ count: games.filter(game => game.game === type).length
+ })).sort((a, b) => b.count - a.count);
+
+ return {
+ labels: gameCounts.map(game => game.displayName),
+ datasets: [
+ {
+ label: 'Games Played',
+ data: gameCounts.map(game => game.count),
+ backgroundColor: 'rgba(255, 165, 0, 0.7)',
+ borderColor: '#FFA500',
+ borderWidth: 1
+ }
+ ]
+ };
};
- // Filter and paginate users
- const filteredUsers = useMemo(() => {
- if (!userSearch) return users;
- const searchLower = userSearch.toLowerCase();
- return users.filter(user =>
- user.ref_id.toLowerCase().includes(searchLower) ||
- (user.username || '').toLowerCase().includes(searchLower) ||
- (user.active_wallet || '').toLowerCase().includes(searchLower) ||
- (user.x_profile_url || '').toLowerCase().includes(searchLower) ||
- (user.referred_id || '').toLowerCase().includes(searchLower)
- );
- }, [users, userSearch]);
+ const chartOptions = {
+ responsive: true,
+ plugins: {
+ legend: {
+ display: false,
+ labels: {
+ color: '#ffffff'
+ }
+ },
+ tooltip: {
+ titleColor: '#ffffff',
+ bodyColor: '#ffffff',
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
+ borderColor: 'rgba(255, 255, 255, 0.1)',
+ borderWidth: 1
+ }
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ grid: {
+ color: 'rgba(255, 255, 255, 0.1)'
+ },
+ ticks: {
+ color: '#ffffff'
+ }
+ },
+ x: {
+ grid: {
+ color: 'rgba(255, 255, 255, 0.1)'
+ },
+ ticks: {
+ color: '#ffffff'
+ }
+ }
+ }
+ };
- // Filter and paginate games
- const filteredGames = useMemo(() => {
- if (!gameSearch) return games;
- const searchLower = gameSearch.toLowerCase();
- return games.filter(game =>
- game.ended_time.toLowerCase().includes(searchLower) ||
- game.game.toLowerCase().includes(searchLower) ||
- game.winner.toLowerCase().includes(searchLower) ||
- game.master_id.toLowerCase().includes(searchLower) ||
- game.client_id.toLowerCase().includes(searchLower) ||
- game.master_score.includes(searchLower) ||
- game.client_score.includes(searchLower) ||
- game.wager.includes(searchLower)
- );
- }, [games, gameSearch]);
+ const barChartOptions = {
+ responsive: true,
+ plugins: {
+ legend: {
+ display: false,
+ labels: {
+ color: '#ffffff'
+ }
+ },
+ tooltip: {
+ titleColor: '#ffffff',
+ bodyColor: '#ffffff',
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
+ borderColor: 'rgba(255, 255, 255, 0.1)',
+ borderWidth: 1
+ }
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ grid: {
+ color: 'rgba(255, 255, 255, 0.1)'
+ },
+ ticks: {
+ color: '#ffffff'
+ }
+ },
+ x: {
+ grid: {
+ color: 'rgba(255, 255, 255, 0.1)'
+ },
+ ticks: {
+ color: '#ffffff'
+ }
+ }
+ }
+ };
- const currentUsers = filteredUsers.slice(
- (currentUserPage - 1) * ITEMS_PER_PAGE,
- currentUserPage * ITEMS_PER_PAGE
- );
-
- const currentGames = filteredGames.slice(
- (currentGamePage - 1) * ITEMS_PER_PAGE,
- currentGamePage * ITEMS_PER_PAGE
+ const TimeRangeButton = ({
+ range,
+ currentRange,
+ onChange
+ }: {
+ range: TimeRange;
+ currentRange: TimeRange;
+ onChange: (range: TimeRange) => void;
+ }) => (
+
);
if (loading) {
return (
-
Loading...
+
Loading...
);
}
@@ -115,166 +229,62 @@ export default function Dashboard() {
if (error) {
return (
);
}
return (
- {/* Users Section */}
-
-
-
Users
-
-
-
- Total Users: {filteredUsers.length}
-
+
+ {/* Users Card */}
+
+
+
+ Users - {users.length}
+
+
+ View All →
+
-
-
-
-
- |
- Ref ID
- |
-
- Username
- |
-
- Wallet
- |
-
- X Profile
- |
-
- Referred By
- |
-
-
-
- {currentUsers.map((user) => (
-
- |
- {user.ref_id}
- |
-
- {user.username || '-'}
- |
-
- {truncateAddress(user.active_wallet) || '-'}
- |
-
- {user.x_profile_url ? (
-
- View Profile
-
- ) : '-'}
- |
-
- {user.referred_id === '-1' ? '-' : user.referred_id}
- |
-
- ))}
-
-
-
+
+ {/* Game Count Card */}
+
+
+
+ Games - {games.length}
+
+
+ View All →
+
+
+
+
+
+
+
+
+
+
- {/* Games History Section */}
-
+ {/* Game Popularity Card */}
+
-
Game History
-
-
-
- Total Games: {filteredGames.length}
-
-
+
+ Game Popularity
+
-
-
-
-
- |
- Date
- |
-
- Game
- |
-
- Winner
- |
-
- Players
- |
-
- Score
- |
-
- Result
- |
-
- Wager (SOL)
- |
-
-
-
- {currentGames.map((game) => (
-
- |
- {formatDate(game.ended_time)}
- |
-
- {game.game}
- |
-
- {game.winner}
- |
-
- Master: {truncateAddress(game.master_id)}
- Client: {truncateAddress(game.client_id)}
- |
-
- {game.master_score} - {game.client_score}
- |
-
- {getGameResult(game)}
- |
-
- {(parseInt(game.wager) / 1000000).toFixed(2)}
- |
-
- ))}
-
-
-
+
+
diff --git a/app/games/page.tsx b/app/games/page.tsx
new file mode 100644
index 0000000..a8a8c5c
--- /dev/null
+++ b/app/games/page.tsx
@@ -0,0 +1,243 @@
+'use client';
+
+import { useState, useEffect, useMemo } from 'react';
+import { Game, User, fetchGames, fetchUsers, truncateAddress, getDisplayGameName } from '../utils/api';
+import Pagination from '../components/Pagination';
+import SearchInput from '../components/SearchInput';
+import Modal from '../components/Modal';
+
+const ITEMS_PER_PAGE = 20;
+
+export default function Games() {
+ const [games, setGames] = useState
([]);
+ const [users, setUsers] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [search, setSearch] = useState('');
+ const [selectedUser, setSelectedUser] = useState(null);
+ const [showUserModal, setShowUserModal] = useState(false);
+
+ const handlePlayerClick = (e: React.MouseEvent, playerId: string) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const user = users.find(u => u.active_wallet === playerId);
+ if (user) {
+ setSelectedUser(user);
+ setShowUserModal(true);
+ }
+ };
+
+ const getUserDisplayName = (userId: string): string => {
+ if (userId === 'na') return 'N/A';
+ const user = users.find(u => u.id === userId || u.active_wallet === userId);
+ if (user?.username) {
+ return user.username;
+ }
+ return truncateAddress(userId);
+ };
+
+ const formatDate = (dateStr: string) => {
+ const date = new Date(dateStr);
+ return date.toLocaleString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ };
+
+
+ // Filter and paginate games
+ const filteredGames = useMemo(() => {
+ if (!search) return games;
+ const searchLower = search.toLowerCase();
+ return games.filter(game =>
+ game.ended_time.toLowerCase().includes(searchLower) ||
+ game.game.toLowerCase().includes(searchLower) ||
+ game.winner.toLowerCase().includes(searchLower) ||
+ game.master_id.toLowerCase().includes(searchLower) ||
+ game.client_id.toLowerCase().includes(searchLower) ||
+ game.master_score.includes(searchLower) ||
+ game.client_score.includes(searchLower) ||
+ game.wager.includes(searchLower)
+ );
+ }, [games, search]);
+
+ const currentGames = filteredGames.slice(
+ (currentPage - 1) * ITEMS_PER_PAGE,
+ currentPage * ITEMS_PER_PAGE
+ );
+
+ useEffect(() => {
+ const loadData = async () => {
+ try {
+ const [gamesData, usersData] = await Promise.all([
+ fetchGames(),
+ fetchUsers()
+ ]);
+ setGames(gamesData);
+ setUsers(usersData);
+ } catch (err) {
+ setError('Failed to load data. Please try again later.');
+ console.error('Error loading data:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadData();
+ }, []);
+
+ useEffect(() => {
+ setCurrentPage(1);
+ }, [search]);
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* User Details Modal */}
+
{
+ setShowUserModal(false);
+ setSelectedUser(null);
+ }}
+ title="User Details"
+ >
+ {selectedUser && (
+
+
+
+
Ref ID
+
{selectedUser.ref_id}
+
+
+
Username
+
{selectedUser.username || '-'}
+
+
+
Wallet Address
+
{selectedUser.active_wallet || '-'}
+
+
+
+
Referred By
+
{selectedUser.referred_id === '-1' ? '-' : selectedUser.referred_id}
+
+
+
+ )}
+
+
+ {/* Games History Section */}
+
+
+
Game History
+
+
+
+ Total Games: {filteredGames.length}
+
+
+
+
+
+
+
+ | Date |
+ Game |
+ Winner |
+ Players |
+ Score |
+ Wager (SOL) |
+
+
+
+ {currentGames.map((game) => (
+
+ |
+ {formatDate(game.ended_time)}
+ |
+
+ {getDisplayGameName(game.game)}
+ |
+
+ {game.winner}
+ |
+
+
+ Master:
+
+
+ Client:
+
+ |
+
+ {game.master_score} - {game.client_score}
+ |
+
+
+ {(parseInt(game.wager) / 100000000).toFixed(2)}
+ |
+
+ ))}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/globals.css b/app/globals.css
index a2dc41e..f66ceee 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,8 +1,15 @@
@import "tailwindcss";
:root {
- --background: #ffffff;
- --foreground: #171717;
+ --background: #121212;
+ --foreground: #ffffff;
+ --accent: #ff6b00;
+ --accent-hover: #ff8533;
+ --card-bg: #1e1e1e;
+ --card-border: #2d2d2d;
+ --text-primary: #ffffff;
+ --text-secondary: #a0a0a0;
+ --text-muted: #666666;
}
@theme inline {
@@ -21,6 +28,25 @@
body {
background: var(--background);
- color: var(--foreground);
+ color: var(--text-primary);
font-family: Arial, Helvetica, sans-serif;
}
+
+/* Custom scrollbar */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--card-bg);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--accent);
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--accent-hover);
+}
diff --git a/app/lib/utils.ts b/app/lib/utils.ts
new file mode 100644
index 0000000..c8d29eb
--- /dev/null
+++ b/app/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
\ No newline at end of file
diff --git a/app/page.tsx b/app/page.tsx
index 6c0bf21..2ad2c83 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -2,7 +2,6 @@
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
-import Image from "next/image";
export default function Home() {
const router = useRouter();
diff --git a/app/users/page.tsx b/app/users/page.tsx
new file mode 100644
index 0000000..27a8ad4
--- /dev/null
+++ b/app/users/page.tsx
@@ -0,0 +1,205 @@
+'use client';
+
+import { useState, useEffect, useMemo } from 'react';
+import { User, fetchUsers, truncateAddress } from '../utils/api';
+import Pagination from '../components/Pagination';
+import SearchInput from '../components/SearchInput';
+import Modal from '../components/Modal';
+
+const ITEMS_PER_PAGE = 20;
+
+export default function Users() {
+ const [users, setUsers] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [search, setSearch] = useState('');
+ const [selectedUser, setSelectedUser] = useState(null);
+ const [showUserModal, setShowUserModal] = useState(false);
+
+ const handlePlayerClick = (e: React.MouseEvent, playerId: string) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const user = users.find(u => u.active_wallet === playerId);
+ if (user) {
+ setSelectedUser(user);
+ setShowUserModal(true);
+ }
+ };
+
+ // Filter and paginate users
+ const filteredUsers = useMemo(() => {
+ if (!search) return users;
+ const searchLower = search.toLowerCase();
+ return users.filter(user =>
+ user.ref_id.toLowerCase().includes(searchLower) ||
+ (user.username || '').toLowerCase().includes(searchLower) ||
+ (user.active_wallet || '').toLowerCase().includes(searchLower) ||
+ (user.x_profile_url || '').toLowerCase().includes(searchLower) ||
+ (user.referred_id || '').toLowerCase().includes(searchLower)
+ );
+ }, [users, search]);
+
+ const currentUsers = filteredUsers.slice(
+ (currentPage - 1) * ITEMS_PER_PAGE,
+ currentPage * ITEMS_PER_PAGE
+ );
+
+ useEffect(() => {
+ const loadData = async () => {
+ try {
+ const usersData = await fetchUsers();
+ setUsers(usersData);
+ } catch (err) {
+ setError('Failed to load data. Please try again later.');
+ console.error('Error loading data:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadData();
+ }, []);
+
+ useEffect(() => {
+ setCurrentPage(1);
+ }, [search]);
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* User Details Modal */}
+
{
+ setShowUserModal(false);
+ setSelectedUser(null);
+ }}
+ title="User Details"
+ >
+ {selectedUser && (
+
+
+
+
Ref ID
+
{selectedUser.ref_id}
+
+
+
Username
+
{selectedUser.username || '-'}
+
+
+
Wallet Address
+
{selectedUser.active_wallet || '-'}
+
+
+
+
Referred By
+
{selectedUser.referred_id === '-1' ? '-' : selectedUser.referred_id}
+
+
+
+ )}
+
+
+ {/* Users Section */}
+
+
+
Users
+
+
+
+ Total Users: {filteredUsers.length}
+
+
+
+
+
+
+
+ | Ref ID |
+ Username |
+ Bio |
+ X Profile |
+ Referred By |
+ Active Wallet |
+
+
+
+ {currentUsers.map((user) => (
+ handlePlayerClick(e, user.active_wallet)}
+ >
+ | {user.ref_id} |
+ {user.username || '-'} |
+ {user.bio || '-'} |
+
+ {user.x_profile_url ? (
+ e.stopPropagation()}
+ >
+ {user.x_profile_url}
+
+ ) : '-'}
+ |
+
+ {user.referred_id === "-1" ? '-' : user.referred_id}
+ |
+
+ {user.active_wallet ? truncateAddress(user.active_wallet) : '-'}
+ |
+
+ ))}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/utils/api.ts b/app/utils/api.ts
index 66c15b1..fbff92b 100644
--- a/app/utils/api.ts
+++ b/app/utils/api.ts
@@ -22,6 +22,7 @@ export interface User {
x_profile_url: string; // ""
referred_id: string; // "-1"
active_wallet: string; // ""
+ joined_date: string; // "2024-03-20T12:00:00Z"
}
// Helper function to truncate wallet address
@@ -30,6 +31,17 @@ export function truncateAddress(address: string): string {
return `${address.slice(0, 4)}...${address.slice(-4)}`;
}
+export function getDisplayGameName(gameName: string): string {
+ switch (gameName) {
+ case 'walls':
+ return 'Bubble Shooter';
+ case 'bubbles':
+ return 'Wall Smash';
+ default:
+ return gameName;
+ }
+}
+
export async function fetchGames(): Promise {
try {
const response = await fetch('https://api.duelfi.io/v1/admin/get_all_games.php');
diff --git a/package-lock.json b/package-lock.json
index 2c41b61..5632837 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,10 +9,16 @@
"version": "0.1.0",
"dependencies": {
"@types/js-cookie": "^3.0.6",
+ "@types/socket.io-client": "^3.0.0",
+ "chart.js": "^4.4.9",
+ "clsx": "^2.1.1",
"js-cookie": "^3.0.5",
"next": "15.3.3",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-chartjs-2": "^5.3.0",
+ "react-dom": "^19.0.0",
+ "socket.io-client": "^4.8.1",
+ "tailwind-merge": "^3.3.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
@@ -755,6 +761,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+ "license": "MIT"
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.10.tgz",
@@ -974,6 +986,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+ "license": "MIT"
+ },
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
@@ -1333,6 +1351,16 @@
"@types/react": "^19.0.0"
}
},
+ "node_modules/@types/socket.io-client": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-3.0.0.tgz",
+ "integrity": "sha512-s+IPvFoEIjKA3RdJz/Z2dGR4gLgysKi8owcnrVwNjgvc01Lk68LJDDsG2GRqegFITcxmvCMYM7bhMpwEMlHmDg==",
+ "deprecated": "This is a stub types definition. socket.io-client provides its own type definitions, so you do not need this installed.",
+ "license": "MIT",
+ "dependencies": {
+ "socket.io-client": "*"
+ }
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.33.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.1.tgz",
@@ -2286,6 +2314,18 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/chart.js": {
+ "version": "4.4.9",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz",
+ "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==",
+ "license": "MIT",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
"node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
@@ -2302,6 +2342,15 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -2543,6 +2592,45 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/engine.io-client": {
+ "version": "6.6.3",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
+ "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1",
+ "engine.io-parser": "~5.2.1",
+ "ws": "~8.17.1",
+ "xmlhttprequest-ssl": "~2.1.1"
+ }
+ },
+ "node_modules/engine.io-client/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/engine.io-parser": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
@@ -4599,7 +4687,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
@@ -5076,6 +5163,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-chartjs-2": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz",
+ "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "chart.js": "^4.1.1",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/react-dom": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
@@ -5489,6 +5586,68 @@
"is-arrayish": "^0.3.1"
}
},
+ "node_modules/socket.io-client": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
+ "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.2",
+ "engine.io-client": "~6.6.1",
+ "socket.io-parser": "~4.2.4"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-client/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-parser": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -5712,6 +5871,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/tailwind-merge": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.0.tgz",
+ "integrity": "sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
"node_modules/tailwindcss": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.8.tgz",
@@ -6126,6 +6295,35 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ws": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xmlhttprequest-ssl": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
+ "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
diff --git a/package.json b/package.json
index a831876..4fea8f6 100644
--- a/package.json
+++ b/package.json
@@ -3,17 +3,23 @@
"version": "0.1.0",
"private": true,
"scripts": {
- "dev": "next dev --turbopack",
+ "dev": "next dev --turbopack -p 3045",
"build": "next build",
- "start": "next start",
+ "start": "next start -p 3045",
"lint": "next lint"
},
"dependencies": {
"@types/js-cookie": "^3.0.6",
+ "@types/socket.io-client": "^3.0.0",
+ "chart.js": "^4.4.9",
+ "clsx": "^2.1.1",
"js-cookie": "^3.0.5",
"next": "15.3.3",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-chartjs-2": "^5.3.0",
+ "react-dom": "^19.0.0",
+ "socket.io-client": "^4.8.1",
+ "tailwind-merge": "^3.3.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
diff --git a/tsconfig.json b/tsconfig.json
index d8b9323..8ea4ed2 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,6 @@
{
"compilerOptions": {
- "target": "ES2017",
+ "target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
@@ -19,7 +19,7 @@
}
],
"paths": {
- "@/*": ["./*"]
+ "@/*": ["./app/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],