Files
thrilltrack-explorer/src/components/moderation/displays/RichParkDisplay.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

306 lines
12 KiB
TypeScript

import { Building2, MapPin, Calendar, Globe, ExternalLink, Users, AlertCircle } 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 { DatePrecision } from '@/components/ui/flexible-date-input';
import type { ParkSubmissionData } from '@/types/submission-data';
import { useEffect, useState } from 'react';
import { supabase } from '@/lib/supabaseClient';
interface RichParkDisplayProps {
data: ParkSubmissionData;
actionType: 'create' | 'edit' | 'delete';
showAllFields?: boolean;
}
export function RichParkDisplay({ data, actionType, showAllFields = true }: RichParkDisplayProps) {
const [location, setLocation] = useState<any>(null);
const [operator, setOperator] = useState<string | null>(null);
const [propertyOwner, setPropertyOwner] = useState<string | null>(null);
useEffect(() => {
// Guard against null/undefined data
if (!data) return;
const fetchRelatedData = async () => {
// Fetch location if location_id exists (for edits)
if (data.location_id) {
const { data: locationData } = await supabase
.from('locations')
.select('*')
.eq('id', data.location_id)
.single();
setLocation(locationData);
}
// Otherwise fetch from park_submission_locations (for new submissions)
else if (data.id) {
const { data: locationData } = await supabase
.from('park_submission_locations')
.select('*')
.eq('park_submission_id', data.id)
.maybeSingle();
setLocation(locationData);
}
// Fetch operator
if (data.operator_id) {
const { data: operatorData } = await supabase
.from('companies')
.select('name')
.eq('id', data.operator_id)
.single();
setOperator(operatorData?.name || null);
}
// Fetch property owner
if (data.property_owner_id) {
const { data: ownerData } = await supabase
.from('companies')
.select('name')
.eq('id', data.property_owner_id)
.single();
setPropertyOwner(ownerData?.name || null);
}
};
fetchRelatedData();
}, [data.location_id, data.id, data.operator_id, data.property_owner_id]);
const getStatusColor = (status: string | undefined) => {
if (!status) return 'bg-gray-500';
switch (status.toLowerCase()) {
case 'operating': return 'bg-green-500';
case 'closed': return 'bg-red-500';
case 'under_construction': return 'bg-blue-500';
case 'planned': return 'bg-purple-500';
default: return 'bg-gray-500';
}
};
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">
<Building2 className="h-5 w-5" />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-xl font-bold text-foreground truncate">{data.name}</h3>
<div className="flex items-center gap-2 mt-1 flex-wrap">
<Badge variant="secondary" className="text-xs">
{data.park_type?.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</Badge>
<Badge className={`${getStatusColor(data.status)} text-white text-xs`}>
{data.status?.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</Badge>
{actionType === 'create' && (
<Badge className="bg-green-600 text-white text-xs">New Park</Badge>
)}
{actionType === 'edit' && (
<Badge className="bg-amber-600 text-white text-xs">Edit</Badge>
)}
{actionType === 'delete' && (
<Badge variant="destructive" className="text-xs">Delete</Badge>
)}
</div>
</div>
</div>
{/* Location Section */}
{location && (
<div className="bg-muted/50 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<MapPin className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-semibold text-foreground">Location</span>
</div>
<div className="text-sm space-y-1 ml-6">
{location.street_address && <div><span className="text-muted-foreground">Street:</span> <span className="font-medium">{location.street_address}</span></div>}
{location.city && <div><span className="text-muted-foreground">City:</span> <span className="font-medium">{location.city}</span></div>}
{location.state_province && <div><span className="text-muted-foreground">State/Province:</span> <span className="font-medium">{location.state_province}</span></div>}
{location.country && <div><span className="text-muted-foreground">Country:</span> <span className="font-medium">{location.country}</span></div>}
{location.postal_code && <div><span className="text-muted-foreground">Postal Code:</span> <span className="font-medium">{location.postal_code}</span></div>}
{location.formatted_address && (
<div className="text-xs text-muted-foreground mt-2">{location.formatted_address}</div>
)}
</div>
</div>
)}
{/* Key Information Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{/* Contact Information */}
{(data.phone || data.email) && (
<div className="bg-muted/50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-2">
<Globe className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-semibold">Contact</span>
</div>
<div className="text-sm space-y-1 ml-6">
{data.phone && (
<div>
<span className="text-muted-foreground">Phone:</span>{' '}
<a href={`tel:${data.phone}`} className="font-medium hover:underline">{data.phone}</a>
</div>
)}
{data.email && (
<div>
<span className="text-muted-foreground">Email:</span>{' '}
<a href={`mailto:${data.email}`} className="font-medium hover:underline">{data.email}</a>
</div>
)}
</div>
</div>
)}
{/* Dates */}
{(data.opening_date || data.closing_date) && (
<div className="bg-muted/50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-2">
<Calendar className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-semibold">Dates</span>
</div>
<div className="text-sm space-y-1 ml-6">
{data.opening_date && (
<div>
<span className="text-muted-foreground">Opened:</span>{' '}
<FlexibleDateDisplay
date={data.opening_date}
precision={(data.opening_date_precision as DatePrecision) || 'exact'}
className="font-medium"
/>
</div>
)}
{data.closing_date && (
<div>
<span className="text-muted-foreground">Closed:</span>{' '}
<FlexibleDateDisplay
date={data.closing_date}
precision={(data.closing_date_precision as DatePrecision) || 'exact'}
className="font-medium"
/>
</div>
)}
</div>
</div>
)}
{/* Companies */}
{(operator || propertyOwner) && (
<div className="bg-muted/50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-2">
<Users className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-semibold">Companies</span>
</div>
<div className="text-sm space-y-1 ml-6">
{operator && (
<div>
<span className="text-muted-foreground">Operator:</span>{' '}
<span className="font-medium">{operator}</span>
</div>
)}
{propertyOwner && (
<div>
<span className="text-muted-foreground">Owner:</span>{' '}
<span className="font-medium">{propertyOwner}</span>
</div>
)}
</div>
</div>
)}
</div>
{/* Description */}
{data.description && (
<div className="bg-muted/50 rounded-lg p-4">
<div className="text-sm font-semibold mb-2">Description</div>
<div className="text-sm text-muted-foreground leading-relaxed">
{data.description}
</div>
</div>
)}
{/* Website & Source */}
{(data.website_url || data.source_url) && (
<div className="flex items-center gap-3 flex-wrap">
{data.website_url && (
<a
href={data.website_url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-sm text-primary hover:underline"
>
<Globe className="h-3.5 w-3.5" />
Official Website
<ExternalLink className="h-3 w-3" />
</a>
)}
{data.source_url && (
<a
href={data.source_url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:underline"
>
Source
<ExternalLink className="h-3 w-3" />
</a>
)}
</div>
)}
{/* Submission Notes */}
{data.submission_notes && (
<div className="bg-amber-50 dark:bg-amber-950 rounded-lg p-3 border border-amber-200 dark:border-amber-800">
<div className="flex items-center gap-2 mb-1">
<AlertCircle className="h-4 w-4 text-amber-600 dark:text-amber-400" />
<span className="text-sm font-semibold text-amber-900 dark:text-amber-100">Submitter Notes</span>
</div>
<div className="text-sm text-amber-800 dark:text-amber-200 ml-6">
{data.submission_notes}
</div>
</div>
)}
{/* Images Preview */}
{(data.banner_image_url || data.card_image_url) && (
<div className="space-y-2">
<Separator />
<div className="text-sm font-semibold">Images</div>
<div className="grid grid-cols-2 gap-2">
{data.banner_image_url && (
<div className="space-y-1">
<img
src={data.banner_image_url}
alt="Banner"
className="w-full h-24 object-cover rounded border"
/>
<div className="text-xs text-center text-muted-foreground">
Banner
{data.banner_image_id && (
<span className="block font-mono text-[10px] mt-0.5">ID: {data.banner_image_id.slice(0, 8)}...</span>
)}
</div>
</div>
)}
{data.card_image_url && (
<div className="space-y-1">
<img
src={data.card_image_url}
alt="Card"
className="w-full h-24 object-cover rounded border"
/>
<div className="text-xs text-center text-muted-foreground">
Card
{data.card_image_id && (
<span className="block font-mono text-[10px] mt-0.5">ID: {data.card_image_id.slice(0, 8)}...</span>
)}
</div>
</div>
)}
</div>
</div>
)}
</div>
);
}