196 lines
7.5 KiB
TypeScript
196 lines
7.5 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { useSession } from 'next-auth/react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { Header } from '@/components/header'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Card } from '@/components/ui/card'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Label } from '@/components/ui/label'
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
import { toast } from 'sonner'
|
|
import { Crown, DollarSign, Search } from 'lucide-react'
|
|
|
|
export default function AdminPage() {
|
|
const { data: session, status } = useSession()
|
|
const router = useRouter()
|
|
const [targetUser, setTargetUser] = useState('')
|
|
const [amount, setAmount] = useState('')
|
|
const [action, setAction] = useState('add')
|
|
const [loading, setLoading] = useState(false)
|
|
const [noticeMessage, setNoticeMessage] = useState('')
|
|
const [noticeReason, setNoticeReason] = useState('')
|
|
const [noticeLoading, setNoticeLoading] = useState(false)
|
|
|
|
if (status === 'loading') return null
|
|
|
|
if (!session?.user?.isAdmin) {
|
|
router.push('/')
|
|
return null
|
|
}
|
|
|
|
const handleUpdateBalance = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
setLoading(true)
|
|
|
|
try {
|
|
const res = await fetch('/api/admin/balance', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
usernameOrId: targetUser,
|
|
amount: parseFloat(amount),
|
|
action
|
|
})
|
|
})
|
|
|
|
const data = await res.json()
|
|
if (res.ok) {
|
|
toast.success(`Success! ${data.user} balance updated to ${data.newBalance.toFixed(4)} SOL`)
|
|
setAmount('')
|
|
} else {
|
|
toast.error(data.error || 'Failed')
|
|
}
|
|
} catch (e) {
|
|
toast.error('Error executing admin command')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleSetNotice = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
setNoticeLoading(true)
|
|
|
|
try {
|
|
const res = await fetch('/api/notice', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
message: noticeMessage,
|
|
reason: noticeReason || undefined
|
|
})
|
|
})
|
|
|
|
const data = await res.json()
|
|
if (res.ok) {
|
|
toast.success('Notice set successfully!')
|
|
setNoticeMessage('')
|
|
setNoticeReason('')
|
|
} else {
|
|
toast.error(data.error || 'Failed to set notice')
|
|
}
|
|
} catch (e) {
|
|
toast.error('Error setting notice')
|
|
} finally {
|
|
setNoticeLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background">
|
|
<Header />
|
|
<div className="container mx-auto py-20 px-4">
|
|
<div className="flex items-center gap-2 mb-8">
|
|
<Crown className="w-8 h-8 text-red-500" />
|
|
<h1 className="text-3xl font-bold">Admin Dashboard</h1>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
<Card className="p-6">
|
|
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
|
|
<DollarSign className="w-5 h-5" />
|
|
Manage User Balance
|
|
</h2>
|
|
<form onSubmit={handleUpdateBalance} className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label>Target User</Label>
|
|
<div className="relative">
|
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="Username or Discord ID"
|
|
className="pl-8"
|
|
value={targetUser}
|
|
onChange={e => setTargetUser(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>Action</Label>
|
|
<Select value={action} onValueChange={setAction}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="add">Add (+)</SelectItem>
|
|
<SelectItem value="subtract">Subtract (-)</SelectItem>
|
|
<SelectItem value="set">Set (=)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Amount (SOL)</Label>
|
|
<Input
|
|
type="number"
|
|
step="0.0001"
|
|
placeholder="0.00"
|
|
value={amount}
|
|
onChange={e => setAmount(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<Button type="submit" className="w-full" disabled={loading || !targetUser || !amount}>
|
|
{loading ? 'Executing...' : 'Update Balance'}
|
|
</Button>
|
|
</form>
|
|
</Card>
|
|
|
|
<Card className="p-6">
|
|
<h2 className="text-xl font-semibold mb-4">Set Site Notice</h2>
|
|
<form onSubmit={handleSetNotice} className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label>Notice Message</Label>
|
|
<Input
|
|
placeholder="Enter notice message..."
|
|
value={noticeMessage}
|
|
onChange={e => setNoticeMessage(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>Reason (Optional)</Label>
|
|
<Input
|
|
placeholder="Reason for notice..."
|
|
value={noticeReason}
|
|
onChange={e => setNoticeReason(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<Button type="submit" className="w-full" disabled={noticeLoading || !noticeMessage.trim()}>
|
|
{noticeLoading ? 'Setting...' : 'Set Notice'}
|
|
</Button>
|
|
</form>
|
|
</Card>
|
|
|
|
<Card className="p-6">
|
|
<h2 className="text-xl font-semibold mb-4">Quick Links</h2>
|
|
<div className="space-y-2">
|
|
<p className="text-sm text-muted-foreground">
|
|
Use the scripts in /scripts folder for bulk actions:
|
|
</p>
|
|
<ul className="list-disc list-inside text-sm space-y-1 font-mono bg-muted/30 p-4 rounded-lg">
|
|
<li>npx ts-node scripts/set-role.ts <user> admin true</li>
|
|
<li>npx ts-node scripts/give-beta-to-all.ts</li>
|
|
<li>npx ts-node scripts/database-reset.ts [reason]</li>
|
|
</ul>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|