From 2dae3acd212f67bf416cb422c1e420dd9eab3e9a Mon Sep 17 00:00:00 2001 From: warlock Date: Thu, 5 Jun 2025 20:50:51 +0530 Subject: [PATCH] init --- app/components/Pagination.tsx | 85 ++++++++++ app/components/SearchInput.tsx | 24 +++ app/dashboard/layout.tsx | 45 ++++++ app/dashboard/page.tsx | 282 +++++++++++++++++++++++++++++++++ app/login/page.tsx | 84 ++++++++++ app/page.tsx | 108 ++----------- app/utils/api.ts | 60 +++++++ middleware.ts | 21 +++ package-lock.json | 17 ++ package.json | 14 +- 10 files changed, 636 insertions(+), 104 deletions(-) create mode 100644 app/components/Pagination.tsx create mode 100644 app/components/SearchInput.tsx create mode 100644 app/dashboard/layout.tsx create mode 100644 app/dashboard/page.tsx create mode 100644 app/login/page.tsx create mode 100644 app/utils/api.ts create mode 100644 middleware.ts 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 ( +
+
Loading...
+
+ ); + } + + if (error) { + return ( +
+
{error}
+
+ ); + } + + return ( +
+ {/* Users Section */} +
+
+

Users

+
+ +
+ Total Users: {filteredUsers.length} +
+
+
+
+ + + + + + + + + + + + {currentUsers.map((user) => ( + + + + + + + + ))} + +
+ Ref ID + + Username + + Wallet + + X Profile + + Referred By +
+ {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} +
+
+
+
+ + + + + + + + + + + + + + {currentGames.map((game) => ( + + + + + + + + + + ))} + +
+ Date + + Game + + Winner + + Players + + Score + + Result + + Wager (SOL) +
+ {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 +

+
+
+ {error && ( +
{error}
+ )} +
+
+ + setUsername(e.target.value)} + /> +
+
+ + setPassword(e.target.value)} + /> +
+
+ +
+ +
+
+
+
+ ); +} \ 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 ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
+ 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" } }