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} + ) : ( +
+ +
+ )} +
+

{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 && ( +
+ {park.name} +
+ )} + + {/* 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} + + + + + +
)}