feat: melhorias no formulário de eventos e mapa interativo

- Adiciona campo de número de pessoas com validação numérica
- Atualiza tipos de eventos para eventos universitários (formatura, colação, etc)
- Substitui horário único por horários de início e término obrigatórios
- Adiciona campo de curso relacionado ao evento
- Reorganiza ordem dos campos no formulário de instituição (CEP primeiro)
- Atualiza coordenadas padrão do mapa para Americana-SP
- Melhora layout do mapa: cards de coordenadas e instruções abaixo do mapa
This commit is contained in:
João Vitor 2025-12-03 14:51:15 -03:00
parent 31325d50eb
commit ac274e5c91
4 changed files with 187 additions and 171 deletions

View file

@ -63,6 +63,8 @@ export const EventForm: React.FC<EventFormProps> = ({
name: "", name: "",
date: "", date: "",
time: "", time: "",
startTime: "",
endTime: "",
type: "", type: "",
status: EventStatus.PLANNING, status: EventStatus.PLANNING,
address: { address: {
@ -71,8 +73,8 @@ export const EventForm: React.FC<EventFormProps> = ({
city: "", city: "",
state: "", state: "",
zip: "", zip: "",
lat: -23.5505, lat: -22.7394,
lng: -46.6333, lng: -47.3314,
mapLink: "", mapLink: "",
} as Address, } as Address,
briefing: "", briefing: "",
@ -81,6 +83,8 @@ export const EventForm: React.FC<EventFormProps> = ({
coverImage: coverImage:
"https://images.unsplash.com/photo-1511795409834-ef04bbd61622?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=80", // Default "https://images.unsplash.com/photo-1511795409834-ef04bbd61622?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=80", // Default
institutionId: "", institutionId: "",
attendees: "",
course: "",
} }
); );
@ -336,30 +340,42 @@ export const EventForm: React.FC<EventFormProps> = ({
<div className="grid grid-cols-1 gap-6"> <div className="grid grid-cols-1 gap-6">
<Input <Input
label="Nome do Evento (Opcional)" label="Nome do Evento (Opcional)"
placeholder="Ex: Casamento Silva & Souza" placeholder="Ex: Formatura Educação Física 2025"
value={formData.name} value={formData.name}
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, name: e.target.value }) setFormData({ ...formData, name: e.target.value })
} }
/> />
<Input
label="Data Pretendida"
type="date"
value={formData.date}
onChange={(e) =>
setFormData({ ...formData, date: e.target.value })
}
/>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<Input <Input
label="Data Pretendida" label="Horário de Início*"
type="date" type="time"
value={formData.date} value={formData.startTime}
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, date: e.target.value }) setFormData({ ...formData, startTime: e.target.value })
} }
required
/> />
<Input <Input
label="Horário Aproximado" label="Horário de Término*"
type="time" type="time"
value={formData.time} value={formData.endTime}
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, time: e.target.value }) setFormData({ ...formData, endTime: e.target.value })
} }
required
/> />
</div> </div>
<Select <Select
label="Tipo de Evento" label="Tipo de Evento"
options={Object.values(EventType).map((t) => ({ options={Object.values(EventType).map((t) => ({
@ -372,6 +388,30 @@ export const EventForm: React.FC<EventFormProps> = ({
} }
/> />
<Input
label="Curso"
placeholder="Ex: Engenharia Civil, Medicina, Direito"
value={formData.course}
onChange={(e) =>
setFormData({ ...formData, course: e.target.value })
}
/>
<Input
label="Número de Pessoas"
placeholder="Ex: 150"
value={formData.attendees}
onChange={(e) => {
const value = e.target.value;
// Permite apenas números
if (value === "" || /^\d+$/.test(value)) {
setFormData({ ...formData, attendees: value });
}
}}
type="text"
inputMode="numeric"
/>
{/* Institution Selection - OBRIGATÓRIO */} {/* Institution Selection - OBRIGATÓRIO */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs"> <label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
@ -640,8 +680,8 @@ export const EventForm: React.FC<EventFormProps> = ({
)} )}
</label> </label>
<MapboxMap <MapboxMap
initialLat={formData.address.lat || -23.5505} initialLat={formData.address.lat || -22.7394}
initialLng={formData.address.lng || -46.6333} initialLng={formData.address.lng || -47.3314}
onLocationChange={handleMapLocationChange} onLocationChange={handleMapLocationChange}
height="450px" height="450px"
/> />

View file

@ -1,9 +1,8 @@
import React, { useState } from "react";
import React, { useState } from 'react'; import { Institution, Address } from "../types";
import { Institution, Address } from '../types'; import { Input, Select } from "./Input";
import { Input, Select } from './Input'; import { Button } from "./Button";
import { Button } from './Button'; import { Building2, X, Check } from "lucide-react";
import { Building2, X, Check } from 'lucide-react';
interface InstitutionFormProps { interface InstitutionFormProps {
onCancel: () => void; onCancel: () => void;
@ -13,39 +12,41 @@ interface InstitutionFormProps {
} }
const INSTITUTION_TYPES = [ const INSTITUTION_TYPES = [
'Universidade Pública', "Universidade Pública",
'Universidade Privada', "Universidade Privada",
'Faculdade', "Faculdade",
'Instituto Federal', "Instituto Federal",
'Centro Universitário', "Centro Universitário",
'Campus Universitário' "Campus Universitário",
]; ];
export const InstitutionForm: React.FC<InstitutionFormProps> = ({ export const InstitutionForm: React.FC<InstitutionFormProps> = ({
onCancel, onCancel,
onSubmit, onSubmit,
initialData, initialData,
userId userId,
}) => { }) => {
const [formData, setFormData] = useState<Partial<Institution>>(initialData || { const [formData, setFormData] = useState<Partial<Institution>>(
name: '', initialData || {
type: '', name: "",
cnpj: '', type: "",
phone: '', cnpj: "",
email: '', phone: "",
description: '', email: "",
ownerId: userId, description: "",
address: { ownerId: userId,
street: '', address: {
number: '', street: "",
city: '', number: "",
state: '', city: "",
zip: '' state: "",
zip: "",
},
} }
}); );
const [showToast, setShowToast] = useState(false); const [showToast, setShowToast] = useState(false);
const [stateError, setStateError] = useState(''); const [stateError, setStateError] = useState("");
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@ -56,29 +57,30 @@ export const InstitutionForm: React.FC<InstitutionFormProps> = ({
}; };
const handleChange = (field: keyof Institution, value: any) => { const handleChange = (field: keyof Institution, value: any) => {
setFormData(prev => ({ ...prev, [field]: value })); setFormData((prev) => ({ ...prev, [field]: value }));
}; };
const handleAddressChange = (field: keyof Address, value: string) => { const handleAddressChange = (field: keyof Address, value: string) => {
setFormData(prev => ({ setFormData((prev) => ({
...prev, ...prev,
address: { address: {
...prev.address!, ...prev.address!,
[field]: value [field]: value,
} },
})); }));
}; };
return ( return (
<div className="bg-white rounded-lg shadow-xl overflow-hidden max-w-2xl mx-auto border border-gray-100 slide-up relative"> <div className="bg-white rounded-lg shadow-xl overflow-hidden max-w-2xl mx-auto border border-gray-100 slide-up relative">
{/* Success Toast */} {/* Success Toast */}
{showToast && ( {showToast && (
<div className="absolute top-4 right-4 z-50 bg-brand-black text-white px-6 py-4 rounded shadow-2xl flex items-center space-x-3 fade-in"> <div className="absolute top-4 right-4 z-50 bg-brand-black text-white px-6 py-4 rounded shadow-2xl flex items-center space-x-3 fade-in">
<Check className="text-brand-gold h-6 w-6" /> <Check className="text-brand-gold h-6 w-6" />
<div> <div>
<h4 className="font-bold text-sm">Sucesso!</h4> <h4 className="font-bold text-sm">Sucesso!</h4>
<p className="text-xs text-gray-300">Universidade cadastrada com sucesso.</p> <p className="text-xs text-gray-300">
Universidade cadastrada com sucesso.
</p>
</div> </div>
</div> </div>
)} )}
@ -89,10 +91,11 @@ export const InstitutionForm: React.FC<InstitutionFormProps> = ({
<Building2 className="text-brand-gold h-8 w-8" /> <Building2 className="text-brand-gold h-8 w-8" />
<div> <div>
<h2 className="text-2xl font-serif text-brand-black"> <h2 className="text-2xl font-serif text-brand-black">
{initialData ? 'Editar Universidade' : 'Cadastrar Universidade'} {initialData ? "Editar Universidade" : "Cadastrar Universidade"}
</h2> </h2>
<p className="text-sm text-gray-500 mt-1"> <p className="text-sm text-gray-500 mt-1">
Registre a universidade onde os eventos fotográficos serão realizados Registre a universidade onde os eventos fotográficos serão
realizados
</p> </p>
</div> </div>
</div> </div>
@ -105,60 +108,37 @@ export const InstitutionForm: React.FC<InstitutionFormProps> = ({
</div> </div>
<form onSubmit={handleSubmit} className="p-8 space-y-6"> <form onSubmit={handleSubmit} className="p-8 space-y-6">
{/* Informações Básicas */} {/* Informações Básicas */}
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-sm font-semibold text-gray-700 tracking-wide uppercase"> <h3 className="text-sm font-semibold text-gray-700 tracking-wide uppercase">
Informações Básicas Informações Básicas
</h3> </h3>
<Input <Input
label="Nome da Universidade*" label="Nome da Universidade*"
placeholder="Ex: Universidade Federal do Rio Grande do Sul" placeholder="Ex: Universidade Federal do Rio Grande do Sul"
value={formData.name || ''} value={formData.name || ""}
onChange={(e) => handleChange('name', e.target.value)} onChange={(e) => handleChange("name", e.target.value)}
required required
/> />
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<Select <Select
label="Tipo de Universidade*" label="Tipo de Universidade*"
options={INSTITUTION_TYPES.map(t => ({ value: t, label: t }))} options={INSTITUTION_TYPES.map((t) => ({ value: t, label: t }))}
value={formData.type || ''} value={formData.type || ""}
onChange={(e) => handleChange('type', e.target.value)} onChange={(e) => handleChange("type", e.target.value)}
required required
/> />
<Input <Input
label="CNPJ (Opcional)" label="CNPJ (Opcional)"
placeholder="00.000.000/0000-00" placeholder="00.000.000/0000-00"
value={formData.cnpj || ''} value={formData.cnpj || ""}
onChange={(e) => handleChange('cnpj', e.target.value)} onChange={(e) => handleChange("cnpj", e.target.value)}
mask="cnpj" mask="cnpj"
/> />
</div> </div>
<div className="grid grid-cols-2 gap-4">
<Input
label="Telefone*"
type="tel"
placeholder="(00) 00000-0000"
value={formData.phone || ''}
onChange={(e) => handleChange('phone', e.target.value)}
mask="phone"
required
/>
<Input
label="E-mail*"
type="email"
placeholder="contato@instituicao.com"
value={formData.email || ''}
onChange={(e) => handleChange('email', e.target.value)}
required
/>
</div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs"> <label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
Descrição (Opcional) Descrição (Opcional)
@ -166,8 +146,8 @@ export const InstitutionForm: React.FC<InstitutionFormProps> = ({
<textarea <textarea
className="w-full border border-gray-300 rounded-sm p-3 focus:outline-none focus:border-brand-gold h-24 text-sm" className="w-full border border-gray-300 rounded-sm p-3 focus:outline-none focus:border-brand-gold h-24 text-sm"
placeholder="Ex: Campus principal, informações sobre o campus, áreas para eventos..." placeholder="Ex: Campus principal, informações sobre o campus, áreas para eventos..."
value={formData.description || ''} value={formData.description || ""}
onChange={(e) => handleChange('description', e.target.value)} onChange={(e) => handleChange("description", e.target.value)}
/> />
</div> </div>
</div> </div>
@ -177,59 +157,62 @@ export const InstitutionForm: React.FC<InstitutionFormProps> = ({
<h3 className="text-sm font-semibold text-gray-700 tracking-wide uppercase"> <h3 className="text-sm font-semibold text-gray-700 tracking-wide uppercase">
Endereço (Opcional) Endereço (Opcional)
</h3> </h3>
<Input
label="CEP"
placeholder="00000-000"
value={formData.address?.zip || ""}
onChange={(e) => handleAddressChange("zip", e.target.value)}
mask="cep"
/>
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">
<div className="col-span-2"> <div className="col-span-2">
<Input <Input
label="Rua" label="Rua"
placeholder="Nome da rua" placeholder="Nome da rua"
value={formData.address?.street || ''} value={formData.address?.street || ""}
onChange={(e) => handleAddressChange('street', e.target.value)} onChange={(e) => handleAddressChange("street", e.target.value)}
/> />
</div> </div>
<Input <Input
label="Número" label="Número"
placeholder="123" placeholder="123"
value={formData.address?.number || ''} value={formData.address?.number || ""}
onChange={(e) => { onChange={(e) => {
const value = e.target.value.replace(/\D/g, ''); const value = e.target.value.replace(/\D/g, "");
handleAddressChange('number', value); handleAddressChange("number", value);
}} }}
type="text" type="text"
inputMode="numeric" inputMode="numeric"
/> />
</div> </div>
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-2 gap-4">
<Input <Input
label="Cidade" label="Cidade"
placeholder="Cidade" placeholder="Cidade"
value={formData.address?.city || ''} value={formData.address?.city || ""}
onChange={(e) => handleAddressChange('city', e.target.value)} onChange={(e) => handleAddressChange("city", e.target.value)}
/> />
<Input <Input
label="Estado" label="Estado"
placeholder="UF" placeholder="UF"
value={formData.address?.state || ''} value={formData.address?.state || ""}
onChange={(e) => { onChange={(e) => {
const hasNumbers = /[0-9]/.test(e.target.value); const hasNumbers = /[0-9]/.test(e.target.value);
if (hasNumbers) { if (hasNumbers) {
setStateError('O campo Estado aceita apenas letras'); setStateError("O campo Estado aceita apenas letras");
setTimeout(() => setStateError(''), 3000); setTimeout(() => setStateError(""), 3000);
} }
const value = e.target.value.replace(/[0-9]/g, '').toUpperCase(); const value = e.target.value
handleAddressChange('state', value); .replace(/[0-9]/g, "")
.toUpperCase();
handleAddressChange("state", value);
}} }}
maxLength={2} maxLength={2}
error={stateError} error={stateError}
/> />
<Input
label="CEP"
placeholder="00000-000"
value={formData.address?.zip || ''}
onChange={(e) => handleAddressChange('zip', e.target.value)}
mask="cep"
/>
</div> </div>
</div> </div>
@ -239,7 +222,7 @@ export const InstitutionForm: React.FC<InstitutionFormProps> = ({
Cancelar Cancelar
</Button> </Button>
<Button type="submit" variant="secondary"> <Button type="submit" variant="secondary">
{initialData ? 'Salvar Alterações' : 'Cadastrar Universidade'} {initialData ? "Salvar Alterações" : "Cadastrar Universidade"}
</Button> </Button>
</div> </div>
</form> </form>

View file

@ -153,72 +153,57 @@ export const MapboxMap: React.FC<MapboxMapProps> = ({
}; };
return ( return (
<div className="relative"> <div>
{error && ( {error && (
<div className="absolute top-0 left-0 right-0 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded z-10"> <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-3">
{error} {error}
</div> </div>
)} )}
<div <div className="relative">
ref={mapContainer} <div
className="w-full rounded-lg border-2 border-gray-300 overflow-hidden shadow-md" ref={mapContainer}
style={{ height }} className="w-full rounded-lg border-2 border-gray-300 overflow-hidden shadow-md"
/> style={{ height }}
/>
{/* Info overlay - Responsivo */}
<div className="absolute bottom-2 sm:bottom-4 left-2 sm:left-4 bg-white/95 backdrop-blur-sm px-2 py-2 sm:px-4 sm:py-3 rounded-lg shadow-lg border border-gray-200 max-w-[160px] sm:max-w-none">
<div className="flex items-center space-x-2 sm:space-x-3">
<MapPin
size={16}
className="text-brand-gold flex-shrink-0 hidden sm:block"
/>
<div className="min-w-0">
<p className="text-[10px] sm:text-xs text-gray-500 font-medium hidden sm:block">
Coordenadas
</p>
<p className="text-[10px] sm:text-sm font-mono text-gray-800 truncate">
{currentLat.toFixed(4)}, {currentLng.toFixed(4)}
</p>
</div>
</div>
</div> </div>
{/* Botão de centralizar - Responsivo */} {/* Info cards abaixo do mapa */}
<button <div className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-3">
onClick={centerOnMarker} {/* Card de Coordenadas */}
className="absolute bottom-2 sm:bottom-4 right-2 sm:right-4 bg-white hover:bg-gray-50 p-2 sm:p-3 rounded-full shadow-lg border border-gray-200 transition-colors group" <div className="bg-white rounded-lg shadow border border-gray-200 p-3">
title="Centralizar no marcador" <div className="flex items-center space-x-2">
> <MapPin size={16} className="text-brand-gold flex-shrink-0" />
<Target <div className="min-w-0 flex-1">
size={18} <p className="text-xs text-gray-500 font-medium">Coordenadas</p>
className="text-gray-600 group-hover:text-brand-gold transition-colors sm:w-5 sm:h-5" <p className="text-sm font-mono text-gray-800 truncate">
/> {currentLat.toFixed(4)}, {currentLng.toFixed(4)}
</button> </p>
</div>
</div>
</div>
{/* Instruções - Responsivo */} {/* Card de Instruções */}
<div className="mt-3 bg-blue-50 border border-blue-200 rounded-lg p-2 sm:p-3 text-xs sm:text-sm text-blue-800"> <div className="bg-blue-50 rounded-lg shadow border border-blue-200 p-3">
<p className="font-medium mb-1 text-xs sm:text-sm">💡 Como usar:</p> <p className="font-medium mb-1.5 text-xs text-blue-800">
<ul className="text-[11px] sm:text-xs space-y-0.5 sm:space-y-1 text-blue-700"> 💡 Como usar:
<li className="flex items-start"> </p>
<span className="mr-1"></span> <ul className="text-xs space-y-1 text-blue-700">
<span> <li className="flex items-start">
<strong>Arraste o marcador</strong> para a posição exata <span className="mr-1"></span>
</span> <span>
</li> <strong>Arraste o marcador</strong> ou{" "}
<li className="flex items-start"> <strong>clique no mapa</strong>
<span className="mr-1"></span> </span>
<span> </li>
<strong>Clique no mapa</strong> para mover o marcador <li className="flex items-start">
</span> <span className="mr-1"></span>
</li> <span>
<li className="flex items-start hidden sm:flex"> Use os <strong>controles</strong> para navegação
<span className="mr-1"></span> </span>
<span> </li>
Use os <strong>controles</strong> para zoom e navegação </ul>
</span> </div>
</li>
</ul>
</div> </div>
</div> </div>
); );

View file

@ -15,10 +15,14 @@ export enum EventStatus {
} }
export enum EventType { export enum EventType {
WEDDING = "Casamento", GRADUATION = "Formatura",
CORPORATE = "Corporativo", COLATION = "Colação de Grau",
BIRTHDAY = "Aniversário", ACADEMIC_WEEK = "Semana Acadêmica",
DEBUTANTE = "Debutante", FRESHMAN_WEEK = "Semana de Calouros",
SYMPOSIUM = "Simpósio/Congresso",
DEFENSE = "Defesa de TCC/Mestrado/Doutorado",
SPORTS_EVENT = "Evento Esportivo Universitário",
CULTURAL_EVENT = "Evento Cultural",
OTHER = "Outro", OTHER = "Outro",
} }
@ -73,7 +77,9 @@ export interface EventData {
id: string; id: string;
name: string; name: string;
date: string; date: string;
time: string; time: string; // Mantido por compatibilidade, mas deprecated
startTime?: string; // Horário de início
endTime?: string; // Horário de término
type: EventType; type: EventType;
status: EventStatus; status: EventStatus;
address: Address; address: Address;
@ -84,4 +90,6 @@ export interface EventData {
ownerId: string; // ID do cliente dono do evento ownerId: string; // ID do cliente dono do evento
photographerIds: string[]; // IDs dos fotógrafos designados photographerIds: string[]; // IDs dos fotógrafos designados
institutionId?: string; // ID da instituição vinculada (obrigatório) institutionId?: string; // ID da instituição vinculada (obrigatório)
attendees?: number; // Número de pessoas participantes
course?: string; // Curso relacionado ao evento
} }