Refactor code structure and remove redundant changes

This commit is contained in:
pacnpal
2025-11-09 16:31:34 -05:00
parent 2884bc23ce
commit eb68cf40c6
1080 changed files with 27361 additions and 56687 deletions

View File

@@ -0,0 +1,117 @@
import { Shield, ArrowLeft, Settings, Menu } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { RefreshButton } from '@/components/ui/refresh-button';
import { Link, useLocation } from 'react-router-dom';
import { ThemeToggle } from '@/components/theme/ThemeToggle';
import { AuthButtons } from '@/components/auth/AuthButtons';
import { NotificationCenter } from '@/components/notifications/NotificationCenter';
import { useUserRole } from '@/hooks/useUserRole';
import { useAuth } from '@/hooks/useAuth';
import { useIsMobile } from '@/hooks/use-mobile';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
export function AdminHeader({ onRefresh, isRefreshing }: { onRefresh?: () => void; isRefreshing?: boolean }) {
const { permissions } = useUserRole();
const { user } = useAuth();
const location = useLocation();
const isMobile = useIsMobile();
const isSettingsPage = location.pathname === '/admin/settings';
const backLink = isSettingsPage ? '/admin' : '/';
const backText = isSettingsPage ? 'Back to Admin' : 'Back to ThrillWiki';
const pageTitle = isSettingsPage ? 'Admin Settings' : 'Admin Dashboard';
return (
<header className="sticky top-0 z-50 w-full border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container flex h-16 items-center justify-between px-4">
{/* Left Section - Navigation */}
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" asChild>
<Link to={backLink} className="flex items-center gap-2">
<ArrowLeft className="w-4 h-4" />
<span className="hidden sm:inline">{backText}</span>
</Link>
</Button>
<div className="h-6 w-px bg-border hidden sm:block" />
<div className="flex items-center gap-2">
<Shield className="w-6 h-6 text-primary" />
<h1 className="text-lg font-semibold">
<span className="sm:hidden">Admin</span>
<span className="hidden sm:inline">{pageTitle}</span>
</h1>
</div>
</div>
{/* Right Section - Admin actions */}
<div className="flex items-center gap-2">
{/* Mobile Menu */}
<Sheet>
<SheetTrigger asChild className="md:hidden">
<Button variant="ghost" size="icon">
<Menu className="h-5 w-5" />
<span className="sr-only">Open menu</span>
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-[300px] sm:w-[400px]">
<SheetHeader>
<SheetTitle>Admin Menu</SheetTitle>
</SheetHeader>
<div className="flex flex-col gap-4 mt-6">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Theme</span>
<ThemeToggle />
</div>
<RefreshButton
onRefresh={onRefresh!}
isLoading={isRefreshing}
variant="ghost"
className="justify-start w-full"
/>
{permissions?.role_level === 'superuser' && !isSettingsPage && (
<Button variant="ghost" asChild className="justify-start">
<Link to="/admin/settings">
<Settings className="w-4 h-4 mr-2" />
Settings
</Link>
</Button>
)}
</div>
</SheetContent>
</Sheet>
{/* Desktop Actions */}
{onRefresh && (
<RefreshButton
onRefresh={onRefresh}
isLoading={isRefreshing}
variant="ghost"
size="sm"
className="hidden md:flex"
/>
)}
{permissions?.role_level === 'superuser' && !isSettingsPage && (
<Button variant="ghost" size="sm" asChild className="hidden md:flex">
<Link to="/admin/settings">
<Settings className="w-4 h-4" />
<span className="hidden sm:ml-2 sm:inline">Settings</span>
</Link>
</Button>
)}
<div className="hidden md:block">
<ThemeToggle />
</div>
{user && <NotificationCenter />}
<AuthButtons />
</div>
</div>
</header>
);
}

View File

@@ -0,0 +1,58 @@
import { ReactNode } from 'react';
import { SidebarProvider } from '@/components/ui/sidebar';
import { AdminSidebar } from './AdminSidebar';
import { AdminTopBar } from './AdminTopBar';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { AlertTriangle } from 'lucide-react';
import { useSessionMonitor } from '@/hooks/useSessionMonitor';
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) {
const { aalWarning } = useSessionMonitor();
return (
<SidebarProvider defaultOpen={true}>
<div className="flex min-h-screen w-full">
<AdminSidebar />
<main className="flex-1 flex flex-col">
<AdminTopBar
onRefresh={onRefresh}
refreshMode={refreshMode}
pollInterval={pollInterval}
lastUpdated={lastUpdated}
isRefreshing={isRefreshing}
/>
<div className="flex-1 overflow-y-auto bg-muted/30">
<div className="container mx-auto px-6 py-8 max-w-7xl">
{aalWarning && (
<Alert variant="destructive" className="mb-6">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Session Verification Required</AlertTitle>
<AlertDescription>
Your session requires re-verification. You will be redirected to verify your identity in 30 seconds.
</AlertDescription>
</Alert>
)}
{children}
</div>
</div>
</main>
</div>
</SidebarProvider>
);
}

View File

@@ -0,0 +1,154 @@
import { LayoutDashboard, FileText, Flag, Users, Settings, ArrowLeft, ScrollText, BookOpen, Inbox, Mail, AlertTriangle } from 'lucide-react';
import { NavLink } from 'react-router-dom';
import { useUserRole } from '@/hooks/useUserRole';
import { useSidebar } from '@/hooks/useSidebar';
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from '@/components/ui/sidebar';
export function AdminSidebar() {
const { state } = useSidebar();
const { permissions } = useUserRole();
const isSuperuser = permissions?.role_level === 'superuser';
const isAdmin = permissions?.role_level === 'admin' || isSuperuser;
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: 'Inbox',
url: '/admin/contact',
icon: Inbox,
},
{
title: 'System Log',
url: '/admin/system-log',
icon: ScrollText,
},
{
title: 'Error Monitoring',
url: '/admin/error-monitoring',
icon: AlertTriangle,
},
{
title: 'Users',
url: '/admin/users',
icon: Users,
},
...(isAdmin ? [{
title: 'Blog',
url: '/admin/blog',
icon: BookOpen,
}] : []),
...(isSuperuser ? [{
title: 'Settings',
url: '/admin/settings',
icon: Settings,
}, {
title: 'Email Settings',
url: '/admin/email-settings',
icon: Mail,
}] : []),
];
return (
<Sidebar collapsible="icon">
<SidebarHeader className="border-b border-border/40 px-4 py-4">
<div className="flex items-center gap-2 min-h-[32px]">
<div className="flex items-center justify-center flex-shrink-0">
<img
src="https://cdn.thrillwiki.com/images/5d06b122-a3a3-47bc-6176-f93ad8f0ce00/favicon512"
alt="ThrillWiki"
width="32"
height="32"
loading="eager"
decoding="async"
draggable="false"
className={`
object-contain
transition-all duration-200 ease-in-out
${collapsed ? 'w-6 h-6' : 'w-8 h-8'}
`}
onError={(e) => {
const img = e.target as HTMLImageElement;
if (!img.src.includes('favicon128')) {
img.src = 'https://cdn.thrillwiki.com/images/5d06b122-a3a3-47bc-6176-f93ad8f0ce00/favicon128';
}
}}
/>
</div>
{!collapsed && (
<div className="flex flex-col overflow-hidden">
<span className="text-sm font-semibold truncate">ThrillWiki</span>
<span className="text-xs text-muted-foreground truncate">Admin Panel</span>
</div>
)}
</div>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{navItems.map((item) => (
<SidebarMenuItem key={item.url}>
<SidebarMenuButton asChild tooltip={collapsed ? item.title : undefined}>
<NavLink
to={item.url}
end={item.url === '/admin'}
className={({ isActive }) =>
isActive
? 'bg-sidebar-accent text-sidebar-accent-foreground font-medium'
: 'hover:bg-sidebar-accent/50 hover:text-sidebar-accent-foreground'
}
>
<item.icon className="w-4 h-4" />
{!collapsed && <span>{item.title}</span>}
</NavLink>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter className="border-t border-border/40">
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip={collapsed ? 'Back to ThrillWiki' : undefined}>
<NavLink to="/" className="hover:bg-sidebar-accent/50 hover:text-sidebar-accent-foreground">
<ArrowLeft className="w-4 h-4" />
{!collapsed && <span>Back to ThrillWiki</span>}
</NavLink>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</Sidebar>
);
}

View File

@@ -0,0 +1,67 @@
import { RefreshCw } from 'lucide-react';
import { RefreshButton } from '@/components/ui/refresh-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 (
<header className="sticky top-0 z-40 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex h-14 items-center justify-between px-4 gap-4">
{/* Left Section */}
<div className="flex items-center gap-3">
<SidebarTrigger className="-ml-1" />
{refreshMode && (
<div className="hidden sm:flex items-center gap-2 text-xs text-muted-foreground">
<RefreshCw className="w-3 h-3" />
{refreshMode === 'auto' ? (
<span>Auto: {pollInterval ? pollInterval / 1000 : 30}s</span>
) : (
<span>Manual</span>
)}
{lastUpdated && (
<span className="hidden md:inline">
{lastUpdated.toLocaleTimeString()}
</span>
)}
</div>
)}
</div>
{/* Right Section */}
<div className="flex items-center gap-2">
{onRefresh && (
<RefreshButton
onRefresh={onRefresh}
isLoading={isRefreshing}
variant="ghost"
size="sm"
/>
)}
<ThemeToggle />
{user && <NotificationCenter />}
<AuthButtons />
</div>
</div>
</header>
);
}

View File

@@ -0,0 +1,47 @@
import { Link } from 'react-router-dom';
export function Footer() {
return (
<footer className="border-t border-border bg-background py-4">
<div className="container mx-auto px-4">
<div className="flex flex-col sm:flex-row justify-between items-center gap-2 text-xs text-muted-foreground">
<div>
© {new Date().getFullYear()} ThrillWiki. All rights reserved.
</div>
<div className="flex items-center gap-4">
<Link
to="/contact"
className="hover:text-foreground transition-colors"
>
Contact
</Link>
<Link
to="/terms"
className="hover:text-foreground transition-colors"
>
Terms of Service
</Link>
<Link
to="/privacy"
className="hover:text-foreground transition-colors"
>
Privacy Policy
</Link>
<Link
to="/submission-guidelines"
className="hover:text-foreground transition-colors"
>
Submission Guidelines
</Link>
<Link
to="/blog"
className="hover:text-foreground transition-colors"
>
Blog
</Link>
</div>
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,256 @@
import { useState } from 'react';
import { Search, Menu, Sparkles, MapPin, Star, ChevronDown, Building, Users, Crown, Palette, Shield, FerrisWheel, Factory } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import { Badge } from '@/components/ui/badge';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { Link, useNavigate } from 'react-router-dom';
import { SearchDropdown } from '@/components/search/SearchDropdown';
import { MobileSearch } from '@/components/search/MobileSearch';
import { AuthButtons } from '@/components/auth/AuthButtons';
import { ThemeToggle } from '@/components/theme/ThemeToggle';
import { NotificationCenter } from '@/components/notifications/NotificationCenter';
import { useAuth } from '@/hooks/useAuth';
import { useUserRole } from '@/hooks/useUserRole';
import { useIsMobile } from '@/hooks/use-mobile';
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
} from "@/components/ui/navigation-menu";
export function Header() {
const navigate = useNavigate();
const [open, setOpen] = useState(false);
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
const { user } = useAuth();
const { isModerator, loading: rolesLoading } = useUserRole();
const isMobile = useIsMobile();
return (
<>
<header className="sticky top-0 z-40 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container flex h-14 md:h-16 items-center justify-between gap-2 md:gap-4 px-3 md:px-4">
{/* Mobile: Menu + Logo */}
<div className="flex items-center gap-2 md:gap-6 flex-1 md:flex-initial min-w-0">
{/* Mobile Menu */}
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="md:hidden h-9 w-9">
<Menu className="h-5 w-5" />
<span className="sr-only">Toggle menu</span>
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-[280px] sm:w-[320px]">
<nav className="flex flex-col gap-1 mt-8">
<div className="mb-4">
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-2 px-3">
Explore
</h3>
</div>
<Link
to="/rides"
className="px-3 py-2.5 text-base font-medium hover:bg-accent hover:text-accent-foreground rounded-md transition-colors"
onClick={() => setOpen(false)}
>
Rides
</Link>
<Link
to="/parks"
className="px-3 py-2.5 text-base font-medium hover:bg-accent hover:text-accent-foreground rounded-md transition-colors"
onClick={() => setOpen(false)}
>
Parks
</Link>
<Link
to="/manufacturers"
className="px-3 py-2.5 text-base font-medium hover:bg-accent hover:text-accent-foreground rounded-md transition-colors"
onClick={() => setOpen(false)}
>
Manufacturers
</Link>
<Link
to="/designers"
className="px-3 py-2.5 text-base font-medium hover:bg-accent hover:text-accent-foreground rounded-md transition-colors"
onClick={() => setOpen(false)}
>
Designers
</Link>
<Link
to="/operators"
className="px-3 py-2.5 text-base font-medium hover:bg-accent hover:text-accent-foreground rounded-md transition-colors"
onClick={() => setOpen(false)}
>
Operators
</Link>
<Link
to="/owners"
className="px-3 py-2.5 text-base font-medium hover:bg-accent hover:text-accent-foreground rounded-md transition-colors"
onClick={() => setOpen(false)}
>
Property Owners
</Link>
{!rolesLoading && isModerator() && (
<>
<div className="my-2 border-t border-border" />
<Link
to="/admin"
className="px-3 py-2.5 text-base font-medium hover:bg-accent hover:text-accent-foreground rounded-md transition-colors"
onClick={() => setOpen(false)}
>
Admin
</Link>
</>
)}
<div className="my-2 border-t border-border" />
<div className="px-3 py-2.5 flex items-center justify-between">
<span className="text-base font-medium">Theme</span>
<ThemeToggle />
</div>
</nav>
</SheetContent>
</Sheet>
{/* Logo */}
<Link to="/" className="flex items-center hover:opacity-80 transition-opacity flex-shrink-0">
<span className="font-bold text-base sm:text-lg md:text-xl bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent whitespace-nowrap">
ThrillWiki
</span>
</Link>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center space-x-1">
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger className="h-9">Explore</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid min-w-[320px] max-w-[500px] w-fit gap-3 p-4">
<li>
<NavigationMenuLink asChild>
<Link
to="/rides"
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent/20 focus:bg-accent/20"
>
<div className="text-sm font-medium leading-none">Rides</div>
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
Discover exciting rides and attractions
</p>
</Link>
</NavigationMenuLink>
</li>
<li>
<NavigationMenuLink asChild>
<Link
to="/parks"
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent/20 focus:bg-accent/20"
>
<div className="text-sm font-medium leading-none">Parks</div>
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
Browse theme parks around the world
</p>
</Link>
</NavigationMenuLink>
</li>
<li>
<NavigationMenuLink asChild>
<Link
to="/manufacturers"
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent/20 focus:bg-accent/20"
>
<div className="text-sm font-medium leading-none">Manufacturers</div>
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
Explore ride manufacturers
</p>
</Link>
</NavigationMenuLink>
</li>
<li>
<NavigationMenuLink asChild>
<Link
to="/designers"
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent/20 focus:bg-accent/20"
>
<div className="text-sm font-medium leading-none">Designers</div>
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
View ride designers
</p>
</Link>
</NavigationMenuLink>
</li>
<li>
<NavigationMenuLink asChild>
<Link
to="/operators"
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent/20 focus:bg-accent/20"
>
<div className="text-sm font-medium leading-none">Operators</div>
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
Find park operators
</p>
</Link>
</NavigationMenuLink>
</li>
<li>
<NavigationMenuLink asChild>
<Link
to="/owners"
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent/20 focus:bg-accent/20"
>
<div className="text-sm font-medium leading-none">Property Owners</div>
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
View property owners
</p>
</Link>
</NavigationMenuLink>
</li>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
{!rolesLoading && isModerator() && (
<Button variant="ghost" size="sm" className="h-9" asChild>
<Link to="/admin">Admin</Link>
</Button>
)}
</nav>
</div>
{/* Right side: Search, Theme, Auth */}
<div className="flex items-center gap-1.5 md:gap-2 flex-shrink-0">
{/* Desktop Search */}
<div className="hidden lg:block lg:w-64 xl:w-80 min-w-0">
<SearchDropdown />
</div>
{/* Mobile Search Button */}
<Button
variant="ghost"
size="icon"
className="lg:hidden h-9 w-9 min-w-[36px]"
onClick={() => setMobileSearchOpen(true)}
>
<Search className="h-5 w-5" />
<span className="sr-only">Search</span>
</Button>
<div className="hidden md:block">
<ThemeToggle />
</div>
{user && <NotificationCenter />}
<AuthButtons />
</div>
</div>
</header>
{/* Mobile Search Modal */}
<MobileSearch open={mobileSearchOpen} onOpenChange={setMobileSearchOpen} />
</>
);
}

View File

@@ -0,0 +1,61 @@
import { ReactNode } from 'react';
import { NetworkErrorBanner } from '@/components/error/NetworkErrorBanner';
import { SubmissionQueueIndicator } from '@/components/submission/SubmissionQueueIndicator';
import { useNetworkStatus } from '@/hooks/useNetworkStatus';
import { useSubmissionQueue } from '@/hooks/useSubmissionQueue';
interface ResilienceProviderProps {
children: ReactNode;
}
/**
* ResilienceProvider wraps the app with network error handling
* and submission queue management UI
*/
export function ResilienceProvider({ children }: ResilienceProviderProps) {
const { isOnline } = useNetworkStatus();
const {
queuedItems,
lastSyncTime,
nextRetryTime,
retryItem,
retryAll,
removeItem,
clearQueue,
} = useSubmissionQueue({
autoRetry: true,
retryDelayMs: 5000,
maxRetries: 3,
});
return (
<>
{/* Network Error Banner - Shows at top when offline or errors present */}
<NetworkErrorBanner
isOffline={!isOnline}
pendingCount={queuedItems.length}
onRetryNow={retryAll}
estimatedRetryTime={nextRetryTime || undefined}
/>
{/* Main Content */}
<div className="min-h-screen">
{children}
</div>
{/* Floating Queue Indicator - Shows in bottom right */}
{queuedItems.length > 0 && (
<div className="fixed bottom-6 right-6 z-40">
<SubmissionQueueIndicator
queuedItems={queuedItems}
lastSyncTime={lastSyncTime || undefined}
onRetryItem={retryItem}
onRetryAll={retryAll}
onRemoveItem={removeItem}
onClearQueue={clearQueue}
/>
</div>
)}
</>
);
}