From 361231bfac077f6b2527fc296301fc4bf279d6ca Mon Sep 17 00:00:00 2001
From: "gpt-engineer-app[bot]"
<159125892+gpt-engineer-app[bot]@users.noreply.github.com>
Date: Wed, 12 Nov 2025 03:44:01 +0000
Subject: [PATCH] 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
---
src/components/preview/CompanyPreviewCard.tsx | 80 +++++++++++++
src/components/preview/ParkPreviewCard.tsx | 112 ++++++++++++++++++
src/components/rides/RideListView.tsx | 21 +++-
src/hooks/preview/useCompanyPreview.ts | 36 ++++++
src/hooks/preview/useParkPreview.ts | 39 ++++++
src/lib/queryKeys.ts | 5 +
src/pages/ParkDetail.tsx | 21 +++-
src/pages/RideDetail.tsx | 59 ++++++---
8 files changed, 345 insertions(+), 28 deletions(-)
create mode 100644 src/components/preview/CompanyPreviewCard.tsx
create mode 100644 src/components/preview/ParkPreviewCard.tsx
create mode 100644 src/hooks/preview/useCompanyPreview.ts
create mode 100644 src/hooks/preview/useParkPreview.ts
diff --git a/src/components/preview/CompanyPreviewCard.tsx b/src/components/preview/CompanyPreviewCard.tsx
new file mode 100644
index 00000000..b4c9f056
--- /dev/null
+++ b/src/components/preview/CompanyPreviewCard.tsx
@@ -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 (
+
+ );
+ }
+
+ if (!company) {
+ return (
+
+ Company not found
+
+ );
+ }
+
+ const formatCompanyType = (type: string) => {
+ return type.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
+ };
+
+ return (
+
+ {/* Header with logo */}
+
+ {company.logo_url ? (
+

+ ) : (
+
+
+
+ )}
+
+
{company.name}
+
+ {formatCompanyType(company.company_type)}
+
+
+
+
+ {/* Location and Founded */}
+
+ {company.headquarters_location && (
+
+
+ {company.headquarters_location}
+
+ )}
+ {company.founded_year && (
+
+
+ Founded {company.founded_year}
+
+ )}
+
+
+
+ Click to view full details
+
+
+ );
+}
diff --git a/src/components/preview/ParkPreviewCard.tsx b/src/components/preview/ParkPreviewCard.tsx
new file mode 100644
index 00000000..2d8e1aa0
--- /dev/null
+++ b/src/components/preview/ParkPreviewCard.tsx
@@ -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 (
+
+ );
+ }
+
+ if (!park) {
+ return (
+
+ Park not found
+
+ );
+ }
+
+ 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 (
+
+ {/* Image */}
+ {park.card_image_url && (
+
+

+
+ )}
+
+ {/* Header */}
+
+
{park.name}
+
+
+ {park.status.replace('_', ' ').toUpperCase()}
+
+
+ {formatParkType(park.park_type)}
+
+
+
+
+ {/* Location */}
+ {park.location && (
+
+
+
+ {[park.location.city, park.location.state_province, park.location.country]
+ .filter(Boolean)
+ .join(', ')}
+
+
+ )}
+
+
+
+ {/* Stats */}
+
+
+
+ {park.ride_count || 0}
+ rides
+
+
+
+ {park.coaster_count || 0}
+ coasters
+
+ {park.average_rating && park.average_rating > 0 && (
+
+
+ {park.average_rating.toFixed(1)}
+ ({park.review_count} reviews)
+
+ )}
+
+
+ );
+}
diff --git a/src/components/rides/RideListView.tsx b/src/components/rides/RideListView.tsx
index 96039130..0849f010 100644
--- a/src/components/rides/RideListView.tsx
+++ b/src/components/rides/RideListView.tsx
@@ -5,6 +5,8 @@ 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[];
@@ -116,12 +118,19 @@ export function RideListView({ rides, onRideClick }: RideListViewProps) {
{formatCategory(ride.category)}
{ride.manufacturer && (
-
-
-
- {ride.manufacturer.name}
-
-
+
+
+
+
+
+ {ride.manufacturer.name}
+
+
+
+
+
+
+
)}
diff --git a/src/hooks/preview/useCompanyPreview.ts b/src/hooks/preview/useCompanyPreview.ts
new file mode 100644
index 00000000..4d2b6de0
--- /dev/null
+++ b/src/hooks/preview/useCompanyPreview.ts
@@ -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
+ });
+}
diff --git a/src/hooks/preview/useParkPreview.ts b/src/hooks/preview/useParkPreview.ts
new file mode 100644
index 00000000..720137a7
--- /dev/null
+++ b/src/hooks/preview/useParkPreview.ts
@@ -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
+ });
+}
diff --git a/src/lib/queryKeys.ts b/src/lib/queryKeys.ts
index 55c19a28..a689a7ea 100644
--- a/src/lib/queryKeys.ts
+++ b/src/lib/queryKeys.ts
@@ -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,
diff --git a/src/pages/ParkDetail.tsx b/src/pages/ParkDetail.tsx
index f3f4afdd..37acf001 100644
--- a/src/pages/ParkDetail.tsx
+++ b/src/pages/ParkDetail.tsx
@@ -1,5 +1,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 { Header } from '@/components/layout/Header';
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
import { trackPageView } from '@/lib/viewTracking';
@@ -435,12 +437,19 @@ export default function ParkDetail() {
Operator
-
- {park.operator.name}
-
+
+
+
+ {park.operator.name}
+
+
+
+
+
+
}
diff --git a/src/pages/RideDetail.tsx b/src/pages/RideDetail.tsx
index 41b7611b..a74d5913 100644
--- a/src/pages/RideDetail.tsx
+++ b/src/pages/RideDetail.tsx
@@ -1,5 +1,8 @@
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 { ParkPreviewCard } from '@/components/preview/ParkPreviewCard';
import { Header } from '@/components/layout/Header';
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
import { trackPageView } from '@/lib/viewTracking';
@@ -255,10 +258,20 @@ export default function RideDetail() {
{ride.name}
-
-
- {ride.park.name}
-
+
+
+
+
+ {ride.park.name}
+
+
+
+
+
+
Manufacturer
-
- {ride.manufacturer.name}
-
+
+
+
+ {ride.manufacturer.name}
+
+
+
+
+
+
)}
@@ -486,12 +506,19 @@ export default function RideDetail() {
Designer
-
- {ride.designer.name}
-
+
+
+
+ {ride.designer.name}
+
+
+
+
+
+
)}