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
This commit is contained in:
parent
7f5c8ae2be
commit
d087cefb1b
17 changed files with 1169 additions and 363 deletions
12
.env.example
Normal file
12
.env.example
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Configuração da API do Mapbox
|
||||
# Obtenha sua chave gratuita em: https://account.mapbox.com/
|
||||
|
||||
# Para usar:
|
||||
# 1. Copie este arquivo para .env.local
|
||||
# 2. Substitua YOUR_MAPBOX_TOKEN_HERE pela sua chave real
|
||||
# 3. Atualize o arquivo services/mapboxService.ts com sua chave
|
||||
|
||||
VITE_MAPBOX_TOKEN=YOUR_MAPBOX_TOKEN_HERE
|
||||
|
||||
# Nota: A chave já está configurada diretamente no código para demonstração
|
||||
# Em produção, use variáveis de ambiente como acima
|
||||
17
App.tsx
17
App.tsx
|
|
@ -8,7 +8,7 @@ import { CalendarPage } from "./pages/Calendar";
|
|||
import { TeamPage } from "./pages/Team";
|
||||
import { FinancePage } from "./pages/Finance";
|
||||
import { SettingsPage } from "./pages/Settings";
|
||||
import { AlbumsPage } from "./pages/Albums";
|
||||
import { InspirationPage } from "./pages/Inspiration";
|
||||
import { PrivacyPolicy } from "./pages/PrivacyPolicy";
|
||||
import { TermsOfUse } from "./pages/TermsOfUse";
|
||||
import { LGPD } from "./pages/LGPD";
|
||||
|
|
@ -47,8 +47,8 @@ const AppContent: React.FC = () => {
|
|||
case "request-event":
|
||||
return <Dashboard initialView="create" />;
|
||||
|
||||
case "uploads":
|
||||
return <Dashboard initialView="uploads" />;
|
||||
case "inspiration":
|
||||
return <InspirationPage />;
|
||||
|
||||
case "calendar":
|
||||
return <CalendarPage />;
|
||||
|
|
@ -62,9 +62,6 @@ const AppContent: React.FC = () => {
|
|||
case "settings":
|
||||
return <SettingsPage />;
|
||||
|
||||
case "albums":
|
||||
return <AlbumsPage />;
|
||||
|
||||
default:
|
||||
return <Dashboard initialView="list" />;
|
||||
}
|
||||
|
|
@ -91,10 +88,10 @@ const AppContent: React.FC = () => {
|
|||
<div>
|
||||
<h4 className="font-bold text-brand-black mb-4 md:mb-6 uppercase tracking-wider text-sm sm:text-base md:text-lg">Serviços</h4>
|
||||
<ul className="space-y-2 md:space-y-3 text-brand-black/70 text-sm sm:text-base md:text-lg">
|
||||
<li>Fotografia de Eventos</li>
|
||||
<li>Álbuns Personalizados</li>
|
||||
<li>Gestão de Formaturas</li>
|
||||
<li>Cobertura Completa</li>
|
||||
<li>Fotografia de Formatura</li>
|
||||
<li>Baile de Gala</li>
|
||||
<li>Cerimônia de Colação</li>
|
||||
<li>Ensaios de Turma</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
72
MAPBOX_SETUP.md
Normal file
72
MAPBOX_SETUP.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# 🗺️ Configuração do Mapbox
|
||||
|
||||
## Token do Mapbox Inválido
|
||||
|
||||
O sistema está configurado para usar a API do Mapbox, mas o token atual é inválido ou expirado.
|
||||
|
||||
## Como Obter um Token Válido (GRATUITO)
|
||||
|
||||
### 1. Crie uma conta no Mapbox
|
||||
- Acesse: **https://account.mapbox.com/**
|
||||
- Clique em **"Sign up"** (ou faça login se já tiver conta)
|
||||
- É **100% gratuito** para até 50.000 requisições/mês
|
||||
|
||||
### 2. Acesse a página de Tokens
|
||||
- Após fazer login, vá para: **https://account.mapbox.com/access-tokens/**
|
||||
- Ou clique no menu em **"Access tokens"**
|
||||
|
||||
### 3. Crie um novo token
|
||||
- Clique em **"Create a token"**
|
||||
- Dê um nome (ex: "Photum Forms")
|
||||
- Selecione os escopos necessários (deixe os padrões marcados)
|
||||
- Clique em **"Create token"**
|
||||
|
||||
### 4. Copie o token
|
||||
- O token será algo como: `pk.eyJ1IjoiZXhhbXBsZSIsImEiOiJja...`
|
||||
- **COPIE O TOKEN COMPLETO**
|
||||
|
||||
### 5. Configure no Projeto
|
||||
|
||||
Abra o arquivo **`services/mapboxService.ts`** e substitua o token na linha 26:
|
||||
|
||||
```typescript
|
||||
const MAPBOX_TOKEN = 'SEU_TOKEN_AQUI'; // Cole o token do Mapbox aqui
|
||||
```
|
||||
|
||||
### 6. Salve e recarregue
|
||||
|
||||
Após salvar o arquivo, o Vite recarregará automaticamente e o mapa funcionará!
|
||||
|
||||
---
|
||||
|
||||
## Verificação
|
||||
|
||||
Se tudo estiver correto, você verá:
|
||||
- ✅ "Inicializando mapa Mapbox..." no console
|
||||
- ✅ "Mapa criado com sucesso" no console
|
||||
- ✅ Mapa interativo carregado na tela de criação de eventos
|
||||
|
||||
Se houver erro:
|
||||
- ❌ Verifique se copiou o token completo (incluindo `pk.`)
|
||||
- ❌ Verifique se não há espaços extras antes/depois do token
|
||||
- ❌ Certifique-se de que o token não expirou
|
||||
|
||||
---
|
||||
|
||||
## Recursos do Mapbox no Sistema
|
||||
|
||||
- 🔍 Busca de endereços com autocomplete
|
||||
- 📍 Mapa interativo com pin arrastável
|
||||
- 🌍 Geocoding e Reverse Geocoding
|
||||
- 🗺️ Integração com Google Maps para compartilhamento
|
||||
|
||||
## Limites Gratuitos
|
||||
|
||||
O plano gratuito do Mapbox inclui:
|
||||
- 50.000 requisições de geocoding/mês
|
||||
- 50.000 carregamentos de mapa/mês
|
||||
- Mais que suficiente para este projeto!
|
||||
|
||||
---
|
||||
|
||||
**Precisa de ajuda?** Acesse a documentação: https://docs.mapbox.com/api/
|
||||
|
|
@ -4,11 +4,12 @@ import { EventType, EventStatus, Address } from '../types';
|
|||
import { Input, Select } from './Input';
|
||||
import { Button } from './Button';
|
||||
import { MapPin, Upload, Plus, X, Check, FileText, ExternalLink, Search, CheckCircle, Building2, AlertCircle } from 'lucide-react';
|
||||
import { searchLocationWithGemini, GeoResult } from '../services/genaiService';
|
||||
import { searchMapboxLocation, MapboxResult, reverseGeocode } from '../services/mapboxService';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useData } from '../contexts/DataContext';
|
||||
import { UserRole } from '../types';
|
||||
import { InstitutionForm } from './InstitutionForm';
|
||||
import { MapboxMap } from './MapboxMap';
|
||||
|
||||
interface EventFormProps {
|
||||
onCancel: () => void;
|
||||
|
|
@ -21,8 +22,9 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
|
|||
const { institutions, getInstitutionsByUserId, addInstitution } = useData();
|
||||
const [activeTab, setActiveTab] = useState<'details' | 'location' | 'briefing' | 'files'>('details');
|
||||
const [addressQuery, setAddressQuery] = useState('');
|
||||
const [addressResults, setAddressResults] = useState<GeoResult[]>([]);
|
||||
const [addressResults, setAddressResults] = useState<MapboxResult[]>([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [isGeocoding, setIsGeocoding] = useState(false);
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const [showInstitutionForm, setShowInstitutionForm] = useState(false);
|
||||
|
||||
|
|
@ -41,7 +43,16 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
|
|||
time: '',
|
||||
type: '',
|
||||
status: EventStatus.PLANNING,
|
||||
address: { street: '', number: '', city: '', state: '', zip: '', mapLink: '' } as Address,
|
||||
address: {
|
||||
street: '',
|
||||
number: '',
|
||||
city: '',
|
||||
state: '',
|
||||
zip: '',
|
||||
lat: -23.5505,
|
||||
lng: -46.6333,
|
||||
mapLink: ''
|
||||
} as Address,
|
||||
briefing: '',
|
||||
contacts: [{ name: '', role: '', phone: '' }],
|
||||
files: [] as File[],
|
||||
|
|
@ -55,22 +66,22 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
|
|||
: (isClientRequest ? "Solicitar Orçamento/Evento" : "Cadastrar Novo Evento");
|
||||
const submitLabel = initialData ? "Salvar Alterações" : (isClientRequest ? "Enviar Solicitação" : "Criar Evento");
|
||||
|
||||
// Address Autocomplete Logic using Gemini
|
||||
// Address Autocomplete Logic using Mapbox
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(async () => {
|
||||
if (addressQuery.length > 4) { // Increased threshold to avoid spamming API
|
||||
if (addressQuery.length > 3) {
|
||||
setIsSearching(true);
|
||||
const results = await searchLocationWithGemini(addressQuery);
|
||||
const results = await searchMapboxLocation(addressQuery);
|
||||
setAddressResults(results);
|
||||
setIsSearching(false);
|
||||
} else {
|
||||
setAddressResults([]);
|
||||
}
|
||||
}, 800); // Increased debounce for API efficiency
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, [addressQuery]);
|
||||
|
||||
const handleAddressSelect = (addr: GeoResult) => {
|
||||
const handleAddressSelect = (addr: MapboxResult) => {
|
||||
setFormData((prev: any) => ({
|
||||
...prev,
|
||||
address: {
|
||||
|
|
@ -79,6 +90,8 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
|
|||
city: addr.city,
|
||||
state: addr.state,
|
||||
zip: addr.zip,
|
||||
lat: addr.lat,
|
||||
lng: addr.lng,
|
||||
mapLink: addr.mapLink
|
||||
}
|
||||
}));
|
||||
|
|
@ -86,6 +99,72 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
|
|||
setAddressResults([]);
|
||||
};
|
||||
|
||||
const handleMapLocationChange = async (lat: number, lng: number) => {
|
||||
// Buscar endereço baseado nas coordenadas
|
||||
const addressData = await reverseGeocode(lat, lng);
|
||||
|
||||
if (addressData) {
|
||||
setFormData((prev: any) => ({
|
||||
...prev,
|
||||
address: {
|
||||
street: addressData.street,
|
||||
number: addressData.number,
|
||||
city: addressData.city,
|
||||
state: addressData.state,
|
||||
zip: addressData.zip,
|
||||
lat: addressData.lat,
|
||||
lng: addressData.lng,
|
||||
mapLink: addressData.mapLink
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
// Se não conseguir o endereço, atualiza apenas as coordenadas
|
||||
setFormData((prev: any) => ({
|
||||
...prev,
|
||||
address: {
|
||||
...prev.address,
|
||||
lat,
|
||||
lng,
|
||||
mapLink: `https://www.google.com/maps/search/?api=1&query=${lat},${lng}`
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Geocoding quando o usuário digita o endereço manualmente
|
||||
const handleManualAddressChange = async () => {
|
||||
const { street, number, city, state } = formData.address;
|
||||
|
||||
// Montar query de busca
|
||||
const query = `${street} ${number}, ${city}, ${state}`.trim();
|
||||
|
||||
if (query.length < 5) return; // Endereço muito curto
|
||||
|
||||
setIsGeocoding(true);
|
||||
|
||||
try {
|
||||
const results = await searchMapboxLocation(query);
|
||||
|
||||
if (results.length > 0) {
|
||||
const firstResult = results[0];
|
||||
|
||||
setFormData((prev: any) => ({
|
||||
...prev,
|
||||
address: {
|
||||
...prev.address,
|
||||
lat: firstResult.lat,
|
||||
lng: firstResult.lng,
|
||||
mapLink: firstResult.mapLink
|
||||
}
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao geocodificar endereço manual:', error);
|
||||
} finally {
|
||||
setIsGeocoding(false);
|
||||
}
|
||||
};
|
||||
|
||||
const addContact = () => {
|
||||
setFormData((prev: any) => ({
|
||||
...prev,
|
||||
|
|
@ -351,7 +430,7 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
|
|||
<div className="space-y-6 fade-in">
|
||||
<div className="relative">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
|
||||
Busca Google Maps (Powered by Gemini)
|
||||
Busca de Endereço (Powered by Mapbox)
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
|
|
@ -400,30 +479,76 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
|
|||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-2">
|
||||
<Input label="Rua" value={formData.address.street} readOnly />
|
||||
<Input
|
||||
label="Rua"
|
||||
value={formData.address.street}
|
||||
onChange={(e) => {
|
||||
setFormData({ ...formData, address: { ...formData.address, street: e.target.value } });
|
||||
}}
|
||||
onBlur={handleManualAddressChange}
|
||||
placeholder="Digite o nome da rua"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
label="Número"
|
||||
placeholder="123"
|
||||
value={formData.address.number}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.replace(/\D/g, '');
|
||||
const value = e.target.value;
|
||||
setFormData({ ...formData, address: { ...formData.address, number: value } });
|
||||
}}
|
||||
onBlur={handleManualAddressChange}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input label="Cidade" value={formData.address.city} readOnly />
|
||||
<Input label="Estado" value={formData.address.state} readOnly />
|
||||
<Input
|
||||
label="Cidade"
|
||||
value={formData.address.city}
|
||||
onChange={(e) => {
|
||||
setFormData({ ...formData, address: { ...formData.address, city: e.target.value } });
|
||||
}}
|
||||
onBlur={handleManualAddressChange}
|
||||
placeholder="Digite a cidade"
|
||||
/>
|
||||
<Input
|
||||
label="Estado"
|
||||
value={formData.address.state}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.toUpperCase().slice(0, 2);
|
||||
setFormData({ ...formData, address: { ...formData.address, state: value } });
|
||||
}}
|
||||
onBlur={handleManualAddressChange}
|
||||
placeholder="SP"
|
||||
maxLength={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mapa Interativo */}
|
||||
<div className="mt-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3 tracking-wide uppercase text-xs flex items-center justify-between">
|
||||
<span>📍 Mapa Interativo - Ajuste a Localização Exata</span>
|
||||
{isGeocoding && (
|
||||
<span className="text-xs text-brand-gold flex items-center normal-case">
|
||||
<div className="animate-spin h-3 w-3 border-2 border-brand-gold rounded-full border-t-transparent mr-2"></div>
|
||||
Localizando no mapa...
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
<MapboxMap
|
||||
initialLat={formData.address.lat || -23.5505}
|
||||
initialLng={formData.address.lng || -46.6333}
|
||||
onLocationChange={handleMapLocationChange}
|
||||
height="450px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{formData.address.mapLink && (
|
||||
<div className="bg-gray-50 p-3 rounded border border-gray-200 flex items-center justify-between">
|
||||
<span className="text-xs text-gray-500 flex items-center">
|
||||
<Check size={14} className="mr-1 text-green-500" />
|
||||
Localização verificada via Google Maps
|
||||
Localização verificada via Mapbox
|
||||
</span>
|
||||
<a href={formData.address.mapLink} target="_blank" rel="noreferrer" className="text-xs text-brand-gold flex items-center hover:underline">
|
||||
Ver no mapa <ExternalLink size={12} className="ml-1" />
|
||||
|
|
|
|||
195
components/MapboxMap.tsx
Normal file
195
components/MapboxMap.tsx
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import { MapPin, Target } from 'lucide-react';
|
||||
|
||||
interface MapboxMapProps {
|
||||
initialLat?: number;
|
||||
initialLng?: number;
|
||||
onLocationChange?: (lat: number, lng: number) => void;
|
||||
height?: string;
|
||||
}
|
||||
|
||||
export const MapboxMap: React.FC<MapboxMapProps> = ({
|
||||
initialLat = -23.5505, // São Paulo como padrão
|
||||
initialLng = -46.6333,
|
||||
onLocationChange,
|
||||
height = '400px'
|
||||
}) => {
|
||||
const mapContainer = useRef<HTMLDivElement>(null);
|
||||
const map = useRef<mapboxgl.Map | null>(null);
|
||||
const marker = useRef<mapboxgl.Marker | null>(null);
|
||||
const [currentLat, setCurrentLat] = useState(initialLat);
|
||||
const [currentLng, setCurrentLng] = useState(initialLng);
|
||||
const [mapLoaded, setMapLoaded] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapContainer.current || map.current) return;
|
||||
|
||||
try {
|
||||
console.log('🗺️ Inicializando mapa Mapbox...');
|
||||
// Configurar token
|
||||
const token = import.meta.env.VITE_MAPBOX_TOKEN;
|
||||
if (!token) {
|
||||
setError('❌ Token do Mapbox não encontrado. Configure VITE_MAPBOX_TOKEN no arquivo .env.local');
|
||||
return;
|
||||
}
|
||||
mapboxgl.accessToken = token;
|
||||
|
||||
// Inicializar mapa
|
||||
map.current = new mapboxgl.Map({
|
||||
container: mapContainer.current,
|
||||
style: 'mapbox://styles/mapbox/streets-v12',
|
||||
center: [initialLng, initialLat],
|
||||
zoom: 15
|
||||
});
|
||||
|
||||
console.log('✅ Mapa criado com sucesso');
|
||||
|
||||
// Adicionar controles de navegação
|
||||
map.current.addControl(new mapboxgl.NavigationControl(), 'top-right');
|
||||
|
||||
// Adicionar controle de localização
|
||||
map.current.addControl(
|
||||
new mapboxgl.GeolocateControl({
|
||||
positionOptions: {
|
||||
enableHighAccuracy: true
|
||||
},
|
||||
trackUserLocation: true,
|
||||
showUserHeading: true
|
||||
}),
|
||||
'top-right'
|
||||
);
|
||||
|
||||
// Criar marcador arrastável
|
||||
marker.current = new mapboxgl.Marker({
|
||||
color: '#c5a059',
|
||||
draggable: true
|
||||
})
|
||||
.setLngLat([initialLng, initialLat])
|
||||
.addTo(map.current);
|
||||
|
||||
// Evento quando o marcador é arrastado
|
||||
marker.current.on('dragend', () => {
|
||||
if (marker.current) {
|
||||
const lngLat = marker.current.getLngLat();
|
||||
setCurrentLat(lngLat.lat);
|
||||
setCurrentLng(lngLat.lng);
|
||||
if (onLocationChange) {
|
||||
onLocationChange(lngLat.lat, lngLat.lng);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Evento de clique no mapa para mover o marcador
|
||||
map.current.on('click', (e) => {
|
||||
if (marker.current) {
|
||||
marker.current.setLngLat([e.lngLat.lng, e.lngLat.lat]);
|
||||
setCurrentLat(e.lngLat.lat);
|
||||
setCurrentLng(e.lngLat.lng);
|
||||
if (onLocationChange) {
|
||||
onLocationChange(e.lngLat.lat, e.lngLat.lng);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
map.current.on('load', () => {
|
||||
setMapLoaded(true);
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Erro ao inicializar mapa:', error);
|
||||
const errorMsg = error?.message || String(error);
|
||||
if (errorMsg.includes('token') || errorMsg.includes('Unauthorized') || errorMsg.includes('401')) {
|
||||
setError('❌ Token do Mapbox inválido. Obtenha um token gratuito em https://account.mapbox.com/ e configure em services/mapboxService.ts');
|
||||
} else {
|
||||
setError(`Erro ao carregar o mapa: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
if (marker.current) {
|
||||
marker.current.remove();
|
||||
}
|
||||
if (map.current) {
|
||||
map.current.remove();
|
||||
map.current = null;
|
||||
}
|
||||
};
|
||||
}, []); // Executar apenas uma vez
|
||||
|
||||
// Atualizar posição do marcador quando as coordenadas mudarem externamente
|
||||
useEffect(() => {
|
||||
if (marker.current && map.current && mapLoaded) {
|
||||
marker.current.setLngLat([initialLng, initialLat]);
|
||||
map.current.flyTo({
|
||||
center: [initialLng, initialLat],
|
||||
zoom: 15,
|
||||
duration: 1500
|
||||
});
|
||||
setCurrentLat(initialLat);
|
||||
setCurrentLng(initialLng);
|
||||
}
|
||||
}, [initialLat, initialLng, mapLoaded]);
|
||||
|
||||
const centerOnMarker = () => {
|
||||
if (map.current && marker.current) {
|
||||
const lngLat = marker.current.getLngLat();
|
||||
map.current.flyTo({
|
||||
center: [lngLat.lng, lngLat.lat],
|
||||
zoom: 17,
|
||||
duration: 1000
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{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">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={mapContainer}
|
||||
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>
|
||||
|
||||
{/* Botão de centralizar - Responsivo */}
|
||||
<button
|
||||
onClick={centerOnMarker}
|
||||
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"
|
||||
title="Centralizar no marcador"
|
||||
>
|
||||
<Target size={18} className="text-gray-600 group-hover:text-brand-gold transition-colors sm:w-5 sm:h-5" />
|
||||
</button>
|
||||
|
||||
{/* Instruções - Responsivo */}
|
||||
<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">
|
||||
<p className="font-medium mb-1 text-xs sm:text-sm">💡 Como usar:</p>
|
||||
<ul className="text-[11px] sm:text-xs space-y-0.5 sm:space-y-1 text-blue-700">
|
||||
<li className="flex items-start"><span className="mr-1">•</span><span><strong>Arraste o marcador</strong> para a posição exata</span></li>
|
||||
<li className="flex items-start"><span className="mr-1">•</span><span><strong>Clique no mapa</strong> para mover o marcador</span></li>
|
||||
<li className="flex items-start hidden sm:flex"><span className="mr-1">•</span><span>Use os <strong>controles</strong> para zoom e navegação</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -39,13 +39,11 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
|||
case UserRole.EVENT_OWNER:
|
||||
return [
|
||||
{ name: 'Meus Eventos', path: 'dashboard' },
|
||||
{ name: 'Solicitar Evento', path: 'request-event' },
|
||||
{ name: 'Álbuns Entregues', path: 'albums' }
|
||||
{ name: 'Solicitar Evento', path: 'request-event' }
|
||||
];
|
||||
case UserRole.PHOTOGRAPHER:
|
||||
return [
|
||||
{ name: 'Eventos Designados', path: 'dashboard' },
|
||||
{ name: 'Meus Uploads', path: 'uploads' },
|
||||
{ name: 'Agenda', path: 'calendar' }
|
||||
];
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
||||
import { EventData, EventStatus, EventType, Attachment, Institution } from '../types';
|
||||
import { EventData, EventStatus, EventType, Institution } from '../types';
|
||||
|
||||
// Initial Mock Data
|
||||
const INITIAL_INSTITUTIONS: Institution[] = [
|
||||
|
|
@ -41,10 +41,6 @@ const INITIAL_EVENTS: EventData[] = [
|
|||
coverImage: 'https://picsum.photos/id/1059/800/400',
|
||||
contacts: [{ id: 'c1', name: 'Cerimonial Silva', role: 'Cerimonialista', phone: '9999-9999', email: 'c@teste.com'}],
|
||||
checklist: [],
|
||||
attachments: [
|
||||
{ name: 'Ensaio 1', size: '2mb', type: 'image/jpeg', url: 'https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=400&q=80' },
|
||||
{ name: 'Ensaio 2', size: '2mb', type: 'image/jpeg', url: 'https://images.unsplash.com/photo-1511285560982-1351cdeb9821?auto=format&fit=crop&w=400&q=80' }
|
||||
],
|
||||
ownerId: 'client-1',
|
||||
photographerIds: ['photographer-1'],
|
||||
institutionId: 'inst-1'
|
||||
|
|
@ -67,7 +63,6 @@ const INITIAL_EVENTS: EventData[] = [
|
|||
coverImage: 'https://picsum.photos/id/3/800/400',
|
||||
contacts: [],
|
||||
checklist: [],
|
||||
attachments: [],
|
||||
ownerId: 'client-2', // Other client
|
||||
photographerIds: []
|
||||
}
|
||||
|
|
@ -80,7 +75,6 @@ interface DataContextType {
|
|||
updateEventStatus: (id: string, status: EventStatus) => void;
|
||||
assignPhotographer: (eventId: string, photographerId: string) => void;
|
||||
getEventsByRole: (userId: string, role: string) => EventData[];
|
||||
addAttachment: (eventId: string, attachment: Attachment) => void;
|
||||
addInstitution: (institution: Institution) => void;
|
||||
updateInstitution: (id: string, institution: Partial<Institution>) => void;
|
||||
getInstitutionsByUserId: (userId: string) => Institution[];
|
||||
|
|
@ -126,15 +120,6 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||
return [];
|
||||
};
|
||||
|
||||
const addAttachment = (eventId: string, attachment: Attachment) => {
|
||||
setEvents(prev => prev.map(e => {
|
||||
if (e.id === eventId) {
|
||||
return { ...e, attachments: [...e.attachments, attachment] };
|
||||
}
|
||||
return e;
|
||||
}));
|
||||
};
|
||||
|
||||
const addInstitution = (institution: Institution) => {
|
||||
setInstitutions(prev => [...prev, institution]);
|
||||
};
|
||||
|
|
@ -161,7 +146,6 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||
updateEventStatus,
|
||||
assignPhotographer,
|
||||
getEventsByRole,
|
||||
addAttachment,
|
||||
addInstitution,
|
||||
updateInstitution,
|
||||
getInstitutionsByUserId,
|
||||
|
|
|
|||
45
index.html
45
index.html
|
|
@ -4,7 +4,11 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PhotumFormaturas - Gestão de Eventos</title>
|
||||
<title>PhotumFormaturas</title>
|
||||
|
||||
<!-- Mapbox GL CSS (versão CDN confiável) -->
|
||||
<link href='https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.css' rel='stylesheet' />
|
||||
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,400;0,600;1,400&display=swap"
|
||||
|
|
@ -180,6 +184,45 @@
|
|||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mapbox custom styles */
|
||||
.mapboxgl-ctrl-group {
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15) !important;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-group button {
|
||||
width: 36px !important;
|
||||
height: 36px !important;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-geolocate {
|
||||
background-color: white !important;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate .mapboxgl-ctrl-icon {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%23333' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2v20M2 12h20'/%3E%3Ccircle cx='12' cy='12' r='4'/%3E%3C/svg%3E") !important;
|
||||
}
|
||||
|
||||
.custom-marker {
|
||||
cursor: grab !important;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.custom-marker:active {
|
||||
cursor: grabbing !important;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.mapboxgl-popup {
|
||||
max-width: 300px !important;
|
||||
}
|
||||
|
||||
.mapboxgl-popup-content {
|
||||
border-radius: 8px !important;
|
||||
padding: 15px !important;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
|
|
|||
278
package-lock.json
generated
278
package-lock.json
generated
|
|
@ -9,7 +9,9 @@
|
|||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.30.0",
|
||||
"@types/mapbox-gl": "^3.4.1",
|
||||
"lucide-react": "^0.554.0",
|
||||
"mapbox-gl": "^3.16.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.9.6"
|
||||
|
|
@ -834,6 +836,58 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/jsonlint-lines-primitives": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
|
||||
"integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/mapbox-gl-supported": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-3.0.0.tgz",
|
||||
"integrity": "sha512-2XghOwu16ZwPJLOFVuIOaLbN0iKMn867evzXFyf0P22dqugezfJwLmdanAgU25ITvz1TvOfVP4jsDImlDJzcWg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@mapbox/point-geometry": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz",
|
||||
"integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@mapbox/tiny-sdf": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz",
|
||||
"integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@mapbox/unitbezier": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
|
||||
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@mapbox/vector-tile": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz",
|
||||
"integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "~1.1.0",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"pbf": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/whoots-js": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
|
||||
"integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
|
|
@ -1211,6 +1265,36 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/geojson-vt": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz",
|
||||
"integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mapbox__point-geometry": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
|
||||
"integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mapbox-gl": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-3.4.1.tgz",
|
||||
"integrity": "sha512-NsGKKtgW93B+UaLPti6B7NwlxYlES5DpV5Gzj9F75rK5ALKsqSk15CiEHbOnTr09RGbr6ZYiCdI+59NNNcAImg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
|
||||
|
|
@ -1222,6 +1306,21 @@
|
|||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pbf": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
|
||||
"integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/supercluster": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
|
||||
"integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz",
|
||||
|
|
@ -1392,6 +1491,12 @@
|
|||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/cheap-ruler": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cheap-ruler/-/cheap-ruler-4.0.0.tgz",
|
||||
"integrity": "sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
|
@ -1444,6 +1549,12 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/csscolorparser": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz",
|
||||
"integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
|
|
@ -1470,6 +1581,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/earcut": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz",
|
||||
"integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
|
|
@ -1679,6 +1796,18 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/geojson-vt": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz",
|
||||
"integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/gl-matrix": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
|
||||
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
|
|
@ -1726,6 +1855,12 @@
|
|||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/grid-index": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz",
|
||||
"integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/gtoken": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
|
||||
|
|
@ -1845,6 +1980,12 @@
|
|||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/kdbush": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
|
||||
"integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
|
|
@ -1864,6 +2005,62 @@
|
|||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mapbox-gl": {
|
||||
"version": "3.16.0",
|
||||
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.16.0.tgz",
|
||||
"integrity": "sha512-rluV1Zp/0oHf1Y9BV+nePRNnKyTdljko3E19CzO5rBqtQaNUYS0ePCMPRtxOuWRwSdKp3f9NWJkOCjemM8nmjw==",
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"workspaces": [
|
||||
"src/style-spec",
|
||||
"test/build/typings"
|
||||
],
|
||||
"dependencies": {
|
||||
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
|
||||
"@mapbox/mapbox-gl-supported": "^3.0.0",
|
||||
"@mapbox/point-geometry": "^1.1.0",
|
||||
"@mapbox/tiny-sdf": "^2.0.6",
|
||||
"@mapbox/unitbezier": "^0.0.1",
|
||||
"@mapbox/vector-tile": "^2.0.4",
|
||||
"@mapbox/whoots-js": "^3.1.0",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"@types/geojson-vt": "^3.2.5",
|
||||
"@types/mapbox__point-geometry": "^0.1.4",
|
||||
"@types/pbf": "^3.0.5",
|
||||
"@types/supercluster": "^7.1.3",
|
||||
"cheap-ruler": "^4.0.0",
|
||||
"csscolorparser": "~1.0.3",
|
||||
"earcut": "^3.0.1",
|
||||
"geojson-vt": "^4.0.2",
|
||||
"gl-matrix": "^3.4.4",
|
||||
"grid-index": "^1.1.0",
|
||||
"kdbush": "^4.0.2",
|
||||
"martinez-polygon-clipping": "^0.7.4",
|
||||
"murmurhash-js": "^1.0.0",
|
||||
"pbf": "^4.0.1",
|
||||
"potpack": "^2.0.0",
|
||||
"quickselect": "^3.0.0",
|
||||
"serialize-to-js": "^3.1.2",
|
||||
"supercluster": "^8.0.1",
|
||||
"tinyqueue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/martinez-polygon-clipping": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/martinez-polygon-clipping/-/martinez-polygon-clipping-0.7.4.tgz",
|
||||
"integrity": "sha512-jBEwrKtA0jTagUZj2bnmb4Yg2s4KnJGRePStgI7bAVjtcipKiF39R4LZ2V/UT61jMYWrTcBhPazexeqd6JAVtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"robust-predicates": "^2.0.4",
|
||||
"splaytree": "^0.1.4",
|
||||
"tinyqueue": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/martinez-polygon-clipping/node_modules/tinyqueue": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-1.2.3.tgz",
|
||||
"integrity": "sha512-Qz9RgWuO9l8lT+Y9xvbzhPT2efIUIFd69N7eF7tJ9lnQl0iLj1M7peK7IoUGZL9DJHw9XftqLreccfxcQgYLxA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
|
|
@ -1894,6 +2091,12 @@
|
|||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/murmurhash-js": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
|
||||
"integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
|
|
@ -1995,6 +2198,18 @@
|
|||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/pbf": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz",
|
||||
"integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"resolve-protobuf-schema": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"pbf": "bin/pbf"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
|
@ -2045,6 +2260,24 @@
|
|||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/potpack": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz",
|
||||
"integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/protocol-buffers-schema": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
|
||||
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/quickselect": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
|
||||
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
|
|
@ -2116,6 +2349,15 @@
|
|||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-protobuf-schema": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
|
||||
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"protocol-buffers-schema": "^3.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "5.0.10",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
|
||||
|
|
@ -2131,6 +2373,12 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/robust-predicates": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-2.0.4.tgz",
|
||||
"integrity": "sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
|
||||
|
|
@ -2209,6 +2457,15 @@
|
|||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/serialize-to-js": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/serialize-to-js/-/serialize-to-js-3.1.2.tgz",
|
||||
"integrity": "sha512-owllqNuDDEimQat7EPG0tH7JjO090xKNzUtYz6X+Sk2BXDnOCilDdNLwjWeFywG9xkJul1ULvtUQa9O4pUaY0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||
|
|
@ -2258,6 +2515,12 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/splaytree": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/splaytree/-/splaytree-0.1.4.tgz",
|
||||
"integrity": "sha512-D50hKrjZgBzqD3FT2Ek53f2dcDLAQT8SSGrzj3vidNH5ISRgceeGVJ2dQIthKOuayqFXfFjXheHNo4bbt9LhRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
|
|
@ -2354,6 +2617,15 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supercluster": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
|
||||
"integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"kdbush": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
|
|
@ -2371,6 +2643,12 @@
|
|||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyqueue": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
|
||||
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.30.0",
|
||||
"@types/mapbox-gl": "^3.4.1",
|
||||
"lucide-react": "^0.554.0",
|
||||
"mapbox-gl": "^3.16.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.9.6"
|
||||
|
|
|
|||
190
pages/Albums.tsx
190
pages/Albums.tsx
|
|
@ -1,190 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Search, Filter, Calendar, MapPin, Image as ImageIcon, ExternalLink, Download, Share2 } from 'lucide-react';
|
||||
import { Button } from '../components/Button';
|
||||
|
||||
interface Album {
|
||||
id: string;
|
||||
eventName: string;
|
||||
clientName: string;
|
||||
date: string;
|
||||
coverImage: string;
|
||||
photoCount: number;
|
||||
size: string;
|
||||
status: 'delivered' | 'archived';
|
||||
link: string;
|
||||
}
|
||||
|
||||
const MOCK_ALBUMS: Album[] = [
|
||||
{
|
||||
id: '1',
|
||||
eventName: 'Casamento Juliana & Marcos',
|
||||
clientName: 'Juliana Noiva',
|
||||
date: '2024-10-15',
|
||||
coverImage: 'https://images.unsplash.com/photo-1511795409834-ef04bbd61622?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=80',
|
||||
photoCount: 450,
|
||||
size: '2.4 GB',
|
||||
status: 'delivered',
|
||||
link: '#'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
eventName: 'Formatura Medicina UFPR',
|
||||
clientName: 'Comissão de Formatura',
|
||||
date: '2024-09-20',
|
||||
coverImage: 'https://images.unsplash.com/photo-1523580494863-6f3031224c94?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=80',
|
||||
photoCount: 1200,
|
||||
size: '8.5 GB',
|
||||
status: 'delivered',
|
||||
link: '#'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
eventName: 'Aniversário 15 Anos Sofia',
|
||||
clientName: 'Ana Paula (Mãe)',
|
||||
date: '2024-08-05',
|
||||
coverImage: 'https://images.unsplash.com/photo-1530103862676-de3c9a59af57?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=80',
|
||||
photoCount: 320,
|
||||
size: '1.8 GB',
|
||||
status: 'archived',
|
||||
link: '#'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
eventName: 'Evento Corporativo TechSummit',
|
||||
clientName: 'Tech Solutions Inc.',
|
||||
date: '2024-11-01',
|
||||
coverImage: 'https://images.unsplash.com/photo-1515187029135-18ee286d815b?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=80',
|
||||
photoCount: 580,
|
||||
size: '3.1 GB',
|
||||
status: 'delivered',
|
||||
link: '#'
|
||||
}
|
||||
];
|
||||
|
||||
export const AlbumsPage: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [filter, setFilter] = useState<'all' | 'delivered' | 'archived'>('all');
|
||||
|
||||
const filteredAlbums = MOCK_ALBUMS.filter(album => {
|
||||
const matchesSearch = album.eventName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
album.clientName.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesFilter = filter === 'all' || album.status === filter;
|
||||
return matchesSearch && matchesFilter;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 pt-24 pb-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-serif font-bold text-brand-black">Álbuns Entregues</h1>
|
||||
<p className="text-gray-500 mt-1">Gerencie e compartilhe os álbuns finalizados com seus clientes.</p>
|
||||
</div>
|
||||
{/* Placeholder for future actions if needed */}
|
||||
</div>
|
||||
|
||||
{/* Filters & Search */}
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-100 mb-8 flex flex-col md:flex-row gap-4 items-center justify-between">
|
||||
<div className="relative flex-1 w-full md:max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={20} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar por evento ou cliente..."
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 w-full md:w-auto overflow-x-auto pb-2 md:pb-0">
|
||||
<button
|
||||
onClick={() => setFilter('all')}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium whitespace-nowrap transition-colors ${filter === 'all' ? 'bg-brand-black text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
Todos
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('delivered')}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium whitespace-nowrap transition-colors ${filter === 'delivered' ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
Entregues
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('archived')}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium whitespace-nowrap transition-colors ${filter === 'archived' ? 'bg-gray-200 text-gray-700' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
Arquivados
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Albums Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{filteredAlbums.map((album) => (
|
||||
<div key={album.id} className="bg-white rounded-lg shadow-sm border border-gray-100 overflow-hidden hover:shadow-md transition-shadow group">
|
||||
<div className="relative h-48 overflow-hidden">
|
||||
<img
|
||||
src={album.coverImage}
|
||||
alt={album.eventName}
|
||||
className="w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-500"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-80"></div>
|
||||
<div className="absolute bottom-4 left-4 text-white">
|
||||
<h3 className="font-bold text-lg leading-tight mb-1">{album.eventName}</h3>
|
||||
<p className="text-xs opacity-90 flex items-center">
|
||||
<Calendar size={12} className="mr-1" /> {new Date(album.date).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="absolute top-4 right-4">
|
||||
<span className={`px-2 py-1 rounded text-xs font-bold uppercase tracking-wide ${album.status === 'delivered' ? 'bg-green-500 text-white' : 'bg-gray-500 text-white'
|
||||
}`}>
|
||||
{album.status === 'delivered' ? 'Entregue' : 'Arquivado'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide font-bold mb-1">Cliente</p>
|
||||
<p className="text-sm font-medium text-gray-800">{album.clientName}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide font-bold mb-1">Fotos</p>
|
||||
<p className="text-sm font-medium text-gray-800">{album.photoCount}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-4 border-t border-gray-100">
|
||||
<span className="text-xs text-gray-400 font-medium">{album.size}</span>
|
||||
<div className="flex gap-2">
|
||||
<button className="p-2 text-gray-400 hover:text-brand-gold hover:bg-yellow-50 rounded-full transition-colors" title="Download">
|
||||
<Download size={18} />
|
||||
</button>
|
||||
<button className="p-2 text-gray-400 hover:text-blue-500 hover:bg-blue-50 rounded-full transition-colors" title="Compartilhar Link">
|
||||
<Share2 size={18} />
|
||||
</button>
|
||||
<Button size="sm" variant="outline" className="ml-2">
|
||||
Ver Álbum <ExternalLink size={14} className="ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredAlbums.length === 0 && (
|
||||
<div className="text-center py-20 bg-white rounded-lg border border-dashed border-gray-200">
|
||||
<ImageIcon className="mx-auto h-12 w-12 text-gray-300 mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900">Nenhum álbum encontrado</h3>
|
||||
<p className="text-gray-500">Tente ajustar seus filtros de busca.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -4,19 +4,19 @@ import { UserRole, EventData, EventStatus, EventType } from '../types';
|
|||
import { EventCard } from '../components/EventCard';
|
||||
import { EventForm } from '../components/EventForm';
|
||||
import { Button } from '../components/Button';
|
||||
import { PlusCircle, Search, CheckCircle, Clock, Upload, Edit, Users, Map, Image as ImageIcon, Building2 } from 'lucide-react';
|
||||
import { PlusCircle, Search, CheckCircle, Clock, Edit, Users, Map, Building2 } from 'lucide-react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useData } from '../contexts/DataContext';
|
||||
import { STATUS_COLORS } from '../constants';
|
||||
|
||||
interface DashboardProps {
|
||||
initialView?: 'list' | 'create' | 'uploads';
|
||||
initialView?: 'list' | 'create';
|
||||
}
|
||||
|
||||
export const Dashboard: React.FC<DashboardProps> = ({ initialView = 'list' }) => {
|
||||
const { user } = useAuth();
|
||||
const { events, getEventsByRole, addEvent, updateEventStatus, assignPhotographer, addAttachment, getInstitutionById } = useData();
|
||||
const [view, setView] = useState<'list' | 'create' | 'edit' | 'details' | 'uploads'>(initialView);
|
||||
const { events, getEventsByRole, addEvent, updateEventStatus, assignPhotographer, getInstitutionById } = useData();
|
||||
const [view, setView] = useState<'list' | 'create' | 'edit' | 'details'>(initialView);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedEvent, setSelectedEvent] = useState<EventData | null>(null);
|
||||
const [activeFilter, setActiveFilter] = useState<string>('all');
|
||||
|
|
@ -58,7 +58,6 @@ export const Dashboard: React.FC<DashboardProps> = ({ initialView = 'list' }) =>
|
|||
id: Math.random().toString(36).substr(2, 9),
|
||||
status: initialStatus,
|
||||
checklist: [],
|
||||
attachments: [],
|
||||
ownerId: isClient ? user.id : 'unknown',
|
||||
photographerIds: []
|
||||
};
|
||||
|
|
@ -94,24 +93,6 @@ export const Dashboard: React.FC<DashboardProps> = ({ initialView = 'list' }) =>
|
|||
}
|
||||
};
|
||||
|
||||
const handleUploadPhoto = () => {
|
||||
if (!selectedEvent) return;
|
||||
// Mock Upload Action
|
||||
const newPhoto = {
|
||||
name: `Foto_${Date.now()}.jpg`,
|
||||
size: '3.5MB',
|
||||
type: 'image/jpeg',
|
||||
url: `https://picsum.photos/id/${Math.floor(Math.random() * 100)}/400/400`
|
||||
};
|
||||
addAttachment(selectedEvent.id, newPhoto);
|
||||
// Force refresh of selectedEvent state from context source
|
||||
const updated = events.find(e => e.id === selectedEvent.id);
|
||||
if (updated) {
|
||||
// manually inject the new attachment for immediate UI feedback if context isn't enough
|
||||
setSelectedEvent({...updated, attachments: [...updated.attachments, newPhoto]});
|
||||
}
|
||||
};
|
||||
|
||||
// --- RENDERS PER ROLE ---
|
||||
|
||||
const renderRoleSpecificHeader = () => {
|
||||
|
|
@ -127,7 +108,7 @@ export const Dashboard: React.FC<DashboardProps> = ({ initialView = 'list' }) =>
|
|||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-serif font-bold text-brand-black">Eventos Designados</h1>
|
||||
<p className="text-gray-500 mt-1">Gerencie seus trabalhos e realize uploads.</p>
|
||||
<p className="text-gray-500 mt-1">Gerencie seus trabalhos e visualize detalhes.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -276,11 +257,6 @@ export const Dashboard: React.FC<DashboardProps> = ({ initialView = 'list' }) =>
|
|||
<div className="col-span-2 space-y-8">
|
||||
{/* Actions Toolbar */}
|
||||
<div className="flex flex-wrap gap-3 border-b pb-4">
|
||||
{user.role === UserRole.PHOTOGRAPHER && (
|
||||
<Button onClick={() => setView('uploads')} className="flex items-center">
|
||||
<Upload size={16} className="mr-2" /> Gerenciar Uploads
|
||||
</Button>
|
||||
)}
|
||||
{(user.role === UserRole.BUSINESS_OWNER || user.role === UserRole.SUPERADMIN) && (
|
||||
<>
|
||||
<Button variant="outline" onClick={() => setView('edit')}>
|
||||
|
|
@ -414,94 +390,6 @@ export const Dashboard: React.FC<DashboardProps> = ({ initialView = 'list' }) =>
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{view === 'uploads' && (
|
||||
<div className="fade-in">
|
||||
{/* Check if user came from 'details' of a selected event OR came from Navbar */}
|
||||
{selectedEvent ? (
|
||||
<div>
|
||||
<Button variant="ghost" onClick={() => setView('details')} className="mb-4 pl-0">
|
||||
← Voltar para Detalhes
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-serif text-brand-black">Galeria de Evento: {selectedEvent.name}</h2>
|
||||
<p className="text-gray-500 text-sm">Gerencie as fotos e faça novos uploads.</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={() => setSelectedEvent(null)}>
|
||||
Trocar Evento
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Drag and Drop Area */}
|
||||
<div
|
||||
className="border-2 border-dashed border-gray-300 rounded-lg p-12 text-center bg-gray-50 hover:bg-gray-100 transition-colors cursor-pointer group mb-8"
|
||||
onClick={handleUploadPhoto}
|
||||
>
|
||||
<Upload size={48} className="mx-auto text-gray-400 mb-4 group-hover:text-brand-gold transition-colors" />
|
||||
<h3 className="text-xl font-medium text-gray-700 mb-2">Adicionar Novas Fotos</h3>
|
||||
<p className="text-gray-500">Clique aqui para simular o upload de uma nova imagem</p>
|
||||
</div>
|
||||
|
||||
{/* Gallery Grid */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-bold text-lg flex items-center">
|
||||
<ImageIcon className="mr-2 text-brand-gold" size={20}/>
|
||||
Fotos do Evento ({selectedEvent.attachments.filter(a => a.type.startsWith('image')).length})
|
||||
</h3>
|
||||
|
||||
{selectedEvent.attachments.length > 0 ? (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||
{selectedEvent.attachments.map((file, idx) => (
|
||||
<div key={idx} className="relative group aspect-square bg-gray-100 rounded overflow-hidden shadow-sm hover:shadow-md transition-all">
|
||||
{file.url ? (
|
||||
<img src={file.url} alt={file.name} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-gray-400">
|
||||
<ImageIcon size={32}/>
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-end p-2">
|
||||
<span className="text-white text-xs truncate w-full">{file.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-10 bg-white border rounded">
|
||||
<p className="text-gray-400">Nenhuma foto carregada ainda.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// Logic when clicking "Meus Uploads" in navbar: Select an Event first
|
||||
<div>
|
||||
<h2 className="text-2xl font-serif text-brand-black mb-6">Selecione um evento para gerenciar uploads</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{myEvents.map(event => (
|
||||
<div
|
||||
key={event.id}
|
||||
className="bg-white border hover:border-brand-gold rounded-lg p-6 cursor-pointer hover:shadow-lg transition-all"
|
||||
onClick={() => setSelectedEvent(event)}
|
||||
>
|
||||
<h3 className="font-bold text-lg mb-2">{event.name}</h3>
|
||||
<p className="text-gray-500 text-sm mb-4">{new Date(event.date).toLocaleDateString()}</p>
|
||||
<div className="flex items-center text-brand-gold text-sm font-medium">
|
||||
<ImageIcon size={16} className="mr-2"/>
|
||||
{event.attachments.length} arquivos
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{myEvents.length === 0 && (
|
||||
<p className="text-gray-500 col-span-3 text-center py-10">Você não possui eventos designados no momento.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
181
pages/Inspiration.tsx
Normal file
181
pages/Inspiration.tsx
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Heart, Search, Filter } from 'lucide-react';
|
||||
import { Construction } from 'lucide-react';
|
||||
|
||||
const MOCK_GALLERIES = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Formatura Medicina UNICAMP 2024',
|
||||
category: 'Medicina',
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1523050854058-8df90110c9f1?w=800',
|
||||
'https://images.unsplash.com/photo-1541339907198-e08756dedf3f?w=800',
|
||||
'https://images.unsplash.com/photo-1523240795612-9a054b0db644?w=800',
|
||||
],
|
||||
likes: 234,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Engenharia Civil - USP 2024',
|
||||
category: 'Engenharia',
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1513542789411-b6a5d4f31634?w=800',
|
||||
'https://images.unsplash.com/photo-1511632765486-a01980e01a18?w=800',
|
||||
'https://images.unsplash.com/photo-1523050854058-8df90110c9f1?w=800',
|
||||
],
|
||||
likes: 189,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Direito PUC 2023',
|
||||
category: 'Direito',
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1519389950473-47ba0277781c?w=800',
|
||||
'https://images.unsplash.com/photo-1521737711867-e3b97375f902?w=800',
|
||||
'https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=800',
|
||||
],
|
||||
likes: 312,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Arquitetura UNESP 2024',
|
||||
category: 'Arquitetura',
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1528605248644-14dd04022da1?w=800',
|
||||
'https://images.unsplash.com/photo-1523050854058-8df90110c9f1?w=800',
|
||||
'https://images.unsplash.com/photo-1519337265831-281ec6cc8514?w=800',
|
||||
],
|
||||
likes: 278,
|
||||
},
|
||||
];
|
||||
|
||||
const CATEGORIES = ['Todas', 'Medicina', 'Engenharia', 'Direito', 'Arquitetura', 'Administração'];
|
||||
|
||||
export const InspirationPage: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('Todas');
|
||||
|
||||
const filteredGalleries = MOCK_GALLERIES.filter((gallery) => {
|
||||
const matchesSearch = gallery.title.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesCategory = selectedCategory === 'Todas' || gallery.category === selectedCategory;
|
||||
return matchesSearch && matchesCategory;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 pt-24 pb-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-12 fade-in">
|
||||
<h1 className="text-4xl sm:text-5xl font-serif font-bold text-brand-black mb-4">
|
||||
Galeria de Inspiração
|
||||
</h1>
|
||||
<p className="text-gray-600 text-lg max-w-2xl mx-auto">
|
||||
Explore álbuns de formaturas anteriores e inspire-se para criar o seu evento perfeito
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search and Filter */}
|
||||
<div className="mb-8 space-y-4">
|
||||
{/* Search Bar */}
|
||||
<div className="relative max-w-2xl mx-auto">
|
||||
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar por curso, universidade..."
|
||||
className="w-full pl-12 pr-4 py-3 bg-white border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-gold focus:border-transparent text-sm shadow-sm"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Category Filter */}
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
{CATEGORIES.map((category) => (
|
||||
<button
|
||||
key={category}
|
||||
onClick={() => setSelectedCategory(category)}
|
||||
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
|
||||
selectedCategory === category
|
||||
? 'bg-brand-gold text-white shadow-lg scale-105'
|
||||
: 'bg-white text-gray-700 hover:bg-gray-50 border border-gray-200'
|
||||
}`}
|
||||
>
|
||||
{category}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Gallery Grid */}
|
||||
{filteredGalleries.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredGalleries.map((gallery) => (
|
||||
<div
|
||||
key={gallery.id}
|
||||
className="bg-white rounded-xl overflow-hidden shadow-md hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2 group"
|
||||
>
|
||||
{/* Main Image */}
|
||||
<div className="relative h-64 overflow-hidden">
|
||||
<img
|
||||
src={gallery.images[0]}
|
||||
alt={gallery.title}
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
|
||||
{/* Category Badge */}
|
||||
<div className="absolute top-3 left-3 bg-white/95 backdrop-blur-sm px-3 py-1 rounded-full">
|
||||
<span className="text-xs font-semibold text-brand-gold">{gallery.category}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-5">
|
||||
<h3 className="text-lg font-bold text-brand-black mb-2 line-clamp-1">
|
||||
{gallery.title}
|
||||
</h3>
|
||||
|
||||
{/* Thumbnail Preview */}
|
||||
<div className="flex gap-2 mb-3">
|
||||
{gallery.images.slice(1, 3).map((img, idx) => (
|
||||
<div key={idx} className="w-16 h-16 rounded-lg overflow-hidden border border-gray-200">
|
||||
<img src={img} alt="" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
))}
|
||||
<div className="w-16 h-16 rounded-lg bg-gray-100 flex items-center justify-center text-xs text-gray-500 font-medium border border-gray-200">
|
||||
+12
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between pt-3 border-t border-gray-100">
|
||||
<div className="flex items-center gap-2 text-gray-500">
|
||||
<Heart size={16} className="text-red-400" />
|
||||
<span className="text-sm font-medium">{gallery.likes} curtidas</span>
|
||||
</div>
|
||||
<button className="text-sm text-brand-gold font-semibold hover:underline">
|
||||
Ver álbum →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-16">
|
||||
<p className="text-gray-500 text-lg">Nenhuma galeria encontrada</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Coming Soon Banner */}
|
||||
<div className="mt-16 bg-gradient-to-r from-brand-purple to-brand-gold/20 rounded-2xl p-8 text-center">
|
||||
<Construction className="mx-auto text-white mb-4" size={48} />
|
||||
<h2 className="text-2xl font-bold text-white mb-2">Em Breve: Mais Funcionalidades</h2>
|
||||
<p className="text-white/90 max-w-2xl mx-auto">
|
||||
Estamos trabalhando para trazer mais galerias, filtros avançados e a possibilidade de salvar seus favoritos!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Users, Camera, Mail, Phone, MapPin, Star, Plus, Search, Filter, User } from 'lucide-react';
|
||||
import { Users, Camera, Mail, Phone, MapPin, Star, Plus, Search, Filter, User, Upload, X } from 'lucide-react';
|
||||
import { Button } from '../components/Button';
|
||||
|
||||
interface Photographer {
|
||||
|
|
@ -108,7 +108,27 @@ export const TeamPage: React.FC = () => {
|
|||
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) {
|
||||
|
|
@ -340,16 +360,67 @@ export const TeamPage: React.FC = () => {
|
|||
|
||||
<form className="space-y-6" onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
alert('Fotógrafo adicionado com sucesso!\n\n' + JSON.stringify(newPhotographer, null, 2));
|
||||
alert('Fotógrafo adicionado com sucesso!\n\n' + JSON.stringify({...newPhotographer, avatarFile: avatarFile?.name}, null, 2));
|
||||
setShowAddModal(false);
|
||||
setNewPhotographer({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
location: '',
|
||||
specialties: []
|
||||
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 *
|
||||
|
|
|
|||
149
services/mapboxService.ts
Normal file
149
services/mapboxService.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
// Mapbox Geocoding Service
|
||||
// Docs: https://docs.mapbox.com/api/search/geocoding/
|
||||
|
||||
export interface MapboxFeature {
|
||||
id: string;
|
||||
place_name: string;
|
||||
center: [number, number]; // [longitude, latitude]
|
||||
geometry: {
|
||||
coordinates: [number, number];
|
||||
};
|
||||
context?: Array<{
|
||||
id: string;
|
||||
text: string;
|
||||
short_code?: string;
|
||||
}>;
|
||||
place_type: string[];
|
||||
text: string;
|
||||
address?: string;
|
||||
}
|
||||
|
||||
export interface MapboxResult {
|
||||
description: string;
|
||||
street: string;
|
||||
number: string;
|
||||
city: string;
|
||||
state: string;
|
||||
zip: string;
|
||||
lat: number;
|
||||
lng: number;
|
||||
mapLink: string;
|
||||
}
|
||||
|
||||
// Token do Mapbox configurado no arquivo .env.local
|
||||
const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_TOKEN || '';
|
||||
|
||||
/**
|
||||
* Busca endereços usando a API de Geocoding do Mapbox
|
||||
* @param query - Texto de busca (endereço, local, etc)
|
||||
* @param country - Código do país (ex: 'br' para Brasil)
|
||||
*/
|
||||
export async function searchMapboxLocation(
|
||||
query: string,
|
||||
country: string = 'br'
|
||||
): Promise<MapboxResult[]> {
|
||||
if (!MAPBOX_TOKEN || MAPBOX_TOKEN.startsWith('YOUR_')) {
|
||||
console.warn('⚠️ Mapbox Token não configurado. Configure em services/mapboxService.ts');
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const encodedQuery = encodeURIComponent(query);
|
||||
const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodedQuery}.json?` +
|
||||
`access_token=${MAPBOX_TOKEN}&` +
|
||||
`country=${country}&` +
|
||||
`language=pt&` +
|
||||
`limit=5`;
|
||||
|
||||
console.log('🔍 Buscando endereço:', query);
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('❌ Erro na API Mapbox:', response.statusText);
|
||||
throw new Error(`Mapbox API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('✅ Resultados encontrados:', data.features?.length || 0);
|
||||
|
||||
if (!data.features || data.features.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.features.map((feature: MapboxFeature) => {
|
||||
// Extrair informações do contexto
|
||||
const context = feature.context || [];
|
||||
const place = context.find(c => c.id.startsWith('place'));
|
||||
const region = context.find(c => c.id.startsWith('region'));
|
||||
const postcode = context.find(c => c.id.startsWith('postcode'));
|
||||
|
||||
// Extrair número do endereço
|
||||
const addressMatch = feature.address || feature.text.match(/\d+/)?.[0] || '';
|
||||
|
||||
return {
|
||||
description: feature.place_name,
|
||||
street: feature.text,
|
||||
number: addressMatch,
|
||||
city: place?.text || '',
|
||||
state: region?.short_code?.replace('BR-', '') || region?.text || '',
|
||||
zip: postcode?.text || '',
|
||||
lat: feature.center[1],
|
||||
lng: feature.center[0],
|
||||
mapLink: `https://www.google.com/maps/search/?api=1&query=${feature.center[1]},${feature.center[0]}`
|
||||
};
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar localização no Mapbox:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca reversa: converte coordenadas em endereço
|
||||
* Retorna o endereço completo baseado nas coordenadas do pin
|
||||
*/
|
||||
export async function reverseGeocode(lat: number, lng: number): Promise<MapboxResult | null> {
|
||||
if (!MAPBOX_TOKEN || MAPBOX_TOKEN.startsWith('YOUR_')) {
|
||||
console.warn('⚠️ Mapbox Token não configurado');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${lng},${lat}.json?` +
|
||||
`access_token=${MAPBOX_TOKEN}&` +
|
||||
`language=pt&` +
|
||||
`types=address,place`;
|
||||
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.features && data.features.length > 0) {
|
||||
const feature = data.features[0];
|
||||
const context = feature.context || [];
|
||||
const place = context.find((c: any) => c.id.startsWith('place'));
|
||||
const region = context.find((c: any) => c.id.startsWith('region'));
|
||||
const postcode = context.find((c: any) => c.id.startsWith('postcode'));
|
||||
|
||||
// Extrair número se houver
|
||||
const addressMatch = feature.address || '';
|
||||
|
||||
return {
|
||||
description: feature.place_name,
|
||||
street: feature.text,
|
||||
number: addressMatch.toString(),
|
||||
city: place?.text || '',
|
||||
state: region?.short_code?.replace('BR-', '') || region?.text || '',
|
||||
zip: postcode?.text || '',
|
||||
lat,
|
||||
lng,
|
||||
mapLink: `https://www.google.com/maps/search/?api=1&query=${lat},${lng}`
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Erro no reverse geocode:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
8
types.ts
8
types.ts
|
|
@ -70,13 +70,6 @@ export interface ChecklistItem {
|
|||
required: boolean;
|
||||
}
|
||||
|
||||
export interface Attachment {
|
||||
name: string;
|
||||
size: string;
|
||||
type: string;
|
||||
url?: string; // Added URL for gallery display
|
||||
}
|
||||
|
||||
export interface EventData {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
@ -89,7 +82,6 @@ export interface EventData {
|
|||
checklist: ChecklistItem[];
|
||||
briefing: string;
|
||||
coverImage: string;
|
||||
attachments: Attachment[];
|
||||
ownerId: string; // ID do cliente dono do evento
|
||||
photographerIds: string[]; // IDs dos fotógrafos designados
|
||||
institutionId?: string; // ID da instituição vinculada (obrigatório)
|
||||
|
|
|
|||
9
vite-env.d.ts
vendored
Normal file
9
vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_MAPBOX_TOKEN: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
Loading…
Reference in a new issue