feat: add admin battle pass and matches pages

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
achmad
2026-05-29 17:03:28 +07:00
parent 2239f590ab
commit e742b662c4
6 changed files with 233 additions and 0 deletions
@@ -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<any>(null);
const [quests, setQuests] = useState<any[]>([]);
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 <div className="text-gray-400">Loading...</div>;
return (
<div>
<a href="/admin/battlepass" className="text-amber-400 mb-4 block">&larr; Back</a>
<h1 className="text-2xl font-bold mb-2">Battle Pass</h1>
<p className="font-mono text-sm text-gray-400 mb-4">Steam ID: {steamId}</p>
{msg && <p className="mb-4 text-sm text-green-400">{msg}</p>}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-gray-800 rounded-lg p-4">
<h2 className="text-lg font-semibold mb-3">Settings</h2>
<div className="grid grid-cols-3 gap-3">
<div>
<label className="text-xs text-gray-400 block mb-1">Level</label>
<input type="number" value={editLevel} onChange={e => setEditLevel(Number(e.target.value))}
className="w-full px-2 py-1 bg-gray-700 rounded text-white text-sm" />
</div>
<div>
<label className="text-xs text-gray-400 block mb-1">XP</label>
<input type="number" value={editXp} onChange={e => setEditXp(Number(e.target.value))}
className="w-full px-2 py-1 bg-gray-700 rounded text-white text-sm" />
</div>
<div>
<label className="text-xs text-gray-400 block mb-1">Premium</label>
<input type="checkbox" checked={editPremium} onChange={e => setEditPremium(e.target.checked)}
className="mt-2 block" />
</div>
</div>
<button onClick={saveBp} className="mt-4 bg-amber-500 hover:bg-amber-600 text-black px-4 py-2 rounded font-semibold text-sm">Save BP</button>
</div>
<div className="bg-gray-800 rounded-lg p-4">
<h2 className="text-lg font-semibold mb-3">Quests ({quests.length})</h2>
<div className="space-y-2 max-h-96 overflow-y-auto">
{quests.map((q: any) => (
<div key={q.id} className="bg-gray-700 rounded p-3 text-sm flex items-center justify-between">
<div>
<div className="text-amber-300">{q.name || q.quest_id}</div>
<div className="text-gray-400 text-xs">{q.type} &mdash; {q.progress}/{q.target}</div>
<div className="text-xs">
<span className={q.completed ? 'text-green-400' : 'text-yellow-400'}>
{q.completed ? 'Completed' : 'In Progress'}
</span>
{q.claimed ? ' &mdash; Claimed' : ''}
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
}
+41
View File
@@ -0,0 +1,41 @@
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
export default function BattlePassListPage() {
const [bps, setBps] = useState<any[]>([]);
useEffect(() => {
fetch('/api/admin/battlepass').then(r => r.json()).then(setBps).catch(() => {});
}, []);
return (
<div>
<h1 className="text-2xl font-bold mb-4">Battle Passes</h1>
<div className="bg-gray-800 rounded-lg overflow-hidden">
<table className="w-full text-left">
<thead>
<tr className="border-b border-gray-700 text-sm text-gray-400">
<th className="p-3">Steam ID</th>
<th className="p-3">Level</th>
<th className="p-3">XP</th>
<th className="p-3">Premium</th>
</tr>
</thead>
<tbody>
{bps.map(bp => (
<tr key={bp.steam_id} className="border-b border-gray-700 hover:bg-gray-750">
<td className="p-3">
<Link href={`/admin/battlepass/${bp.steam_id}`} className="text-amber-400 font-mono text-sm">{bp.steam_id}</Link>
</td>
<td className="p-3">{bp.level}</td>
<td className="p-3">{bp.experience}</td>
<td className="p-3">{bp.has_premium ? 'Yes' : 'No'}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
+57
View File
@@ -0,0 +1,57 @@
'use client';
import { useEffect, useState } from 'react';
export default function MatchesPage() {
const [matches, setMatches] = useState<any[]>([]);
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 (
<div>
<h1 className="text-2xl font-bold mb-4">Match History</h1>
<div className="flex gap-4 mb-4">
<input type="text" placeholder="Filter by hero..." value={heroFilter} onChange={e => setHeroFilter(e.target.value)}
className="px-3 py-2 bg-gray-800 rounded text-white text-sm" />
<select value={diffFilter} onChange={e => setDiffFilter(e.target.value)}
className="px-3 py-2 bg-gray-800 rounded text-white text-sm">
{['', 'easy', 'normal', 'hard', 'impossible', 'death_sentence'].map(d =>
<option key={d} value={d}>{d || 'All Difficulties'}</option>
)}
</select>
</div>
<div className="bg-gray-800 rounded-lg overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-gray-700 text-gray-400">
<th className="p-2">Steam ID</th><th className="p-2">Hero</th><th className="p-2">Result</th>
<th className="p-2">Difficulty</th><th className="p-2">K/D</th><th className="p-2">Duration</th>
<th className="p-2">Date</th>
</tr>
</thead>
<tbody>
{matches.map((m: any) => (
<tr key={m.id} className="border-b border-gray-700">
<td className="p-2 font-mono text-xs">{m.steam_id}</td>
<td className="p-2">{m.hero}</td>
<td className={`p-2 font-semibold ${m.result === 'win' ? 'text-green-400' : 'text-red-400'}`}>{m.result}</td>
<td className="p-2">{m.difficulty}</td>
<td className="p-2">{m.kills}/{m.deaths}</td>
<td className="p-2">{Math.floor((m.duration || 0) / 60)}m</td>
<td className="p-2 text-gray-400 text-xs">{m.created_at}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
@@ -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 });
}
@@ -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);
}
@@ -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);
}