diff --git a/app/components/Pagination.tsx b/app/components/Pagination.tsx
new file mode 100644
index 0000000..f53b699
--- /dev/null
+++ b/app/components/Pagination.tsx
@@ -0,0 +1,85 @@
+interface PaginationProps {
+ totalItems: number;
+ itemsPerPage: number;
+ currentPage: number;
+ onPageChange: (page: number) => void;
+}
+
+export default function Pagination({ totalItems, itemsPerPage, currentPage, onPageChange }: PaginationProps) {
+ const totalPages = Math.ceil(totalItems / itemsPerPage);
+
+ if (totalPages <= 1) return null;
+
+ const pageNumbers = [];
+ const maxVisiblePages = 5;
+ let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
+ let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
+
+ if (endPage - startPage + 1 < maxVisiblePages) {
+ startPage = Math.max(1, endPage - maxVisiblePages + 1);
+ }
+
+ for (let i = startPage; i <= endPage; i++) {
+ pageNumbers.push(i);
+ }
+
+ return (
+
+
+
+ {startPage > 1 && (
+ <>
+
+ {startPage > 2 && ...}
+ >
+ )}
+
+ {pageNumbers.map((number) => (
+
+ ))}
+
+ {endPage < totalPages && (
+ <>
+ {endPage < totalPages - 1 && ...}
+
+ >
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/SearchInput.tsx b/app/components/SearchInput.tsx
new file mode 100644
index 0000000..a7995af
--- /dev/null
+++ b/app/components/SearchInput.tsx
@@ -0,0 +1,24 @@
+interface SearchInputProps {
+ placeholder: string;
+ value: string;
+ onChange: (value: string) => void;
+}
+
+export default function SearchInput({ placeholder, value, onChange }: SearchInputProps) {
+ return (
+
+
+
onChange(e.target.value)}
+ />
+
+ );
+}
\ No newline at end of file
diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx
new file mode 100644
index 0000000..d422f16
--- /dev/null
+++ b/app/dashboard/layout.tsx
@@ -0,0 +1,45 @@
+'use client';
+
+import { useRouter } from 'next/navigation';
+import Cookies from 'js-cookie';
+
+export default function DashboardLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const router = useRouter();
+
+ const handleLogout = () => {
+ Cookies.remove('isAuthenticated', { path: '/' });
+ router.push('/login');
+ };
+
+ return (
+
+
+
+
+ {children}
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
new file mode 100644
index 0000000..44e6169
--- /dev/null
+++ b/app/dashboard/page.tsx
@@ -0,0 +1,282 @@
+'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';
+
+const ITEMS_PER_PAGE = 20;
+
+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('');
+
+ 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(() => {
+ // Reset to first page when search changes
+ setCurrentUserPage(1);
+ }, [userSearch]);
+
+ useEffect(() => {
+ // Reset to first page when search changes
+ setCurrentGamePage(1);
+ }, [gameSearch]);
+
+ 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 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';
+ };
+
+ // 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]);
+
+ // 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 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
+ );
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* Users Section */}
+
+
+
Users
+
+
+
+ Total Users: {filteredUsers.length}
+
+
+
+
+
+
+
+ |
+ 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}
+ |
+
+ ))}
+
+
+
+
+
+
+ {/* Games History Section */}
+
+
+
Game History
+
+
+
+ Total Games: {filteredGames.length}
+
+
+
+
+
+
+
+ |
+ 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)}
+ |
+
+ ))}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/login/page.tsx b/app/login/page.tsx
new file mode 100644
index 0000000..fe4e475
--- /dev/null
+++ b/app/login/page.tsx
@@ -0,0 +1,84 @@
+'use client';
+
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
+import Cookies from 'js-cookie';
+
+export default function LoginPage() {
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const router = useRouter();
+
+ const handleLogin = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ // For demo purposes, using hardcoded credentials
+ // In production, this should be replaced with proper authentication
+ if (username === 'admin' && password === 'admin123') {
+ // Set authentication cookie
+ Cookies.set('isAuthenticated', 'true', { path: '/' });
+ router.push('/dashboard');
+ } else {
+ setError('Invalid username or password');
+ }
+ };
+
+ return (
+
+
+
+
+ Admin Login
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/page.tsx b/app/page.tsx
index 88f0cc9..6c0bf21 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,103 +1,15 @@
+'use client';
+
+import { useEffect } from 'react';
+import { useRouter } from 'next/navigation';
import Image from "next/image";
export default function Home() {
- return (
-
-
-
-
- -
- Get started by editing{" "}
-
- app/page.tsx
-
- .
-
- -
- Save and see your changes instantly.
-
-
+ const router = useRouter();
-
-
-
-
- );
+ useEffect(() => {
+ router.push('/login');
+ }, [router]);
+
+ return null;
}
diff --git a/app/utils/api.ts b/app/utils/api.ts
new file mode 100644
index 0000000..66c15b1
--- /dev/null
+++ b/app/utils/api.ts
@@ -0,0 +1,60 @@
+export interface Game {
+ ended_time: string; // "2025-06-04 13:24:52"
+ address: string; // "12oDoNd73dCcHqq2fB5QxpzE8TTQQyMVj4mGavm2pGX8"
+ game: string; // "tetris"
+ master_score: string; // "0"
+ client_score: string; // "0"
+ winner: string; // "master"
+ wager: string; // "1000000"
+ master_id: string; // "did:privy:cm9a3f19v00iri90mag51e0zk"
+ client_id: string; // "na"
+ reward_tx: string; // "xYJnbWNTyMU36J7HL188Rq6tMHYf8X6ausPXP6izedNAiuzdPmwYkeCNFeRejT2mU8uXpjPjVPAaK5GD42dMbGM"
+ rematch_address: string | null; // null
+ owner_referree: string | null; // "9esrj2X33pr5og6fdkDMjaW6fdnnb9hT1cWshamxTdL4"
+ joiner_referree: string | null; // "9esrj2X33pr5og6fdkDMjaW6fdnnb9hT1cWshamxTdL4"
+}
+
+export interface User {
+ ref_id: string; // "146"
+ id: string; // ""
+ username: string; // ""
+ bio: string; // ""
+ x_profile_url: string; // ""
+ referred_id: string; // "-1"
+ active_wallet: string; // ""
+}
+
+// Helper function to truncate wallet address
+export function truncateAddress(address: string): string {
+ if (!address || address === 'na' || address.startsWith('did:privy:')) return address;
+ return `${address.slice(0, 4)}...${address.slice(-4)}`;
+}
+
+export async function fetchGames(): Promise {
+ try {
+ const response = await fetch('https://api.duelfi.io/v1/admin/get_all_games.php');
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const data = await response.json();
+
+ return data;
+ } catch (error) {
+ console.error('Error fetching games:', error);
+ throw error;
+ }
+}
+
+export async function fetchUsers(): Promise {
+ try {
+ const response = await fetch('https://api.duelfi.io/v1/admin/get_all_users.php');
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const data = await response.json();
+ return data;
+ } catch (error) {
+ console.error('Error fetching users:', error);
+ throw error;
+ }
+}
\ No newline at end of file
diff --git a/middleware.ts b/middleware.ts
new file mode 100644
index 0000000..de463d0
--- /dev/null
+++ b/middleware.ts
@@ -0,0 +1,21 @@
+import { NextResponse } from 'next/server';
+import type { NextRequest } from 'next/server';
+
+export function middleware(request: NextRequest) {
+ const isAuthenticated = request.cookies.get('isAuthenticated')?.value === 'true';
+ const isLoginPage = request.nextUrl.pathname === '/login';
+
+ if (!isAuthenticated && request.nextUrl.pathname.startsWith('/dashboard')) {
+ return NextResponse.redirect(new URL('/login', request.url));
+ }
+
+ if (isAuthenticated && isLoginPage) {
+ return NextResponse.redirect(new URL('/dashboard', request.url));
+ }
+
+ return NextResponse.next();
+}
+
+export const config = {
+ matcher: ['/dashboard/:path*', '/login']
+};
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 183bc08..2c41b61 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,8 @@
"name": "duelfi_admin",
"version": "0.1.0",
"dependencies": {
+ "@types/js-cookie": "^3.0.6",
+ "js-cookie": "^3.0.5",
"next": "15.3.3",
"react": "^19.0.0",
"react-dom": "^19.0.0"
@@ -1281,6 +1283,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/js-cookie": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
+ "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==",
+ "license": "MIT"
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -4083,6 +4091,15 @@
"jiti": "lib/jiti-cli.mjs"
}
},
+ "node_modules/js-cookie": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+ "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
diff --git a/package.json b/package.json
index 0bb81a3..a831876 100644
--- a/package.json
+++ b/package.json
@@ -9,19 +9,21 @@
"lint": "next lint"
},
"dependencies": {
+ "@types/js-cookie": "^3.0.6",
+ "js-cookie": "^3.0.5",
+ "next": "15.3.3",
"react": "^19.0.0",
- "react-dom": "^19.0.0",
- "next": "15.3.3"
+ "react-dom": "^19.0.0"
},
"devDependencies": {
- "typescript": "^5",
+ "@eslint/eslintrc": "^3",
+ "@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
- "@tailwindcss/postcss": "^4",
- "tailwindcss": "^4",
"eslint": "^9",
"eslint-config-next": "15.3.3",
- "@eslint/eslintrc": "^3"
+ "tailwindcss": "^4",
+ "typescript": "^5"
}
}