Files
thrilltrack-explorer/src/components/timeline/TimelineEventCard.tsx
gpt-engineer-app[bot] dce8747651 Migrate date precision handling tests
Update park and ride submission forms to support and persist all new date precision options (exact, month, year, decade, century, approximate), ensure default and validation align with backend, and verify submissions save without errors. Includes front-end tests scaffolding and adjustments to submission helpers to store updated precision fields.
2025-11-11 22:11:16 +00:00

120 lines
3.9 KiB
TypeScript

import { Edit, Trash, Clock, CheckCircle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { format } from 'date-fns';
import { parseDateForDisplay } from '@/lib/dateUtils';
import type { TimelineEvent } from '@/types/timeline';
interface TimelineEventCardProps {
event: TimelineEvent;
onEdit?: (event: TimelineEvent) => void;
onDelete?: (eventId: string) => void;
canEdit: boolean;
canDelete: boolean;
isPending?: boolean;
}
// ⚠️ IMPORTANT: Use parseDateForDisplay to prevent timezone shifts
// YYYY-MM-DD strings must be interpreted as local dates, not UTC
const formatEventDate = (date: string, precision: string = 'exact') => {
const dateObj = parseDateForDisplay(date);
switch (precision) {
case 'year':
return format(dateObj, 'yyyy');
case 'month':
return format(dateObj, 'MMMM yyyy');
case 'day':
default:
return format(dateObj, 'MMMM d, yyyy');
}
};
const getEventTypeLabel = (type: string): string => {
return type.split('_').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
};
export function TimelineEventCard({
event,
onEdit,
onDelete,
canEdit,
canDelete,
isPending = false
}: TimelineEventCardProps) {
return (
<Card className={isPending ? 'border-yellow-500/50 bg-yellow-500/5' : ''}>
<CardContent className="p-4">
<div className="flex justify-between items-start gap-4">
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2 flex-wrap">
<Badge variant={isPending ? 'secondary' : 'default'}>
{getEventTypeLabel(event.event_type)}
</Badge>
{isPending && (
<Badge variant="outline" className="gap-1">
<Clock className="w-3 h-3" />
Pending Approval
</Badge>
)}
{!isPending && (
<Badge variant="outline" className="gap-1">
<CheckCircle className="w-3 h-3" />
Approved
</Badge>
)}
</div>
<div>
<h4 className="font-semibold text-lg">{event.title}</h4>
<p className="text-sm text-muted-foreground">
{formatEventDate(event.event_date, event.event_date_precision)}
</p>
</div>
{event.description && (
<p className="text-sm">{event.description}</p>
)}
{(event.from_value || event.to_value) && (
<div className="text-sm text-muted-foreground">
{event.from_value && <span>From: {event.from_value}</span>}
{event.from_value && event.to_value && <span className="mx-2"></span>}
{event.to_value && <span>To: {event.to_value}</span>}
</div>
)}
</div>
{(canEdit || canDelete) && (
<div className="flex gap-2">
{canEdit && (
<Button
size="icon"
variant="ghost"
onClick={() => onEdit?.(event)}
title="Edit event"
>
<Edit className="w-4 h-4" />
</Button>
)}
{canDelete && (
<Button
size="icon"
variant="ghost"
onClick={() => onDelete?.(event.id)}
title="Delete event"
className="text-destructive hover:text-destructive"
>
<Trash className="w-4 h-4" />
</Button>
)}
</div>
)}
</div>
</CardContent>
</Card>
);
}