atualização

This commit is contained in:
Yago Santana 2025-11-25 13:56:11 -03:00
parent 9ec05a0599
commit 4e686caa3f
18 changed files with 2261 additions and 293 deletions

52
App.tsx
View file

@ -3,6 +3,11 @@ import { Navbar } from "./components/Navbar";
import { Home } from "./pages/Home";
import { Dashboard } from "./pages/Dashboard";
import { Login } from "./pages/Login";
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 { AuthProvider, useAuth } from "./contexts/AuthContext";
import { DataProvider } from "./contexts/DataContext";
import { Construction } from "lucide-react";
@ -37,39 +42,20 @@ const AppContent: React.FC = () => {
case "uploads":
return <Dashboard initialView="uploads" />;
case "team":
case "finance":
case "settings":
case "albums":
case "calendar":
return (
<div className="min-h-screen bg-white pt-32 px-4 text-center fade-in">
<div className="max-w-md mx-auto bg-gray-50 p-12 rounded-lg border border-gray-100 shadow-sm">
<div className="mx-auto w-16 h-16 bg-gray-200 rounded-full flex items-center justify-center mb-6 text-gray-400">
<Construction size={32} />
</div>
<h2 className="text-2xl font-serif font-bold mb-3 text-brand-black capitalize">
{currentPage === "team"
? "Equipe & Fotógrafos"
: currentPage === "finance"
? "Financeiro"
: currentPage === "calendar"
? "Agenda"
: currentPage}
</h2>
<p className="text-gray-500 mb-8 leading-relaxed">
Esta funcionalidade está em desenvolvimento e estará disponível
em breve no seu painel.
</p>
<button
onClick={() => setCurrentPage("dashboard")}
className="px-6 py-2 bg-brand-black text-white rounded-sm hover:bg-gray-800 transition-colors font-medium text-sm"
>
Voltar ao Dashboard
</button>
</div>
</div>
);
return <CalendarPage />;
case "team":
return <TeamPage />;
case "finance":
return <FinancePage />;
case "settings":
return <SettingsPage />;
case "albums":
return <AlbumsPage />;
default:
return <Dashboard initialView="list" />;
@ -84,7 +70,7 @@ const AppContent: React.FC = () => {
{currentPage === "home" && (
<footer className="bg-white border-t border-gray-100 py-12">
<div className="max-w-7xl mx-auto px-4 flex flex-col md:flex-row justify-between items-center text-sm text-gray-500">
<p>&copy; 2024 PhotumManager. Todos os direitos reservados.</p>
<p>&copy; 2024 PhotumFormaturas. Todos os direitos reservados.</p>
<div className="flex space-x-6 mt-4 md:mt-0">
<a href="#" className="hover:text-brand-black">
Política de Privacidade

View file

@ -368,7 +368,7 @@ Este é um projeto em desenvolvimento ativo. Contribuições são bem-vindas!
## 📄 Licença
Projeto privado - Todos os direitos reservados © 2024 PhotumManager
Projeto privado - Todos os direitos reservados © 2024 PhotumFormaturas
---

View file

@ -2,23 +2,23 @@ import React from 'react';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
size?: 'sm' | 'md' | 'lg' | 'xl';
isLoading?: boolean;
}
export const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
size = 'md',
export const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
size = 'md',
isLoading,
className = '',
...props
className = '',
...props
}) => {
const baseStyles = "inline-flex items-center justify-center font-medium transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed";
const variants = {
primary: "bg-brand-black text-white hover:bg-gray-800 focus:ring-brand-black",
secondary: "bg-brand-gold text-white hover:bg-amber-600 focus:ring-brand-gold",
secondary: "bg-[#B9CF32] text-white hover:bg-[#a5bd2e] focus:ring-[#B9CF32]",
outline: "border border-brand-black text-brand-black hover:bg-gray-50 focus:ring-brand-black",
ghost: "text-brand-black hover:bg-gray-100 hover:text-gray-900"
};
@ -26,11 +26,12 @@ export const Button: React.FC<ButtonProps> = ({
const sizes = {
sm: "text-xs px-3 py-1.5 rounded-sm",
md: "text-sm px-5 py-2.5 rounded-sm",
lg: "text-base px-8 py-3 rounded-sm"
lg: "text-base px-8 py-3 rounded-sm",
xl: "text-lg px-10 py-4 rounded-md font-semibold"
};
return (
<button
<button
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
disabled={isLoading || props.disabled}
{...props}

View file

@ -21,7 +21,7 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
const [addressResults, setAddressResults] = useState<GeoResult[]>([]);
const [isSearching, setIsSearching] = useState(false);
const [showToast, setShowToast] = useState(false);
// Default State or Initial Data
const [formData, setFormData] = useState(initialData || {
name: '',
@ -37,8 +37,8 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
});
const isClientRequest = user?.role === UserRole.EVENT_OWNER;
const formTitle = initialData
? "Editar Evento"
const formTitle = initialData
? "Editar Evento"
: (isClientRequest ? "Solicitar Orçamento/Evento" : "Cadastrar Novo Evento");
const submitLabel = initialData ? "Salvar Alterações" : (isClientRequest ? "Enviar Solicitação" : "Criar Evento");
@ -100,21 +100,21 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
setShowToast(true);
// Call original submit after small delay for visual effect or immediately
setTimeout(() => {
onSubmit(formData);
onSubmit(formData);
}, 1000);
};
return (
<div className="bg-white rounded-lg shadow-xl overflow-hidden max-w-4xl mx-auto border border-gray-100 slide-up relative">
{/* Success Toast */}
{showToast && (
<div className="absolute top-4 right-4 z-50 bg-brand-black text-white px-6 py-4 rounded shadow-2xl flex items-center space-x-3 fade-in">
<CheckCircle className="text-brand-gold h-6 w-6" />
<div>
<h4 className="font-bold text-sm">Sucesso!</h4>
<p className="text-xs text-gray-300">As informações foram salvas.</p>
</div>
<CheckCircle className="text-brand-gold h-6 w-6" />
<div>
<h4 className="font-bold text-sm">Sucesso!</h4>
<p className="text-xs text-gray-300">As informações foram salvas.</p>
</div>
</div>
)}
@ -123,19 +123,19 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
<div>
<h2 className="text-2xl font-serif text-brand-black">{formTitle}</h2>
<p className="text-sm text-gray-500 mt-1">
{isClientRequest
? "Preencha os detalhes do seu sonho. Nossa equipe analisará em breve."
: "Preencha as informações técnicas do evento."}
{isClientRequest
? "Preencha os detalhes do seu sonho. Nossa equipe analisará em breve."
: "Preencha as informações técnicas do evento."}
</p>
</div>
<div className="flex space-x-2">
{['details', 'location', 'briefing', 'files'].map((tab, idx) => (
<div key={tab} className={`flex flex-col items-center ${activeTab === tab ? 'opacity-100' : 'opacity-40'}`}>
<span className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold mb-1 ${activeTab === tab ? 'bg-brand-black text-white' : 'bg-gray-200 text-gray-600'}`}>
{idx + 1}
</span>
</div>
))}
{['details', 'location', 'briefing', 'files'].map((tab, idx) => (
<div key={tab} className={`flex flex-col items-center ${activeTab === tab ? 'opacity-100' : 'opacity-40'}`}>
<span className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold mb-1 ${activeTab === tab ? 'bg-brand-black text-white' : 'bg-gray-200 text-gray-600'}`}>
{idx + 1}
</span>
</div>
))}
</div>
</div>
@ -151,11 +151,10 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
<button
key={item.id}
onClick={() => setActiveTab(item.id as any)}
className={`w-full text-left px-4 py-3 rounded-sm text-sm font-medium transition-colors ${
activeTab === item.id
? 'bg-white shadow-sm text-brand-gold border-l-4 border-brand-gold'
className={`w-full text-left px-4 py-3 rounded-sm text-sm font-medium transition-colors ${activeTab === item.id
? 'bg-white shadow-sm text-brand-gold border-l-4 border-brand-gold'
: 'text-gray-500 hover:bg-gray-100'
}`}
}`}
>
{item.label}
</button>
@ -164,45 +163,77 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
{/* Form Content */}
<div className="col-span-3 p-8">
{activeTab === 'details' && (
<div className="space-y-6 fade-in">
<div className="grid grid-cols-1 gap-6">
<Input
label="Nome do Evento (Opcional)"
placeholder="Ex: Casamento Silva & Souza"
<Input
label="Nome do Evento (Opcional)"
placeholder="Ex: Casamento Silva & Souza"
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
<div className="grid grid-cols-2 gap-4">
<Input
label="Data Pretendida"
<Input
label="Data Pretendida"
type="date"
value={formData.date}
onChange={(e) => setFormData({...formData, date: e.target.value})}
onChange={(e) => setFormData({ ...formData, date: e.target.value })}
/>
<Input
label="Horário Aproximado"
<Input
label="Horário Aproximado"
type="time"
value={formData.time}
onChange={(e) => setFormData({...formData, time: e.target.value})}
onChange={(e) => setFormData({ ...formData, time: e.target.value })}
/>
</div>
<Select
<Select
label="Tipo de Evento"
options={Object.values(EventType).map(t => ({ value: t, label: t }))}
value={formData.type}
onChange={(e) => setFormData({...formData, type: e.target.value})}
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
/>
{/* Cover Image Upload (Basic URL input for now) */}
<Input
label="URL Imagem de Capa"
placeholder="https://..."
value={formData.coverImage}
onChange={(e) => setFormData({...formData, coverImage: e.target.value})}
/>
{/* Cover Image Upload */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
Imagem de Capa
</label>
<div className="relative border border-gray-300 rounded-sm p-2 flex items-center bg-white">
<input
type="file"
accept="image/*"
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
onChange={(e) => {
if (e.target.files && e.target.files[0]) {
const file = e.target.files[0];
const imageUrl = URL.createObjectURL(file);
setFormData({ ...formData, coverImage: imageUrl });
}
}}
/>
<div className="flex items-center justify-between w-full px-2">
<span className="text-sm text-gray-500 truncate max-w-[200px]">
{formData.coverImage && !formData.coverImage.startsWith('http')
? "Imagem selecionada"
: (formData.coverImage ? "Imagem atual (URL)" : "Clique para selecionar...")}
</span>
<div className="bg-gray-100 p-1.5 rounded hover:bg-gray-200">
<Upload size={16} className="text-gray-600" />
</div>
</div>
</div>
{formData.coverImage && (
<div className="mt-2 h-32 w-full rounded-sm overflow-hidden border border-gray-200 relative group">
<img src={formData.coverImage} alt="Preview" className="w-full h-full object-cover" />
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center text-white text-xs">
Visualização da Capa
</div>
</div>
)}
</div>
</div>
<div className="flex justify-end mt-8">
<Button onClick={() => setActiveTab('location')}>Próximo: Localização</Button>
</div>
@ -213,46 +244,46 @@ 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 Google Maps (Powered by Gemini)
</label>
<div className="relative">
<input
className="w-full px-4 py-2 border border-gray-300 rounded-sm focus:outline-none focus:ring-1 focus:ring-brand-gold focus:border-brand-gold transition-colors pr-10"
placeholder="Digite o nome do local ou endereço..."
value={addressQuery}
onChange={(e) => setAddressQuery(e.target.value)}
/>
<div className="absolute right-3 top-2.5 text-gray-400">
{isSearching ? (
<div className="animate-spin h-5 w-5 border-2 border-brand-gold rounded-full border-t-transparent"></div>
) : (
<Search size={20} />
)}
</div>
<input
className="w-full px-4 py-2 border border-gray-300 rounded-sm focus:outline-none focus:ring-1 focus:ring-brand-gold focus:border-brand-gold transition-colors pr-10"
placeholder="Digite o nome do local ou endereço..."
value={addressQuery}
onChange={(e) => setAddressQuery(e.target.value)}
/>
<div className="absolute right-3 top-2.5 text-gray-400">
{isSearching ? (
<div className="animate-spin h-5 w-5 border-2 border-brand-gold rounded-full border-t-transparent"></div>
) : (
<Search size={20} />
)}
</div>
</div>
{addressResults.length > 0 && (
<ul className="absolute z-10 w-full bg-white border mt-1 shadow-lg rounded-sm max-h-64 overflow-y-auto">
{addressResults.map((addr, idx) => (
<li
<li
key={idx}
className="px-4 py-3 hover:bg-gray-50 cursor-pointer text-sm border-b border-gray-50 last:border-0"
onClick={() => handleAddressSelect(addr)}
>
<div className="flex justify-between items-start">
<div className="flex items-start">
<MapPin size={16} className="mt-0.5 mr-2 text-brand-gold flex-shrink-0"/>
<div>
<p className="font-medium text-gray-800">{addr.description}</p>
<p className="text-xs text-gray-500 mt-0.5">{addr.city}, {addr.state}</p>
</div>
<div className="flex items-start">
<MapPin size={16} className="mt-0.5 mr-2 text-brand-gold flex-shrink-0" />
<div>
<p className="font-medium text-gray-800">{addr.description}</p>
<p className="text-xs text-gray-500 mt-0.5">{addr.city}, {addr.state}</p>
</div>
{addr.mapLink && (
<span className="flex items-center text-[10px] text-blue-600 bg-blue-50 px-2 py-1 rounded ml-2">
<img src="https://www.google.com/images/branding/product/ico/maps15_bnuw3a_32dp.png" alt="Maps" className="w-3 h-3 mr-1"/>
Maps
</span>
)}
</div>
{addr.mapLink && (
<span className="flex items-center text-[10px] text-blue-600 bg-blue-50 px-2 py-1 rounded ml-2">
<img src="https://www.google.com/images/branding/product/ico/maps15_bnuw3a_32dp.png" alt="Maps" className="w-3 h-3 mr-1" />
Maps
</span>
)}
</div>
</li>
))}
@ -264,28 +295,28 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
<div className="col-span-2">
<Input label="Rua" value={formData.address.street} readOnly />
</div>
<Input
label="Número"
placeholder="123"
<Input
label="Número"
placeholder="123"
value={formData.address.number}
onChange={(e) => setFormData({...formData, address: {...formData.address, number: e.target.value}})}
onChange={(e) => setFormData({ ...formData, address: { ...formData.address, number: e.target.value } })}
/>
</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} readOnly />
<Input label="Estado" value={formData.address.state} readOnly />
</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
</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"/>
</a>
</div>
<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
</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" />
</a>
</div>
)}
<div className="flex justify-between mt-8">
@ -299,47 +330,47 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
<div className="space-y-6 fade-in">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
{isClientRequest ? "Conte-nos sobre o seu sonho" : "Briefing Técnico"}
{isClientRequest ? "Conte-nos sobre o seu sonho" : "Briefing Técnico"}
</label>
<textarea
<textarea
className="w-full border border-gray-300 rounded-sm p-3 focus:outline-none focus:border-brand-gold h-32 text-sm"
placeholder={isClientRequest ? "Qual o estilo do casamento? Quais fotos são indispensáveis? Fale um pouco sobre vocês..." : "Instruções técnicas..."}
value={formData.briefing}
onChange={(e) => setFormData({...formData, briefing: e.target.value})}
onChange={(e) => setFormData({ ...formData, briefing: e.target.value })}
></textarea>
</div>
<div>
<div className="flex justify-between items-center mb-2">
<label className="text-sm font-medium text-gray-700 tracking-wide uppercase text-xs">Contatos / Responsáveis</label>
<button onClick={addContact} className="text-xs text-brand-gold font-bold hover:underline flex items-center">
<Plus size={14} className="mr-1"/> Adicionar
</button>
<label className="text-sm font-medium text-gray-700 tracking-wide uppercase text-xs">Contatos / Responsáveis</label>
<button onClick={addContact} className="text-xs text-brand-gold font-bold hover:underline flex items-center">
<Plus size={14} className="mr-1" /> Adicionar
</button>
</div>
<div className="space-y-3">
{formData.contacts.map((contact: any, idx: number) => (
<div key={idx} className="flex space-x-2 items-start">
<Input
label={idx === 0 ? "Nome" : ""}
<Input
label={idx === 0 ? "Nome" : ""}
placeholder="Nome"
value={contact.name}
onChange={(e) => {
const newContacts = [...formData.contacts];
newContacts[idx].name = e.target.value;
setFormData({...formData, contacts: newContacts});
const newContacts = [...formData.contacts];
newContacts[idx].name = e.target.value;
setFormData({ ...formData, contacts: newContacts });
}}
/>
<Input
label={idx === 0 ? "Papel" : ""}
<Input
label={idx === 0 ? "Papel" : ""}
placeholder="Ex: Cerimonialista"
value={contact.role}
onChange={(e) => {
const newContacts = [...formData.contacts];
newContacts[idx].role = e.target.value;
setFormData({...formData, contacts: newContacts});
const newContacts = [...formData.contacts];
newContacts[idx].role = e.target.value;
setFormData({ ...formData, contacts: newContacts });
}}
/>
<button
<button
onClick={() => removeContact(idx)}
className={`mt-1 p-2 text-gray-400 hover:text-red-500 ${idx === 0 ? 'mt-7' : ''}`}
>
@ -357,38 +388,38 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
</div>
)}
{activeTab === 'files' && (
{activeTab === 'files' && (
<div className="space-y-6 fade-in">
<div className="border-2 border-dashed border-gray-300 rounded-lg p-10 text-center hover:bg-gray-50 transition-colors relative">
<input
type="file"
multiple
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
onChange={handleFileUpload}
<input
type="file"
multiple
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
onChange={handleFileUpload}
/>
<Upload size={40} className="mx-auto text-gray-400 mb-4" />
<p className="text-sm text-gray-600 font-medium">
{isClientRequest ? "Anexe referências visuais (Moodboard)" : "Anexe contratos e cronogramas"}
{isClientRequest ? "Anexe referências visuais (Moodboard)" : "Anexe contratos e cronogramas"}
</p>
<p className="text-xs text-gray-400 mt-1">PDF, JPG, PNG (Max 10MB)</p>
</div>
{formData.files.length > 0 && (
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-700">Arquivos Selecionados:</h4>
{formData.files.map((file: any, idx: number) => (
<div key={idx} className="flex items-center justify-between p-3 bg-gray-50 rounded border border-gray-100">
<div className="flex items-center">
<FileText size={18} className="text-brand-gold mr-3"/>
<div>
<p className="text-sm font-medium">{file.name}</p>
<p className="text-xs text-gray-400">{(file.size / 1024).toFixed(1)} KB</p>
</div>
</div>
<Check size={16} className="text-green-500" />
</div>
))}
</div>
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-700">Arquivos Selecionados:</h4>
{formData.files.map((file: any, idx: number) => (
<div key={idx} className="flex items-center justify-between p-3 bg-gray-50 rounded border border-gray-100">
<div className="flex items-center">
<FileText size={18} className="text-brand-gold mr-3" />
<div>
<p className="text-sm font-medium">{file.name}</p>
<p className="text-xs text-gray-400">{(file.size / 1024).toFixed(1)} KB</p>
</div>
</div>
<Check size={16} className="text-green-500" />
</div>
))}
</div>
)}
<div className="flex justify-between mt-8">

View file

@ -53,58 +53,47 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
};
const getRoleLabel = () => {
if (!user) return "";
if (user.role === UserRole.BUSINESS_OWNER) return "Empresa";
if (user.role === UserRole.EVENT_OWNER) return "Cliente";
if (user.role === UserRole.PHOTOGRAPHER) return "Fotógrafo";
if (user.role === UserRole.SUPERADMIN) return "Super Admin";
return "";
if (!user) return "";
if (user.role === UserRole.BUSINESS_OWNER) return "Empresa";
if (user.role === UserRole.EVENT_OWNER) return "Cliente";
if (user.role === UserRole.PHOTOGRAPHER) return "Fotógrafo";
if (user.role === UserRole.SUPERADMIN) return "Super Admin";
return "";
};
return (
<nav
className={`fixed w-full z-50 transition-all duration-300 border-b ${
isScrolled ? 'bg-white/95 backdrop-blur-md shadow-sm border-gray-100 py-2' : 'bg-white border-transparent py-4'
}`}
<nav
className={`fixed w-full z-50 transition-all duration-300 ${isScrolled ? 'bg-white/95 backdrop-blur-md shadow-sm py-2' : 'bg-white py-4'
}`}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-12">
{/* Logo */}
<div
<div
className="flex-shrink-0 flex items-center cursor-pointer"
onClick={() => onNavigate('home')}
>
<img
src="https://photum.com.br/wp-content/uploads/2019/09/logo-photum.png"
alt="Photum Formaturas"
className="h-10 md:h-12 w-auto object-contain transition-all hover:opacity-90"
onError={(e) => {
e.currentTarget.onerror = null;
e.currentTarget.style.display = 'none';
e.currentTarget.nextElementSibling?.classList.remove('hidden');
}}
<img
src="/logo.png"
alt="Photum Formaturas"
className="h-24 md:h-25 w-auto object-contain transition-all hover:opacity-100"
/>
{/* Fallback Text Logo if image fails */}
<span className="hidden font-sans font-bold text-xl tracking-tight text-brand-black ml-2">
PHOTUM<span className="font-light text-brand-gold">FORMATURAS</span>
</span>
</div>
{/* Desktop Navigation */}
{user && (
<div className="hidden md:flex items-center space-x-6">
{getLinks().map((link) => (
{getLinks().map((link) => (
<button
key={link.path}
onClick={() => onNavigate(link.path)}
className={`text-sm font-medium tracking-wide uppercase hover:text-brand-gold transition-colors pb-1 ${
currentPage === link.path ? 'text-brand-gold border-b-2 border-brand-gold' : 'text-gray-600 border-b-2 border-transparent'
key={link.path}
onClick={() => onNavigate(link.path)}
className={`text-sm font-medium tracking-wide uppercase hover:text-brand-gold transition-colors pb-1 ${currentPage === link.path ? 'text-brand-gold border-b-2 border-brand-gold' : 'text-gray-600 border-b-2 border-transparent'
}`}
>
{link.name}
{link.name}
</button>
))}
))}
</div>
)}
@ -112,16 +101,16 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
<div className="hidden md:flex items-center space-x-4">
{user ? (
<div className="flex items-center space-x-4 pl-4 border-l border-gray-200">
<div className="flex flex-col items-end mr-2">
<span className="text-sm font-bold text-brand-black leading-tight">{user.name}</span>
<span className="text-[10px] uppercase tracking-wider text-brand-gold leading-tight">{getRoleLabel()}</span>
</div>
<div className="h-9 w-9 rounded-full bg-gray-100 overflow-hidden border border-gray-200 ring-2 ring-transparent hover:ring-brand-gold transition-all">
<img src={user.avatar} alt="Avatar" className="w-full h-full object-cover" />
</div>
<button onClick={logout} className="text-gray-400 hover:text-red-500 transition-colors p-1" title="Sair">
<LogOut size={18} />
</button>
<div className="flex flex-col items-end mr-2">
<span className="text-sm font-bold text-brand-black leading-tight">{user.name}</span>
<span className="text-[10px] uppercase tracking-wider text-brand-gold leading-tight">{getRoleLabel()}</span>
</div>
<div className="h-9 w-9 rounded-full bg-gray-100 overflow-hidden border border-gray-200 ring-2 ring-transparent hover:ring-brand-gold transition-all">
<img src={user.avatar} alt="Avatar" className="w-full h-full object-cover" />
</div>
<button onClick={logout} className="text-gray-400 hover:text-red-500 transition-colors p-1" title="Sair">
<LogOut size={18} />
</button>
</div>
) : (
<Button onClick={() => onNavigate('login')} size="sm">
@ -146,7 +135,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
{isMobileMenuOpen && (
<div className="md:hidden absolute top-full left-0 w-full bg-white border-b border-gray-100 shadow-lg fade-in">
<div className="px-4 py-4 space-y-3">
{user && getLinks().map((link) => (
{user && getLinks().map((link) => (
<button
key={link.path}
onClick={() => {
@ -158,24 +147,24 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
{link.name}
</button>
))}
<div className="pt-4">
{user ? (
<div className="flex items-center justify-between">
<div className="flex items-center">
<img src={user.avatar} className="w-8 h-8 rounded-full mr-2"/>
<div>
<span className="font-bold text-sm block">{user.name}</span>
<span className="text-xs text-brand-gold">{getRoleLabel()}</span>
</div>
</div>
<Button variant="ghost" size="sm" onClick={logout}>Sair</Button>
<div className="pt-4">
{user ? (
<div className="flex items-center justify-between">
<div className="flex items-center">
<img src={user.avatar} className="w-8 h-8 rounded-full mr-2" />
<div>
<span className="font-bold text-sm block">{user.name}</span>
<span className="text-xs text-brand-gold">{getRoleLabel()}</span>
</div>
) : (
<Button className="w-full" onClick={() => onNavigate('login')}>
Acessar Painel
</Button>
)}
</div>
</div>
<Button variant="ghost" size="sm" onClick={logout}>Sair</Button>
</div>
) : (
<Button className="w-full" onClick={() => onNavigate('login')}>
Acessar Painel
</Button>
)}
</div>
</div>
</div>
)}

View file

@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PhotumManager - Gestão de Eventos</title>
<title>PhotumFormaturas - Gestão de Eventos</title>
<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"
@ -38,25 +38,25 @@
width: 8px;
}
::-webkit-scrollbar-track {
::- webkit - scrollbar - track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
:: -webkit - scrollbar - thumb {
background: #B9CF33;
border-radius: 4px;
border - radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
:: -webkit - scrollbar - thumb:hover {
background: #a5bd2e;
}
.fade-in {
animation: fadeIn 0.5s ease-out forwards;
.fade -in {
animation: fadeIn 0.5s ease- out forwards;
}
.slide-up {
animation: slideUp 0.6s ease-out forwards;
.slide - up {
animation: slideUp 0.6s ease - out forwards;
}
@keyframes fadeIn {

View file

@ -1,5 +1,5 @@
{
"name": "PhotumManager",
"name": "PhotumFormaturas",
"description": "Sistema de gerenciamento de eventos premium inspirado na identidade visual do Photum.com.br. Foco em experiência do usuário, design minimalista e funcionalidades robustas para fotógrafos e donos de eventos.",
"requestFramePermissions": [
"geolocation"

7
package-lock.json generated
View file

@ -1,11 +1,11 @@
{
"name": "photummanager",
"name": "photumformaturas",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "photummanager",
"name": "photumformaturas",
"version": "0.0.0",
"dependencies": {
"@google/genai": "^1.30.0",
@ -2022,7 +2022,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@ -2585,4 +2584,4 @@
"license": "ISC"
}
}
}
}

View file

@ -1,5 +1,5 @@
{
"name": "photummanager",
"name": "photumformaturas",
"private": true,
"version": "0.0.0",
"type": "module",
@ -20,4 +20,4 @@
"typescript": "~5.8.2",
"vite": "^6.2.0"
}
}
}

190
pages/Albums.tsx Normal file
View file

@ -0,0 +1,190 @@
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>
);
};

357
pages/Calendar.tsx Normal file
View file

@ -0,0 +1,357 @@
import React, { useState } from 'react';
import { Calendar, Clock, MapPin, User, ChevronLeft, ChevronRight } from 'lucide-react';
interface Event {
id: string;
title: string;
date: string;
time: string;
location: string;
client: string;
status: 'confirmed' | 'pending' | 'completed';
type: 'formatura' | 'casamento' | 'evento';
}
const MOCK_EVENTS: Event[] = [
{
id: '1',
title: 'Formatura Medicina UFPR',
date: '2025-12-15',
time: '19:00',
location: 'Teatro Guaíra, Curitiba',
client: 'Ana Paula Silva',
status: 'confirmed',
type: 'formatura'
},
{
id: '2',
title: 'Casamento Maria & João',
date: '2025-12-20',
time: '16:00',
location: 'Fazenda Vista Alegre',
client: 'Maria Santos',
status: 'confirmed',
type: 'casamento'
},
{
id: '3',
title: 'Formatura Direito PUC',
date: '2025-12-22',
time: '20:00',
location: 'Centro de Convenções',
client: 'Carlos Eduardo',
status: 'pending',
type: 'formatura'
},
{
id: '4',
title: 'Formatura Engenharia UTFPR',
date: '2025-12-28',
time: '18:30',
location: 'Espaço Nobre Eventos',
client: 'Roberto Mendes',
status: 'confirmed',
type: 'formatura'
},
{
id: '5',
title: 'Evento Corporativo Tech Summit',
date: '2026-01-10',
time: '09:00',
location: 'Hotel Bourbon',
client: 'TechCorp Ltda',
status: 'pending',
type: 'evento'
},
{
id: '6',
title: 'Formatura Odontologia',
date: '2026-01-15',
time: '19:30',
location: 'Clube Curitibano',
client: 'Juliana Costa',
status: 'confirmed',
type: 'formatura'
}
];
export const CalendarPage: React.FC = () => {
const [selectedMonth, setSelectedMonth] = useState(new Date());
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
const getStatusColor = (status: Event['status']) => {
switch (status) {
case 'confirmed':
return 'bg-green-100 text-green-800';
case 'pending':
return 'bg-yellow-100 text-yellow-800';
case 'completed':
return 'bg-gray-100 text-gray-800';
}
};
const getStatusLabel = (status: Event['status']) => {
switch (status) {
case 'confirmed':
return 'Confirmado';
case 'pending':
return 'Pendente';
case 'completed':
return 'Concluído';
}
};
const getTypeColor = (type: Event['type']) => {
switch (type) {
case 'formatura':
return 'bg-blue-100 text-blue-800';
case 'casamento':
return 'bg-pink-100 text-pink-800';
case 'evento':
return 'bg-purple-100 text-purple-800';
}
};
const getTypeLabel = (type: Event['type']) => {
switch (type) {
case 'formatura':
return 'Formatura';
case 'casamento':
return 'Casamento';
case 'evento':
return 'Evento';
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString + 'T00:00:00');
return date.toLocaleDateString('pt-BR', {
day: '2-digit',
month: 'long',
year: 'numeric'
});
};
const nextMonth = () => {
setSelectedMonth(new Date(selectedMonth.getFullYear(), selectedMonth.getMonth() + 1));
};
const prevMonth = () => {
setSelectedMonth(new Date(selectedMonth.getFullYear(), selectedMonth.getMonth() - 1));
};
const currentMonthName = selectedMonth.toLocaleDateString('pt-BR', {
month: 'long',
year: 'numeric'
});
// Filter events for selected month
const monthEvents = MOCK_EVENTS.filter(event => {
const eventDate = new Date(event.date + 'T00:00:00');
return eventDate.getMonth() === selectedMonth.getMonth() &&
eventDate.getFullYear() === selectedMonth.getFullYear();
});
// Sort events by date
const sortedEvents = [...MOCK_EVENTS].sort((a, b) =>
new Date(a.date).getTime() - new Date(b.date).getTime()
);
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">
Minha Agenda
</h1>
<p className="text-gray-600">
Gerencie seus eventos e compromissos fotográficos
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Calendar Navigation */}
<div className="lg:col-span-1">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-6">
<button
onClick={prevMonth}
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
>
<ChevronLeft size={20} />
</button>
<h2 className="text-lg font-semibold capitalize">
{currentMonthName}
</h2>
<button
onClick={nextMonth}
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
>
<ChevronRight size={20} />
</button>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600">Eventos este mês:</span>
<span className="font-semibold text-brand-gold">{monthEvents.length}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600">Total de eventos:</span>
<span className="font-semibold">{MOCK_EVENTS.length}</span>
</div>
</div>
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold mb-3">Legenda</h3>
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm">
<div className="w-3 h-3 rounded-full bg-green-500"></div>
<span>Confirmado</span>
</div>
<div className="flex items-center gap-2 text-sm">
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
<span>Pendente</span>
</div>
<div className="flex items-center gap-2 text-sm">
<div className="w-3 h-3 rounded-full bg-gray-500"></div>
<span>Concluído</span>
</div>
</div>
</div>
</div>
</div>
{/* Events List */}
<div className="lg:col-span-2">
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
<div className="p-6 border-b border-gray-200">
<h2 className="text-xl font-semibold">Próximos Eventos</h2>
</div>
<div className="divide-y divide-gray-200">
{sortedEvents.length === 0 ? (
<div className="p-12 text-center text-gray-500">
<Calendar size={48} className="mx-auto mb-4 text-gray-300" />
<p>Nenhum evento agendado</p>
</div>
) : (
sortedEvents.map((event) => (
<div
key={event.id}
className="p-6 hover:bg-gray-50 transition-colors cursor-pointer"
onClick={() => setSelectedEvent(event)}
>
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<h3 className="text-lg font-semibold text-brand-black">
{event.title}
</h3>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getTypeColor(event.type)}`}>
{getTypeLabel(event.type)}
</span>
</div>
<div className="space-y-2">
<div className="flex items-center text-sm text-gray-600">
<Calendar size={16} className="mr-2 text-brand-gold" />
{formatDate(event.date)}
</div>
<div className="flex items-center text-sm text-gray-600">
<Clock size={16} className="mr-2 text-brand-gold" />
{event.time}
</div>
<div className="flex items-center text-sm text-gray-600">
<MapPin size={16} className="mr-2 text-brand-gold" />
{event.location}
</div>
<div className="flex items-center text-sm text-gray-600">
<User size={16} className="mr-2 text-brand-gold" />
{event.client}
</div>
</div>
</div>
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(event.status)}`}>
{getStatusLabel(event.status)}
</span>
</div>
</div>
))
)}
</div>
</div>
</div>
</div>
</div>
{/* Event Detail Modal */}
{selectedEvent && (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
onClick={() => setSelectedEvent(null)}
>
<div
className="bg-white rounded-lg max-w-2xl w-full p-8"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-start justify-between mb-6">
<div>
<h2 className="text-2xl font-serif font-bold text-brand-black mb-2">
{selectedEvent.title}
</h2>
<div className="flex gap-2">
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getTypeColor(selectedEvent.type)}`}>
{getTypeLabel(selectedEvent.type)}
</span>
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(selectedEvent.status)}`}>
{getStatusLabel(selectedEvent.status)}
</span>
</div>
</div>
<button
onClick={() => setSelectedEvent(null)}
className="text-gray-400 hover:text-gray-600"
>
</button>
</div>
<div className="space-y-4">
<div className="flex items-center text-gray-700">
<Calendar size={20} className="mr-3 text-brand-gold" />
<span className="font-medium">{formatDate(selectedEvent.date)}</span>
</div>
<div className="flex items-center text-gray-700">
<Clock size={20} className="mr-3 text-brand-gold" />
<span>{selectedEvent.time}</span>
</div>
<div className="flex items-center text-gray-700">
<MapPin size={20} className="mr-3 text-brand-gold" />
<span>{selectedEvent.location}</span>
</div>
<div className="flex items-center text-gray-700">
<User size={20} className="mr-3 text-brand-gold" />
<span>{selectedEvent.client}</span>
</div>
</div>
<div className="mt-8 pt-6 border-t border-gray-200 flex gap-3">
<button
onClick={() => setSelectedEvent(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 Detalhes do Evento
</button>
</div>
</div>
</div>
)}
</div>
);
};

371
pages/Finance.tsx Normal file
View file

@ -0,0 +1,371 @@
import React, { useState } from 'react';
import { DollarSign, TrendingUp, TrendingDown, Calendar, Download, Filter, CreditCard, CheckCircle, Clock, XCircle } from 'lucide-react';
interface Transaction {
id: string;
type: 'income' | 'expense';
category: string;
description: string;
amount: number;
date: string;
status: 'paid' | 'pending' | 'overdue';
client?: string;
event?: string;
}
const MOCK_TRANSACTIONS: Transaction[] = [
{
id: '1',
type: 'income',
category: 'Formatura',
description: 'Pagamento Formatura Medicina UFPR',
amount: 8500.00,
date: '2025-12-01',
status: 'paid',
client: 'Ana Paula Silva',
event: 'Formatura Medicina UFPR'
},
{
id: '2',
type: 'income',
category: 'Casamento',
description: 'Sinal Casamento Maria & João',
amount: 3000.00,
date: '2025-12-05',
status: 'paid',
client: 'Maria Santos',
event: 'Casamento Maria & João'
},
{
id: '3',
type: 'expense',
category: 'Equipamento',
description: 'Manutenção Câmera Canon',
amount: 450.00,
date: '2025-12-03',
status: 'paid'
},
{
id: '4',
type: 'income',
category: 'Formatura',
description: 'Pagamento Formatura Direito PUC',
amount: 7200.00,
date: '2025-12-10',
status: 'pending',
client: 'Carlos Eduardo',
event: 'Formatura Direito PUC'
},
{
id: '5',
type: 'expense',
category: 'Transporte',
description: 'Combustível - Eventos Dezembro',
amount: 320.00,
date: '2025-12-08',
status: 'paid'
},
{
id: '6',
type: 'income',
category: 'Evento Corporativo',
description: 'Tech Summit 2026',
amount: 5500.00,
date: '2025-12-15',
status: 'pending',
client: 'TechCorp Ltda',
event: 'Tech Summit'
},
{
id: '7',
type: 'expense',
category: 'Software',
description: 'Assinatura Adobe Creative Cloud',
amount: 180.00,
date: '2025-12-01',
status: 'paid'
},
{
id: '8',
type: 'income',
category: 'Formatura',
description: 'Saldo Final Formatura Engenharia',
amount: 4500.00,
date: '2025-11-20',
status: 'overdue',
client: 'Roberto Mendes',
event: 'Formatura Engenharia UTFPR'
}
];
export const FinancePage: React.FC = () => {
const [filterType, setFilterType] = useState<'all' | 'income' | 'expense'>('all');
const [filterStatus, setFilterStatus] = useState<'all' | 'paid' | 'pending' | 'overdue'>('all');
const filteredTransactions = MOCK_TRANSACTIONS.filter(transaction => {
const matchesType = filterType === 'all' || transaction.type === filterType;
const matchesStatus = filterStatus === 'all' || transaction.status === filterStatus;
return matchesType && matchesStatus;
});
const totalIncome = MOCK_TRANSACTIONS
.filter(t => t.type === 'income' && t.status === 'paid')
.reduce((sum, t) => sum + t.amount, 0);
const totalExpense = MOCK_TRANSACTIONS
.filter(t => t.type === 'expense' && t.status === 'paid')
.reduce((sum, t) => sum + t.amount, 0);
const pendingIncome = MOCK_TRANSACTIONS
.filter(t => t.type === 'income' && (t.status === 'pending' || t.status === 'overdue'))
.reduce((sum, t) => sum + t.amount, 0);
const balance = totalIncome - totalExpense;
const getStatusIcon = (status: Transaction['status']) => {
switch (status) {
case 'paid':
return <CheckCircle size={16} className="text-green-600" />;
case 'pending':
return <Clock size={16} className="text-yellow-600" />;
case 'overdue':
return <XCircle size={16} className="text-red-600" />;
}
};
const getStatusColor = (status: Transaction['status']) => {
switch (status) {
case 'paid':
return 'bg-green-100 text-green-800';
case 'pending':
return 'bg-yellow-100 text-yellow-800';
case 'overdue':
return 'bg-red-100 text-red-800';
}
};
const getStatusLabel = (status: Transaction['status']) => {
switch (status) {
case 'paid':
return 'Pago';
case 'pending':
return 'Pendente';
case 'overdue':
return 'Atrasado';
}
};
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL'
}).format(value);
};
const formatDate = (dateString: string) => {
const date = new Date(dateString + 'T00:00:00');
return date.toLocaleDateString('pt-BR', {
day: '2-digit',
month: 'short',
year: 'numeric'
});
};
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 flex items-center justify-between">
<div>
<h1 className="text-3xl font-serif font-bold text-brand-black mb-2">
Financeiro
</h1>
<p className="text-gray-600">
Acompanhe receitas, despesas e fluxo de caixa
</p>
</div>
<button className="flex items-center gap-2 px-4 py-2 bg-brand-gold text-white rounded-md hover:bg-[#a5bd2e] transition-colors font-medium">
<Download size={20} />
Exportar Relatório
</button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg: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 mb-2">
<p className="text-sm text-gray-600">Receitas</p>
<TrendingUp className="text-green-600" size={24} />
</div>
<p className="text-3xl font-bold text-green-600">{formatCurrency(totalIncome)}</p>
<p className="text-xs text-gray-500 mt-1">Pagamentos recebidos</p>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-2">
<p className="text-sm text-gray-600">Despesas</p>
<TrendingDown className="text-red-600" size={24} />
</div>
<p className="text-3xl font-bold text-red-600">{formatCurrency(totalExpense)}</p>
<p className="text-xs text-gray-500 mt-1">Gastos do período</p>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-2">
<p className="text-sm text-gray-600">Saldo</p>
<DollarSign className="text-brand-gold" size={24} />
</div>
<p className={`text-3xl font-bold ${balance >= 0 ? 'text-brand-gold' : 'text-red-600'}`}>
{formatCurrency(balance)}
</p>
<p className="text-xs text-gray-500 mt-1">Receitas - Despesas</p>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-2">
<p className="text-sm text-gray-600">A Receber</p>
<Clock className="text-yellow-600" size={24} />
</div>
<p className="text-3xl font-bold text-yellow-600">{formatCurrency(pendingIncome)}</p>
<p className="text-xs text-gray-500 mt-1">Pagamentos pendentes</p>
</div>
</div>
{/* Filters */}
<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 gap-2">
<button
onClick={() => setFilterType('all')}
className={`px-4 py-2 rounded-md font-medium transition-colors ${filterType === 'all'
? 'bg-brand-gold text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Todas
</button>
<button
onClick={() => setFilterType('income')}
className={`px-4 py-2 rounded-md font-medium transition-colors ${filterType === 'income'
? 'bg-green-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Receitas
</button>
<button
onClick={() => setFilterType('expense')}
className={`px-4 py-2 rounded-md font-medium transition-colors ${filterType === 'expense'
? 'bg-red-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Despesas
</button>
</div>
<div className="flex gap-2">
<button
onClick={() => setFilterStatus('all')}
className={`px-4 py-2 rounded-md font-medium transition-colors ${filterStatus === 'all'
? 'bg-brand-black text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Todos Status
</button>
<button
onClick={() => setFilterStatus('paid')}
className={`px-4 py-2 rounded-md font-medium transition-colors ${filterStatus === 'paid'
? 'bg-green-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Pagos
</button>
<button
onClick={() => setFilterStatus('pending')}
className={`px-4 py-2 rounded-md font-medium transition-colors ${filterStatus === 'pending'
? 'bg-yellow-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Pendentes
</button>
</div>
</div>
</div>
{/* Transactions List */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
<div className="p-6 border-b border-gray-200">
<h2 className="text-xl font-semibold">Transações</h2>
</div>
<div className="divide-y divide-gray-200">
{filteredTransactions.length === 0 ? (
<div className="p-12 text-center text-gray-500">
<CreditCard size={48} className="mx-auto mb-4 text-gray-300" />
<p>Nenhuma transação encontrada</p>
</div>
) : (
filteredTransactions.map((transaction) => (
<div
key={transaction.id}
className="p-6 hover:bg-gray-50 transition-colors"
>
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${transaction.type === 'income' ? 'bg-green-100' : 'bg-red-100'
}`}>
{transaction.type === 'income' ? (
<TrendingUp size={20} className="text-green-600" />
) : (
<TrendingDown size={20} className="text-red-600" />
)}
</div>
<div>
<h3 className="font-semibold text-brand-black">
{transaction.description}
</h3>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs text-gray-500">{transaction.category}</span>
{transaction.client && (
<>
<span className="text-xs text-gray-300"></span>
<span className="text-xs text-gray-500">{transaction.client}</span>
</>
)}
</div>
</div>
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<p className={`text-lg font-bold ${transaction.type === 'income' ? 'text-green-600' : 'text-red-600'
}`}>
{transaction.type === 'income' ? '+' : '-'} {formatCurrency(transaction.amount)}
</p>
<div className="flex items-center gap-1 text-xs text-gray-500 mt-1">
<Calendar size={12} />
{formatDate(transaction.date)}
</div>
</div>
<div className="flex items-center gap-2">
{getStatusIcon(transaction.status)}
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(transaction.status)}`}>
{getStatusLabel(transaction.status)}
</span>
</div>
</div>
</div>
</div>
))
)}
</div>
</div>
</div>
</div>
);
};

View file

@ -3,9 +3,8 @@ import { Button } from '../components/Button';
import { Camera, Heart, Shield, Star } from 'lucide-react';
const HERO_IMAGES = [
"https://images.unsplash.com/photo-1511285560982-1351cdeb9821?ixlib=rb-1.2.1&auto=format&fit=crop&w=1920&q=80",
"https://images.unsplash.com/photo-1519741497674-611481863552?ixlib=rb-1.2.1&auto=format&fit=crop&w=1920&q=80",
"https://images.unsplash.com/photo-1472653431158-6364773b2710?ixlib=rb-1.2.1&auto=format&fit=crop&w=1920&q=80"
"/banner2.jpg",
"/HOME_01.jpg"
];
interface HomeProps {
@ -27,7 +26,7 @@ export const Home: React.FC<HomeProps> = ({ onEnter }) => {
{/* Hero Section */}
<div className="relative h-screen w-full overflow-hidden">
{HERO_IMAGES.map((img, idx) => (
<div
<div
key={idx}
className={`absolute inset-0 transition-opacity duration-1000 ease-in-out ${idx === currentSlide ? 'opacity-100' : 'opacity-0'}`}
>
@ -35,8 +34,9 @@ export const Home: React.FC<HomeProps> = ({ onEnter }) => {
<div className="absolute inset-0 bg-black/40"></div>
</div>
))}
<div className="absolute inset-0 flex items-center justify-center text-center px-4">
{/* Text and Buttons - Only visible on first slide */}
<div className={`absolute inset-0 flex items-center justify-center text-center px-4 transition-opacity duration-500 ${currentSlide === 0 ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}>
<div className="max-w-4xl space-y-6 slide-up">
<h1 className="text-4xl md:text-6xl lg:text-7xl font-serif text-white leading-tight">
Eternizando Momentos <br />
@ -46,7 +46,7 @@ export const Home: React.FC<HomeProps> = ({ onEnter }) => {
Gestão completa para eventos inesquecíveis. Do planejamento à entrega do álbum perfeito.
</p>
<div className="pt-8 space-x-4">
<Button size="lg" variant="secondary" onClick={onEnter}>
<Button size="xl" variant="secondary" onClick={onEnter}>
Área do Cliente
</Button>
<Button size="lg" variant="outline" className="border-white text-white hover:bg-white hover:text-black">
@ -58,54 +58,54 @@ export const Home: React.FC<HomeProps> = ({ onEnter }) => {
{/* Carousel Dots */}
<div className="absolute bottom-10 left-0 right-0 flex justify-center space-x-3">
{HERO_IMAGES.map((_, idx) => (
<button
key={idx}
className={`w-2 h-2 rounded-full transition-all ${idx === currentSlide ? 'bg-brand-gold w-8' : 'bg-white/50'}`}
onClick={() => setCurrentSlide(idx)}
/>
))}
{HERO_IMAGES.map((_, idx) => (
<button
key={idx}
className={`w-2 h-2 rounded-full transition-all ${idx === currentSlide ? 'bg-brand-gold w-8' : 'bg-white/50'}`}
onClick={() => setCurrentSlide(idx)}
/>
))}
</div>
</div>
{/* Features Section */}
<section className="py-20 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-sm font-bold tracking-widest text-brand-gold uppercase mb-2">Por que nós?</h2>
<h3 className="text-3xl md:text-4xl font-serif text-brand-black">Excelência em cada detalhe</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
{[
{ icon: <Camera size={32}/>, title: "Qualidade Impecável", desc: "Equipamentos de última geração e profissionais premiados." },
{ icon: <Shield size={32}/>, title: "Segurança Total", desc: "Backup duplo em nuvem e contratos transparentes." },
{ icon: <Heart size={32}/>, title: "Atendimento Humanizado", desc: "Entendemos que seu evento é um sonho a ser realizado." }
].map((feature, idx) => (
<div key={idx} className="text-center group p-6 rounded-lg hover:bg-gray-50 transition-colors">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gray-100 text-brand-black mb-6 group-hover:bg-brand-gold group-hover:text-white transition-colors">
{feature.icon}
</div>
<h4 className="text-xl font-medium mb-3">{feature.title}</h4>
<p className="text-gray-500 font-light leading-relaxed">{feature.desc}</p>
<div className="text-center mb-16">
<h2 className="text-sm font-bold tracking-widest text-brand-gold uppercase mb-2">Por que nós?</h2>
<h3 className="text-3xl md:text-4xl font-serif text-brand-black">Excelência em cada detalhe</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
{[
{ icon: <Camera size={32} />, title: "Qualidade Impecável", desc: "Equipamentos de última geração e profissionais premiados." },
{ icon: <Shield size={32} />, title: "Segurança Total", desc: "Backup duplo em nuvem e contratos transparentes." },
{ icon: <Heart size={32} />, title: "Atendimento Humanizado", desc: "Entendemos que seu evento é um sonho a ser realizado." }
].map((feature, idx) => (
<div key={idx} className="text-center group p-6 rounded-lg hover:bg-gray-50 transition-colors">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gray-100 text-brand-black mb-6 group-hover:bg-brand-gold group-hover:text-white transition-colors">
{feature.icon}
</div>
))}
</div>
<h4 className="text-xl font-medium mb-3">{feature.title}</h4>
<p className="text-gray-500 font-light leading-relaxed">{feature.desc}</p>
</div>
))}
</div>
</div>
</section>
{/* Testimonials */}
<section className="py-20 bg-brand-black text-white">
<div className="max-w-4xl mx-auto px-4 text-center">
<Star className="text-brand-gold mx-auto mb-6" size={40} fill="#c5a059" />
<blockquote className="text-2xl md:text-3xl font-serif italic leading-relaxed mb-8">
"A equipe do Photum superou todas as expectativas. O sistema de acompanhamento nos deixou tranquilos durante todo o processo e as fotos ficaram incríveis."
</blockquote>
<cite className="not-italic">
<span className="font-bold block text-brand-gold">Mariana & Pedro</span>
<span className="text-sm text-gray-400 uppercase tracking-widest">Casamento em Campos do Jordão</span>
</cite>
</div>
<div className="max-w-4xl mx-auto px-4 text-center">
<Star className="text-brand-gold mx-auto mb-6" size={40} fill="#c5a059" />
<blockquote className="text-2xl md:text-3xl font-serif italic leading-relaxed mb-8">
"A equipe do Photum superou todas as expectativas. O sistema de acompanhamento nos deixou tranquilos durante todo o processo e as fotos ficaram incríveis."
</blockquote>
<cite className="not-italic">
<span className="font-bold block text-brand-gold">Mariana & Pedro</span>
<span className="text-sm text-gray-400 uppercase tracking-widest">Casamento em Campos do Jordão</span>
</cite>
</div>
</section>
</div>
);

482
pages/Settings.tsx Normal file
View file

@ -0,0 +1,482 @@
import React, { useState } from 'react';
import { User, Mail, Phone, MapPin, Lock, Bell, Palette, Globe, Save, Camera } from 'lucide-react';
import { Button } from '../components/Button';
export const SettingsPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'profile' | 'account' | 'notifications' | 'appearance'>('profile');
const [profileData, setProfileData] = useState({
name: 'João Silva',
email: 'joao.silva@photum.com',
phone: '(41) 99999-0000',
location: 'Curitiba, PR',
bio: 'Fotógrafo profissional especializado em eventos e formaturas há mais de 10 anos.',
avatar: 'https://i.pravatar.cc/150?img=68'
});
const [notificationSettings, setNotificationSettings] = useState({
emailNotifications: true,
pushNotifications: true,
smsNotifications: false,
eventReminders: true,
paymentAlerts: true,
teamUpdates: false
});
const [appearanceSettings, setAppearanceSettings] = useState({
theme: 'light',
language: 'pt-BR',
dateFormat: 'DD/MM/YYYY',
currency: 'BRL'
});
const handleSaveProfile = () => {
alert('Perfil atualizado com sucesso!');
};
const handleSaveNotifications = () => {
alert('Configurações de notificações salvas!');
};
const handleSaveAppearance = () => {
alert('Configurações de aparência salvas!');
};
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">
Configurações
</h1>
<p className="text-gray-600">
Gerencie suas preferências e informações da conta
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sidebar */}
<div className="lg:col-span-1">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<nav className="space-y-1">
<button
onClick={() => setActiveTab('profile')}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-md transition-colors ${activeTab === 'profile'
? 'bg-brand-gold text-white'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
<User size={20} />
<span className="font-medium">Perfil</span>
</button>
<button
onClick={() => setActiveTab('account')}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-md transition-colors ${activeTab === 'account'
? 'bg-brand-gold text-white'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
<Lock size={20} />
<span className="font-medium">Conta</span>
</button>
<button
onClick={() => setActiveTab('notifications')}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-md transition-colors ${activeTab === 'notifications'
? 'bg-brand-gold text-white'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
<Bell size={20} />
<span className="font-medium">Notificações</span>
</button>
<button
onClick={() => setActiveTab('appearance')}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-md transition-colors ${activeTab === 'appearance'
? 'bg-brand-gold text-white'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
<Palette size={20} />
<span className="font-medium">Aparência</span>
</button>
</nav>
</div>
</div>
{/* Content */}
<div className="lg:col-span-3">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
{/* Profile Tab */}
{activeTab === 'profile' && (
<div>
<h2 className="text-2xl font-semibold mb-6">Informações do Perfil</h2>
<div className="mb-8">
<div className="flex items-center gap-6">
<div className="relative">
<img
src={profileData.avatar}
alt="Avatar"
className="w-24 h-24 rounded-full object-cover"
/>
<button className="absolute bottom-0 right-0 w-8 h-8 bg-brand-gold text-white rounded-full flex items-center justify-center hover:bg-[#a5bd2e] transition-colors">
<Camera size={16} />
</button>
</div>
<div>
<h3 className="font-semibold text-lg">{profileData.name}</h3>
<p className="text-sm text-gray-600">{profileData.email}</p>
<button className="text-sm text-brand-gold hover:underline mt-1">
Alterar foto
</button>
</div>
</div>
</div>
<div className="space-y-6">
<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"
value={profileData.name}
onChange={(e) => setProfileData({ ...profileData, name: e.target.value })}
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"
value={profileData.email}
onChange={(e) => setProfileData({ ...profileData, email: e.target.value })}
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"
value={profileData.phone}
onChange={(e) => setProfileData({ ...profileData, phone: e.target.value })}
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"
value={profileData.location}
onChange={(e) => setProfileData({ ...profileData, location: e.target.value })}
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">
Biografia
</label>
<textarea
value={profileData.bio}
onChange={(e) => setProfileData({ ...profileData, bio: e.target.value })}
rows={4}
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
/>
</div>
<div className="pt-4">
<Button size="lg" variant="secondary" onClick={handleSaveProfile}>
<Save size={20} className="mr-2" />
Salvar Alterações
</Button>
</div>
</div>
</div>
)}
{/* Account Tab */}
{activeTab === 'account' && (
<div>
<h2 className="text-2xl font-semibold mb-6">Segurança da Conta</h2>
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Senha Atual
</label>
<input
type="password"
placeholder="Digite sua senha atual"
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Nova Senha
</label>
<input
type="password"
placeholder="Digite sua nova senha"
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Confirmar Nova Senha
</label>
<input
type="password"
placeholder="Confirme sua nova senha"
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
/>
</div>
<div className="pt-4">
<Button size="lg" variant="secondary">
<Lock size={20} className="mr-2" />
Atualizar Senha
</Button>
</div>
<div className="pt-8 border-t border-gray-200">
<h3 className="text-lg font-semibold mb-4">Autenticação em Dois Fatores</h3>
<p className="text-gray-600 mb-4">
Adicione uma camada extra de segurança à sua conta
</p>
<Button size="md" variant="outline">
Ativar 2FA
</Button>
</div>
</div>
</div>
)}
{/* Notifications Tab */}
{activeTab === 'notifications' && (
<div>
<h2 className="text-2xl font-semibold mb-6">Preferências de Notificações</h2>
<div className="space-y-6">
<div className="flex items-center justify-between py-4 border-b border-gray-200">
<div>
<h3 className="font-medium">Notificações por Email</h3>
<p className="text-sm text-gray-600">Receba atualizações por email</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={notificationSettings.emailNotifications}
onChange={(e) => setNotificationSettings({
...notificationSettings,
emailNotifications: e.target.checked
})}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-gold/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand-gold"></div>
</label>
</div>
<div className="flex items-center justify-between py-4 border-b border-gray-200">
<div>
<h3 className="font-medium">Notificações Push</h3>
<p className="text-sm text-gray-600">Receba notificações no navegador</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={notificationSettings.pushNotifications}
onChange={(e) => setNotificationSettings({
...notificationSettings,
pushNotifications: e.target.checked
})}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-gold/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand-gold"></div>
</label>
</div>
<div className="flex items-center justify-between py-4 border-b border-gray-200">
<div>
<h3 className="font-medium">SMS</h3>
<p className="text-sm text-gray-600">Receba mensagens de texto</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={notificationSettings.smsNotifications}
onChange={(e) => setNotificationSettings({
...notificationSettings,
smsNotifications: e.target.checked
})}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-gold/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand-gold"></div>
</label>
</div>
<div className="flex items-center justify-between py-4 border-b border-gray-200">
<div>
<h3 className="font-medium">Lembretes de Eventos</h3>
<p className="text-sm text-gray-600">Receba lembretes antes dos eventos</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={notificationSettings.eventReminders}
onChange={(e) => setNotificationSettings({
...notificationSettings,
eventReminders: e.target.checked
})}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-gold/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand-gold"></div>
</label>
</div>
<div className="flex items-center justify-between py-4 border-b border-gray-200">
<div>
<h3 className="font-medium">Alertas de Pagamento</h3>
<p className="text-sm text-gray-600">Notificações sobre pagamentos</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={notificationSettings.paymentAlerts}
onChange={(e) => setNotificationSettings({
...notificationSettings,
paymentAlerts: e.target.checked
})}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-gold/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand-gold"></div>
</label>
</div>
<div className="pt-4">
<Button size="lg" variant="secondary" onClick={handleSaveNotifications}>
<Save size={20} className="mr-2" />
Salvar Preferências
</Button>
</div>
</div>
</div>
)}
{/* Appearance Tab */}
{activeTab === 'appearance' && (
<div>
<h2 className="text-2xl font-semibold mb-6">Aparência e Idioma</h2>
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Tema
</label>
<select
value={appearanceSettings.theme}
onChange={(e) => setAppearanceSettings({
...appearanceSettings,
theme: e.target.value
})}
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
>
<option value="light">Claro</option>
<option value="dark">Escuro</option>
<option value="auto">Automático</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Idioma
</label>
<select
value={appearanceSettings.language}
onChange={(e) => setAppearanceSettings({
...appearanceSettings,
language: e.target.value
})}
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
>
<option value="pt-BR">Português (Brasil)</option>
<option value="en-US">English (US)</option>
<option value="es-ES">Español</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Formato de Data
</label>
<select
value={appearanceSettings.dateFormat}
onChange={(e) => setAppearanceSettings({
...appearanceSettings,
dateFormat: e.target.value
})}
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
>
<option value="DD/MM/YYYY">DD/MM/YYYY</option>
<option value="MM/DD/YYYY">MM/DD/YYYY</option>
<option value="YYYY-MM-DD">YYYY-MM-DD</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Moeda
</label>
<select
value={appearanceSettings.currency}
onChange={(e) => setAppearanceSettings({
...appearanceSettings,
currency: e.target.value
})}
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
>
<option value="BRL">Real (R$)</option>
<option value="USD">Dólar ($)</option>
<option value="EUR">Euro ()</option>
</select>
</div>
<div className="pt-4">
<Button size="lg" variant="secondary" onClick={handleSaveAppearance}>
<Save size={20} className="mr-2" />
Salvar Configurações
</Button>
</div>
</div>
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
};

562
pages/Team.tsx Normal file
View file

@ -0,0 +1,562 @@
import React, { useState } from 'react';
import { Users, Camera, Mail, Phone, MapPin, Star, Plus, Search, Filter, User } 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[],
});
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, null, 2));
setShowAddModal(false);
setNewPhotographer({
name: '',
email: '',
phone: '',
location: '',
specialties: []
});
}}>
<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>
);
};

BIN
public/HOME_01.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 KiB

BIN
public/banner2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB