feat: add admin promocodes and store pages
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
Reference in New Issue
Block a user