165 lines
6.4 KiB
TypeScript
165 lines
6.4 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect } from "react";
|
||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "./ui/card";
|
||
import { Button } from "./ui/button";
|
||
import { Input } from "./ui/input";
|
||
import { Badge } from "./ui/badge";
|
||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogTrigger } from "./ui/dialog";
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
||
import { Separator } from "./ui/separator";
|
||
|
||
type Campaign = {
|
||
id: string;
|
||
name: string;
|
||
gridSize: number;
|
||
status: string;
|
||
createdAt: string;
|
||
};
|
||
|
||
export function AdminDashboard() {
|
||
const [campaigns, setCampaigns] = useState<Campaign[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [newName, setNewName] = useState("");
|
||
const [newGridSize, setNewGridSize] = useState("5");
|
||
const [creating, setCreating] = useState(false);
|
||
const [dialogOpen, setDialogOpen] = useState(false);
|
||
|
||
const fetchCampaigns = async () => {
|
||
const res = await fetch("/api/campaigns");
|
||
const data = await res.json();
|
||
setCampaigns(data);
|
||
setLoading(false);
|
||
};
|
||
|
||
useEffect(() => { fetchCampaigns(); }, []);
|
||
|
||
const createCampaign = async () => {
|
||
if (!newName) return;
|
||
setCreating(true);
|
||
await fetch("/api/campaigns", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ name: newName, gridSize: parseInt(newGridSize) }),
|
||
});
|
||
setNewName("");
|
||
setDialogOpen(false);
|
||
await fetchCampaigns();
|
||
setCreating(false);
|
||
};
|
||
|
||
const deleteCampaign = async (id: string) => {
|
||
await fetch(`/api/campaigns/${id}`, { method: "DELETE" });
|
||
await fetchCampaigns();
|
||
};
|
||
|
||
const statusColors: Record<string, "success" | "warning" | "secondary"> = {
|
||
active: "success",
|
||
completed: "warning",
|
||
archived: "secondary",
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-6 w-full max-w-2xl mx-auto">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h2 className="text-xl font-mono text-cyan-300 uppercase tracking-wider">⚙ Command Center</h2>
|
||
<p className="text-xs text-slate-500 font-mono">Admin terminal — v1.0</p>
|
||
</div>
|
||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||
<DialogTrigger asChild>
|
||
<Button className="font-mono text-xs">
|
||
+ NEW CAMPAIGN
|
||
</Button>
|
||
</DialogTrigger>
|
||
<DialogContent>
|
||
<DialogHeader>
|
||
<DialogTitle className="font-mono text-cyan-300">New Campaign</DialogTitle>
|
||
<DialogDescription className="font-mono text-xs text-slate-500">
|
||
Deploy a new bingo operation
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="space-y-4 py-4">
|
||
<div className="space-y-2">
|
||
<label className="text-xs font-mono text-slate-500 uppercase">Mission Name</label>
|
||
<Input
|
||
placeholder='e.g. "Traitor Hunt #42"'
|
||
value={newName}
|
||
onChange={e => setNewName(e.target.value)}
|
||
className="font-mono"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label className="text-xs font-mono text-slate-500 uppercase">Grid Size</label>
|
||
<Select value={newGridSize} onValueChange={setNewGridSize}>
|
||
<SelectTrigger className="font-mono">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="3">3×3 (Quick)</SelectItem>
|
||
<SelectItem value="4">4×4</SelectItem>
|
||
<SelectItem value="5">5×5 (Classic)</SelectItem>
|
||
<SelectItem value="6">6×6 (Chaotic)</SelectItem>
|
||
<SelectItem value="7">7×7 (Meltdown)</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => setDialogOpen(false)} className="font-mono">Cancel</Button>
|
||
<Button onClick={createCampaign} disabled={creating || !newName} className="font-mono">
|
||
{creating ? "DEPLOYING..." : "▶ DEPLOY"}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>
|
||
|
||
<Separator />
|
||
|
||
{loading ? (
|
||
<div className="text-center text-slate-500 font-mono text-sm py-8">
|
||
Loading submarine manifests...
|
||
</div>
|
||
) : campaigns.length === 0 ? (
|
||
<Card className="border-dashed border-slate-700/30">
|
||
<CardContent className="py-12 text-center">
|
||
<div className="text-4xl mb-3">🗺️💀</div>
|
||
<p className="text-slate-500 font-mono text-sm">No campaigns deployed</p>
|
||
<p className="text-slate-600 font-mono text-xs mt-1">Click “New Campaign” to start the chaos</p>
|
||
</CardContent>
|
||
</Card>
|
||
) : (
|
||
<div className="space-y-2">
|
||
{campaigns.map(c => (
|
||
<Card key={c.id} className="border-slate-700/30 hover:border-cyan-800/30 transition-colors">
|
||
<CardContent className="p-4 flex items-center justify-between">
|
||
<div>
|
||
<div className="flex items-center gap-2">
|
||
<span className="font-mono text-sm text-slate-200">{c.name}</span>
|
||
<Badge variant={statusColors[c.status] || "secondary"} className="text-[9px] uppercase">
|
||
{c.status}
|
||
</Badge>
|
||
</div>
|
||
<div className="flex gap-3 mt-1 text-[10px] text-slate-600 font-mono">
|
||
<span>{c.gridSize}×{c.gridSize}</span>
|
||
<span>{new Date(c.createdAt).toLocaleDateString()}</span>
|
||
</div>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<a href={`/admin/campaigns/${c.id}`}>
|
||
<Button variant="ghost" size="sm" className="text-xs font-mono">EDIT</Button>
|
||
</a>
|
||
<Button variant="destructive" size="sm" className="text-xs font-mono" onClick={() => deleteCampaign(c.id)}>
|
||
DEL
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|