From 4d7b00e4e7b80699d64e1a53c03dc053edf76c7c Mon Sep 17 00:00:00 2001
From: "gpt-engineer-app[bot]"
<159125892+gpt-engineer-app[bot]@users.noreply.github.com>
Date: Thu, 6 Nov 2025 15:24:46 +0000
Subject: [PATCH] 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.
---
.../moderation/SubmissionItemsList.tsx | 25 ++
.../moderation/TimelineEventPreview.tsx | 95 ++++++-
.../displays/RichTimelineEventDisplay.tsx | 266 ++++++++++++++++++
3 files changed, 373 insertions(+), 13 deletions(-)
create mode 100644 src/components/moderation/displays/RichTimelineEventDisplay.tsx
diff --git a/src/components/moderation/SubmissionItemsList.tsx b/src/components/moderation/SubmissionItemsList.tsx
index 6256c706..b9bac9f5 100644
--- a/src/components/moderation/SubmissionItemsList.tsx
+++ b/src/components/moderation/SubmissionItemsList.tsx
@@ -6,6 +6,7 @@ import { RichParkDisplay } from './displays/RichParkDisplay';
import { RichRideDisplay } from './displays/RichRideDisplay';
import { RichCompanyDisplay } from './displays/RichCompanyDisplay';
import { RichRideModelDisplay } from './displays/RichRideModelDisplay';
+import { RichTimelineEventDisplay } from './displays/RichTimelineEventDisplay';
import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
@@ -13,6 +14,7 @@ import { AlertCircle, Loader2 } from 'lucide-react';
import { format } from 'date-fns';
import type { SubmissionItemData } from '@/types/submissions';
import type { ParkSubmissionData, RideSubmissionData, CompanySubmissionData, RideModelSubmissionData } from '@/types/submission-data';
+import type { TimelineSubmissionData } from '@/types/timeline';
import { getErrorMessage, handleNonCriticalError } from '@/lib/errorHandler';
import { ModerationErrorBoundary } from '@/components/error/ModerationErrorBoundary';
@@ -270,6 +272,29 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
);
}
+ if ((item.item_type === 'milestone' || item.item_type === 'timeline_event') && entityData) {
+ return (
+ <>
+ {itemMetadata}
+
+
+
+ All Fields (Detailed View)
+
+
+
+ >
+ );
+ }
+
// Fallback to SubmissionChangesDisplay
return (
<>
diff --git a/src/components/moderation/TimelineEventPreview.tsx b/src/components/moderation/TimelineEventPreview.tsx
index 582a4fec..fe597702 100644
--- a/src/components/moderation/TimelineEventPreview.tsx
+++ b/src/components/moderation/TimelineEventPreview.tsx
@@ -1,38 +1,93 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Calendar, Tag } from 'lucide-react';
+import { Calendar, Tag, Building2, MapPin } from 'lucide-react';
+import { Badge } from '@/components/ui/badge';
+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 TimelineEventPreviewProps {
data: TimelineSubmissionData;
}
export function TimelineEventPreview({ data }: TimelineEventPreviewProps) {
+ const [entityName, setEntityName] = useState(null);
+
+ useEffect(() => {
+ if (!data?.entity_id || !data?.entity_type) return;
+
+ const fetchEntityName = async () => {
+ const table = data.entity_type === 'park' ? 'parks' : 'rides';
+ const { data: entity } = await supabase
+ .from(table)
+ .select('name')
+ .eq('id', data.entity_id)
+ .single();
+ setEntityName(entity?.name || null);
+ };
+
+ fetchEntityName();
+ }, [data?.entity_id, data?.entity_type]);
+
const formatEventType = (type: string) => {
return type.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
};
+
+ const getEventTypeColor = (type: string) => {
+ const colors: Record = {
+ opening: 'bg-green-600',
+ closure: 'bg-red-600',
+ reopening: 'bg-blue-600',
+ renovation: 'bg-purple-600',
+ expansion: 'bg-indigo-600',
+ acquisition: 'bg-amber-600',
+ name_change: 'bg-cyan-600',
+ operator_change: 'bg-orange-600',
+ owner_change: 'bg-orange-600',
+ location_change: 'bg-pink-600',
+ status_change: 'bg-yellow-600',
+ milestone: 'bg-emerald-600',
+ };
+ return colors[type] || 'bg-gray-600';
+ };
return (
-
- Timeline Event: {data.title}
+
+ {data.title}
+
+
+ {formatEventType(data.event_type)}
+
+
+ {data.entity_type}
+
+
+ {entityName && (
+
+
+ Entity:
+ {entityName}
+
+ )}
+
-
Event Type:
-
- {formatEventType(data.event_type)}
-
-
-
-
Date:
-
+ Event Date:
+
- {new Date(data.event_date).toLocaleDateString()}
- ({data.event_date_precision})
+
+
+
+ Precision: {data.event_date_precision}
@@ -45,6 +100,20 @@ export function TimelineEventPreview({ data }: TimelineEventPreviewProps) {
)}
+
+ {(data.from_entity_id || data.to_entity_id) && (
+
+
+ Related entities: {data.from_entity_id ? 'From entity' : ''} {data.to_entity_id ? 'To entity' : ''}
+
+ )}
+
+ {(data.from_location_id || data.to_location_id) && (
+
+
+ Location change involved
+
+ )}
{data.description && (
diff --git a/src/components/moderation/displays/RichTimelineEventDisplay.tsx b/src/components/moderation/displays/RichTimelineEventDisplay.tsx
new file mode 100644
index 00000000..ab715731
--- /dev/null
+++ b/src/components/moderation/displays/RichTimelineEventDisplay.tsx
@@ -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
(null);
+ const [parkContext, setParkContext] = useState(null);
+ const [fromEntity, setFromEntity] = useState(null);
+ const [toEntity, setToEntity] = useState(null);
+ const [fromLocation, setFromLocation] = useState(null);
+ const [toLocation, setToLocation] = useState(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 (
+
+ {/* Header Section */}
+
+
+
+
+
+
{data.title}
+
+
+ {formatEventType(data.event_type)}
+
+ {actionType === 'create' && (
+ New Event
+ )}
+ {actionType === 'edit' && (
+ Edit Event
+ )}
+ {actionType === 'delete' && (
+ Delete Event
+ )}
+
+
+
+
+
+
+ {/* Entity Context Section */}
+
+
+
+ Event For:
+
+ {entityName || 'Loading...'}
+
+ {data.entity_type}
+
+
+
+
+ {parkContext && (
+
+
+ Park:
+ {parkContext}
+
+ )}
+
+
+
+
+ {/* Event Date Section */}
+
+
+
+ Event Date:
+
+
+
{getPrecisionIcon(data.event_date_precision)}
+
+
+
+
+
+ Precision: {data.event_date_precision}
+
+
+
+
+
+ {/* Change Details Section */}
+ {(data.from_value || data.to_value || fromEntity || toEntity) && (
+ <>
+
+
+
Change Details:
+
+
+
From
+
+ {fromEntity || data.from_value || '—'}
+
+
+
+
+
To
+
+ {toEntity || data.to_value || '—'}
+
+
+
+
+ >
+ )}
+
+ {/* Location Change Section */}
+ {(fromLocation || toLocation) && (
+ <>
+
+
+
+
+ Location Change:
+
+
+
+
From
+
+ {formatLocation(fromLocation) || '—'}
+
+
+
+
+
To
+
+ {formatLocation(toLocation) || '—'}
+
+
+
+
+ >
+ )}
+
+ {/* Description Section */}
+ {data.description && (
+ <>
+
+
+
Description:
+
+ {data.description}
+
+
+ >
+ )}
+
+ );
+}