Compare commits

...

4 Commits

Author SHA1 Message Date
gpt-engineer-app[bot]
3867d30aac Enhance loading skeletons and breadcrumbs
- Add content-m matching loading skeletons for ParkDetail, RideDetail, CompanyDetail, etc., replacing generic spinners to preserve layout during load
- Remove redundant Back to Parent Entity buttons in detail pages in favor of breadcrumb navigation
- Prepare groundwork for breadcrumbs across detail pages to improve cohesion and navigation
2025-11-12 03:51:15 +00:00
gpt-engineer-app[bot]
fdfa1739e5 Add breadcrumb and transitions
Introduce breadcrumb navigation component and integrate into detail pages with hover previews; add PageTransition to App for smooth navigations and loading animations.
2025-11-12 03:46:34 +00:00
gpt-engineer-app[bot]
361231bfac Add hover preview cards
Adds hover-preview UX by introducing preview cards for entities and wiring hoverable links:
- Implements CompanyPreviewCard and ParkPreviewCard components plus hooks to fetch preview data
- Adds HoverCard usage to ParkDetail and RideDetail for operator, manufacturer, and designer links
- Creates preview wrappers for manufacturer/designer/operator links and updates related pages to use hover previews
- Includes supporting updates to query keys and preview hooks to fetch minimal data for previews
2025-11-12 03:44:01 +00:00
gpt-engineer-app[bot]
2ccfe8c48a Make entity names clickable
Update various components to wrap display names (parks, rides, manufacturers, designers, operators, etc.) in Link elements so they navigate to detail pages, aligning with the ParkDetail change. This includesRideDetail, RideListView, RideCreditCard, ParkDetail, and related imports, to enhance cohesion, interactivity, and browseability across the app.
2025-11-12 03:40:29 +00:00
19 changed files with 894 additions and 126 deletions

View File

@@ -24,6 +24,7 @@ import { ResilienceProvider } from "@/components/layout/ResilienceProvider";
import { useAdminRoutePreload } from "@/hooks/useAdminRoutePreload";
import { useVersionCheck } from "@/hooks/useVersionCheck";
import { cn } from "@/lib/utils";
import { PageTransition } from "@/components/layout/PageTransition";
// Core routes (eager-loaded for best UX)
import Index from "./pages/Index";
@@ -164,8 +165,9 @@ function AppContent(): React.JSX.Element {
<div className="min-h-screen flex flex-col">
<div className="flex-1">
<Suspense fallback={<PageLoader />}>
<RouteErrorBoundary>
<Routes>
<PageTransition>
<RouteErrorBoundary>
<Routes>
{/* Core routes - eager loaded */}
<Route path="/" element={<Index />} />
<Route path="/parks" element={<Parks />} />
@@ -443,7 +445,8 @@ function AppContent(): React.JSX.Element {
<Route path="*" element={<NotFound />} />
</Routes>
</RouteErrorBoundary>
</Suspense>
</PageTransition>
</Suspense>
</div>
<Footer />
</div>

View File

@@ -0,0 +1,34 @@
import { ReactNode, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
interface PageTransitionProps {
children: ReactNode;
}
export function PageTransition({ children }: PageTransitionProps) {
const location = useLocation();
const [displayLocation, setDisplayLocation] = useState(location);
const [transitionStage, setTransitionStage] = useState<'fade-in' | 'fade-out'>('fade-in');
useEffect(() => {
if (location !== displayLocation) {
setTransitionStage('fade-out');
}
}, [location, displayLocation]);
const onAnimationEnd = () => {
if (transitionStage === 'fade-out') {
setTransitionStage('fade-in');
setDisplayLocation(location);
}
};
return (
<div
className={`${transitionStage === 'fade-out' ? 'animate-fade-out' : 'animate-fade-in'}`}
onAnimationEnd={onAnimationEnd}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,98 @@
import { Card, CardContent, CardHeader } from '@/components/ui/card';
export function CompanyDetailSkeleton() {
return (
<div className="container mx-auto px-4 py-8 max-w-7xl animate-pulse">
{/* Breadcrumb */}
<div className="h-4 bg-muted rounded w-56 mb-4" />
{/* Edit Button Area */}
<div className="flex justify-end mb-6">
<div className="h-10 bg-muted rounded w-32" />
</div>
{/* Hero Banner */}
<div className="aspect-[21/9] bg-muted rounded-lg mb-8" />
{/* Stats Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-12 max-w-6xl mx-auto">
{[1, 2, 3, 4].map((i) => (
<Card key={i} className="border-0 bg-gradient-to-br from-muted/50 to-muted/30">
<CardContent className="p-4 text-center">
<div className="h-8 bg-muted rounded w-16 mx-auto mb-2" />
<div className="h-3 bg-muted rounded w-20 mx-auto" />
</CardContent>
</Card>
))}
</div>
{/* Tabs */}
<div className="flex gap-2 border-b mb-6">
{['Overview', 'Rides', 'Models', 'Photos'].map((tab) => (
<div key={tab} className="h-10 bg-muted rounded w-20" />
))}
</div>
{/* Content Grid */}
<div className="grid lg:grid-cols-3 gap-6">
{/* Main Content */}
<div className="lg:col-span-2 space-y-6">
{/* Description Card */}
<Card>
<CardHeader>
<div className="h-6 bg-muted rounded w-48" />
</CardHeader>
<CardContent className="space-y-3">
<div className="h-4 bg-muted rounded w-full" />
<div className="h-4 bg-muted rounded w-full" />
<div className="h-4 bg-muted rounded w-4/5" />
</CardContent>
</Card>
{/* Products Grid */}
<Card>
<CardHeader>
<div className="h-6 bg-muted rounded w-40" />
</CardHeader>
<CardContent>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="space-y-2">
<div className="aspect-square bg-muted rounded-lg" />
<div className="h-4 bg-muted rounded w-full" />
<div className="h-3 bg-muted rounded w-2/3" />
</div>
))}
</div>
</CardContent>
</Card>
</div>
{/* Sidebar */}
<div className="space-y-6">
{/* Company Info Card */}
<Card>
<CardHeader>
<div className="h-6 bg-muted rounded w-40" />
</CardHeader>
<CardContent className="space-y-4">
{/* Logo */}
<div className="w-32 h-32 bg-muted rounded mx-auto mb-4" />
{/* Info Items */}
{[1, 2, 3].map((i) => (
<div key={i} className="flex items-center gap-3">
<div className="w-4 h-4 bg-muted rounded" />
<div className="flex-1">
<div className="h-4 bg-muted rounded w-24 mb-1" />
<div className="h-3 bg-muted rounded w-32" />
</div>
</div>
))}
</CardContent>
</Card>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,101 @@
import { Card, CardContent, CardHeader } from '@/components/ui/card';
export function ParkDetailSkeleton() {
return (
<div className="container mx-auto px-4 py-8 max-w-7xl animate-pulse">
{/* Breadcrumb */}
<div className="h-4 bg-muted rounded w-48 mb-4" />
{/* Edit Button Area */}
<div className="flex justify-end mb-6">
<div className="h-10 bg-muted rounded w-32" />
</div>
{/* Hero Banner */}
<div className="aspect-[21/9] bg-muted rounded-lg mb-8" />
{/* Stats Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-12 max-w-6xl mx-auto">
{[1, 2, 3, 4].map((i) => (
<Card key={i} className="border-0 bg-gradient-to-br from-muted/50 to-muted/30">
<CardContent className="p-4 text-center">
<div className="h-8 bg-muted rounded w-16 mx-auto mb-2" />
<div className="h-3 bg-muted rounded w-20 mx-auto" />
</CardContent>
</Card>
))}
</div>
{/* Tabs */}
<div className="flex gap-2 border-b mb-6">
{['Overview', 'Rides', 'Reviews', 'Photos', 'History'].map((tab) => (
<div key={tab} className="h-10 bg-muted rounded w-24" />
))}
</div>
{/* Content Grid */}
<div className="grid lg:grid-cols-3 gap-6">
{/* Main Content */}
<div className="lg:col-span-2 space-y-6">
{/* Description Card */}
<Card>
<CardHeader>
<div className="h-6 bg-muted rounded w-48" />
</CardHeader>
<CardContent className="space-y-3">
<div className="h-4 bg-muted rounded w-full" />
<div className="h-4 bg-muted rounded w-full" />
<div className="h-4 bg-muted rounded w-3/4" />
</CardContent>
</Card>
{/* Featured Rides Card */}
<Card>
<CardHeader>
<div className="h-6 bg-muted rounded w-40" />
</CardHeader>
<CardContent>
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="space-y-2">
<div className="aspect-square bg-muted rounded-lg" />
<div className="h-4 bg-muted rounded w-full" />
<div className="h-3 bg-muted rounded w-3/4" />
</div>
))}
</div>
</CardContent>
</Card>
</div>
{/* Sidebar */}
<div className="space-y-6">
{/* Info Card */}
<Card>
<CardHeader>
<div className="h-6 bg-muted rounded w-40" />
</CardHeader>
<CardContent className="space-y-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="flex items-center gap-3">
<div className="w-4 h-4 bg-muted rounded" />
<div className="flex-1">
<div className="h-4 bg-muted rounded w-24 mb-1" />
<div className="h-3 bg-muted rounded w-32" />
</div>
</div>
))}
</CardContent>
</Card>
{/* Map Card */}
<Card>
<CardContent className="p-0">
<div className="aspect-square bg-muted rounded-lg" />
</CardContent>
</Card>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,106 @@
import { Card, CardContent } from '@/components/ui/card';
export function RideDetailSkeleton() {
return (
<div className="container mx-auto px-4 py-8 max-w-7xl animate-pulse">
{/* Breadcrumb */}
<div className="h-4 bg-muted rounded w-64 mb-4" />
{/* Edit Button Area */}
<div className="flex justify-end mb-6">
<div className="h-10 bg-muted rounded w-32" />
</div>
{/* Hero Banner */}
<div className="aspect-[21/9] bg-muted rounded-lg mb-8" />
{/* Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 mb-12">
{[1, 2, 3, 4, 5, 6].map((i) => (
<Card key={i} className="border-0 bg-gradient-to-br from-muted/50 to-muted/30">
<CardContent className="p-4 text-center">
<div className="w-6 h-6 bg-muted rounded mx-auto mb-2" />
<div className="h-8 bg-muted rounded w-16 mx-auto mb-1" />
<div className="h-3 bg-muted rounded w-12 mx-auto" />
</CardContent>
</Card>
))}
</div>
{/* Tabs */}
<div className="flex gap-2 border-b mb-6">
{['Overview', 'Reviews', 'Photos', 'History'].map((tab) => (
<div key={tab} className="h-10 bg-muted rounded w-24" />
))}
</div>
{/* Content Grid */}
<div className="grid lg:grid-cols-3 gap-6">
{/* Main Content */}
<div className="lg:col-span-2 space-y-6">
{/* Description Card */}
<Card>
<CardContent className="p-6 space-y-3">
<div className="h-6 bg-muted rounded w-48 mb-4" />
<div className="h-4 bg-muted rounded w-full" />
<div className="h-4 bg-muted rounded w-full" />
<div className="h-4 bg-muted rounded w-5/6" />
</CardContent>
</Card>
{/* Technical Specs */}
<Card>
<CardContent className="p-6 space-y-4">
<div className="h-6 bg-muted rounded w-56 mb-4" />
<div className="grid grid-cols-2 gap-4">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="space-y-2">
<div className="h-3 bg-muted rounded w-24" />
<div className="h-5 bg-muted rounded w-32" />
</div>
))}
</div>
</CardContent>
</Card>
</div>
{/* Sidebar */}
<div className="space-y-6">
{/* Ride Info Card */}
<Card>
<CardContent className="p-6 space-y-4">
<div className="h-6 bg-muted rounded w-40 mb-4" />
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="flex items-center gap-3">
<div className="w-4 h-4 bg-muted rounded" />
<div className="flex-1">
<div className="h-4 bg-muted rounded w-20 mb-1" />
<div className="h-3 bg-muted rounded w-28" />
</div>
</div>
))}
</CardContent>
</Card>
{/* Similar Rides */}
<Card>
<CardContent className="p-6">
<div className="h-6 bg-muted rounded w-32 mb-4" />
<div className="space-y-3">
{[1, 2, 3].map((i) => (
<div key={i} className="flex gap-3">
<div className="w-16 h-16 bg-muted rounded" />
<div className="flex-1 space-y-2">
<div className="h-4 bg-muted rounded w-full" />
<div className="h-3 bg-muted rounded w-3/4" />
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,87 @@
import { Link } from 'react-router-dom';
import { Home } from 'lucide-react';
import {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
import { ParkPreviewCard } from '@/components/preview/ParkPreviewCard';
import { CompanyPreviewCard } from '@/components/preview/CompanyPreviewCard';
interface BreadcrumbSegment {
label: string;
href?: string;
showPreview?: boolean;
previewType?: 'park' | 'company';
previewSlug?: string;
}
interface EntityBreadcrumbProps {
segments: BreadcrumbSegment[];
className?: string;
}
export function EntityBreadcrumb({ segments, className }: EntityBreadcrumbProps) {
return (
<Breadcrumb className={className}>
<BreadcrumbList>
{/* Home link */}
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="/" className="flex items-center gap-1 hover:text-primary transition-colors">
<Home className="w-3.5 h-3.5" />
<span>Home</span>
</Link>
</BreadcrumbLink>
</BreadcrumbItem>
{segments.map((segment, index) => {
const isLast = index === segments.length - 1;
return (
<BreadcrumbItem key={index}>
<BreadcrumbSeparator />
{isLast ? (
<BreadcrumbPage>{segment.label}</BreadcrumbPage>
) : segment.showPreview && segment.previewSlug ? (
<HoverCard openDelay={300}>
<HoverCardTrigger asChild>
<BreadcrumbLink asChild>
<Link
to={segment.href || '#'}
className="hover:text-primary transition-colors"
>
{segment.label}
</Link>
</BreadcrumbLink>
</HoverCardTrigger>
<HoverCardContent side="bottom" align="start" className="w-auto">
{segment.previewType === 'park' && (
<ParkPreviewCard slug={segment.previewSlug} />
)}
{segment.previewType === 'company' && (
<CompanyPreviewCard slug={segment.previewSlug} />
)}
</HoverCardContent>
</HoverCard>
) : (
<BreadcrumbLink asChild>
<Link
to={segment.href || '#'}
className="hover:text-primary transition-colors"
>
{segment.label}
</Link>
</BreadcrumbLink>
)}
</BreadcrumbItem>
);
})}
</BreadcrumbList>
</Breadcrumb>
);
}

View File

@@ -0,0 +1,80 @@
import { Building2, MapPin, Calendar } from 'lucide-react';
import { useCompanyPreview } from '@/hooks/preview/useCompanyPreview';
import { Badge } from '@/components/ui/badge';
interface CompanyPreviewCardProps {
slug: string;
}
export function CompanyPreviewCard({ slug }: CompanyPreviewCardProps) {
const { data: company, isLoading } = useCompanyPreview(slug);
if (isLoading) {
return (
<div className="w-80">
<div className="animate-pulse space-y-3">
<div className="h-16 bg-muted rounded" />
<div className="h-4 bg-muted rounded w-3/4" />
<div className="h-4 bg-muted rounded w-1/2" />
</div>
</div>
);
}
if (!company) {
return (
<div className="w-80 p-4 text-center text-muted-foreground">
Company not found
</div>
);
}
const formatCompanyType = (type: string) => {
return type.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
};
return (
<div className="w-80 space-y-3">
{/* Header with logo */}
<div className="flex items-start gap-3">
{company.logo_url ? (
<img
src={company.logo_url}
alt={company.name}
className="w-12 h-12 object-contain rounded"
/>
) : (
<div className="w-12 h-12 bg-muted rounded flex items-center justify-center">
<Building2 className="w-6 h-6 text-muted-foreground" />
</div>
)}
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-base line-clamp-1">{company.name}</h3>
<Badge variant="secondary" className="mt-1">
{formatCompanyType(company.company_type)}
</Badge>
</div>
</div>
{/* Location and Founded */}
<div className="space-y-2 text-sm">
{company.headquarters_location && (
<div className="flex items-center gap-2 text-muted-foreground">
<MapPin className="w-4 h-4 flex-shrink-0" />
<span className="line-clamp-1">{company.headquarters_location}</span>
</div>
)}
{company.founded_year && (
<div className="flex items-center gap-2 text-muted-foreground">
<Calendar className="w-4 h-4 flex-shrink-0" />
<span>Founded {company.founded_year}</span>
</div>
)}
</div>
<p className="text-xs text-muted-foreground">
Click to view full details
</p>
</div>
);
}

View File

@@ -0,0 +1,112 @@
import { MapPin, Star, FerrisWheel, Zap } from 'lucide-react';
import { useParkPreview } from '@/hooks/preview/useParkPreview';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
interface ParkPreviewCardProps {
slug: string;
}
export function ParkPreviewCard({ slug }: ParkPreviewCardProps) {
const { data: park, isLoading } = useParkPreview(slug);
if (isLoading) {
return (
<div className="w-80">
<div className="animate-pulse space-y-3">
<div className="h-32 bg-muted rounded" />
<div className="h-4 bg-muted rounded w-3/4" />
<div className="h-4 bg-muted rounded w-1/2" />
</div>
</div>
);
}
if (!park) {
return (
<div className="w-80 p-4 text-center text-muted-foreground">
Park not found
</div>
);
}
const getStatusColor = (status: string) => {
switch (status) {
case 'operating':
return 'bg-green-500/20 text-green-400 border-green-500/30';
case 'seasonal':
return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30';
case 'under_construction':
return 'bg-blue-500/20 text-blue-400 border-blue-500/30';
default:
return 'bg-red-500/20 text-red-400 border-red-500/30';
}
};
const formatParkType = (type: string) => {
return type.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
};
return (
<div className="w-80 space-y-3">
{/* Image */}
{park.card_image_url && (
<div className="aspect-video rounded-lg overflow-hidden bg-muted">
<img
src={park.card_image_url}
alt={park.name}
className="w-full h-full object-cover"
/>
</div>
)}
{/* Header */}
<div>
<h3 className="font-semibold text-base line-clamp-1 mb-2">{park.name}</h3>
<div className="flex items-center gap-2 flex-wrap">
<Badge className={`${getStatusColor(park.status)} border text-xs`}>
{park.status.replace('_', ' ').toUpperCase()}
</Badge>
<Badge variant="outline" className="text-xs">
{formatParkType(park.park_type)}
</Badge>
</div>
</div>
{/* Location */}
{park.location && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<MapPin className="w-4 h-4 flex-shrink-0" />
<span className="line-clamp-1">
{[park.location.city, park.location.state_province, park.location.country]
.filter(Boolean)
.join(', ')}
</span>
</div>
)}
<Separator />
{/* Stats */}
<div className="grid grid-cols-2 gap-3 text-sm">
<div className="flex items-center gap-2">
<FerrisWheel className="w-4 h-4 text-primary" />
<span className="font-medium">{park.ride_count || 0}</span>
<span className="text-muted-foreground">rides</span>
</div>
<div className="flex items-center gap-2">
<Zap className="w-4 h-4 text-accent" />
<span className="font-medium">{park.coaster_count || 0}</span>
<span className="text-muted-foreground">coasters</span>
</div>
{park.average_rating && park.average_rating > 0 && (
<div className="flex items-center gap-2 col-span-2">
<Star className="w-4 h-4 text-yellow-500 fill-yellow-500" />
<span className="font-medium">{park.average_rating.toFixed(1)}</span>
<span className="text-muted-foreground">({park.review_count} reviews)</span>
</div>
)}
</div>
</div>
);
}

View File

@@ -4,6 +4,9 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Ride } from '@/types/database';
import { cn } from '@/lib/utils';
import { Link } from 'react-router-dom';
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
import { CompanyPreviewCard } from '@/components/preview/CompanyPreviewCard';
interface RideListViewProps {
rides: Ride[];
@@ -115,10 +118,19 @@ export function RideListView({ rides, onRideClick }: RideListViewProps) {
{formatCategory(ride.category)}
</Badge>
{ride.manufacturer && (
<Badge variant="outline" className="text-xs backdrop-blur-sm border-accent/20 group-hover:border-accent/40 transition-colors duration-300">
<Factory className="w-3 h-3 mr-1" />
{ride.manufacturer.name}
</Badge>
<HoverCard openDelay={300}>
<HoverCardTrigger asChild>
<Link to={`/manufacturers/${ride.manufacturer.slug}`}>
<Badge variant="outline" className="text-xs backdrop-blur-sm border-accent/20 group-hover:border-accent/40 transition-colors duration-300 hover:bg-accent/10 cursor-pointer">
<Factory className="w-3 h-3 mr-1" />
{ride.manufacturer.name}
</Badge>
</Link>
</HoverCardTrigger>
<HoverCardContent side="top" className="w-auto">
<CompanyPreviewCard slug={ride.manufacturer.slug} />
</HoverCardContent>
</HoverCard>
)}
</div>
</div>

View File

@@ -0,0 +1,36 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/lib/supabaseClient';
import { queryKeys } from '@/lib/queryKeys';
/**
* Hook to fetch company preview data for hover cards
*/
export function useCompanyPreview(slug: string | undefined, enabled = true) {
return useQuery({
queryKey: queryKeys.companies.detail(slug || ''),
queryFn: async () => {
if (!slug) throw new Error('Slug is required');
const { data, error } = await supabase
.from('companies')
.select(`
id,
name,
slug,
company_type,
person_type,
headquarters_location,
founded_year,
logo_url
`)
.eq('slug', slug)
.maybeSingle();
if (error) throw error;
return data;
},
enabled: enabled && !!slug,
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 15 * 60 * 1000, // 15 minutes
});
}

View File

@@ -0,0 +1,39 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/lib/supabaseClient';
import { queryKeys } from '@/lib/queryKeys';
/**
* Hook to fetch park preview data for hover cards
*/
export function useParkPreview(slug: string | undefined, enabled = true) {
return useQuery({
queryKey: queryKeys.parks.detail(slug || ''),
queryFn: async () => {
if (!slug) throw new Error('Slug is required');
const { data, error } = await supabase
.from('parks')
.select(`
id,
name,
slug,
park_type,
status,
card_image_url,
ride_count,
coaster_count,
average_rating,
review_count,
location:locations(city, state_province, country)
`)
.eq('slug', slug)
.maybeSingle();
if (error) throw error;
return data;
},
enabled: enabled && !!slug,
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 15 * 60 * 1000, // 15 minutes
});
}

View File

@@ -106,6 +106,11 @@ export const queryKeys = {
maintenanceTables: () => ['admin', 'maintenance-tables'] as const,
},
// Companies queries
companies: {
detail: (slug: string) => ['companies', 'detail', slug] as const,
},
// Analytics queries
analytics: {
all: ['analytics'] as const,

View File

@@ -1,5 +1,7 @@
import { useState, useEffect, lazy, Suspense } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { EntityBreadcrumb } from '@/components/navigation/EntityBreadcrumb';
import { CompanyDetailSkeleton } from '@/components/loading/CompanyDetailSkeleton';
import { Header } from '@/components/layout/Header';
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
import { Button } from '@/components/ui/button';
@@ -149,12 +151,7 @@ export default function DesignerDetail() {
return (
<div className="min-h-screen bg-background">
<Header />
<div className="container mx-auto px-4 py-8">
<div className="animate-pulse space-y-6">
<div className="h-64 bg-muted rounded-lg"></div>
<div className="h-8 bg-muted rounded w-1/2"></div>
</div>
</div>
<CompanyDetailSkeleton />
</div>
);
}
@@ -181,13 +178,17 @@ export default function DesignerDetail() {
<Header />
<main className="container mx-auto px-4 py-8 max-w-7xl">
{/* Back Button and Edit Button */}
<div className="flex items-center justify-between mb-6">
<Button variant="ghost" onClick={() => navigate('/designers')}>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Designers
</Button>
{/* Breadcrumb Navigation */}
<EntityBreadcrumb
segments={[
{ label: 'Designers', href: '/designers' },
{ label: designer.name }
]}
className="mb-4"
/>
{/* Edit Button */}
<div className="flex justify-end mb-6">
<Button
variant="outline"
onClick={() => requireAuth(() => setIsEditModalOpen(true), "Sign in to edit this designer")}

View File

@@ -1,5 +1,7 @@
import { useState, useEffect, lazy, Suspense } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { EntityBreadcrumb } from '@/components/navigation/EntityBreadcrumb';
import { CompanyDetailSkeleton } from '@/components/loading/CompanyDetailSkeleton';
import { Header } from '@/components/layout/Header';
import { trackPageView } from '@/lib/viewTracking';
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
@@ -159,12 +161,7 @@ export default function ManufacturerDetail() {
return (
<div className="min-h-screen bg-background">
<Header />
<div className="container mx-auto px-4 py-8">
<div className="animate-pulse space-y-6">
<div className="h-64 bg-muted rounded-lg"></div>
<div className="h-8 bg-muted rounded w-1/2"></div>
</div>
</div>
<CompanyDetailSkeleton />
</div>
);
}
@@ -191,14 +188,17 @@ export default function ManufacturerDetail() {
<Header />
<main className="container mx-auto px-4 py-8 max-w-7xl">
{/* Back Button and Edit Button */}
<div className="flex items-center justify-between mb-6">
<Button variant="ghost" onClick={() => navigate('/manufacturers')}>
<ArrowLeft className="w-4 h-4 mr-2" />
<span className="md:hidden">Back</span>
<span className="hidden md:inline">Back to Manufacturers</span>
</Button>
{/* Breadcrumb Navigation */}
<EntityBreadcrumb
segments={[
{ label: 'Manufacturers', href: '/manufacturers' },
{ label: manufacturer.name }
]}
className="mb-4"
/>
{/* Edit Button */}
<div className="flex justify-end mb-6">
<Button
variant="outline"
onClick={() => requireAuth(() => setIsEditModalOpen(true), "Sign in to edit this manufacturer")}

View File

@@ -1,5 +1,7 @@
import { useState, useEffect, lazy, Suspense } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { EntityBreadcrumb } from '@/components/navigation/EntityBreadcrumb';
import { CompanyDetailSkeleton } from '@/components/loading/CompanyDetailSkeleton';
import { Header } from '@/components/layout/Header';
import { trackPageView } from '@/lib/viewTracking';
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
@@ -188,12 +190,7 @@ export default function OperatorDetail() {
return (
<div className="min-h-screen bg-background">
<Header />
<div className="container mx-auto px-4 py-8">
<div className="animate-pulse space-y-6">
<div className="h-64 bg-muted rounded-lg"></div>
<div className="h-8 bg-muted rounded w-1/2"></div>
</div>
</div>
<CompanyDetailSkeleton />
</div>
);
}
@@ -220,13 +217,17 @@ export default function OperatorDetail() {
<Header />
<main className="container mx-auto px-4 py-8 max-w-7xl">
{/* Back Button and Edit Button */}
<div className="flex items-center justify-between mb-6">
<Button variant="ghost" onClick={() => navigate('/operators')}>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Operators
</Button>
{/* Breadcrumb Navigation */}
<EntityBreadcrumb
segments={[
{ label: 'Operators', href: '/operators' },
{ label: operator.name }
]}
className="mb-4"
/>
{/* Edit Button */}
<div className="flex justify-end mb-6">
<Button
variant="outline"
onClick={() => requireAuth(() => setIsEditModalOpen(true), "Sign in to edit this operator")}

View File

@@ -1,5 +1,9 @@
import { useState, lazy, Suspense, useEffect } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
import { CompanyPreviewCard } from '@/components/preview/CompanyPreviewCard';
import { EntityBreadcrumb } from '@/components/navigation/EntityBreadcrumb';
import { ParkDetailSkeleton } from '@/components/loading/ParkDetailSkeleton';
import { Header } from '@/components/layout/Header';
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
import { trackPageView } from '@/lib/viewTracking';
@@ -161,13 +165,7 @@ export default function ParkDetail() {
if (loading) {
return <div className="min-h-screen bg-background">
<Header />
<div className="container mx-auto px-4 py-8">
<div className="animate-pulse space-y-6">
<div className="h-64 bg-muted rounded-lg"></div>
<div className="h-8 bg-muted rounded w-1/2"></div>
<div className="h-4 bg-muted rounded w-1/3"></div>
</div>
</div>
<ParkDetailSkeleton />
</div>;
}
if (!park) {
@@ -191,13 +189,17 @@ export default function ParkDetail() {
<Header />
<main className="container mx-auto px-4 py-8 max-w-7xl">
{/* Back Button and Edit Button */}
<div className="flex items-center justify-between mb-6">
<Button variant="ghost" onClick={() => navigate('/parks')}>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Parks
</Button>
{/* Breadcrumb Navigation */}
<EntityBreadcrumb
segments={[
{ label: 'Parks', href: '/parks' },
{ label: park.name }
]}
className="mb-4"
/>
{/* Edit Button */}
<div className="flex justify-end mb-6">
<Button
variant="outline"
onClick={() => requireAuth(() => setIsEditParkModalOpen(true), "Sign in to edit this park")}
@@ -435,12 +437,19 @@ export default function ParkDetail() {
<Users className="w-4 h-4 text-muted-foreground" />
<div>
<div className="font-medium">Operator</div>
<Link
to={`/operators/${park.operator.slug}`}
className="text-sm text-primary hover:underline"
>
{park.operator.name}
</Link>
<HoverCard openDelay={300}>
<HoverCardTrigger asChild>
<Link
to={`/operators/${park.operator.slug}`}
className="text-sm text-primary hover:underline"
>
{park.operator.name}
</Link>
</HoverCardTrigger>
<HoverCardContent side="right" className="w-auto">
<CompanyPreviewCard slug={park.operator.slug} />
</HoverCardContent>
</HoverCard>
</div>
</div>}

View File

@@ -1,6 +1,8 @@
import { useState, useEffect, lazy, Suspense } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Header } from '@/components/layout/Header';
import { EntityBreadcrumb } from '@/components/navigation/EntityBreadcrumb';
import { CompanyDetailSkeleton } from '@/components/loading/CompanyDetailSkeleton';
import { trackPageView } from '@/lib/viewTracking';
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
import { Button } from '@/components/ui/button';
@@ -188,12 +190,7 @@ export default function PropertyOwnerDetail() {
return (
<div className="min-h-screen bg-background">
<Header />
<div className="container mx-auto px-4 py-8">
<div className="animate-pulse space-y-6">
<div className="h-64 bg-muted rounded-lg"></div>
<div className="h-8 bg-muted rounded w-1/2"></div>
</div>
</div>
<CompanyDetailSkeleton />
</div>
);
}
@@ -220,13 +217,17 @@ export default function PropertyOwnerDetail() {
<Header />
<main className="container mx-auto px-4 py-8 max-w-7xl">
{/* Back Button and Edit Button */}
<div className="flex items-center justify-between mb-6">
<Button variant="ghost" onClick={() => navigate('/owners')}>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Property Owners
</Button>
{/* Breadcrumb Navigation */}
<EntityBreadcrumb
segments={[
{ label: 'Property Owners', href: '/owners' },
{ label: owner.name }
]}
className="mb-4"
/>
{/* Edit Button */}
<div className="flex justify-end mb-6">
<Button
variant="outline"
onClick={() => requireAuth(() => setIsEditModalOpen(true), "Sign in to edit this property owner")}

View File

@@ -1,5 +1,10 @@
import { useState, lazy, Suspense, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
import { CompanyPreviewCard } from '@/components/preview/CompanyPreviewCard';
import { ParkPreviewCard } from '@/components/preview/ParkPreviewCard';
import { EntityBreadcrumb } from '@/components/navigation/EntityBreadcrumb';
import { RideDetailSkeleton } from '@/components/loading/RideDetailSkeleton';
import { Header } from '@/components/layout/Header';
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
import { trackPageView } from '@/lib/viewTracking';
@@ -160,13 +165,7 @@ export default function RideDetail() {
return (
<div className="min-h-screen bg-background">
<Header />
<div className="container mx-auto px-4 py-8">
<div className="animate-pulse space-y-6">
<div className="h-64 bg-muted rounded-lg"></div>
<div className="h-8 bg-muted rounded w-1/2"></div>
<div className="h-4 bg-muted rounded w-1/3"></div>
</div>
</div>
<RideDetailSkeleton />
</div>
);
}
@@ -194,18 +193,27 @@ export default function RideDetail() {
return (
<div className="min-h-screen bg-background">
<Header />
<main className="container mx-auto px-4 py-8 max-w-7xl">
{/* Back Button and Edit Button */}
<div className="flex items-center justify-between mb-6">
<Button
variant="ghost"
onClick={() => navigate(`/parks/${ride.park?.slug}`)}
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to {ride.park?.name}
</Button>
{/* Breadcrumb Navigation */}
<EntityBreadcrumb
segments={[
{ label: 'Parks', href: '/parks' },
{
label: ride.park.name,
href: `/parks/${ride.park.slug}`,
showPreview: true,
previewType: 'park',
previewSlug: ride.park.slug
},
{ label: 'Rides', href: `/parks/${ride.park.slug}#rides` },
{ label: ride.name }
]}
className="mb-4"
/>
{/* Edit Button */}
<div className="flex justify-end mb-6">
<Button
variant="outline"
onClick={() => requireAuth(() => setIsEditModalOpen(true), "Sign in to edit this ride")}
@@ -255,10 +263,20 @@ export default function RideDetail() {
<h1 className="text-4xl md:text-6xl font-bold text-white mb-2">
{ride.name}
</h1>
<div className="flex items-center text-white/90 text-lg">
<MapPin className="w-5 h-5 mr-2" />
{ride.park.name}
</div>
<HoverCard openDelay={300}>
<HoverCardTrigger asChild>
<Link
to={`/parks/${ride.park.slug}`}
className="flex items-center text-white/90 text-lg hover:text-white transition-colors"
>
<MapPin className="w-5 h-5 mr-2" />
<span className="hover:underline">{ride.park.name}</span>
</Link>
</HoverCardTrigger>
<HoverCardContent side="bottom" align="start" className="w-auto">
<ParkPreviewCard slug={ride.park.slug} />
</HoverCardContent>
</HoverCard>
<div className="mt-3">
<VersionIndicator
entityType="ride"
@@ -471,9 +489,19 @@ export default function RideDetail() {
<Users className="w-4 h-4 text-muted-foreground" />
<div>
<div className="font-medium">Manufacturer</div>
<div className="text-sm text-muted-foreground">
{ride.manufacturer.name}
</div>
<HoverCard openDelay={300}>
<HoverCardTrigger asChild>
<Link
to={`/manufacturers/${ride.manufacturer.slug}`}
className="text-sm text-primary hover:underline"
>
{ride.manufacturer.name}
</Link>
</HoverCardTrigger>
<HoverCardContent side="right" className="w-auto">
<CompanyPreviewCard slug={ride.manufacturer.slug} />
</HoverCardContent>
</HoverCard>
</div>
</div>
)}
@@ -483,9 +511,19 @@ export default function RideDetail() {
<Users className="w-4 h-4 text-muted-foreground" />
<div>
<div className="font-medium">Designer</div>
<div className="text-sm text-muted-foreground">
{ride.designer.name}
</div>
<HoverCard openDelay={300}>
<HoverCardTrigger asChild>
<Link
to={`/designers/${ride.designer.slug}`}
className="text-sm text-primary hover:underline"
>
{ride.designer.name}
</Link>
</HoverCardTrigger>
<HoverCardContent side="right" className="w-auto">
<CompanyPreviewCard slug={ride.designer.slug} />
</HoverCardContent>
</HoverCard>
</div>
</div>
)}

View File

@@ -1,6 +1,8 @@
import { useState, useEffect, useCallback, lazy, Suspense } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Header } from '@/components/layout/Header';
import { EntityBreadcrumb } from '@/components/navigation/EntityBreadcrumb';
import { CompanyDetailSkeleton } from '@/components/loading/CompanyDetailSkeleton';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent } from '@/components/ui/card';
@@ -167,17 +169,7 @@ export default function RideModelDetail() {
return (
<div className="min-h-screen bg-background">
<Header />
<div className="container mx-auto px-4 py-8">
<div className="animate-pulse space-y-6">
<div className="h-12 bg-muted rounded w-1/3"></div>
<div className="h-64 bg-muted rounded"></div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{[...Array(6)].map((_, i) => (
<div key={i} className="h-48 bg-muted rounded-lg"></div>
))}
</div>
</div>
</div>
<CompanyDetailSkeleton />
</div>
);
}
@@ -204,12 +196,25 @@ export default function RideModelDetail() {
<Header />
<main className="container mx-auto px-4 py-8">
<div className="flex items-center justify-between mb-6">
<Button variant="ghost" onClick={() => navigate(`/manufacturers/${manufacturerSlug}/models`)}>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to {manufacturer.name} Models
</Button>
{/* Breadcrumb Navigation */}
<EntityBreadcrumb
segments={[
{ label: 'Manufacturers', href: '/manufacturers' },
{
label: manufacturer.name,
href: `/manufacturers/${manufacturerSlug}`,
showPreview: true,
previewType: 'company',
previewSlug: manufacturerSlug || ''
},
{ label: 'Models', href: `/manufacturers/${manufacturerSlug}/models` },
{ label: model.name }
]}
className="mb-4"
/>
{/* Edit Button */}
<div className="flex justify-end mb-6">
<Button
variant="outline"
onClick={() => requireAuth(() => setIsEditModalOpen(true), "Sign in to edit this ride model")}