mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 02:51:12 -05:00
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:
@@ -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>
|
||||
|
||||
34
src/components/layout/PageTransition.tsx
Normal file
34
src/components/layout/PageTransition.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
87
src/components/navigation/EntityBreadcrumb.tsx
Normal file
87
src/components/navigation/EntityBreadcrumb.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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')}>
|
||||
|
||||
@@ -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')}>
|
||||
|
||||
@@ -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')}>
|
||||
|
||||
@@ -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')}>
|
||||
|
||||
@@ -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';
|
||||
@@ -197,8 +198,25 @@ 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">
|
||||
{/* 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
|
||||
|
||||
Reference in New Issue
Block a user