diff --git a/frontend/src/app/admin/page.tsx b/frontend/src/app/admin/page.tsx index 586cd3a..2a50b48 100644 --- a/frontend/src/app/admin/page.tsx +++ b/frontend/src/app/admin/page.tsx @@ -1,331 +1,254 @@ -'use client'; +"use client"; -import { useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { useStore } from '@/lib/store'; -import { adminApi } from '@/lib/api'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Badge } from '@/components/ui/badge'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { - Users, - Settings, - Pause, - Play, - TrendingUp, - Search, - Gift -} from 'lucide-react'; -import { formatNumber } from '@/lib/utils'; -import { toast } from 'sonner'; +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 { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { adminApi, api } from "@/lib/api"; +import { useStore } from "@/lib/store"; +import { formatNumber } from "@/lib/utils"; interface Analytics { - users: { - total: number; - with_wallet: number; - new_this_week: number; - }; - transactions: { - total_earned: number; - total_spent: number; - total_transactions: number; - }; - watchStats: { - total_seconds: number; - total_credits: number; - total_events: number; - }; + users: { total: number; with_wallet: number; active_30d: number }; + transactions: { + total_earned: number; + total_spent: number; + total_count: number; + }; + requests: { + total: number; + pending: number; + approved: number; + declined: number; + }; } interface User { - id: string; - plexUsername: string; - email: string | null; - isAdmin: boolean; - isActive: boolean; - totalEarned: number; - totalSpent: number; - watchTimeMinutes: number; - createdAt: string; + id: string; + plexUsername: string; + isAdmin: boolean; + totalEarned: number; + totalSpent: 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 [isLoading, setIsLoading] = useState(true); - const [searchQuery, setSearchQuery] = useState(''); + const router = useRouter(); + const { user, isAuthenticated } = useStore(); + const [analytics, setAnalytics] = useState(null); + const [users, setUsers] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [isLoading, setIsLoading] = useState(true); - useEffect(() => { - if (!isAuthenticated) { - router.push('/login'); - return; - } + useEffect(() => { + if (!isAuthenticated || !user?.isAdmin) { + router.push("/dashboard"); + return; + } + loadData(); + }, [isAuthenticated, user, router]); - if (!user?.isAdmin) { - router.push('/dashboard'); - return; - } + const loadData = async () => { + try { + const [analyticsRes, usersRes] = await Promise.all([ + api.get("/admin/analytics"), + api.get("/admin/users"), + ]); + setAnalytics(analyticsRes.data); + setUsers(usersRes.data); + } catch { + toast.error("Failed to load coop secrets"); + } finally { + setIsLoading(false); + } + }; - loadData(); - }, [isAuthenticated, user, router]); + const adjustUser = async (userId: string, amount: number, reason: string) => { + try { + await adminApi.adjustUser(userId, amount, reason); + toast.success("Feathers adjusted!"); + loadData(); + } catch { + toast.error("Failed to adjust nest"); + } + }; - const loadData = async () => { - try { - const [analyticsRes, usersRes, settingsRes] = await Promise.all([ - adminApi.getAnalytics(), - adminApi.getUsers(), - adminApi.getSettings() - ]); + const toggleAdmin = async (userId: string, isAdmin: boolean) => { + try { + await api.patch(`/admin/users/${userId}`, { isAdmin }); + toast.success(isAdmin ? "Promoted to Rooster!" : "Demoted to Hen."); + loadData(); + } catch { + toast.error("Failed to change pecking order"); + } + }; - setAnalytics(analyticsRes.data); - setUsers(usersRes.data.users); - setSettings(settingsRes.data); - } catch (error) { - toast.error('Failed to load admin data'); - } finally { - setIsLoading(false); - } - }; + const filteredUsers = users.filter((u) => + u.plexUsername?.toLowerCase().includes(searchQuery.toLowerCase()), + ); - const handlePause = async () => { - try { - await adminApi.pause(); - toast.success('Minting paused'); - loadData(); - } catch (error) { - toast.error('Failed to pause minting'); - } - }; + if (isLoading) + return ( +
+ COUNTING THE CHICKENS... +
+ ); - const handleResume = async () => { - try { - await adminApi.resume(); - toast.success('Minting resumed'); - loadData(); - } catch (error) { - toast.error('Failed to resume minting'); - } - }; + return ( +
+
+ {/* Admin Header */} +
+
+ +
+

+ COOP CONTROL +

+

+ Admin Peering Glass +

+
+
+ +
- const handleGrantBonus = async (userId: string) => { - const amount = prompt('Enter bonus amount:'); - if (!amount) return; + {/* 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} +
+
+
- try { - await adminApi.grantBonus(userId, parseInt(amount), 'Admin bonus'); - toast.success('Bonus granted'); - loadData(); - } catch (error) { - toast.error('Failed to grant bonus'); - } - }; + {/* User Management */} +
+
+

+ The Peeps +

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

Loading...

-
- ); - } - - return ( -
-
-
-
-

Admin Dashboard

- {user.plexUsername} -
- -
-
- -
- {/* Stats Overview */} -
- - - Total Users - - - -
{analytics?.users.total || 0}
-

- {analytics?.users.with_wallet || 0} with wallets -

-
-
- - - - Total Minted - - - -
- {formatNumber(analytics?.transactions.total_earned || 0)} $COOP -
-

- {analytics?.transactions.total_transactions || 0} transactions -

-
-
- - - - Watch Time - - - -
- {Math.floor((analytics?.watchStats.total_seconds || 0) / 3600)}h -
-

- {analytics?.watchStats.total_events || 0} watch events -

-
-
- - - - System Status - - - -
-
- - {settings?.mintingPaused ? 'Paused' : 'Active'} - -
-
- - -
- - -
- - {/* Tabs */} - - - Users - Settings - - - -
-
- - setSearchQuery(e.target.value)} - className="pl-10" - /> -
-
- - - -
- {users.map((u) => ( -
-
-

{u.plexUsername}

-

{u.email}

-
- {u.isAdmin && Admin} - {!u.isActive && Inactive} -
-
-
-

- {formatNumber(u.totalEarned - u.totalSpent)} $COOP -

- -
-
- ))} -
-
-
-
- - - - - Minting Settings - Configure how users earn $COOP - - -
-
- - setSettings({ ...settings, creditsPerMinute: parseInt(e.target.value) })} - /> -
-
- - setSettings({ ...settings, minWatchPercent: parseInt(e.target.value) })} - /> -
-
- - setSettings({ ...settings, movieRequestCost: parseInt(e.target.value) })} - /> -
-
- - setSettings({ ...settings, tvRequestCost: parseInt(e.target.value) })} - /> -
-
- -
-
-
-
-
-
- ); +
+ + + +
+
+ ))} +
+
+
+
+ ); } diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 1f6e227..fc39ca9 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -13,7 +13,7 @@ import { Button } from "@/components/ui/button"; export default function LandingPage() { return ( -
+
-
-
+
+
-
- +
+
- + CoopCredits
-
@@ -64,13 +68,13 @@ export default function LandingPage() {
-
- Barnyard rewards +
+ Barnyard rewards
-

- Welcome to the Coop, Friend +

+ Join the Flock!

-

+

Earn $COOP automatically for every minute you watch on Plex. Spend it like golden eggs to request movies and shows.

@@ -78,19 +82,18 @@ export default function LandingPage() {
@@ -98,93 +101,58 @@ export default function LandingPage() {
-
+
-
-

- Why the flock likes it +

+

+ Pecking Order

-

- Simple, warm, and built for watching +

+ Simple. Fun. Fast.

-
-
- -

Watch earns

-

- Every minute on Plex fills your nest with $COOP. +

+
+
+ +
+

Watch

+

+ Every minute on Plex fills your nest with $COOP automatically.

-
- -

Safe balance

-

- Your credits live in the coop database and update fast. +

+
+ +
+

Earn

+

+ Your credits live in the cozy coop database. Safe and warm.

-
- -

Request fast

-

- Spend credits on movie and TV requests with a few taps. +

+
+ +
+

Spend

+

+ Exchange golden eggs for any movie or TV series you desire.

-
-
-
-

- How it works -

-

- A cozy loop: watch, earn, request, repeat -

-

- No weird hoops. Plex watch time becomes credits. Credits become - requests. The coop keeps score. -

-
    -
  • • Log in with Plex.
  • -
  • • Watch content.
  • -
  • • Get $COOP in your balance.
  • -
  • • Request new content from dashboard.
  • -
- - - -
-
-
-
-
- Balance -
-
$COOP
-
-
-
-
- Watch -
-
Earn
-
-
-
- Spend -
-
Request
-
-
-
-
+
+
);