mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:11:13 -05:00
Implement version tracking and recent changes
This commit is contained in:
@@ -4,17 +4,31 @@ import { ParkCard } from '@/components/parks/ParkCard';
|
|||||||
import { RideCard } from '@/components/rides/RideCard';
|
import { RideCard } from '@/components/rides/RideCard';
|
||||||
import { RecentChangeCard } from './RecentChangeCard';
|
import { RecentChangeCard } from './RecentChangeCard';
|
||||||
import { Badge } from '@/components/ui/badge';
|
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 { supabase } from '@/integrations/supabase/client';
|
||||||
import { getErrorMessage } from '@/lib/errorHandler';
|
import { getErrorMessage } from '@/lib/errorHandler';
|
||||||
import { logger } from '@/lib/logger';
|
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() {
|
export function ContentTabs() {
|
||||||
const [trendingParks, setTrendingParks] = useState<Park[]>([]);
|
const [trendingParks, setTrendingParks] = useState<Park[]>([]);
|
||||||
const [trendingRides, setTrendingRides] = useState<Ride[]>([]);
|
const [trendingRides, setTrendingRides] = useState<Ride[]>([]);
|
||||||
const [recentParks, setRecentParks] = useState<Park[]>([]);
|
const [recentParks, setRecentParks] = useState<Park[]>([]);
|
||||||
const [recentRides, setRecentRides] = useState<Ride[]>([]);
|
const [recentRides, setRecentRides] = useState<Ride[]>([]);
|
||||||
const [recentChanges, setRecentChanges] = useState<ActivityEntry[]>([]);
|
const [recentChanges, setRecentChanges] = useState<RecentChange[]>([]);
|
||||||
const [recentlyOpened, setRecentlyOpened] = useState<Array<(Park | Ride) & { entityType: 'park' | 'ride' }>>([]);
|
const [recentlyOpened, setRecentlyOpened] = useState<Array<(Park | Ride) & { entityType: 'park' | 'ride' }>>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
@@ -52,11 +66,122 @@ export function ContentTabs() {
|
|||||||
.order('created_at', { ascending: false })
|
.order('created_at', { ascending: false })
|
||||||
.limit(12);
|
.limit(12);
|
||||||
|
|
||||||
// Recent changes will be populated from other sources since entity_versions requires auth
|
// Fetch recent park versions
|
||||||
const changesData: ActivityEntry[] = [];
|
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
|
// Fetch recent ride versions with park slug for proper routing
|
||||||
const processedChanges: ActivityEntry[] = [];
|
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<string, string>();
|
||||||
|
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
|
// Fetch recently opened parks and rides
|
||||||
const oneYearAgo = new Date();
|
const oneYearAgo = new Date();
|
||||||
@@ -91,7 +216,7 @@ export function ContentTabs() {
|
|||||||
setRecentParks(recent || []);
|
setRecentParks(recent || []);
|
||||||
setTrendingRides(trendingRidesData || []);
|
setTrendingRides(trendingRidesData || []);
|
||||||
setRecentRides(recentRidesData || []);
|
setRecentRides(recentRidesData || []);
|
||||||
setRecentChanges(processedChanges);
|
setRecentChanges(allChanges);
|
||||||
setRecentlyOpened(combinedOpened);
|
setRecentlyOpened(combinedOpened);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error('Failed to fetch content', { error: getErrorMessage(error) });
|
logger.error('Failed to fetch content', { error: getErrorMessage(error) });
|
||||||
@@ -198,9 +323,30 @@ export function ContentTabs() {
|
|||||||
<h2 className="text-2xl font-bold mb-2">Recent Changes</h2>
|
<h2 className="text-2xl font-bold mb-2">Recent Changes</h2>
|
||||||
<p className="text-muted-foreground">Latest updates across all entities</p>
|
<p className="text-muted-foreground">Latest updates across all entities</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
{recentChanges.length > 0 ? (
|
||||||
No recent changes to display
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
|
||||||
</div>
|
{recentChanges.map((change) => (
|
||||||
|
<RecentChangeCard
|
||||||
|
key={`${change.entityType}-${change.entityId}-${change.changedAt}`}
|
||||||
|
entityType={change.entityType}
|
||||||
|
entityId={change.entityId}
|
||||||
|
entityName={change.entityName}
|
||||||
|
entitySlug={change.entitySlug}
|
||||||
|
parkSlug={change.parkSlug}
|
||||||
|
imageUrl={change.imageUrl}
|
||||||
|
changeType={change.changeType}
|
||||||
|
changedAt={change.changedAt}
|
||||||
|
changedByUsername={change.changedByUsername}
|
||||||
|
changedByAvatar={change.changedByAvatar}
|
||||||
|
changeReason={change.changeReason}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
No recent changes to display
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="recently-opened" className="mt-8">
|
<TabsContent value="recently-opened" className="mt-8">
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ interface RecentChangeCardProps {
|
|||||||
entityId: string;
|
entityId: string;
|
||||||
entityName: string;
|
entityName: string;
|
||||||
entitySlug: string;
|
entitySlug: string;
|
||||||
imageUrl?: string;
|
parkSlug?: string;
|
||||||
|
imageUrl?: string | null;
|
||||||
changeType: string;
|
changeType: string;
|
||||||
changedAt: string;
|
changedAt: string;
|
||||||
changedByUsername?: string;
|
changedByUsername?: string | null;
|
||||||
changedByAvatar?: string;
|
changedByAvatar?: string | null;
|
||||||
changeReason?: string;
|
changeReason?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeTypeColors = {
|
const changeTypeColors = {
|
||||||
@@ -37,6 +38,7 @@ export function RecentChangeCard({
|
|||||||
entityId,
|
entityId,
|
||||||
entityName,
|
entityName,
|
||||||
entitySlug,
|
entitySlug,
|
||||||
|
parkSlug,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
changeType,
|
changeType,
|
||||||
changedAt,
|
changedAt,
|
||||||
@@ -47,7 +49,10 @@ export function RecentChangeCard({
|
|||||||
const getEntityPath = () => {
|
const getEntityPath = () => {
|
||||||
if (entityType === 'park') return `/parks/${entitySlug}`;
|
if (entityType === 'park') return `/parks/${entitySlug}`;
|
||||||
if (entityType === 'ride') {
|
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`;
|
return `/rides`;
|
||||||
}
|
}
|
||||||
// Company paths - link to the appropriate company page
|
// Company paths - link to the appropriate company page
|
||||||
|
|||||||
@@ -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();
|
||||||
Reference in New Issue
Block a user