From c8443e05a386b4c814da2d387d776b2e2ee3afcc Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 15:58:54 +0000 Subject: [PATCH] feat: Add recent changes and recently opened tabs --- src/components/homepage/ContentTabs.tsx | 121 ++++++++++++++++++- src/components/homepage/RecentChangeCard.tsx | 112 +++++++++++++++++ 2 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 src/components/homepage/RecentChangeCard.tsx diff --git a/src/components/homepage/ContentTabs.tsx b/src/components/homepage/ContentTabs.tsx index 8c0c8ab5..0cd4befd 100644 --- a/src/components/homepage/ContentTabs.tsx +++ b/src/components/homepage/ContentTabs.tsx @@ -2,6 +2,8 @@ import { useState, useEffect } from 'react'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; 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 } from '@/types/database'; import { supabase } from '@/integrations/supabase/client'; @@ -10,6 +12,8 @@ export function ContentTabs() { const [trendingRides, setTrendingRides] = useState([]); const [recentParks, setRecentParks] = useState([]); const [recentRides, setRecentRides] = useState([]); + const [recentChanges, setRecentChanges] = useState([]); + const [recentlyOpened, setRecentlyOpened] = useState>([]); const [loading, setLoading] = useState(true); useEffect(() => { @@ -46,10 +50,69 @@ export function ContentTabs() { .order('created_at', { ascending: false }) .limit(12); + // Fetch recent entity changes + const { data: changesData } = await supabase + .from('entity_versions') + .select(` + id, + entity_type, + entity_id, + version_number, + version_data, + changed_at, + change_type, + change_reason, + changer_profile:profiles!entity_versions_changed_by_fkey(username, avatar_url) + `) + .order('changed_at', { ascending: false }) + .limit(24); + + // Process changes to extract entity info from version_data + const processedChanges = changesData?.map(change => { + const versionData = change.version_data as any; + return { + ...change, + entity_name: versionData?.name || 'Unknown', + entity_slug: versionData?.slug || '', + entity_image_url: versionData?.card_image_url || versionData?.banner_image_url, + }; + }) || []; + + // Fetch recently opened parks and rides + const oneYearAgo = new Date(); + oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1); + const dateThreshold = oneYearAgo.toISOString().split('T')[0]; + + const { data: openedParks } = await supabase + .from('parks') + .select(`*, location:locations(*), operator:companies!parks_operator_id_fkey(*)`) + .not('opening_date', 'is', null) + .gte('opening_date', dateThreshold) + .order('opening_date', { ascending: false }) + .limit(20); + + const { data: openedRides } = await supabase + .from('rides') + .select(`*, park:parks!inner(name, slug, location:locations(*))`) + .not('opening_date', 'is', null) + .gte('opening_date', dateThreshold) + .order('opening_date', { ascending: false }) + .limit(20); + + // Combine and sort by opening date + const combinedOpened = [ + ...(openedParks || []).map(p => ({ ...p, entityType: 'park' as const })), + ...(openedRides || []).map(r => ({ ...r, entityType: 'ride' as const })) + ] + .sort((a, b) => new Date(b.opening_date).getTime() - new Date(a.opening_date).getTime()) + .slice(0, 24); + setTrendingParks(trending || []); setRecentParks(recent || []); setTrendingRides(trendingRidesData || []); setRecentRides(recentRidesData || []); + setRecentChanges(processedChanges); + setRecentlyOpened(combinedOpened); } catch (error) { console.error('Error fetching content:', error); } finally { @@ -80,7 +143,7 @@ export function ContentTabs() {
- + Trending Parks @@ -93,6 +156,12 @@ export function ContentTabs() { New Rides + + Recent Changes + + + Recently Opened +
@@ -143,6 +212,56 @@ export function ContentTabs() { ))}
+ + +
+

Recent Changes

+

Latest updates across all entities

+
+
+ {recentChanges.map((change) => ( + + ))} +
+
+ + +
+

Recently Opened

+

Parks and rides that opened in the last year

+
+
+ {recentlyOpened.map((entity: any) => ( + entity.entityType === 'park' ? ( +
+ + + {new Date(entity.opening_date).getFullYear()} + +
+ ) : ( +
+ + + {new Date(entity.opening_date).getFullYear()} + +
+ ) + ))} +
+
diff --git a/src/components/homepage/RecentChangeCard.tsx b/src/components/homepage/RecentChangeCard.tsx new file mode 100644 index 00000000..ef135919 --- /dev/null +++ b/src/components/homepage/RecentChangeCard.tsx @@ -0,0 +1,112 @@ +import { Link } from 'react-router-dom'; +import { Card } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Clock, User } from 'lucide-react'; +import { formatDistanceToNow } from 'date-fns'; + +interface RecentChangeCardProps { + entityType: 'park' | 'ride' | 'company'; + entityId: string; + entityName: string; + entitySlug: string; + imageUrl?: string; + changeType: string; + changedAt: string; + changedByUsername?: string; + changedByAvatar?: string; + changeReason?: string; +} + +const changeTypeColors = { + created: 'bg-green-500/10 text-green-700 dark:text-green-400 border-green-500/20', + updated: 'bg-blue-500/10 text-blue-700 dark:text-blue-400 border-blue-500/20', + deleted: 'bg-red-500/10 text-red-700 dark:text-red-400 border-red-500/20', + restored: 'bg-purple-500/10 text-purple-700 dark:text-purple-400 border-purple-500/20', + archived: 'bg-muted text-muted-foreground border-muted', +}; + +const entityTypeColors = { + park: 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 border-emerald-500/20', + ride: 'bg-orange-500/10 text-orange-700 dark:text-orange-400 border-orange-500/20', + company: 'bg-indigo-500/10 text-indigo-700 dark:text-indigo-400 border-indigo-500/20', +}; + +export function RecentChangeCard({ + entityType, + entityId, + entityName, + entitySlug, + imageUrl, + changeType, + changedAt, + changedByUsername, + changedByAvatar, + changeReason, +}: RecentChangeCardProps) { + 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 + return `/rides`; + } + // Company paths - link to the appropriate company page + return '/'; + }; + + return ( + + + {imageUrl && ( +
+ {entityName} +
+ )} + +
+
+
+ + {entityType} + + + {changeType} + +
+

{entityName}

+
+ + {changeReason && ( +

+ {changeReason} +

+ )} + +
+
+ + {formatDistanceToNow(new Date(changedAt), { addSuffix: true })} +
+ + {changedByUsername && ( +
+ + + + + + + {changedByUsername} +
+ )} +
+
+
+ + ); +}