From 49a0aa7a57c81bf001096deadb1411623dda3263 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 01:41:05 +0000 Subject: [PATCH] Implement version tracking and recent changes --- src/components/homepage/ContentTabs.tsx | 166 ++++++++++++++++-- src/components/homepage/RecentChangeCard.tsx | 15 +- ...6_ad44e1ea-5bd9-4c7b-8957-51ad590acb3d.sql | 36 ++++ 3 files changed, 202 insertions(+), 15 deletions(-) create mode 100644 supabase/migrations/20251028013926_ad44e1ea-5bd9-4c7b-8957-51ad590acb3d.sql diff --git a/src/components/homepage/ContentTabs.tsx b/src/components/homepage/ContentTabs.tsx index b8ec5b9a..592de49e 100644 --- a/src/components/homepage/ContentTabs.tsx +++ b/src/components/homepage/ContentTabs.tsx @@ -4,17 +4,31 @@ import { ParkCard } from '@/components/parks/ParkCard'; import { RideCard } from '@/components/rides/RideCard'; import { RecentChangeCard } from './RecentChangeCard'; import { Badge } from '@/components/ui/badge'; -import { Park, Ride, ActivityEntry } from '@/types/database'; +import { Park, Ride } from '@/types/database'; import { supabase } from '@/integrations/supabase/client'; import { getErrorMessage } from '@/lib/errorHandler'; import { logger } from '@/lib/logger'; +interface RecentChange { + entityType: 'park' | 'ride' | 'company'; + entityId: string; + entityName: string; + entitySlug: string; + parkSlug?: string; + imageUrl: string | null; + changeType: string; + changedAt: string; + changedByUsername?: string | null; + changedByAvatar?: string | null; + changeReason: string | null; +} + export function ContentTabs() { const [trendingParks, setTrendingParks] = useState([]); const [trendingRides, setTrendingRides] = useState([]); const [recentParks, setRecentParks] = useState([]); const [recentRides, setRecentRides] = useState([]); - const [recentChanges, setRecentChanges] = useState([]); + const [recentChanges, setRecentChanges] = useState([]); const [recentlyOpened, setRecentlyOpened] = useState>([]); const [loading, setLoading] = useState(true); @@ -52,11 +66,122 @@ export function ContentTabs() { .order('created_at', { ascending: false }) .limit(12); - // Recent changes will be populated from other sources since entity_versions requires auth - const changesData: ActivityEntry[] = []; + // Fetch recent park versions + const { data: parkVersions } = await supabase + .from('park_versions') + .select(` + version_id, + park_id, + name, + slug, + change_type, + created_at, + created_by, + change_reason, + card_image_url, + profiles:profiles!park_versions_created_by_fkey(username, avatar_url) + `) + .eq('is_current', true) + .order('created_at', { ascending: false }) + .limit(12); - // Process changes to extract entity info from version_data - const processedChanges: ActivityEntry[] = []; + // Fetch recent ride versions with park slug for proper routing + const { data: rideVersions } = await supabase + .from('ride_versions') + .select(` + version_id, + ride_id, + name, + slug, + change_type, + created_at, + created_by, + change_reason, + card_image_url, + park_id, + profiles:profiles!ride_versions_created_by_fkey(username, avatar_url) + `) + .eq('is_current', true) + .order('created_at', { ascending: false }) + .limit(12); + + // Fetch park slugs for rides that have a park_id + const rideParksMap = new Map(); + if (rideVersions) { + const parkIds = [...new Set(rideVersions.map(v => v.park_id).filter(Boolean))]; + if (parkIds.length > 0) { + const { data: parks } = await supabase + .from('parks') + .select('id, slug') + .in('id', parkIds); + + parks?.forEach(park => { + rideParksMap.set(park.id, park.slug); + }); + } + } + + // Fetch recent company versions + const { data: companyVersions } = await supabase + .from('company_versions') + .select(` + version_id, + company_id, + name, + slug, + change_type, + created_at, + created_by, + change_reason, + card_image_url, + profiles:profiles!company_versions_created_by_fkey(username, avatar_url) + `) + .eq('is_current', true) + .order('created_at', { ascending: false }) + .limit(12); + + // Combine all changes into a unified structure + const allChanges: RecentChange[] = [ + ...(parkVersions || []).map(v => ({ + entityType: 'park' as const, + entityId: v.park_id, + entityName: v.name, + entitySlug: v.slug, + imageUrl: v.card_image_url, + changeType: v.change_type, + changedAt: v.created_at, + changedByUsername: v.profiles?.username, + changedByAvatar: v.profiles?.avatar_url, + changeReason: v.change_reason, + })), + ...(rideVersions || []).map(v => ({ + entityType: 'ride' as const, + entityId: v.ride_id, + entityName: v.name, + entitySlug: v.slug, + parkSlug: v.park_id ? rideParksMap.get(v.park_id) : undefined, + imageUrl: v.card_image_url, + changeType: v.change_type, + changedAt: v.created_at, + changedByUsername: v.profiles?.username, + changedByAvatar: v.profiles?.avatar_url, + changeReason: v.change_reason, + })), + ...(companyVersions || []).map(v => ({ + entityType: 'company' as const, + entityId: v.company_id, + entityName: v.name, + entitySlug: v.slug, + imageUrl: v.card_image_url, + changeType: v.change_type, + changedAt: v.created_at, + changedByUsername: v.profiles?.username, + changedByAvatar: v.profiles?.avatar_url, + changeReason: v.change_reason, + })) + ] + .sort((a, b) => new Date(b.changedAt).getTime() - new Date(a.changedAt).getTime()) + .slice(0, 24); // Fetch recently opened parks and rides const oneYearAgo = new Date(); @@ -91,7 +216,7 @@ export function ContentTabs() { setRecentParks(recent || []); setTrendingRides(trendingRidesData || []); setRecentRides(recentRidesData || []); - setRecentChanges(processedChanges); + setRecentChanges(allChanges); setRecentlyOpened(combinedOpened); } catch (error: unknown) { logger.error('Failed to fetch content', { error: getErrorMessage(error) }); @@ -198,9 +323,30 @@ export function ContentTabs() {

Recent Changes

Latest updates across all entities

-
- No recent changes to display -
+ {recentChanges.length > 0 ? ( +
+ {recentChanges.map((change) => ( + + ))} +
+ ) : ( +
+ No recent changes to display +
+ )} diff --git a/src/components/homepage/RecentChangeCard.tsx b/src/components/homepage/RecentChangeCard.tsx index adedb942..73e3900f 100644 --- a/src/components/homepage/RecentChangeCard.tsx +++ b/src/components/homepage/RecentChangeCard.tsx @@ -10,12 +10,13 @@ interface RecentChangeCardProps { entityId: string; entityName: string; entitySlug: string; - imageUrl?: string; + parkSlug?: string; + imageUrl?: string | null; changeType: string; changedAt: string; - changedByUsername?: string; - changedByAvatar?: string; - changeReason?: string; + changedByUsername?: string | null; + changedByAvatar?: string | null; + changeReason?: string | null; } const changeTypeColors = { @@ -37,6 +38,7 @@ export function RecentChangeCard({ entityId, entityName, entitySlug, + parkSlug, imageUrl, changeType, changedAt, @@ -47,7 +49,10 @@ export function RecentChangeCard({ const getEntityPath = () => { if (entityType === 'park') return `/parks/${entitySlug}`; if (entityType === 'ride') { - // For rides, we need the park slug too - for now, just link to rides page + // For rides, use park slug if available, otherwise fallback to global rides list + if (parkSlug) { + return `/parks/${parkSlug}/rides/${entitySlug}`; + } return `/rides`; } // Company paths - link to the appropriate company page diff --git a/supabase/migrations/20251028013926_ad44e1ea-5bd9-4c7b-8957-51ad590acb3d.sql b/supabase/migrations/20251028013926_ad44e1ea-5bd9-4c7b-8957-51ad590acb3d.sql new file mode 100644 index 00000000..3c26e37e --- /dev/null +++ b/supabase/migrations/20251028013926_ad44e1ea-5bd9-4c7b-8957-51ad590acb3d.sql @@ -0,0 +1,36 @@ +-- Install missing versioning triggers for automatic version creation +-- These triggers should have been created previously but are missing from the database + +-- Clean up any existing triggers first +DROP TRIGGER IF EXISTS create_park_version_on_change ON public.parks; +DROP TRIGGER IF EXISTS create_ride_version_on_change ON public.rides; +DROP TRIGGER IF EXISTS create_company_version_on_change ON public.companies; +DROP TRIGGER IF EXISTS create_ride_model_version_on_change ON public.ride_models; + +-- Install versioning trigger for parks +-- Automatically creates a version record whenever a park is inserted or updated +CREATE TRIGGER create_park_version_on_change + AFTER INSERT OR UPDATE ON public.parks + FOR EACH ROW + EXECUTE FUNCTION public.create_relational_version(); + +-- Install versioning trigger for rides +-- Automatically creates a version record whenever a ride is inserted or updated +CREATE TRIGGER create_ride_version_on_change + AFTER INSERT OR UPDATE ON public.rides + FOR EACH ROW + EXECUTE FUNCTION public.create_relational_version(); + +-- Install versioning trigger for companies +-- Automatically creates a version record whenever a company is inserted or updated +CREATE TRIGGER create_company_version_on_change + AFTER INSERT OR UPDATE ON public.companies + FOR EACH ROW + EXECUTE FUNCTION public.create_relational_version(); + +-- Install versioning trigger for ride models +-- Automatically creates a version record whenever a ride model is inserted or updated +CREATE TRIGGER create_ride_model_version_on_change + AFTER INSERT OR UPDATE ON public.ride_models + FOR EACH ROW + EXECUTE FUNCTION public.create_relational_version(); \ No newline at end of file