From 2239f590ab64d4956a0223d20965985dda5c132f Mon Sep 17 00:00:00 2001 From: achmad Date: Fri, 29 May 2026 17:01:03 +0700 Subject: [PATCH] feat: add admin dashboard and players pages Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/src/app/admin/page.tsx | 35 ++++++++ .../src/app/admin/players/[steamId]/page.tsx | 90 +++++++++++++++++++ backend/src/app/admin/players/page.tsx | 51 +++++++++++ .../app/api/admin/players/[steamId]/route.ts | 43 +++++++++ backend/src/app/api/admin/players/route.ts | 8 ++ backend/src/app/api/admin/stats/route.ts | 12 +++ 6 files changed, 239 insertions(+) create mode 100644 backend/src/app/admin/page.tsx create mode 100644 backend/src/app/admin/players/[steamId]/page.tsx create mode 100644 backend/src/app/admin/players/page.tsx create mode 100644 backend/src/app/api/admin/players/[steamId]/route.ts create mode 100644 backend/src/app/api/admin/players/route.ts create mode 100644 backend/src/app/api/admin/stats/route.ts diff --git a/backend/src/app/admin/page.tsx b/backend/src/app/admin/page.tsx new file mode 100644 index 0000000..9a3238a --- /dev/null +++ b/backend/src/app/admin/page.tsx @@ -0,0 +1,35 @@ +'use client'; +import { useEffect, useState } from 'react'; + +type Stats = { players: number; games: number; activeBps: number; questsCompleted: number }; + +export default function DashboardPage() { + const [stats, setStats] = useState(null); + + useEffect(() => { + fetch('/api/admin/stats').then(r => r.json()).then(setStats).catch(() => {}); + }, []); + + if (!stats) return
Loading...
; + + const cards = [ + { label: 'Players', value: stats.players, color: 'text-blue-400' }, + { label: 'Games Played', value: stats.games, color: 'text-green-400' }, + { label: 'Active Battle Passes', value: stats.activeBps, color: 'text-amber-400' }, + { label: 'Quests Completed', value: stats.questsCompleted, color: 'text-purple-400' }, + ]; + + return ( +
+

Dashboard

+
+ {cards.map(c => ( +
+
{c.label}
+
{c.value}
+
+ ))} +
+
+ ); +} diff --git a/backend/src/app/admin/players/[steamId]/page.tsx b/backend/src/app/admin/players/[steamId]/page.tsx new file mode 100644 index 0000000..e64354a --- /dev/null +++ b/backend/src/app/admin/players/[steamId]/page.tsx @@ -0,0 +1,90 @@ +'use client'; +import { useEffect, useState } from 'react'; +import { useParams, useRouter } from 'next/navigation'; + +export default function PlayerDetailPage() { + const { steamId } = useParams(); + const router = useRouter(); + const [data, setData] = useState(null); + const [form, setForm] = useState({}); + const [msg, setMsg] = useState(''); + + useEffect(() => { + fetch(`/api/admin/players/${steamId}`).then(r => r.json()).then(d => { + setData(d); + setForm(d.player || {}); + }).catch(() => router.push('/admin/players')); + }, [steamId, router]); + + const save = async () => { + const res = await fetch(`/api/admin/players/${steamId}`, { + method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form), + }); + const result = await res.json(); + setMsg(result.success ? 'Saved!' : 'Error: ' + (result.error || '')); + }; + + if (!data) return
Loading...
; + + return ( +
+ ← Back to Players +

Player: {data.player?.player_name}

+

Steam ID: {steamId}

+ {msg &&

{msg}

} + +
+
+

Profile

+
+ {['player_name', 'profile_level', 'free_currency', 'donate_currency', 'dust_currency'].map(f => ( +
+ + setForm({ ...form, [f]: e.target.value })} + className="w-full px-2 py-1 bg-gray-700 rounded text-white text-sm" /> +
+ ))} +
+ +
+ +
+

Battle Pass

+ {data.battlePass && ( +
+

Level: {data.battlePass.level}

+

XP: {data.battlePass.experience}

+

Premium: {data.battlePass.has_premium ? 'Yes' : 'No'}

+ Edit BP → +
+ )} +
+ +
+

Recent Purchases ({data.purchases?.length})

+
+ {data.purchases?.map((p: any, i: number) => ( +
{p.item_id} ({p.item_category})
+ ))} + {!data.purchases?.length &&

None

} +
+
+ +
+

Recent Matches ({data.matches?.length})

+
+ {data.matches?.map((m: any, i: number) => ( +
+ {m.hero} — {m.result} + ({m.difficulty}) +
+ ))} + {!data.matches?.length &&

None

} +
+
+
+
+ ); +} diff --git a/backend/src/app/admin/players/page.tsx b/backend/src/app/admin/players/page.tsx new file mode 100644 index 0000000..3169d8f --- /dev/null +++ b/backend/src/app/admin/players/page.tsx @@ -0,0 +1,51 @@ +'use client'; +import { useEffect, useState } from 'react'; + +export default function PlayersListPage() { + const [players, setPlayers] = useState([]); + const [search, setSearch] = useState(''); + + useEffect(() => { + fetch('/api/admin/players').then(r => r.json()).then(setPlayers).catch(() => {}); + }, []); + + const filtered = players.filter(p => + p.steam_id?.includes(search) || p.player_name?.toLowerCase().includes(search.toLowerCase()) + ); + + return ( +
+

Players

+ setSearch(e.target.value)} + className="w-full max-w-md px-3 py-2 bg-gray-800 rounded mb-4 text-white" /> +
+ + + + + + + + + + + + + {filtered.map(p => ( + window.location.href = `/admin/players/${p.steam_id}`}> + + + + + + + + ))} + +
Steam IDNameLevelFreeDonateDust
{p.steam_id}{p.player_name}{p.profile_level}{p.free_currency}{p.donate_currency}{p.dust_currency}
+
+
+ ); +} diff --git a/backend/src/app/api/admin/players/[steamId]/route.ts b/backend/src/app/api/admin/players/[steamId]/route.ts new file mode 100644 index 0000000..17eaf8f --- /dev/null +++ b/backend/src/app/api/admin/players/[steamId]/route.ts @@ -0,0 +1,43 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDb } from '@/lib/db'; + +export async function GET(request: NextRequest, { params }: { params: { steamId: string } }) { + const db = getDb(); + const player = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(params.steamId); + if (!player) return NextResponse.json({ error: 'Not found' }, { status: 404 }); + + const bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(params.steamId); + const purchases = db.prepare('SELECT * FROM purchases WHERE steam_id = ? ORDER BY created_at DESC LIMIT 50').all(params.steamId); + const matches = db.prepare('SELECT * FROM game_history WHERE steam_id = ? ORDER BY created_at DESC LIMIT 20').all(params.steamId); + const effects = db.prepare('SELECT effects FROM active_effects WHERE steam_id = ?').get(params.steamId) as any; + + return NextResponse.json({ + player, battlePass: bp, purchases, matches, + activeEffects: effects ? JSON.parse(effects.effects) : {}, + }); +} + +export async function PUT(request: NextRequest, { params }: { params: { steamId: string } }) { + const body = await request.json(); + const db = getDb(); + const fields: string[] = []; + const values: any[] = []; + + for (const key of ['player_name', 'profile_level', 'free_currency', 'donate_currency', 'dust_currency']) { + if (body[key] !== undefined) { + fields.push(`${key} = ?`); + values.push(body[key]); + } + } + if (body.sounds_wheel !== undefined) { + fields.push('sounds_wheel = ?'); + values.push(JSON.stringify(body.sounds_wheel)); + } + if (fields.length === 0) return NextResponse.json({ error: 'No fields to update' }, { status: 400 }); + + fields.push("updated_at = datetime('now')"); + values.push(params.steamId); + db.prepare(`UPDATE players SET ${fields.join(', ')} WHERE steam_id = ?`).run(...values); + + return NextResponse.json({ success: true }); +} diff --git a/backend/src/app/api/admin/players/route.ts b/backend/src/app/api/admin/players/route.ts new file mode 100644 index 0000000..0a1c880 --- /dev/null +++ b/backend/src/app/api/admin/players/route.ts @@ -0,0 +1,8 @@ +import { NextResponse } from 'next/server'; +import { getDb } from '@/lib/db'; + +export async function GET() { + const db = getDb(); + const players = db.prepare('SELECT * FROM players ORDER BY updated_at DESC LIMIT 100').all(); + return NextResponse.json(players); +} diff --git a/backend/src/app/api/admin/stats/route.ts b/backend/src/app/api/admin/stats/route.ts new file mode 100644 index 0000000..f5dfc8b --- /dev/null +++ b/backend/src/app/api/admin/stats/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from 'next/server'; +import { getDb } from '@/lib/db'; + +export async function GET() { + const db = getDb(); + const players = (db.prepare('SELECT COUNT(*) as c FROM players').get() as any).c; + const games = (db.prepare('SELECT COUNT(*) as c FROM game_history').get() as any).c; + const activeBps = (db.prepare('SELECT COUNT(*) as c FROM battle_passes').get() as any).c; + const questsCompleted = (db.prepare('SELECT COUNT(*) as c FROM battle_pass_quests WHERE completed = 1').get() as any).c; + + return NextResponse.json({ players, games, activeBps, questsCompleted }); +}