diff --git a/src/App.tsx b/src/App.tsx index 5fdb92ca..de92886b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,7 +33,10 @@ import NotFound from "./pages/NotFound"; import Terms from "./pages/Terms"; import Privacy from "./pages/Privacy"; import SubmissionGuidelines from "./pages/SubmissionGuidelines"; -import Admin from "./pages/Admin"; +import AdminDashboard from "./pages/AdminDashboard"; +import AdminModeration from "./pages/AdminModeration"; +import AdminReports from "./pages/AdminReports"; +import AdminUsers from "./pages/AdminUsers"; import AdminSettings from "./pages/AdminSettings"; const queryClient = new QueryClient(); @@ -71,7 +74,10 @@ function AppContent() { } /> } /> } /> - } /> + } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/src/components/layout/AdminLayout.tsx b/src/components/layout/AdminLayout.tsx new file mode 100644 index 00000000..5fa4fa0b --- /dev/null +++ b/src/components/layout/AdminLayout.tsx @@ -0,0 +1,62 @@ +import { ReactNode, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { SidebarProvider } from '@/components/ui/sidebar'; +import { AdminSidebar } from './AdminSidebar'; +import { AdminTopBar } from './AdminTopBar'; +import { useAuth } from '@/hooks/useAuth'; +import { useUserRole } from '@/hooks/useUserRole'; + +interface AdminLayoutProps { + children: ReactNode; + onRefresh?: () => void; + isRefreshing?: boolean; +} + +export function AdminLayout({ children, onRefresh, isRefreshing }: AdminLayoutProps) { + const { user, loading: authLoading } = useAuth(); + const { isModerator, loading: roleLoading } = useUserRole(); + const navigate = useNavigate(); + + useEffect(() => { + if (!authLoading && !roleLoading) { + if (!user) { + navigate('/auth'); + return; + } + + if (!isModerator()) { + navigate('/'); + return; + } + } + }, [user, authLoading, roleLoading, navigate, isModerator]); + + if (authLoading || roleLoading) { + return ( +
+
+
+

Loading admin panel...

+
+
+ ); + } + + if (!user || !isModerator()) { + return null; + } + + return ( + +
+ +
+ +
+ {children} +
+
+
+
+ ); +} diff --git a/src/components/layout/AdminSidebar.tsx b/src/components/layout/AdminSidebar.tsx new file mode 100644 index 00000000..1f9e73c5 --- /dev/null +++ b/src/components/layout/AdminSidebar.tsx @@ -0,0 +1,148 @@ +import { LayoutDashboard, FileText, Flag, Users, Settings, ArrowLeft } from 'lucide-react'; +import { NavLink, useLocation } from 'react-router-dom'; +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarFooter, + SidebarHeader, + useSidebar, +} from '@/components/ui/sidebar'; +import { cn } from '@/lib/utils'; +import { useUserRole } from '@/hooks/useUserRole'; + +const navigationItems = [ + { + title: 'Dashboard', + url: '/admin', + icon: LayoutDashboard, + }, + { + title: 'Moderation Queue', + url: '/admin/moderation', + icon: FileText, + }, + { + title: 'Reports', + url: '/admin/reports', + icon: Flag, + }, + { + title: 'User Management', + url: '/admin/users', + icon: Users, + }, +]; + +export function AdminSidebar() { + const location = useLocation(); + const { state } = useSidebar(); + const { isSuperuser } = useUserRole(); + const isCollapsed = state === 'collapsed'; + + const isActive = (path: string) => { + if (path === '/admin') { + return location.pathname === '/admin'; + } + return location.pathname.startsWith(path); + }; + + return ( + + +
+
+ TW +
+ {!isCollapsed && ( +
+ ThrillWiki + Admin Panel +
+ )} +
+
+ + + + Navigation + + + {navigationItems.map((item) => ( + + + + cn( + 'flex items-center gap-3 rounded-md transition-colors', + isActive + ? 'bg-primary/10 text-primary font-medium' + : 'hover:bg-muted/50' + ) + } + > + + {!isCollapsed && {item.title}} + + + + ))} + + {isSuperuser() && ( + + + + cn( + 'flex items-center gap-3 rounded-md transition-colors', + isActive + ? 'bg-primary/10 text-primary font-medium' + : 'hover:bg-muted/50' + ) + } + > + + {!isCollapsed && Settings} + + + + )} + + + + + + + + + + + + {!isCollapsed && Back to ThrillWiki} + + + + + +
+ ); +} diff --git a/src/components/layout/AdminTopBar.tsx b/src/components/layout/AdminTopBar.tsx new file mode 100644 index 00000000..f182ce35 --- /dev/null +++ b/src/components/layout/AdminTopBar.tsx @@ -0,0 +1,38 @@ +import { RefreshCw } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { SidebarTrigger } from '@/components/ui/sidebar'; +import { ThemeToggle } from '@/components/theme/ThemeToggle'; +import { NotificationCenter } from '@/components/notifications/NotificationCenter'; +import { AuthButtons } from '@/components/auth/AuthButtons'; + +interface AdminTopBarProps { + onRefresh?: () => void; + isRefreshing?: boolean; +} + +export function AdminTopBar({ onRefresh, isRefreshing }: AdminTopBarProps) { + return ( +
+ + + {onRefresh && ( + + )} + +
+ + + +
+
+ ); +} diff --git a/src/pages/AdminDashboard.tsx b/src/pages/AdminDashboard.tsx new file mode 100644 index 00000000..bd62a375 --- /dev/null +++ b/src/pages/AdminDashboard.tsx @@ -0,0 +1,116 @@ +import { useRef, useCallback } from 'react'; +import { RefreshCw, FileText, Flag, AlertCircle } from 'lucide-react'; +import { Card, CardContent } from '@/components/ui/card'; +import { AdminLayout } from '@/components/layout/AdminLayout'; +import { useModerationStats } from '@/hooks/useModerationStats'; +import { useAdminSettings } from '@/hooks/useAdminSettings'; +import { useAuth } from '@/hooks/useAuth'; +import { useUserRole } from '@/hooks/useUserRole'; + +export default function AdminDashboard() { + const { user, loading: authLoading } = useAuth(); + const { isModerator, loading: roleLoading } = useUserRole(); + + // Get admin settings for polling configuration + const { + getAdminPanelRefreshMode, + getAdminPanelPollInterval, + } = useAdminSettings(); + + const refreshMode = getAdminPanelRefreshMode(); + const pollInterval = getAdminPanelPollInterval(); + + // Use stats hook with configurable polling + const { stats, refresh: refreshStats, lastUpdated } = useModerationStats({ + enabled: !!user && !authLoading && !roleLoading && isModerator(), + pollingEnabled: refreshMode === 'auto', + pollingInterval: pollInterval, + }); + + const handleRefresh = useCallback(() => { + refreshStats(); + }, [refreshStats]); + + return ( + +
+ {/* Refresh status indicator */} +
+ + {refreshMode === 'auto' ? ( + Auto-refresh: every {pollInterval / 1000}s + ) : ( + Manual refresh + )} + {lastUpdated && ( + • {lastUpdated.toLocaleTimeString()} + )} +
+ + {/* Stats Overview */} +
+

Dashboard Overview

+ +
+ + +
+
+
+ +
+
+

Pending

+

Submissions

+
+
+
+ {stats.pendingSubmissions} +
+
+
+
+ + + +
+
+
+ +
+
+

Open

+

Reports

+
+
+
+ {stats.openReports} +
+
+
+
+ + + +
+
+
+ +
+
+

Flagged

+

Content

+
+
+
+ {stats.flaggedContent} +
+
+
+
+
+
+
+
+ ); +} diff --git a/src/pages/AdminModeration.tsx b/src/pages/AdminModeration.tsx new file mode 100644 index 00000000..68783bff --- /dev/null +++ b/src/pages/AdminModeration.tsx @@ -0,0 +1,22 @@ +import { useRef, useCallback } from 'react'; +import { AdminLayout } from '@/components/layout/AdminLayout'; +import { ModerationQueue, ModerationQueueRef } from '@/components/moderation/ModerationQueue'; + +export default function AdminModeration() { + const moderationQueueRef = useRef(null); + + const handleRefresh = useCallback(() => { + moderationQueueRef.current?.refresh(); + }, []); + + return ( + +
+
+

Moderation Queue

+ +
+
+
+ ); +} diff --git a/src/pages/AdminReports.tsx b/src/pages/AdminReports.tsx new file mode 100644 index 00000000..5c3c3ceb --- /dev/null +++ b/src/pages/AdminReports.tsx @@ -0,0 +1,22 @@ +import { useRef, useCallback } from 'react'; +import { AdminLayout } from '@/components/layout/AdminLayout'; +import { ReportsQueue, ReportsQueueRef } from '@/components/moderation/ReportsQueue'; + +export default function AdminReports() { + const reportsQueueRef = useRef(null); + + const handleRefresh = useCallback(() => { + reportsQueueRef.current?.refresh(); + }, []); + + return ( + +
+
+

Reports Queue

+ +
+
+
+ ); +} diff --git a/src/pages/AdminSettings.tsx b/src/pages/AdminSettings.tsx index 632bdada..0a9997f4 100644 --- a/src/pages/AdminSettings.tsx +++ b/src/pages/AdminSettings.tsx @@ -7,7 +7,7 @@ import { Switch } from '@/components/ui/switch'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Badge } from '@/components/ui/badge'; -import { AdminHeader } from '@/components/layout/AdminHeader'; +import { AdminLayout } from '@/components/layout/AdminLayout'; import { useAuth } from '@/hooks/useAuth'; import { useUserRole } from '@/hooks/useUserRole'; import { useAdminSettings } from '@/hooks/useAdminSettings'; @@ -28,20 +28,18 @@ export default function AdminSettings() { if (roleLoading || isLoading) { return ( - <> - +
- +
); } if (!user || !isSuperuser()) { return ( - <> - -
+ +

Access Denied

You don't have permission to access admin settings.

@@ -52,15 +50,14 @@ export default function AdminSettings() { )}
- +
); } if (!settings || settings.length === 0) { return ( - <> - -
+ +

No Settings Found

@@ -73,7 +70,7 @@ export default function AdminSettings() { )}

- +
); } @@ -431,11 +428,10 @@ export default function AdminSettings() { }; return ( - <> - -
+ +
-

Admin Settings

+

Admin Settings

Configure system-wide settings and preferences with easy-to-use controls

@@ -598,6 +594,6 @@ export default function AdminSettings() {
- + ); } \ No newline at end of file diff --git a/src/pages/AdminUsers.tsx b/src/pages/AdminUsers.tsx new file mode 100644 index 00000000..30a1fd5a --- /dev/null +++ b/src/pages/AdminUsers.tsx @@ -0,0 +1,15 @@ +import { AdminLayout } from '@/components/layout/AdminLayout'; +import { UserManagement } from '@/components/admin/UserManagement'; + +export default function AdminUsers() { + return ( + +
+
+

User Management

+ +
+
+
+ ); +}