mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-29 03:07:06 -05:00
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.
120 lines
3.9 KiB
TypeScript
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>
|
|
);
|
|
} |