Files
ocean-cleanup/src/components/event/EventList.tsx
Paul Bailey 529cdd0c05 Improve event card clickability and add View Details button
- Add hover border effect to event cards for better visual feedback
- Add explicit 'View Details' button to each event card
- Rename register button to 'Quick Register' for clarity
- Makes it obvious that cards are clickable to view full details
2025-11-16 20:26:32 +00:00

201 lines
7.4 KiB
TypeScript

import { format } from 'date-fns';
import type { CleanupEvent, EventStatus } from '@/types';
interface EventListProps {
events: CleanupEvent[];
onEventClick: (event: CleanupEvent) => void;
onRegister?: (eventId: string) => void;
isLoading?: boolean;
}
export function EventList({ events, onEventClick, onRegister, isLoading = false }: EventListProps) {
if (isLoading) {
return (
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<div key={i} className="bg-white rounded-lg shadow p-6 animate-pulse">
<div className="h-6 bg-gray-200 rounded w-3/4 mb-4"></div>
<div className="h-4 bg-gray-200 rounded w-1/2 mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-2/3"></div>
</div>
))}
</div>
);
}
if (events.length === 0) {
return (
<div className="bg-white rounded-lg shadow p-12 text-center">
<svg
className="mx-auto h-12 w-12 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<h3 className="mt-4 text-lg font-medium text-gray-900">No events found</h3>
<p className="mt-2 text-sm text-gray-500">
Try adjusting your filters or create a new cleanup event.
</p>
</div>
);
}
const getStatusBadge = (status: EventStatus) => {
const styles = {
upcoming: 'bg-blue-100 text-blue-800',
completed: 'bg-green-100 text-green-800',
cancelled: 'bg-red-100 text-red-800',
};
const labels = {
upcoming: 'Upcoming',
completed: 'Completed',
cancelled: 'Cancelled',
};
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${styles[status]}`}>
{labels[status]}
</span>
);
};
const formatEventDate = (date: Date) => {
return format(new Date(date), 'EEEE, MMMM d, yyyy');
};
const formatEventTime = (date: Date) => {
return format(new Date(date), 'h:mm a');
};
const formatDuration = (minutes: number) => {
if (minutes < 60) {
return `${minutes} min`;
}
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
};
return (
<div className="space-y-4">
{events.map((event) => (
<div
key={event.id}
className="bg-white rounded-lg shadow hover:shadow-lg transition-all cursor-pointer border-2 border-transparent hover:border-blue-200"
onClick={() => onEventClick(event)}
>
<div className="p-6">
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 mb-1">{event.title}</h3>
<div className="flex items-center gap-2">
{getStatusBadge(event.status)}
<span className="text-sm text-gray-500">
{event.participantCount} {event.participantCount === 1 ? 'participant' : 'participants'}
</span>
</div>
</div>
</div>
<div className="space-y-2 mb-4">
{/* Date and Time */}
<div className="flex items-center text-sm text-gray-600">
<svg className="h-5 w-5 mr-2 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span>
{formatEventDate(event.scheduledDate)} at {formatEventTime(event.scheduledDate)}
</span>
</div>
{/* Duration */}
<div className="flex items-center text-sm text-gray-600">
<svg className="h-5 w-5 mr-2 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{formatDuration(event.duration)}</span>
</div>
{/* Location */}
<div className="flex items-center text-sm text-gray-600">
<svg className="h-5 w-5 mr-2 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>{event.locationName}</span>
</div>
</div>
{/* Description Preview */}
<p className="text-sm text-gray-600 line-clamp-2 mb-4">{event.description}</p>
{/* Completed Event Info */}
{event.status === 'completed' && event.litterCollected !== undefined && (
<div className="bg-green-50 border border-green-200 rounded-lg p-3 mb-4">
<div className="flex items-center text-sm text-green-800">
<svg className="h-5 w-5 mr-2 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
<span className="font-medium">{event.litterCollected} kg of litter collected</span>
</div>
</div>
)}
{/* Action Area */}
<div className="flex items-center justify-between pt-4 border-t border-gray-100">
<button
onClick={() => onEventClick(event)}
className="text-blue-600 hover:text-blue-700 font-medium text-sm flex items-center gap-1"
>
View Details
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
{event.status === 'upcoming' && onRegister && (
<button
onClick={(e) => {
e.stopPropagation();
onRegister(event.id);
}}
className="bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 transition-colors font-medium text-sm"
>
Quick Register
</button>
)}
</div>
</div>
</div>
))}
</div>
);
}