From 3f7bda10eb8ee420f69c63c76edebcb4802bee04 Mon Sep 17 00:00:00 2001 From: achmad Date: Fri, 29 May 2026 17:04:27 +0700 Subject: [PATCH] feat: add admin promocodes and store pages Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/src/app/admin/promocodes/page.tsx | 71 +++++++++++++++++++ backend/src/app/admin/store/page.tsx | 63 ++++++++++++++++ backend/src/app/api/admin/promocodes/route.ts | 25 +++++++ backend/src/app/api/admin/store/route.ts | 13 ++++ 4 files changed, 172 insertions(+) create mode 100644 backend/src/app/admin/promocodes/page.tsx create mode 100644 backend/src/app/admin/store/page.tsx create mode 100644 backend/src/app/api/admin/promocodes/route.ts create mode 100644 backend/src/app/api/admin/store/route.ts diff --git a/backend/src/app/admin/promocodes/page.tsx b/backend/src/app/admin/promocodes/page.tsx new file mode 100644 index 0000000..11a2ee5 --- /dev/null +++ b/backend/src/app/admin/promocodes/page.tsx @@ -0,0 +1,71 @@ +'use client'; +import { useEffect, useState } from 'react'; + +export default function PromoCodesPage() { + const [codes, setCodes] = useState([]); + 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 ( +
+

Promo Codes

+ +
+

Create Code

+
+ setForm({...form, code: e.target.value})} + className="px-2 py-1 bg-gray-700 rounded text-white text-sm" /> + setForm({...form, free_currency: Number(e.target.value)})} + className="px-2 py-1 bg-gray-700 rounded text-white text-sm" /> + setForm({...form, donate_currency: Number(e.target.value)})} + className="px-2 py-1 bg-gray-700 rounded text-white text-sm" /> + setForm({...form, dust_currency: Number(e.target.value)})} + className="px-2 py-1 bg-gray-700 rounded text-white text-sm" /> + setForm({...form, max_uses: Number(e.target.value)})} + className="px-2 py-1 bg-gray-700 rounded text-white text-sm" /> +
+ +
+ +
+ + + + + + + + + + {codes.map(c => ( + + + + + + + + + + ))} + +
CodeFreeDonateDustUsesExpires
{c.code}{c.free_currency}{c.donate_currency}{c.dust_currency}{c.current_uses}/{c.max_uses}{c.expires_at || 'Never'}
+
+
+ ); +} diff --git a/backend/src/app/admin/store/page.tsx b/backend/src/app/admin/store/page.tsx new file mode 100644 index 0000000..ebc2c72 --- /dev/null +++ b/backend/src/app/admin/store/page.tsx @@ -0,0 +1,63 @@ +'use client'; +import { useEffect, useState } from 'react'; + +export default function StorePage() { + const [purchases, setPurchases] = useState([]); + const [effects, setEffects] = useState([]); + + useEffect(() => { + fetch('/api/admin/store').then(r => r.json()).then(d => { + setPurchases(d.purchases || []); + setEffects(d.effects || []); + }); + }, []); + + return ( +
+
+

Store Purchases

+
+ + + + + + + + + {purchases.map((p: any) => ( + + + + + + + + ))} + +
PlayerItemCategoryCostDate
{p.player_name || p.steam_id}{p.item_id}{p.item_category}{p.price_free || p.price_donate || p.price_dust || 0}{p.created_at}
+
+
+
+

Active Effects

+
+ + + + + + + + {effects.map((e: any) => ( + + + + + ))} + +
Steam IDEffects
{e.steam_id}{e.effects}
+
+
+
+ ); +} diff --git a/backend/src/app/api/admin/promocodes/route.ts b/backend/src/app/api/admin/promocodes/route.ts new file mode 100644 index 0000000..27ecab2 --- /dev/null +++ b/backend/src/app/api/admin/promocodes/route.ts @@ -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 }); +} diff --git a/backend/src/app/api/admin/store/route.ts b/backend/src/app/api/admin/store/route.ts new file mode 100644 index 0000000..84f2a3c --- /dev/null +++ b/backend/src/app/api/admin/store/route.ts @@ -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 }); +}