photum/frontend/components/EventCard.tsx
NANDO9322 7a06d4e691 feat(agenda): migração busca POI para Google Maps e correção do horário de término
Frontend:
- Migração da API de Geocoding do Mapbox para o Google Maps Places API (googleMapsService.ts) no formulário de eventos, garantindo a busca correta pelo nome de locais (estádios, teatros) e com autopreenchimento.
- Correção do fluxo de estado do 'horario_fim', propagando e persistindo o 'endTime' pelo DataContext, garantindo a população dos dados na edição do EventForm.
- Adição da visualização do horário final na listagem do Dashboard, no EventCard, painéis de EventDetails e atualização das props defaultEndTime no EventScheduler.

Backend:
- Atualização e migração dos arquivos gerados pelo sqlc (models.go, agenda.sql.go) para suportar operações no novo design do banco.
- Atualização síncrona dos artefatos Swagger de documentação de API (docs.go, swagger.json, swagger.yaml).
2026-02-27 18:48:07 -03:00

100 lines
5.1 KiB
TypeScript

import React, { useState } from 'react';
import { EventData, EventStatus } from '../types';
import { Calendar, MapPin, ArrowRight, UserCheck } from 'lucide-react';
import { STATUS_COLORS } from '../constants';
interface EventCardProps {
event: EventData;
onClick: () => void;
}
export const EventCard: React.FC<EventCardProps> = ({ event, onClick }) => {
const [imageLoaded, setImageLoaded] = useState(false);
const fullAddress = `${event.address.street}, ${event.address.number} - ${event.address.city}/${event.address.state}`;
return (
<div
className="group bg-white rounded-lg border border-gray-100 overflow-hidden hover:shadow-xl transition-all duration-300 cursor-pointer flex flex-col h-full"
onClick={onClick}
>
<div className="relative h-40 sm:h-44 md:h-48 overflow-hidden bg-gray-100">
{/* Skeleton / Loading State */}
{!imageLoaded && (
<div className="absolute inset-0 bg-gray-200 animate-pulse flex items-center justify-center">
<div className="w-6 h-6 sm:w-8 sm:h-8 border-2 border-gray-300 border-t-brand-gold rounded-full animate-spin"></div>
</div>
)}
<img
src={event.coverImage}
alt={event.name}
className={`w-full h-full object-cover transition-all duration-700 group-hover:scale-105 ${imageLoaded ? 'opacity-100' : 'opacity-0'}`}
onLoad={() => setImageLoaded(true)}
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-60"></div>
<div className="absolute top-2 sm:top-3 right-2 sm:right-3">
<span className={`text-[10px] sm:text-xs font-semibold px-1.5 sm:px-2 py-0.5 sm:py-1 rounded-sm uppercase tracking-wide ${STATUS_COLORS[event.status]}`}>
{event.status}
</span>
</div>
<div className="absolute bottom-2 sm:bottom-3 left-3 sm:left-4 text-white">
<p className="text-[10px] sm:text-xs font-light uppercase tracking-widest opacity-90">{event.type}</p>
<h3 className="text-base sm:text-lg font-serif font-medium drop-shadow-md line-clamp-1">{event.name}</h3>
</div>
</div>
<div className="p-3 sm:p-4 md:p-5 flex-1 flex flex-col justify-between">
<div className="space-y-2 sm:space-y-3">
<div className="flex items-center text-gray-500 text-xs sm:text-sm">
<Calendar size={14} className="sm:w-4 sm:h-4 mr-1.5 sm:mr-2 text-brand-gold flex-shrink-0" />
<span className="truncate">{new Date(event.date).toLocaleDateString()} às {event.startTime || event.time}{event.endTime || event.horario_fim ? ` - ${event.endTime || event.horario_fim}` : ''}</span>
</div>
{/* Location with Tooltip */}
<div className="relative group/tooltip">
<div
className="flex items-center text-gray-500 text-xs sm:text-sm"
title={fullAddress} // Native tooltip fallback
>
<MapPin size={14} className="sm:w-4 sm:h-4 mr-1.5 sm:mr-2 text-brand-gold flex-shrink-0" />
<span className="truncate">{event.address.city}, {event.address.state}</span>
</div>
{/* Custom Tooltip - Hidden on mobile */}
<div className="absolute bottom-full left-0 mb-2 hidden sm:group-hover/tooltip:block z-20 pointer-events-none">
<div className="bg-brand-black text-white text-xs rounded py-1.5 px-3 whitespace-nowrap shadow-xl">
{event.address.street}, {event.address.number}
<br />
{event.address.city} - {event.address.state}
{/* Arrow */}
<div className="absolute top-full left-3 -mt-1 border-4 border-transparent border-t-brand-black"></div>
</div>
</div>
</div>
{event.contacts.length > 0 && (
<div className="flex items-center text-gray-500 text-xs sm:text-sm">
<UserCheck size={14} className="sm:w-4 sm:h-4 mr-1.5 sm:mr-2 text-brand-gold flex-shrink-0" />
<span className="truncate">{event.contacts.length} {event.contacts.length === 1 ? 'Contato' : 'Contatos'}</span>
</div>
)}
</div>
<div className="mt-3 sm:mt-4 md:mt-5 pt-3 sm:pt-4 border-t border-gray-50 flex items-center justify-between">
<div className="flex -space-x-1.5 sm:-space-x-2">
{[1, 2, 3].map(i => (
<div key={i} className="w-6 h-6 sm:w-7 sm:h-7 md:w-8 md:h-8 rounded-full border-2 border-white bg-gray-200" style={{ backgroundImage: `url(https://i.pravatar.cc/100?img=${i})`, backgroundSize: 'cover' }}></div>
))}
<div className="w-6 h-6 sm:w-7 sm:h-7 md:w-8 md:h-8 rounded-full border-2 border-white bg-gray-100 flex items-center justify-center text-[10px] sm:text-xs text-gray-500 font-medium">
+4
</div>
</div>
<button className="text-brand-black group-hover:text-brand-gold transition-colors">
<ArrowRight size={18} className="sm:w-5 sm:h-5" />
</button>
</div>
</div>
</div>
);
};