diff --git a/src/App.tsx b/src/App.tsx index 5fdb92ca..e3cf5574 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -34,6 +34,10 @@ 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 +75,10 @@ function AppContent() { } /> } /> } /> - } /> + } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/src/components/admin/UserManagement.tsx b/src/components/admin/UserManagement.tsx index d7558783..535e36a9 100644 --- a/src/components/admin/UserManagement.tsx +++ b/src/components/admin/UserManagement.tsx @@ -1,12 +1,10 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { ProfileManager } from '@/components/moderation/ProfileManager'; import { UserRoleManager } from '@/components/moderation/UserRoleManager'; -import { Users, Shield, UserCheck, UserX } from 'lucide-react'; +import { Shield, UserCheck } from 'lucide-react'; export function UserManagement() { - return
- - + return ( +
@@ -20,22 +18,13 @@ export function UserManagement() { - - - - - - + - - - - - - + -
; +
+ ); } \ No newline at end of file diff --git a/src/components/layout/AdminLayout.tsx b/src/components/layout/AdminLayout.tsx new file mode 100644 index 00000000..bafce325 --- /dev/null +++ b/src/components/layout/AdminLayout.tsx @@ -0,0 +1,44 @@ +import { ReactNode } from 'react'; +import { SidebarProvider } from '@/components/ui/sidebar'; +import { AdminSidebar } from './AdminSidebar'; +import { AdminTopBar } from './AdminTopBar'; + +interface AdminLayoutProps { + children: ReactNode; + onRefresh?: () => void; + refreshMode?: 'auto' | 'manual'; + pollInterval?: number; + lastUpdated?: Date; + isRefreshing?: boolean; +} + +export function AdminLayout({ + children, + onRefresh, + refreshMode, + pollInterval, + lastUpdated, + isRefreshing +}: AdminLayoutProps) { + return ( + +
+ +
+ +
+
+ {children} +
+
+
+
+
+ ); +} diff --git a/src/components/layout/AdminSidebar.tsx b/src/components/layout/AdminSidebar.tsx new file mode 100644 index 00000000..8ca44f47 --- /dev/null +++ b/src/components/layout/AdminSidebar.tsx @@ -0,0 +1,115 @@ +import { LayoutDashboard, FileText, Flag, Users, Settings, ArrowLeft } from 'lucide-react'; +import { NavLink } from 'react-router-dom'; +import { useUserRole } from '@/hooks/useUserRole'; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from '@/components/ui/sidebar'; + +export function AdminSidebar() { + const { state } = useSidebar(); + const { permissions } = useUserRole(); + const isSuperuser = permissions?.role_level === 'superuser'; + const collapsed = state === 'collapsed'; + + const navItems = [ + { + title: 'Dashboard', + url: '/admin', + icon: LayoutDashboard, + }, + { + title: 'Moderation', + url: '/admin/moderation', + icon: FileText, + }, + { + title: 'Reports', + url: '/admin/reports', + icon: Flag, + }, + { + title: 'Users', + url: '/admin/users', + icon: Users, + }, + ...(isSuperuser ? [{ + title: 'Settings', + url: '/admin/settings', + icon: Settings, + }] : []), + ]; + + return ( + + + {!collapsed && ( +
+
+ 🎢 +
+
+ ThrillWiki + Admin Panel +
+
+ )} + {collapsed && ( +
+ 🎢 +
+ )} +
+ + + + Navigation + + + {navItems.map((item) => ( + + + + isActive + ? 'bg-accent text-accent-foreground font-medium' + : 'hover:bg-accent/50' + } + > + + {!collapsed && {item.title}} + + + + ))} + + + + + + + + + + + + {!collapsed && Back to ThrillWiki} + + + + + +
+ ); +} diff --git a/src/components/layout/AdminTopBar.tsx b/src/components/layout/AdminTopBar.tsx new file mode 100644 index 00000000..012aab16 --- /dev/null +++ b/src/components/layout/AdminTopBar.tsx @@ -0,0 +1,71 @@ +import { RefreshCw } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { ThemeToggle } from '@/components/theme/ThemeToggle'; +import { AuthButtons } from '@/components/auth/AuthButtons'; +import { NotificationCenter } from '@/components/notifications/NotificationCenter'; +import { SidebarTrigger } from '@/components/ui/sidebar'; +import { useAuth } from '@/hooks/useAuth'; + +interface AdminTopBarProps { + onRefresh?: () => void; + refreshMode?: 'auto' | 'manual'; + pollInterval?: number; + lastUpdated?: Date; + isRefreshing?: boolean; +} + +export function AdminTopBar({ + onRefresh, + refreshMode, + pollInterval, + lastUpdated, + isRefreshing +}: AdminTopBarProps) { + const { user } = useAuth(); + + return ( +
+
+ {/* Left Section */} +
+ + + {refreshMode && ( +
+ + {refreshMode === 'auto' ? ( + Auto: {pollInterval ? pollInterval / 1000 : 30}s + ) : ( + Manual + )} + {lastUpdated && ( + + • {lastUpdated.toLocaleTimeString()} + + )} +
+ )} +
+ + {/* Right Section */} +
+ {onRefresh && ( + + )} + + {user && } + +
+
+
+ ); +} diff --git a/src/pages/AdminDashboard.tsx b/src/pages/AdminDashboard.tsx new file mode 100644 index 00000000..ea524237 --- /dev/null +++ b/src/pages/AdminDashboard.tsx @@ -0,0 +1,156 @@ +import { useRef, useEffect, useCallback, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { FileText, Flag, AlertCircle } from 'lucide-react'; +import { useUserRole } from '@/hooks/useUserRole'; +import { useAuth } from '@/hooks/useAuth'; +import { Card, CardContent } from '@/components/ui/card'; +import { AdminLayout } from '@/components/layout/AdminLayout'; +import { useModerationStats } from '@/hooks/useModerationStats'; +import { useAdminSettings } from '@/hooks/useAdminSettings'; + +export default function AdminDashboard() { + const { user, loading: authLoading } = useAuth(); + const { isModerator, loading: roleLoading } = useUserRole(); + const navigate = useNavigate(); + const [isRefreshing, setIsRefreshing] = useState(false); + + const { + getAdminPanelRefreshMode, + getAdminPanelPollInterval, + } = useAdminSettings(); + + const refreshMode = getAdminPanelRefreshMode(); + const pollInterval = getAdminPanelPollInterval(); + + const { stats, refresh: refreshStats, lastUpdated } = useModerationStats({ + enabled: !!user && !authLoading && !roleLoading && isModerator(), + pollingEnabled: refreshMode === 'auto', + pollingInterval: pollInterval, + }); + + const handleRefresh = useCallback(async () => { + setIsRefreshing(true); + await refreshStats(); + setTimeout(() => setIsRefreshing(false), 500); + }, [refreshStats]); + + 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; + } + + const statCards = [ + { + label: 'Pending Submissions', + value: stats.pendingSubmissions, + icon: FileText, + color: 'amber', + link: '/admin/moderation', + }, + { + label: 'Open Reports', + value: stats.openReports, + icon: Flag, + color: 'red', + link: '/admin/reports', + }, + { + label: 'Flagged Content', + value: stats.flaggedContent, + icon: AlertCircle, + color: 'orange', + link: '/admin/moderation', + }, + ]; + + return ( + +
+
+

Admin Dashboard

+

+ Overview of moderation activity and pending items +

+
+ +
+ {statCards.map((card) => { + const Icon = card.icon; + const colorClasses = { + amber: { + card: 'hover:border-amber-500/50', + bg: 'bg-amber-500/10', + icon: 'text-amber-600 dark:text-amber-400', + }, + red: { + card: 'hover:border-red-500/50', + bg: 'bg-red-500/10', + icon: 'text-red-600 dark:text-red-400', + }, + orange: { + card: 'hover:border-orange-500/50', + bg: 'bg-orange-500/10', + icon: 'text-orange-600 dark:text-orange-400', + }, + }; + const colors = colorClasses[card.color as keyof typeof colorClasses]; + + return ( + navigate(card.link)} + > + +
+
+ +
+
+

+ {card.label} +

+
+
+
{card.value}
+
+
+ ); + })} +
+
+
+ ); +} diff --git a/src/pages/AdminModeration.tsx b/src/pages/AdminModeration.tsx new file mode 100644 index 00000000..11ee2e52 --- /dev/null +++ b/src/pages/AdminModeration.tsx @@ -0,0 +1,85 @@ +import { useRef, useEffect, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useUserRole } from '@/hooks/useUserRole'; +import { useAuth } from '@/hooks/useAuth'; +import { AdminLayout } from '@/components/layout/AdminLayout'; +import { ModerationQueue, ModerationQueueRef } from '@/components/moderation/ModerationQueue'; +import { useAdminSettings } from '@/hooks/useAdminSettings'; +import { useModerationStats } from '@/hooks/useModerationStats'; + +export default function AdminModeration() { + const { user, loading: authLoading } = useAuth(); + const { isModerator, loading: roleLoading } = useUserRole(); + const navigate = useNavigate(); + const moderationQueueRef = useRef(null); + + const { + getAdminPanelRefreshMode, + getAdminPanelPollInterval, + } = useAdminSettings(); + + const refreshMode = getAdminPanelRefreshMode(); + const pollInterval = getAdminPanelPollInterval(); + + const { refresh: refreshStats, lastUpdated } = useModerationStats({ + enabled: !!user && !authLoading && !roleLoading && isModerator(), + pollingEnabled: refreshMode === 'auto', + pollingInterval: pollInterval, + }); + + const handleRefresh = useCallback(() => { + moderationQueueRef.current?.refresh(); + refreshStats(); + }, [refreshStats]); + + useEffect(() => { + if (!authLoading && !roleLoading) { + if (!user) { + navigate('/auth'); + return; + } + + if (!isModerator()) { + navigate('/'); + return; + } + } + }, [user, authLoading, roleLoading, navigate, isModerator]); + + if (authLoading || roleLoading) { + return ( +
+
+
+
+

Loading moderation queue...

+
+
+
+ ); + } + + if (!user || !isModerator()) { + return null; + } + + return ( + +
+
+

Moderation Queue

+

+ Review and manage pending content submissions +

+
+ + +
+
+ ); +} diff --git a/src/pages/AdminReports.tsx b/src/pages/AdminReports.tsx new file mode 100644 index 00000000..fac21b12 --- /dev/null +++ b/src/pages/AdminReports.tsx @@ -0,0 +1,85 @@ +import { useRef, useEffect, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useUserRole } from '@/hooks/useUserRole'; +import { useAuth } from '@/hooks/useAuth'; +import { AdminLayout } from '@/components/layout/AdminLayout'; +import { ReportsQueue, ReportsQueueRef } from '@/components/moderation/ReportsQueue'; +import { useAdminSettings } from '@/hooks/useAdminSettings'; +import { useModerationStats } from '@/hooks/useModerationStats'; + +export default function AdminReports() { + const { user, loading: authLoading } = useAuth(); + const { isModerator, loading: roleLoading } = useUserRole(); + const navigate = useNavigate(); + const reportsQueueRef = useRef(null); + + const { + getAdminPanelRefreshMode, + getAdminPanelPollInterval, + } = useAdminSettings(); + + const refreshMode = getAdminPanelRefreshMode(); + const pollInterval = getAdminPanelPollInterval(); + + const { refresh: refreshStats, lastUpdated } = useModerationStats({ + enabled: !!user && !authLoading && !roleLoading && isModerator(), + pollingEnabled: refreshMode === 'auto', + pollingInterval: pollInterval, + }); + + const handleRefresh = useCallback(() => { + reportsQueueRef.current?.refresh(); + refreshStats(); + }, [refreshStats]); + + useEffect(() => { + if (!authLoading && !roleLoading) { + if (!user) { + navigate('/auth'); + return; + } + + if (!isModerator()) { + navigate('/'); + return; + } + } + }, [user, authLoading, roleLoading, navigate, isModerator]); + + if (authLoading || roleLoading) { + return ( +
+
+
+
+

Loading reports...

+
+
+
+ ); + } + + if (!user || !isModerator()) { + return null; + } + + return ( + +
+
+

User Reports

+

+ Review and resolve user-submitted reports +

+
+ + +
+
+ ); +} diff --git a/src/pages/AdminSettings.tsx b/src/pages/AdminSettings.tsx index 632bdada..0aa60a6d 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,52 +28,45 @@ export default function AdminSettings() { if (roleLoading || isLoading) { return ( - <> - -
+ +
- +
); } if (!user || !isSuperuser()) { return ( - <> - -
-
-

Access Denied

-

You don't have permission to access admin settings.

- {error && ( -
- Error details: {error.message} -
- )} -
+ +
+

Access Denied

+

You don't have permission to access admin settings.

+ {error && ( +
+ Error details: {error.message} +
+ )}
- +
); } if (!settings || settings.length === 0) { return ( - <> - -
-
-

No Settings Found

-

- No admin settings have been configured yet. Please contact your system administrator. -

- {error && ( -
- Error details: {error.message} -
- )} -
+ +
+

No Settings Found

+

+ No admin settings have been configured yet. Please contact your system administrator. +

+ {error && ( +
+ Error details: {error.message} +
+ )}
- +
); } @@ -431,13 +424,12 @@ export default function AdminSettings() { }; return ( - <> - -
-
-

Admin Settings

-

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

+
+

Admin Settings

+

+ Configure system-wide settings and preferences

@@ -598,6 +590,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..e95969b5 --- /dev/null +++ b/src/pages/AdminUsers.tsx @@ -0,0 +1,58 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useUserRole } from '@/hooks/useUserRole'; +import { useAuth } from '@/hooks/useAuth'; +import { AdminLayout } from '@/components/layout/AdminLayout'; +import { UserManagement } from '@/components/admin/UserManagement'; + +export default function AdminUsers() { + 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 user management...

+
+
+
+ ); + } + + if (!user || !isModerator()) { + return null; + } + + return ( + +
+
+

User Management

+

+ Manage user profiles, roles, and permissions +

+
+ + +
+
+ ); +}