feat: add admin promocodes and store pages

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
achmad
2026-05-29 17:04:27 +07:00
parent e742b662c4
commit 3f7bda10eb
4 changed files with 172 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
'use client';
import { useEffect, useState } from 'react';
export default function PromoCodesPage() {
const [codes, setCodes] = useState<any[]>([]);
const [form, setForm] = useState({ code: '', free_currency: 0, donate_currency: 0, dust_currency: 0, max_uses: 1 });
const load = () => { fetch('/api/admin/promocodes').then(r => r.json()).then(setCodes); };
useEffect(load, []);
const create = async () => {
await fetch('/api/admin/promocodes', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form),
});
setForm({ code: '', free_currency: 0, donate_currency: 0, dust_currency: 0, max_uses: 1 });
load();
};
const del = async (code: string) => {
await fetch(`/api/admin/promocodes?code=${code}`, { method: 'DELETE' });
load();
};
return (
<div>
<h1 className="text-2xl font-bold mb-4">Promo Codes</h1>
<div className="bg-gray-800 rounded-lg p-4 mb-6">
<h2 className="text-lg font-semibold mb-3">Create Code</h2>
<div className="grid grid-cols-5 gap-3">
<input type="text" placeholder="CODE" value={form.code} onChange={e => setForm({...form, code: e.target.value})}
className="px-2 py-1 bg-gray-700 rounded text-white text-sm" />
<input type="number" placeholder="Free" value={form.free_currency} onChange={e => setForm({...form, free_currency: Number(e.target.value)})}
className="px-2 py-1 bg-gray-700 rounded text-white text-sm" />
<input type="number" placeholder="Donate" value={form.donate_currency} onChange={e => setForm({...form, donate_currency: Number(e.target.value)})}
className="px-2 py-1 bg-gray-700 rounded text-white text-sm" />
<input type="number" placeholder="Dust" value={form.dust_currency} onChange={e => setForm({...form, dust_currency: Number(e.target.value)})}
className="px-2 py-1 bg-gray-700 rounded text-white text-sm" />
<input type="number" placeholder="Max uses" value={form.max_uses} onChange={e => setForm({...form, max_uses: Number(e.target.value)})}
className="px-2 py-1 bg-gray-700 rounded text-white text-sm" />
</div>
<button onClick={create} className="mt-3 bg-amber-500 hover:bg-amber-600 text-black px-4 py-2 rounded font-semibold text-sm">Create</button>
</div>
<div className="bg-gray-800 rounded-lg overflow-hidden">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-gray-700 text-gray-400">
<th className="p-3">Code</th><th className="p-3">Free</th><th className="p-3">Donate</th>
<th className="p-3">Dust</th><th className="p-3">Uses</th><th className="p-3">Expires</th>
<th className="p-3"></th>
</tr>
</thead>
<tbody>
{codes.map(c => (
<tr key={c.code} className="border-b border-gray-700">
<td className="p-3 font-mono">{c.code}</td>
<td className="p-3">{c.free_currency}</td>
<td className="p-3">{c.donate_currency}</td>
<td className="p-3">{c.dust_currency}</td>
<td className="p-3">{c.current_uses}/{c.max_uses}</td>
<td className="p-3 text-xs">{c.expires_at || 'Never'}</td>
<td className="p-3"><button onClick={() => del(c.code)} className="text-red-400 hover:text-red-300 text-xs">Delete</button></td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
+63
View File
@@ -0,0 +1,63 @@
'use client';
import { useEffect, useState } from 'react';
export default function StorePage() {
const [purchases, setPurchases] = useState<any[]>([]);
const [effects, setEffects] = useState<any[]>([]);
useEffect(() => {
fetch('/api/admin/store').then(r => r.json()).then(d => {
setPurchases(d.purchases || []);
setEffects(d.effects || []);
});
}, []);
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<h1 className="text-2xl font-bold mb-4">Store Purchases</h1>
<div className="bg-gray-800 rounded-lg overflow-x-auto max-h-[70vh] overflow-y-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-gray-700 text-gray-400">
<th className="p-2">Player</th><th className="p-2">Item</th><th className="p-2">Category</th>
<th className="p-2">Cost</th><th className="p-2">Date</th>
</tr>
</thead>
<tbody>
{purchases.map((p: any) => (
<tr key={p.id} className="border-b border-gray-700">
<td className="p-2 font-mono text-xs">{p.player_name || p.steam_id}</td>
<td className="p-2">{p.item_id}</td>
<td className="p-2 text-gray-400">{p.item_category}</td>
<td className="p-2">{p.price_free || p.price_donate || p.price_dust || 0}</td>
<td className="p-2 text-xs text-gray-500">{p.created_at}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div>
<h1 className="text-2xl font-bold mb-4">Active Effects</h1>
<div className="bg-gray-800 rounded-lg">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-gray-700 text-gray-400">
<th className="p-3">Steam ID</th><th className="p-3">Effects</th>
</tr>
</thead>
<tbody>
{effects.map((e: any) => (
<tr key={e.steam_id} className="border-b border-gray-700">
<td className="p-3 font-mono text-xs">{e.steam_id}</td>
<td className="p-3 text-xs">{e.effects}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}
@@ -0,0 +1,25 @@
import { NextRequest, NextResponse } from 'next/server';
import { getDb } from '@/lib/db';
export async function GET() {
const db = getDb();
const codes = db.prepare('SELECT * FROM promo_codes ORDER BY code').all();
return NextResponse.json(codes);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const db = getDb();
db.prepare('INSERT OR REPLACE INTO promo_codes (code, free_currency, donate_currency, dust_currency, max_uses, expires_at) VALUES (?, ?, ?, ?, ?, ?)')
.run(body.code?.toUpperCase(), body.free_currency || 0, body.donate_currency || 0, body.dust_currency || 0, body.max_uses || 1, body.expires_at || null);
return NextResponse.json({ success: true });
}
export async function DELETE(request: NextRequest) {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
if (!code) return NextResponse.json({ error: 'code required' }, { status: 400 });
const db = getDb();
db.prepare('DELETE FROM promo_codes WHERE code = ?').run(code.toUpperCase());
return NextResponse.json({ success: true });
}
+13
View File
@@ -0,0 +1,13 @@
import { NextResponse } from 'next/server';
import { getDb } from '@/lib/db';
export async function GET() {
const db = getDb();
const purchases = db.prepare(`
SELECT p.*, pl.player_name FROM purchases p
LEFT JOIN players pl ON p.steam_id = pl.steam_id
ORDER BY p.created_at DESC LIMIT 200
`).all();
const effects = db.prepare('SELECT * FROM active_effects').all();
return NextResponse.json({ purchases, effects });
}