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:
@@ -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">← 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} — {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 ? ' — Claimed' : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user