From 1707797df40c1b76802dfcfd9d14d70f098c927f Mon Sep 17 00:00:00 2001 From: hobokenchicken Date: Thu, 23 Apr 2026 10:14:04 -0400 Subject: [PATCH] feat: admin barn rules and custom rewards --- frontend/src/app/admin/page.tsx | 443 +++++++++++++++++++++++--------- 1 file changed, 319 insertions(+), 124 deletions(-) diff --git a/frontend/src/app/admin/page.tsx b/frontend/src/app/admin/page.tsx index 2a50b48..f48af85 100644 --- a/frontend/src/app/admin/page.tsx +++ b/frontend/src/app/admin/page.tsx @@ -1,20 +1,19 @@ "use client"; -import { - ArrowLeft, - ChevronRight, - Database, - Minus, - Plus, - RefreshCcw, - Search, - Settings, - ShieldAlert, - TrendingUp, - Users, -} from "lucide-react"; -import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { + Users, + Database, + Settings, + TrendingUp, + ShieldAlert, + Search, + RefreshCcw, + ArrowLeft, + Save, + Gift, +} from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { @@ -25,7 +24,8 @@ import { CardTitle, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; -import { adminApi, api } from "@/lib/api"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { api, adminApi } from "@/lib/api"; import { useStore } from "@/lib/store"; import { formatNumber } from "@/lib/utils"; @@ -52,13 +52,25 @@ interface User { totalSpent: number; } +interface SystemSettings { + creditsPerMinute: number; + movieRequestCost: number; + tvRequestCost: number; + tvPerSeasonCost: number; + minWatchPercent: number; + minWatchMinutes: number; +} + export default function AdminPage() { const router = useRouter(); const { user, isAuthenticated } = useStore(); const [analytics, setAnalytics] = useState(null); const [users, setUsers] = useState([]); + const [settings, setSettings] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [isLoading, setIsLoading] = useState(true); + const [isSavingSettings, setIsSavingSettings] = useState(false); + const [rewardAmount, setRewardAmount] = useState>({}); useEffect(() => { if (!isAuthenticated || !user?.isAdmin) { @@ -70,12 +82,14 @@ export default function AdminPage() { const loadData = async () => { try { - const [analyticsRes, usersRes] = await Promise.all([ - api.get("/admin/analytics"), - api.get("/admin/users"), + const [analyticsRes, usersRes, settingsRes] = await Promise.all([ + adminApi.getAnalytics(), + adminApi.getUsers(), + adminApi.getSettings(), ]); setAnalytics(analyticsRes.data); setUsers(usersRes.data); + setSettings(settingsRes.data); } catch { toast.error("Failed to load coop secrets"); } finally { @@ -83,6 +97,20 @@ export default function AdminPage() { } }; + const handleUpdateSettings = async (e: React.FormEvent) => { + e.preventDefault(); + if (!settings) return; + setIsSavingSettings(true); + try { + await adminApi.updateSettings(settings); + toast.success("Barn rules updated!"); + } catch { + toast.error("Failed to update rules"); + } finally { + setIsSavingSettings(false); + } + }; + const adjustUser = async (userId: string, amount: number, reason: string) => { try { await adminApi.adjustUser(userId, amount, reason); @@ -109,21 +137,20 @@ export default function AdminPage() { if (isLoading) return ( -
- COUNTING THE CHICKENS... +
+ Peeking into the coop...
); return (
- {/* Admin Header */}
@@ -132,122 +159,290 @@ export default function AdminPage() { COOP CONTROL

- Admin Peering Glass + The Farmer's Desk

- {/* Big Stats */} -
-
-
- Flock Size -
-
- {analytics?.users.total || 0} -
-
-
-
- Total $COOP -
-
- {formatNumber(analytics?.transactions.total_earned || 0)} -
-
-
-
- Requests -
-
- {analytics?.requests.total || 0} -
-
-
-
- Pending -
-
- {analytics?.requests.pending || 0} -
-
-
+ + + + THE FLOCK + + + BARN RULES + + + EGG STATS + + - {/* User Management */} -
-
-

- The Peeps -

-
- - setSearchQuery(e.target.value)} - className="rounded-full border-2 border-foreground pl-10 focus-visible:ring-primary" - /> -
-
- -
- {filteredUsers.map((u) => ( -
-
-
- {u.plexUsername} - {u.isAdmin && ( - - Rooster - - )} -
-
- Balance: {formatNumber(u.totalEarned - u.totalSpent)} $COOP -
-
- -
- - - + +
+
+

+ The Peeps +

+
+ + setSearchQuery(e.target.value)} + className="rounded-full border-2 border-foreground pl-10 focus-visible:ring-primary h-12" + />
- ))} -
-
+ +
+ {filteredUsers.map((u) => ( +
+
+
+ + {u.plexUsername} + + {u.isAdmin && ( + + Rooster + + )} +
+
+ Nest Egg: {formatNumber(u.totalEarned - u.totalSpent)}{" "} + $COOP +
+
+ +
+
+ + setRewardAmount({ + ...rewardAmount, + [u.id]: e.target.value, + }) + } + className="h-8 w-20 border-0 bg-transparent text-center font-bold focus-visible:ring-0" + /> + +
+ +
+ + +
+
+ ))} +
+
+ + + + + + + The Barn Rules + + + Change the physics of the coop + + + + {settings && ( +
+
+

+ Earning Logic +

+
+ + + setSettings({ + ...settings, + creditsPerMinute: Number(e.target.value), + }) + } + className="border-2 border-foreground h-12 font-bold focus-visible:ring-primary" + /> +
+
+ + + setSettings({ + ...settings, + minWatchPercent: Number(e.target.value), + }) + } + className="border-2 border-foreground h-12 font-bold focus-visible:ring-primary" + /> +
+
+ +
+

+ Request Costs +

+
+ + + setSettings({ + ...settings, + movieRequestCost: Number(e.target.value), + }) + } + className="border-2 border-foreground h-12 font-bold focus-visible:ring-primary" + /> +
+
+ + + setSettings({ + ...settings, + tvRequestCost: Number(e.target.value), + }) + } + className="border-2 border-foreground h-12 font-bold focus-visible:ring-primary" + /> +
+
+ +
+ +
+
+ )} +
+
+
+ + +
+
+
+ Flock Size +
+
+ {analytics?.users.total || 0} +
+
+
+
+ Total $COOP +
+
+ {formatNumber(analytics?.transactions.total_earned || 0)} +
+
+
+
+ Requests +
+
+ {analytics?.requests.total || 0} +
+
+
+
+ Pending +
+
+ {analytics?.requests.pending || 0} +
+
+
+
+
);