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.
This commit is contained in:
gpt-engineer-app[bot]
2025-11-12 03:46:34 +00:00
parent 361231bfac
commit fdfa1739e5
8 changed files with 186 additions and 4 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,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

@@ -1,5 +1,6 @@
import { useState, useEffect, lazy, Suspense } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { EntityBreadcrumb } from '@/components/navigation/EntityBreadcrumb';
import { Header } from '@/components/layout/Header';
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
import { Button } from '@/components/ui/button';
@@ -181,6 +182,15 @@ export default function DesignerDetail() {
<Header />
<main className="container mx-auto px-4 py-8 max-w-7xl">
{/* Breadcrumb Navigation */}
<EntityBreadcrumb
segments={[
{ label: 'Designers', href: '/designers' },
{ label: designer.name }
]}
className="mb-4"
/>
{/* Back Button and Edit Button */}
<div className="flex items-center justify-between mb-6">
<Button variant="ghost" onClick={() => navigate('/designers')}>

View File

@@ -1,5 +1,6 @@
import { useState, useEffect, lazy, Suspense } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { EntityBreadcrumb } from '@/components/navigation/EntityBreadcrumb';
import { Header } from '@/components/layout/Header';
import { trackPageView } from '@/lib/viewTracking';
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
@@ -191,6 +192,15 @@ export default function ManufacturerDetail() {
<Header />
<main className="container mx-auto px-4 py-8 max-w-7xl">
{/* Breadcrumb Navigation */}
<EntityBreadcrumb
segments={[
{ label: 'Manufacturers', href: '/manufacturers' },
{ label: manufacturer.name }
]}
className="mb-4"
/>
{/* Back Button and Edit Button */}
<div className="flex items-center justify-between mb-6">
<Button variant="ghost" onClick={() => navigate('/manufacturers')}>

View File

@@ -1,5 +1,6 @@
import { useState, useEffect, lazy, Suspense } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { EntityBreadcrumb } from '@/components/navigation/EntityBreadcrumb';
import { Header } from '@/components/layout/Header';
import { trackPageView } from '@/lib/viewTracking';
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
@@ -220,6 +221,15 @@ export default function OperatorDetail() {
<Header />
<main className="container mx-auto px-4 py-8 max-w-7xl">
{/* Breadcrumb Navigation */}
<EntityBreadcrumb
segments={[
{ label: 'Operators', href: '/operators' },
{ label: operator.name }
]}
className="mb-4"
/>
{/* Back Button and Edit Button */}
<div className="flex items-center justify-between mb-6">
<Button variant="ghost" onClick={() => navigate('/operators')}>

View File

@@ -2,6 +2,7 @@ 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 { Header } from '@/components/layout/Header';
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
import { trackPageView } from '@/lib/viewTracking';
@@ -193,6 +194,15 @@ export default function ParkDetail() {
<Header />
<main className="container mx-auto px-4 py-8 max-w-7xl">
{/* Breadcrumb Navigation */}
<EntityBreadcrumb
segments={[
{ label: 'Parks', href: '/parks' },
{ label: park.name }
]}
className="mb-4"
/>
{/* Back Button and Edit Button */}
<div className="flex items-center justify-between mb-6">
<Button variant="ghost" onClick={() => navigate('/parks')}>

View File

@@ -3,6 +3,7 @@ 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 { Header } from '@/components/layout/Header';
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
import { trackPageView } from '@/lib/viewTracking';
@@ -199,6 +200,23 @@ export default function RideDetail() {
<Header />
<main className="container mx-auto px-4 py-8 max-w-7xl">
{/* 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"
/>
{/* Back Button and Edit Button */}
<div className="flex items-center justify-between mb-6">
<Button