feat: admin requests tab + 30s disk cache + sync all pending

This commit is contained in:
2026-04-23 13:58:31 -04:00
parent 3f37690e50
commit 8b31045dcf
4 changed files with 164 additions and 9 deletions
+77 -8
View File
@@ -4,7 +4,9 @@ import {
ArrowLeft,
Coffee,
Database,
Film,
Gift,
List,
RefreshCcw,
Save,
Search,
@@ -81,6 +83,7 @@ export default function AdminPage() {
const [users, setUsers] = useState<User[]>([]);
const [settings, setSettings] = useState<SystemSettings | null>(null);
const [kofiPayments, setKofiPayments] = useState<KofiPayment[]>([]);
const [requests, setRequests] = useState<any[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [isLoading, setIsLoading] = useState(true);
const [isSavingSettings, setIsSavingSettings] = useState(false);
@@ -96,16 +99,19 @@ export default function AdminPage() {
const loadData = async () => {
try {
const [analyticsRes, usersRes, settingsRes, kofiRes] = await Promise.all([
adminApi.getAnalytics(),
adminApi.getUsers(),
adminApi.getSettings(),
adminApi.getKofiPayments(),
]);
const [analyticsRes, usersRes, settingsRes, kofiRes, requestsRes] =
await Promise.all([
adminApi.getAnalytics(),
adminApi.getUsers(),
adminApi.getSettings(),
adminApi.getKofiPayments(),
adminApi.getAllRequests(),
]);
setAnalytics(analyticsRes.data);
setUsers(usersRes.data.users || []);
setSettings(settingsRes.data);
setKofiPayments(kofiRes.data.payments || []);
setRequests(requestsRes.data.requests || []);
} catch (err) {
console.error("Admin load error:", err);
toast.error("Failed to load coop secrets");
@@ -201,7 +207,7 @@ export default function AdminPage() {
</div>
<Tabs defaultValue="flock" className="w-full">
<TabsList className="grid w-full grid-cols-4 gap-2 rounded-full border-4 border-foreground bg-muted/30 p-2 mb-8 h-auto">
<TabsList className="grid w-full grid-cols-5 gap-2 rounded-full border-4 border-foreground bg-muted/30 p-2 mb-8 h-auto">
<TabsTrigger
value="flock"
className="rounded-full py-3 font-black uppercase data-[state=active]:bg-primary data-[state=active]:text-primary-foreground transition-all"
@@ -220,6 +226,12 @@ export default function AdminPage() {
>
<Coffee className="mr-2 h-4 w-4" /> DONATIONS
</TabsTrigger>
<TabsTrigger
value="requests"
className="rounded-full py-3 font-black uppercase data-[state=active]:bg-primary data-[state=active]:text-primary-foreground transition-all"
>
<Film className="mr-2 h-4 w-4" /> REQUESTS
</TabsTrigger>
<TabsTrigger
value="stats"
className="rounded-full py-3 font-black uppercase data-[state=active]:bg-primary data-[state=active]:text-primary-foreground transition-all"
@@ -502,8 +514,65 @@ export default function AdminPage() {
</div>
</TabsContent>
<TabsContent value="requests" className="focus-visible:outline-none">
<div className="rounded-[2.5rem] border-4 border-foreground bg-card p-6 shadow-[10px_10px_0px_0px_rgba(0,0,0,0.05)]">
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<h2 className="font-display text-3xl uppercase italic">The Feed Queue</h2>
<Button
onClick={async () => {
toast.info("Checking with Overseer...");
try {
const res = await adminApi.syncAllRequests();
toast.success(`Synced ${res.data.updated?.length || 0} requests`);
loadData();
} catch {
toast.error("Sync failed");
}
}}
variant="outline"
className="rounded-full border-2 border-primary font-bold"
>
<RefreshCcw className="mr-2 h-4 w-4" /> SYNC
</Button>
</div>
{requests.length === 0 ? (
<div className="text-center py-10 opacity-40">
<div className="text-6xl mb-4">📭</div>
<div className="font-black uppercase tracking-widest text-sm">No requests in the queue.</div>
</div>
) : (
<div className="space-y-3">
{requests.map((r) => (
<div
key={r.id}
className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 rounded-2xl border-4 border-foreground bg-accent/5 p-4"
>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-black text-lg">{r.title}</span>
<span className={`rounded-full px-2 py-0.5 text-[10px] font-black uppercase ${r.status === "APPROVED" ? "bg-primary text-white" : r.status === "DECLINED" ? "bg-destructive text-white" : "bg-secondary text-secondary-foreground"}`}>
{r.status}
</span>
</div>
<div className="text-sm font-bold text-muted-foreground mt-1">
{r.user?.plexUsername || "Unknown"} • {r.mediaType === "movie" ? "Movie" : "TV"} • {formatNumber(r.creditsCost)} $COOP
</div>
</div>
<div className="text-right">
<div className="text-[10px] font-bold text-muted-foreground uppercase">
{new Date(r.requestedAt).toLocaleDateString()}
</div>
</div>
</div>
))}
</div>
)}
</div>
</TabsContent>
<TabsContent value="stats" className="focus-visible:outline-none">
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-5">
<div className="rounded-3xl border-4 border-foreground bg-card p-6 shadow-[6px_6px_0px_0px_rgba(0,0,0,1)]">
<div className="text-xs font-black uppercase tracking-widest text-muted-foreground mb-2 flex items-center gap-2">
<Users className="h-3 w-3" /> Flock Size
+2
View File
@@ -95,6 +95,8 @@ export const adminApi = {
getSystemStats: () => api.get("/transactions/admin/stats"),
backfillUser: (userId: string) => api.post(`/admin/users/${userId}/backfill`),
getKofiPayments: () => api.get("/admin/kofi-payments"),
getAllRequests: () => api.get("/admin/requests"),
syncAllRequests: () => api.post("/admin/sync-requests"),
};
// Overseer API