feat: Implement rich timeline event display

Implement the plan to enhance the display of timeline event submissions in the moderation queue. This includes fixing the database function to fetch timeline event data, creating a new `RichTimelineEventDisplay` component, and updating the `SubmissionItemsList` and `TimelineEventPreview` components to leverage this new display. The goal is to provide moderators with complete and contextually rich information for timeline events.
This commit is contained in:
gpt-engineer-app[bot]
2025-11-06 15:24:46 +00:00
parent bd4f75bfb2
commit 4d7b00e4e7
3 changed files with 373 additions and 13 deletions

View File

@@ -0,0 +1,266 @@
import { Calendar, Tag, ArrowRight, MapPin, Building2, Clock } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { FlexibleDateDisplay } from '@/components/ui/flexible-date-display';
import type { TimelineSubmissionData } from '@/types/timeline';
import { useEffect, useState } from 'react';
import { supabase } from '@/lib/supabaseClient';
interface RichTimelineEventDisplayProps {
data: TimelineSubmissionData;
actionType: 'create' | 'edit' | 'delete';
}
export function RichTimelineEventDisplay({ data, actionType }: RichTimelineEventDisplayProps) {
const [entityName, setEntityName] = useState<string | null>(null);
const [parkContext, setParkContext] = useState<string | null>(null);
const [fromEntity, setFromEntity] = useState<string | null>(null);
const [toEntity, setToEntity] = useState<string | null>(null);
const [fromLocation, setFromLocation] = useState<any>(null);
const [toLocation, setToLocation] = useState<any>(null);
useEffect(() => {
if (!data) return;
const fetchRelatedData = async () => {
// Fetch the main entity this timeline event is for
if (data.entity_id && data.entity_type) {
if (data.entity_type === 'park') {
const { data: park } = await supabase
.from('parks')
.select('name')
.eq('id', data.entity_id)
.single();
setEntityName(park?.name || null);
} else if (data.entity_type === 'ride') {
const { data: ride } = await supabase
.from('rides')
.select('name, park:parks(name)')
.eq('id', data.entity_id)
.single();
setEntityName(ride?.name || null);
setParkContext((ride?.park as any)?.name || null);
}
}
// Fetch from/to entities for relational changes
if (data.from_entity_id) {
const { data: entity } = await supabase
.from('companies')
.select('name')
.eq('id', data.from_entity_id)
.single();
setFromEntity(entity?.name || null);
}
if (data.to_entity_id) {
const { data: entity } = await supabase
.from('companies')
.select('name')
.eq('id', data.to_entity_id)
.single();
setToEntity(entity?.name || null);
}
// Fetch from/to locations for location changes
if (data.from_location_id) {
const { data: loc } = await supabase
.from('locations')
.select('*')
.eq('id', data.from_location_id)
.single();
setFromLocation(loc);
}
if (data.to_location_id) {
const { data: loc } = await supabase
.from('locations')
.select('*')
.eq('id', data.to_location_id)
.single();
setToLocation(loc);
}
};
fetchRelatedData();
}, [data.entity_id, data.entity_type, data.from_entity_id, data.to_entity_id, data.from_location_id, data.to_location_id]);
const formatEventType = (type: string) => {
return type.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
};
const getEventTypeColor = (type: string) => {
switch (type) {
case 'opening': return 'bg-green-600';
case 'closure': return 'bg-red-600';
case 'reopening': return 'bg-blue-600';
case 'renovation': return 'bg-purple-600';
case 'expansion': return 'bg-indigo-600';
case 'acquisition': return 'bg-amber-600';
case 'name_change': return 'bg-cyan-600';
case 'operator_change':
case 'owner_change': return 'bg-orange-600';
case 'location_change': return 'bg-pink-600';
case 'status_change': return 'bg-yellow-600';
case 'milestone': return 'bg-emerald-600';
default: return 'bg-gray-600';
}
};
const getPrecisionIcon = (precision: string) => {
switch (precision) {
case 'day': return '📅';
case 'month': return '📆';
case 'year': return '🗓️';
default: return '📅';
}
};
const formatLocation = (loc: any) => {
if (!loc) return null;
const parts = [loc.city, loc.state_province, loc.country].filter(Boolean);
return parts.join(', ');
};
return (
<div className="space-y-4">
{/* Header Section */}
<div className="flex items-start gap-3">
<div className="p-2 rounded-lg bg-primary/10 text-primary">
<Calendar className="h-5 w-5" />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-xl font-bold text-foreground">{data.title}</h3>
<div className="flex items-center gap-2 mt-1 flex-wrap">
<Badge className={`${getEventTypeColor(data.event_type)} text-white text-xs`}>
{formatEventType(data.event_type)}
</Badge>
{actionType === 'create' && (
<Badge className="bg-green-600 text-white text-xs">New Event</Badge>
)}
{actionType === 'edit' && (
<Badge className="bg-amber-600 text-white text-xs">Edit Event</Badge>
)}
{actionType === 'delete' && (
<Badge variant="destructive" className="text-xs">Delete Event</Badge>
)}
</div>
</div>
</div>
<Separator />
{/* Entity Context Section */}
<div className="grid gap-3">
<div className="flex items-center gap-2 text-sm">
<Tag className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Event For:</span>
<span className="text-foreground">
{entityName || 'Loading...'}
<Badge variant="outline" className="ml-2 text-xs">
{data.entity_type}
</Badge>
</span>
</div>
{parkContext && (
<div className="flex items-center gap-2 text-sm">
<Building2 className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Park:</span>
<span className="text-foreground">{parkContext}</span>
</div>
)}
</div>
<Separator />
{/* Event Date Section */}
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm">
<Clock className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Event Date:</span>
</div>
<div className="flex items-center gap-3 pl-6">
<span className="text-2xl">{getPrecisionIcon(data.event_date_precision)}</span>
<div>
<div className="text-lg font-semibold">
<FlexibleDateDisplay
date={data.event_date}
precision={data.event_date_precision}
/>
</div>
<div className="text-xs text-muted-foreground">
Precision: {data.event_date_precision}
</div>
</div>
</div>
</div>
{/* Change Details Section */}
{(data.from_value || data.to_value || fromEntity || toEntity) && (
<>
<Separator />
<div className="space-y-2">
<div className="text-sm font-medium">Change Details:</div>
<div className="flex items-center gap-3 pl-6">
<div className="flex-1 p-3 rounded-lg bg-muted/50">
<div className="text-xs text-muted-foreground mb-1">From</div>
<div className="font-medium">
{fromEntity || data.from_value || '—'}
</div>
</div>
<ArrowRight className="h-5 w-5 text-muted-foreground flex-shrink-0" />
<div className="flex-1 p-3 rounded-lg bg-muted/50">
<div className="text-xs text-muted-foreground mb-1">To</div>
<div className="font-medium">
{toEntity || data.to_value || '—'}
</div>
</div>
</div>
</div>
</>
)}
{/* Location Change Section */}
{(fromLocation || toLocation) && (
<>
<Separator />
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm font-medium">
<MapPin className="h-4 w-4" />
Location Change:
</div>
<div className="flex items-center gap-3 pl-6">
<div className="flex-1 p-3 rounded-lg bg-muted/50">
<div className="text-xs text-muted-foreground mb-1">From</div>
<div className="font-medium">
{formatLocation(fromLocation) || '—'}
</div>
</div>
<ArrowRight className="h-5 w-5 text-muted-foreground flex-shrink-0" />
<div className="flex-1 p-3 rounded-lg bg-muted/50">
<div className="text-xs text-muted-foreground mb-1">To</div>
<div className="font-medium">
{formatLocation(toLocation) || '—'}
</div>
</div>
</div>
</div>
</>
)}
{/* Description Section */}
{data.description && (
<>
<Separator />
<div className="space-y-2">
<div className="text-sm font-medium">Description:</div>
<p className="text-sm text-muted-foreground pl-6 leading-relaxed">
{data.description}
</p>
</div>
</>
)}
</div>
);
}