mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:11:11 -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 { useAdminRoutePreload } from "@/hooks/useAdminRoutePreload";
|
||||||
import { useVersionCheck } from "@/hooks/useVersionCheck";
|
import { useVersionCheck } from "@/hooks/useVersionCheck";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { PageTransition } from "@/components/layout/PageTransition";
|
||||||
|
|
||||||
// Core routes (eager-loaded for best UX)
|
// Core routes (eager-loaded for best UX)
|
||||||
import Index from "./pages/Index";
|
import Index from "./pages/Index";
|
||||||
@@ -164,6 +165,7 @@ function AppContent(): React.JSX.Element {
|
|||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Suspense fallback={<PageLoader />}>
|
<Suspense fallback={<PageLoader />}>
|
||||||
|
<PageTransition>
|
||||||
<RouteErrorBoundary>
|
<RouteErrorBoundary>
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Core routes - eager loaded */}
|
{/* Core routes - eager loaded */}
|
||||||
@@ -443,6 +445,7 @@ function AppContent(): React.JSX.Element {
|
|||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</RouteErrorBoundary>
|
</RouteErrorBoundary>
|
||||||
|
</PageTransition>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
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 { useState, useEffect, lazy, Suspense } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import { EntityBreadcrumb } from '@/components/navigation/EntityBreadcrumb';
|
||||||
import { Header } from '@/components/layout/Header';
|
import { Header } from '@/components/layout/Header';
|
||||||
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
|
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -181,6 +182,15 @@ export default function DesignerDetail() {
|
|||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<main className="container mx-auto px-4 py-8 max-w-7xl">
|
<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 */}
|
{/* Back Button and Edit Button */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<Button variant="ghost" onClick={() => navigate('/designers')}>
|
<Button variant="ghost" onClick={() => navigate('/designers')}>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, lazy, Suspense } from 'react';
|
import { useState, useEffect, lazy, Suspense } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import { EntityBreadcrumb } from '@/components/navigation/EntityBreadcrumb';
|
||||||
import { Header } from '@/components/layout/Header';
|
import { Header } from '@/components/layout/Header';
|
||||||
import { trackPageView } from '@/lib/viewTracking';
|
import { trackPageView } from '@/lib/viewTracking';
|
||||||
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
|
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
|
||||||
@@ -191,6 +192,15 @@ export default function ManufacturerDetail() {
|
|||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<main className="container mx-auto px-4 py-8 max-w-7xl">
|
<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 */}
|
{/* Back Button and Edit Button */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<Button variant="ghost" onClick={() => navigate('/manufacturers')}>
|
<Button variant="ghost" onClick={() => navigate('/manufacturers')}>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, lazy, Suspense } from 'react';
|
import { useState, useEffect, lazy, Suspense } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import { EntityBreadcrumb } from '@/components/navigation/EntityBreadcrumb';
|
||||||
import { Header } from '@/components/layout/Header';
|
import { Header } from '@/components/layout/Header';
|
||||||
import { trackPageView } from '@/lib/viewTracking';
|
import { trackPageView } from '@/lib/viewTracking';
|
||||||
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
|
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
|
||||||
@@ -220,6 +221,15 @@ export default function OperatorDetail() {
|
|||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<main className="container mx-auto px-4 py-8 max-w-7xl">
|
<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 */}
|
{/* Back Button and Edit Button */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<Button variant="ghost" onClick={() => navigate('/operators')}>
|
<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 { useParams, useNavigate, Link } from 'react-router-dom';
|
||||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
|
||||||
import { CompanyPreviewCard } from '@/components/preview/CompanyPreviewCard';
|
import { CompanyPreviewCard } from '@/components/preview/CompanyPreviewCard';
|
||||||
|
import { EntityBreadcrumb } from '@/components/navigation/EntityBreadcrumb';
|
||||||
import { Header } from '@/components/layout/Header';
|
import { Header } from '@/components/layout/Header';
|
||||||
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
|
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
|
||||||
import { trackPageView } from '@/lib/viewTracking';
|
import { trackPageView } from '@/lib/viewTracking';
|
||||||
@@ -193,6 +194,15 @@ export default function ParkDetail() {
|
|||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<main className="container mx-auto px-4 py-8 max-w-7xl">
|
<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 */}
|
{/* Back Button and Edit Button */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<Button variant="ghost" onClick={() => navigate('/parks')}>
|
<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 { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
|
||||||
import { CompanyPreviewCard } from '@/components/preview/CompanyPreviewCard';
|
import { CompanyPreviewCard } from '@/components/preview/CompanyPreviewCard';
|
||||||
import { ParkPreviewCard } from '@/components/preview/ParkPreviewCard';
|
import { ParkPreviewCard } from '@/components/preview/ParkPreviewCard';
|
||||||
|
import { EntityBreadcrumb } from '@/components/navigation/EntityBreadcrumb';
|
||||||
import { Header } from '@/components/layout/Header';
|
import { Header } from '@/components/layout/Header';
|
||||||
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
|
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
|
||||||
import { trackPageView } from '@/lib/viewTracking';
|
import { trackPageView } from '@/lib/viewTracking';
|
||||||
@@ -199,6 +200,23 @@ export default function RideDetail() {
|
|||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<main className="container mx-auto px-4 py-8 max-w-7xl">
|
<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 */}
|
{/* Back Button and Edit Button */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Reference in New Issue
Block a user