photum/pages/Team.tsx
João Vitor d087cefb1b feat: Integração completa Mapbox + Upload de avatares
- Integração Mapbox GL JS para seleção interativa de localização
  - Mapa arrastável com pin para localização exata
  - Geocoding e reverse geocoding automático
  - Busca de endereços com autocomplete
  - Campos editáveis que atualizam mapa automaticamente
  - Token configurado via variável de ambiente (.env.local)

- Sistema de upload de fotos de fotógrafos
  - Upload via input de arquivo (substituiu URL)
  - Preview automático com FileReader API
  - Botão para remover foto selecionada
  - Placeholder com ícone de câmera

- Remoção de funcionalidades de uploads/álbuns
  - Removida página Albums.tsx
  - Removido sistema de attachments
  - Removida aba Inspiração para empresas
  - Criada página Inspiração com galeria de exemplo

- Melhorias de responsividade
  - Cards do mapa adaptados para mobile
  - Texto e padding reduzidos em telas pequenas

- Arquivos de configuração
  - .env.example criado
  - vite-env.d.ts para tipagem
  - MAPBOX_SETUP.md com instruções
  - Footer atualizado com serviços universitários
2025-12-02 13:55:56 -03:00

633 lines
33 KiB
TypeScript

import React, { useState } from 'react';
import { Users, Camera, Mail, Phone, MapPin, Star, Plus, Search, Filter, User, Upload, X } from 'lucide-react';
import { Button } from '../components/Button';
interface Photographer {
id: string;
name: string;
email: string;
phone: string;
location: string;
specialties: string[];
rating: number;
eventsCompleted: number;
status: 'active' | 'inactive' | 'busy';
avatar: string;
joinDate: string;
}
const MOCK_PHOTOGRAPHERS: Photographer[] = [
{
id: '1',
name: 'Carlos Silva',
email: 'carlos.silva@photum.com',
phone: '(41) 99999-1111',
location: 'Curitiba, PR',
specialties: ['Formaturas', 'Eventos Corporativos'],
rating: 4.8,
eventsCompleted: 45,
status: 'active',
avatar: 'https://i.pravatar.cc/150?img=12',
joinDate: '2023-01-15'
},
{
id: '2',
name: 'Ana Paula Mendes',
email: 'ana.mendes@photum.com',
phone: '(41) 99999-2222',
location: 'Curitiba, PR',
specialties: ['Casamentos', 'Formaturas'],
rating: 4.9,
eventsCompleted: 62,
status: 'busy',
avatar: 'https://i.pravatar.cc/150?img=5',
joinDate: '2022-08-20'
},
{
id: '3',
name: 'Roberto Costa',
email: 'roberto.costa@photum.com',
phone: '(41) 99999-3333',
location: 'São José dos Pinhais, PR',
specialties: ['Formaturas', 'Eventos Sociais'],
rating: 4.7,
eventsCompleted: 38,
status: 'active',
avatar: 'https://i.pravatar.cc/150?img=33',
joinDate: '2023-03-10'
},
{
id: '4',
name: 'Juliana Santos',
email: 'juliana.santos@photum.com',
phone: '(41) 99999-4444',
location: 'Curitiba, PR',
specialties: ['Casamentos', 'Ensaios'],
rating: 5.0,
eventsCompleted: 71,
status: 'active',
avatar: 'https://i.pravatar.cc/150?img=9',
joinDate: '2022-05-12'
},
{
id: '5',
name: 'Fernando Oliveira',
email: 'fernando.oliveira@photum.com',
phone: '(41) 99999-5555',
location: 'Pinhais, PR',
specialties: ['Eventos Corporativos', 'Formaturas'],
rating: 4.6,
eventsCompleted: 29,
status: 'inactive',
avatar: 'https://i.pravatar.cc/150?img=15',
joinDate: '2023-07-01'
},
{
id: '6',
name: 'Mariana Rodrigues',
email: 'mariana.rodrigues@photum.com',
phone: '(41) 99999-6666',
location: 'Curitiba, PR',
specialties: ['Formaturas', 'Eventos Sociais', 'Casamentos'],
rating: 4.9,
eventsCompleted: 54,
status: 'busy',
avatar: 'https://i.pravatar.cc/150?img=10',
joinDate: '2022-11-05'
}
];
export const TeamPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'busy' | 'inactive'>('all');
const [selectedPhotographer, setSelectedPhotographer] = useState<Photographer | null>(null);
const [showAddModal, setShowAddModal] = useState(false);
const [newPhotographer, setNewPhotographer] = useState({
name: '',
email: '',
phone: '',
location: '',
specialties: [] as string[],
avatar: ''
});
const [avatarFile, setAvatarFile] = useState<File | null>(null);
const [avatarPreview, setAvatarPreview] = useState<string>('');
const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setAvatarFile(file);
const reader = new FileReader();
reader.onloadend = () => {
setAvatarPreview(reader.result as string);
};
reader.readAsDataURL(file);
}
};
const removeAvatar = () => {
setAvatarFile(null);
setAvatarPreview('');
};
const getStatusColor = (status: Photographer['status']) => {
switch (status) {
case 'active':
return 'bg-green-100 text-green-800';
case 'busy':
return 'bg-yellow-100 text-yellow-800';
case 'inactive':
return 'bg-gray-100 text-gray-800';
}
};
const getStatusLabel = (status: Photographer['status']) => {
switch (status) {
case 'active':
return 'Disponível';
case 'busy':
return 'Em Evento';
case 'inactive':
return 'Inativo';
}
};
const filteredPhotographers = MOCK_PHOTOGRAPHERS.filter(photographer => {
const matchesSearch = photographer.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
photographer.email.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || photographer.status === statusFilter;
return matchesSearch && matchesStatus;
});
const stats = {
total: MOCK_PHOTOGRAPHERS.length,
active: MOCK_PHOTOGRAPHERS.filter(p => p.status === 'active').length,
busy: MOCK_PHOTOGRAPHERS.filter(p => p.status === 'busy').length,
avgRating: (MOCK_PHOTOGRAPHERS.reduce((acc, p) => acc + p.rating, 0) / MOCK_PHOTOGRAPHERS.length).toFixed(1)
};
return (
<div className="min-h-screen bg-gray-50 pt-32 pb-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-serif font-bold text-brand-black mb-2">
Equipe & Fotógrafos
</h1>
<p className="text-gray-600">
Gerencie sua equipe de fotógrafos profissionais
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600 mb-1">Total de Fotógrafos</p>
<p className="text-3xl font-bold text-brand-black">{stats.total}</p>
</div>
<Users className="text-brand-gold" size={32} />
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600 mb-1">Disponíveis</p>
<p className="text-3xl font-bold text-green-600">{stats.active}</p>
</div>
<Camera className="text-green-600" size={32} />
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600 mb-1">Em Evento</p>
<p className="text-3xl font-bold text-yellow-600">{stats.busy}</p>
</div>
<Camera className="text-yellow-600" size={32} />
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600 mb-1">Avaliação Média</p>
<p className="text-3xl font-bold text-brand-gold">{stats.avgRating}</p>
</div>
<Star className="text-brand-gold" size={32} fill="#B9CF33" />
</div>
</div>
</div>
{/* Filters and Search */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-6">
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={20} />
<input
type="text"
placeholder="Buscar por nome ou email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
/>
</div>
<div className="flex gap-2">
<button
onClick={() => setStatusFilter('all')}
className={`px-4 py-2 rounded-md font-medium transition-colors ${statusFilter === 'all'
? 'bg-brand-gold text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Todos
</button>
<button
onClick={() => setStatusFilter('active')}
className={`px-4 py-2 rounded-md font-medium transition-colors ${statusFilter === 'active'
? 'bg-green-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Disponíveis
</button>
<button
onClick={() => setStatusFilter('busy')}
className={`px-4 py-2 rounded-md font-medium transition-colors ${statusFilter === 'busy'
? 'bg-yellow-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Em Evento
</button>
</div>
<Button size="md" variant="secondary" onClick={() => setShowAddModal(true)}>
<Plus size={20} className="mr-2" />
Adicionar Fotógrafo
</Button>
</div>
</div>
{/* Photographers Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredPhotographers.map((photographer) => (
<div
key={photographer.id}
className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow cursor-pointer"
onClick={() => setSelectedPhotographer(photographer)}
>
<div className="p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<img
src={photographer.avatar}
alt={photographer.name}
className="w-16 h-16 rounded-full object-cover"
/>
<div>
<h3 className="font-semibold text-lg text-brand-black">{photographer.name}</h3>
<div className="flex items-center gap-1 mt-1">
<Star size={14} fill="#B9CF33" className="text-brand-gold" />
<span className="text-sm font-medium">{photographer.rating}</span>
<span className="text-xs text-gray-500">({photographer.eventsCompleted} eventos)</span>
</div>
</div>
</div>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(photographer.status)}`}>
{getStatusLabel(photographer.status)}
</span>
</div>
<div className="space-y-2 mb-4">
<div className="flex items-center text-sm text-gray-600">
<Mail size={16} className="mr-2 text-brand-gold" />
{photographer.email}
</div>
<div className="flex items-center text-sm text-gray-600">
<Phone size={16} className="mr-2 text-brand-gold" />
{photographer.phone}
</div>
<div className="flex items-center text-sm text-gray-600">
<MapPin size={16} className="mr-2 text-brand-gold" />
{photographer.location}
</div>
</div>
<div className="flex flex-wrap gap-2">
{photographer.specialties.map((specialty, index) => (
<span
key={index}
className="px-2 py-1 bg-gray-100 text-gray-700 rounded-full text-xs font-medium"
>
{specialty}
</span>
))}
</div>
</div>
</div>
))}
</div>
{filteredPhotographers.length === 0 && (
<div className="text-center py-12">
<Users size={48} className="mx-auto text-gray-300 mb-4" />
<p className="text-gray-500">Nenhum fotógrafo encontrado</p>
</div>
)}
</div>
{/* Add Photographer Modal */}
{showAddModal && (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
onClick={() => setShowAddModal(false)}
>
<div
className="bg-white rounded-lg max-w-2xl w-full p-8 max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-serif font-bold text-brand-black">
Adicionar Novo Fotógrafo
</h2>
<button
onClick={() => setShowAddModal(false)}
className="text-gray-400 hover:text-gray-600"
>
</button>
</div>
<form className="space-y-6" onSubmit={(e) => {
e.preventDefault();
alert('Fotógrafo adicionado com sucesso!\n\n' + JSON.stringify({...newPhotographer, avatarFile: avatarFile?.name}, null, 2));
setShowAddModal(false);
setNewPhotographer({
name: '',
email: '',
phone: '',
location: '',
specialties: [],
avatar: ''
});
setAvatarFile(null);
setAvatarPreview('');
}}>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Foto de Perfil
</label>
<div className="flex items-center gap-4">
{avatarPreview ? (
<div className="relative">
<img
src={avatarPreview}
alt="Preview"
className="w-24 h-24 rounded-full object-cover border-2 border-gray-200"
/>
<button
type="button"
onClick={removeAvatar}
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 hover:bg-red-600 transition-colors"
>
<X size={16} />
</button>
</div>
) : (
<div className="w-24 h-24 rounded-full bg-gray-100 border-2 border-dashed border-gray-300 flex items-center justify-center">
<Camera size={32} className="text-gray-400" />
</div>
)}
<div className="flex-1">
<label className="cursor-pointer">
<div className="flex items-center gap-2 px-4 py-2 bg-gray-50 border border-gray-300 rounded-md hover:bg-gray-100 transition-colors w-fit">
<Upload size={18} className="text-gray-600" />
<span className="text-sm font-medium text-gray-700">
{avatarFile ? 'Trocar foto' : 'Selecionar foto'}
</span>
</div>
<input
type="file"
accept="image/*"
onChange={handleAvatarChange}
className="hidden"
/>
</label>
<p className="text-xs text-gray-500 mt-1">JPG, PNG ou GIF (máx. 5MB)</p>
{avatarFile && (
<p className="text-xs text-brand-gold mt-1 font-medium">{avatarFile.name}</p>
)}
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Nome Completo *
</label>
<div className="relative">
<User className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={20} />
<input
type="text"
required
value={newPhotographer.name}
onChange={(e) => setNewPhotographer({ ...newPhotographer, name: e.target.value })}
placeholder="Ex: João Silva"
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email *
</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={20} />
<input
type="email"
required
value={newPhotographer.email}
onChange={(e) => setNewPhotographer({ ...newPhotographer, email: e.target.value })}
placeholder="joao.silva@photum.com"
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Telefone *
</label>
<div className="relative">
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={20} />
<input
type="tel"
required
value={newPhotographer.phone}
onChange={(e) => setNewPhotographer({ ...newPhotographer, phone: e.target.value })}
placeholder="(41) 99999-0000"
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Localização *
</label>
<div className="relative">
<MapPin className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={20} />
<input
type="text"
required
value={newPhotographer.location}
onChange={(e) => setNewPhotographer({ ...newPhotographer, location: e.target.value })}
placeholder="Curitiba, PR"
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Especialidades
</label>
<div className="space-y-2">
{['Formaturas', 'Casamentos', 'Eventos Corporativos', 'Eventos Sociais', 'Ensaios'].map((specialty) => (
<label key={specialty} className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={newPhotographer.specialties.includes(specialty)}
onChange={(e) => {
if (e.target.checked) {
setNewPhotographer({
...newPhotographer,
specialties: [...newPhotographer.specialties, specialty]
});
} else {
setNewPhotographer({
...newPhotographer,
specialties: newPhotographer.specialties.filter(s => s !== specialty)
});
}
}}
className="w-4 h-4 text-brand-gold focus:ring-brand-gold border-gray-300 rounded"
/>
<span className="text-sm text-gray-700">{specialty}</span>
</label>
))}
</div>
</div>
<div className="pt-6 border-t border-gray-200 flex gap-3">
<button
type="button"
onClick={() => setShowAddModal(false)}
className="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition-colors font-medium"
>
Cancelar
</button>
<button
type="submit"
className="flex-1 px-6 py-3 bg-brand-gold text-white rounded-md hover:bg-[#a5bd2e] transition-colors font-medium"
>
Adicionar Fotógrafo
</button>
</div>
</form>
</div>
</div>
)}
{/* Photographer Detail Modal */}
{selectedPhotographer && (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
onClick={() => setSelectedPhotographer(null)}
>
<div
className="bg-white rounded-lg max-w-2xl w-full p-8"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-start gap-4 mb-6">
<img
src={selectedPhotographer.avatar}
alt={selectedPhotographer.name}
className="w-24 h-24 rounded-full object-cover"
/>
<div className="flex-1">
<div className="flex items-start justify-between">
<div>
<h2 className="text-2xl font-serif font-bold text-brand-black mb-1">
{selectedPhotographer.name}
</h2>
<div className="flex items-center gap-2 mb-2">
<Star size={18} fill="#B9CF33" className="text-brand-gold" />
<span className="font-semibold">{selectedPhotographer.rating}</span>
<span className="text-sm text-gray-500">
({selectedPhotographer.eventsCompleted} eventos concluídos)
</span>
</div>
<span className={`inline-block px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(selectedPhotographer.status)}`}>
{getStatusLabel(selectedPhotographer.status)}
</span>
</div>
<button
onClick={() => setSelectedPhotographer(null)}
className="text-gray-400 hover:text-gray-600"
>
</button>
</div>
</div>
</div>
<div className="space-y-4 mb-6">
<div className="flex items-center text-gray-700">
<Mail size={20} className="mr-3 text-brand-gold" />
<span>{selectedPhotographer.email}</span>
</div>
<div className="flex items-center text-gray-700">
<Phone size={20} className="mr-3 text-brand-gold" />
<span>{selectedPhotographer.phone}</span>
</div>
<div className="flex items-center text-gray-700">
<MapPin size={20} className="mr-3 text-brand-gold" />
<span>{selectedPhotographer.location}</span>
</div>
</div>
<div className="mb-6">
<h3 className="font-semibold mb-2">Especialidades</h3>
<div className="flex flex-wrap gap-2">
{selectedPhotographer.specialties.map((specialty, index) => (
<span
key={index}
className="px-3 py-1 bg-brand-gold/10 text-brand-gold rounded-full text-sm font-medium"
>
{specialty}
</span>
))}
</div>
</div>
<div className="pt-6 border-t border-gray-200 flex gap-3">
<button
onClick={() => setSelectedPhotographer(null)}
className="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition-colors font-medium"
>
Fechar
</button>
<button
className="flex-1 px-6 py-3 bg-brand-gold text-white rounded-md hover:bg-[#a5bd2e] transition-colors font-medium"
>
Ver Agenda
</button>
</div>
</div>
</div>
)}
</div>
);
};