From e742b662c44ccef4fa9d7eadebc59add012d6a3e Mon Sep 17 00:00:00 2001 From: achmad Date: Fri, 29 May 2026 17:03:28 +0700 Subject: [PATCH] feat: add admin battle pass and matches pages Co-Authored-By: Claude Opus 4.8 (1M context) --- .../app/admin/battlepass/[steamId]/page.tsx | 87 +++++++++++++++++++ backend/src/app/admin/battlepass/page.tsx | 41 +++++++++ backend/src/app/admin/matches/page.tsx | 57 ++++++++++++ .../api/admin/battlepass/[steamId]/route.ts | 17 ++++ backend/src/app/api/admin/battlepass/route.ts | 8 ++ backend/src/app/api/admin/matches/route.ts | 23 +++++ 6 files changed, 233 insertions(+) create mode 100644 backend/src/app/admin/battlepass/[steamId]/page.tsx create mode 100644 backend/src/app/admin/battlepass/page.tsx create mode 100644 backend/src/app/admin/matches/page.tsx create mode 100644 backend/src/app/api/admin/battlepass/[steamId]/route.ts create mode 100644 backend/src/app/api/admin/battlepass/route.ts create mode 100644 backend/src/app/api/admin/matches/route.ts diff --git a/backend/src/app/admin/battlepass/[steamId]/page.tsx b/backend/src/app/admin/battlepass/[steamId]/page.tsx new file mode 100644 index 0000000..488bc99 --- /dev/null +++ b/backend/src/app/admin/battlepass/[steamId]/page.tsx @@ -0,0 +1,87 @@ +'use client'; +import { useEffect, useState } from 'react'; +import { useParams } from 'next/navigation'; + +export default function BattlePassDetailPage() { + const { steamId } = useParams(); + const [bp, setBp] = useState(null); + const [quests, setQuests] = useState([]); + const [editLevel, setEditLevel] = useState(0); + const [editXp, setEditXp] = useState(0); + const [editPremium, setEditPremium] = useState(false); + const [msg, setMsg] = useState(''); + + const load = () => { + fetch(`/api/admin/battlepass/${steamId}`).then(r => r.json()).then(d => { + setBp(d.battlePass); + setQuests(d.quests || []); + setEditLevel(d.battlePass?.level || 0); + setEditXp(d.battlePass?.experience || 0); + setEditPremium(d.battlePass?.has_premium === 1); + }).catch(() => {}); + }; + useEffect(load, [steamId]); + + const saveBp = async () => { + const res = await fetch(`/api/admin/battlepass/${steamId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ level: editLevel, experience: editXp, has_premium: editPremium }), + }); + const d = await res.json(); + setMsg(d.success ? 'Saved!' : 'Error'); + }; + + if (!bp) return
Loading...
; + + return ( +
+ ← Back +

Battle Pass

+

Steam ID: {steamId}

+ {msg &&

{msg}

} +
+
+

Settings

+
+
+ + setEditLevel(Number(e.target.value))} + className="w-full px-2 py-1 bg-gray-700 rounded text-white text-sm" /> +
+
+ + setEditXp(Number(e.target.value))} + className="w-full px-2 py-1 bg-gray-700 rounded text-white text-sm" /> +
+
+ + setEditPremium(e.target.checked)} + className="mt-2 block" /> +
+
+ +
+
+

Quests ({quests.length})

+
+ {quests.map((q: any) => ( +
+
+
{q.name || q.quest_id}
+
{q.type} — {q.progress}/{q.target}
+
+ + {q.completed ? 'Completed' : 'In Progress'} + + {q.claimed ? ' — Claimed' : ''} +
+
+
+ ))} +
+
+
+
+ ); +} diff --git a/backend/src/app/admin/battlepass/page.tsx b/backend/src/app/admin/battlepass/page.tsx new file mode 100644 index 0000000..5b4bf6c --- /dev/null +++ b/backend/src/app/admin/battlepass/page.tsx @@ -0,0 +1,41 @@ +'use client'; +import { useEffect, useState } from 'react'; +import Link from 'next/link'; + +export default function BattlePassListPage() { + const [bps, setBps] = useState([]); + + useEffect(() => { + fetch('/api/admin/battlepass').then(r => r.json()).then(setBps).catch(() => {}); + }, []); + + return ( +
+

Battle Passes

+
+ + + + + + + + + + + {bps.map(bp => ( + + + + + + + ))} + +
Steam IDLevelXPPremium
+ {bp.steam_id} + {bp.level}{bp.experience}{bp.has_premium ? 'Yes' : 'No'}
+
+
+ ); +} diff --git a/backend/src/app/admin/matches/page.tsx b/backend/src/app/admin/matches/page.tsx new file mode 100644 index 0000000..54ed037 --- /dev/null +++ b/backend/src/app/admin/matches/page.tsx @@ -0,0 +1,57 @@ +'use client'; +import { useEffect, useState } from 'react'; + +export default function MatchesPage() { + const [matches, setMatches] = useState([]); + const [heroFilter, setHeroFilter] = useState(''); + const [diffFilter, setDiffFilter] = useState(''); + + const load = () => { + const params = new URLSearchParams(); + if (heroFilter) params.set('hero', heroFilter); + if (diffFilter) params.set('difficulty', diffFilter); + fetch(`/api/admin/matches?${params}`).then(r => r.json()).then(setMatches).catch(() => {}); + }; + + useEffect(() => { load(); }, [heroFilter, diffFilter]); + + return ( +
+

Match History

+
+ setHeroFilter(e.target.value)} + className="px-3 py-2 bg-gray-800 rounded text-white text-sm" /> + +
+
+ + + + + + + + + + {matches.map((m: any) => ( + + + + + + + + + + ))} + +
Steam IDHeroResultDifficultyK/DDurationDate
{m.steam_id}{m.hero}{m.result}{m.difficulty}{m.kills}/{m.deaths}{Math.floor((m.duration || 0) / 60)}m{m.created_at}
+
+
+ ); +} diff --git a/backend/src/app/api/admin/battlepass/[steamId]/route.ts b/backend/src/app/api/admin/battlepass/[steamId]/route.ts new file mode 100644 index 0000000..e021c0b --- /dev/null +++ b/backend/src/app/api/admin/battlepass/[steamId]/route.ts @@ -0,0 +1,17 @@ +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 battlePass = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(params.steamId); + const quests = db.prepare('SELECT * FROM battle_pass_quests WHERE steam_id = ? ORDER BY id').all(params.steamId); + return NextResponse.json({ battlePass, quests }); +} + +export async function PUT(request: NextRequest, { params }: { params: { steamId: string } }) { + const body = await request.json(); + const db = getDb(); + db.prepare('UPDATE battle_passes SET level = ?, experience = ?, has_premium = ?, updated_at = datetime(\'now\') WHERE steam_id = ?') + .run(body.level, body.experience, body.has_premium ? 1 : 0, params.steamId); + return NextResponse.json({ success: true }); +} diff --git a/backend/src/app/api/admin/battlepass/route.ts b/backend/src/app/api/admin/battlepass/route.ts new file mode 100644 index 0000000..2ac168b --- /dev/null +++ b/backend/src/app/api/admin/battlepass/route.ts @@ -0,0 +1,8 @@ +import { NextResponse } from 'next/server'; +import { getDb } from '@/lib/db'; + +export async function GET() { + const db = getDb(); + const bps = db.prepare('SELECT * FROM battle_passes ORDER BY updated_at DESC').all(); + return NextResponse.json(bps); +} diff --git a/backend/src/app/api/admin/matches/route.ts b/backend/src/app/api/admin/matches/route.ts new file mode 100644 index 0000000..730ddfc --- /dev/null +++ b/backend/src/app/api/admin/matches/route.ts @@ -0,0 +1,23 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDb } from '@/lib/db'; + +export async function GET(request: NextRequest) { + const db = getDb(); + const { searchParams } = new URL(request.url); + const hero = searchParams.get('hero'); + const difficulty = searchParams.get('difficulty'); + const limit = parseInt(searchParams.get('limit') || '100'); + const offset = parseInt(searchParams.get('offset') || '0'); + + let query = 'SELECT * FROM game_history WHERE 1=1'; + const params: any[] = []; + + if (hero) { query += ' AND hero LIKE ?'; params.push(`%${hero}%`); } + if (difficulty) { query += ' AND difficulty = ?'; params.push(difficulty); } + + query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?'; + params.push(limit, offset); + + const matches = db.prepare(query).all(...params); + return NextResponse.json(matches); +}