atualização
This commit is contained in:
parent
9ec05a0599
commit
4e686caa3f
18 changed files with 2261 additions and 293 deletions
52
App.tsx
52
App.tsx
|
|
@ -3,6 +3,11 @@ import { Navbar } from "./components/Navbar";
|
||||||
import { Home } from "./pages/Home";
|
import { Home } from "./pages/Home";
|
||||||
import { Dashboard } from "./pages/Dashboard";
|
import { Dashboard } from "./pages/Dashboard";
|
||||||
import { Login } from "./pages/Login";
|
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 { AuthProvider, useAuth } from "./contexts/AuthContext";
|
||||||
import { DataProvider } from "./contexts/DataContext";
|
import { DataProvider } from "./contexts/DataContext";
|
||||||
import { Construction } from "lucide-react";
|
import { Construction } from "lucide-react";
|
||||||
|
|
@ -37,39 +42,20 @@ const AppContent: React.FC = () => {
|
||||||
case "uploads":
|
case "uploads":
|
||||||
return <Dashboard initialView="uploads" />;
|
return <Dashboard initialView="uploads" />;
|
||||||
|
|
||||||
case "team":
|
|
||||||
case "finance":
|
|
||||||
case "settings":
|
|
||||||
case "albums":
|
|
||||||
case "calendar":
|
case "calendar":
|
||||||
return (
|
return <CalendarPage />;
|
||||||
<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">
|
case "team":
|
||||||
<div className="mx-auto w-16 h-16 bg-gray-200 rounded-full flex items-center justify-center mb-6 text-gray-400">
|
return <TeamPage />;
|
||||||
<Construction size={32} />
|
|
||||||
</div>
|
case "finance":
|
||||||
<h2 className="text-2xl font-serif font-bold mb-3 text-brand-black capitalize">
|
return <FinancePage />;
|
||||||
{currentPage === "team"
|
|
||||||
? "Equipe & Fotógrafos"
|
case "settings":
|
||||||
: currentPage === "finance"
|
return <SettingsPage />;
|
||||||
? "Financeiro"
|
|
||||||
: currentPage === "calendar"
|
case "albums":
|
||||||
? "Agenda"
|
return <AlbumsPage />;
|
||||||
: 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>
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return <Dashboard initialView="list" />;
|
return <Dashboard initialView="list" />;
|
||||||
|
|
@ -84,7 +70,7 @@ const AppContent: React.FC = () => {
|
||||||
{currentPage === "home" && (
|
{currentPage === "home" && (
|
||||||
<footer className="bg-white border-t border-gray-100 py-12">
|
<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">
|
<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>© 2024 PhotumManager. Todos os direitos reservados.</p>
|
<p>© 2024 PhotumFormaturas. Todos os direitos reservados.</p>
|
||||||
<div className="flex space-x-6 mt-4 md:mt-0">
|
<div className="flex space-x-6 mt-4 md:mt-0">
|
||||||
<a href="#" className="hover:text-brand-black">
|
<a href="#" className="hover:text-brand-black">
|
||||||
Política de Privacidade
|
Política de Privacidade
|
||||||
|
|
|
||||||
|
|
@ -368,7 +368,7 @@ Este é um projeto em desenvolvimento ativo. Contribuições são bem-vindas!
|
||||||
|
|
||||||
## 📄 Licença
|
## 📄 Licença
|
||||||
|
|
||||||
Projeto privado - Todos os direitos reservados © 2024 PhotumManager
|
Projeto privado - Todos os direitos reservados © 2024 PhotumFormaturas
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,23 @@ import React from 'react';
|
||||||
|
|
||||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
|
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
|
||||||
size?: 'sm' | 'md' | 'lg';
|
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Button: React.FC<ButtonProps> = ({
|
export const Button: React.FC<ButtonProps> = ({
|
||||||
children,
|
children,
|
||||||
variant = 'primary',
|
variant = 'primary',
|
||||||
size = 'md',
|
size = 'md',
|
||||||
isLoading,
|
isLoading,
|
||||||
className = '',
|
className = '',
|
||||||
...props
|
...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 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 = {
|
const variants = {
|
||||||
primary: "bg-brand-black text-white hover:bg-gray-800 focus:ring-brand-black",
|
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",
|
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"
|
ghost: "text-brand-black hover:bg-gray-100 hover:text-gray-900"
|
||||||
};
|
};
|
||||||
|
|
@ -26,11 +26,12 @@ export const Button: React.FC<ButtonProps> = ({
|
||||||
const sizes = {
|
const sizes = {
|
||||||
sm: "text-xs px-3 py-1.5 rounded-sm",
|
sm: "text-xs px-3 py-1.5 rounded-sm",
|
||||||
md: "text-sm px-5 py-2.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 (
|
return (
|
||||||
<button
|
<button
|
||||||
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
|
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
|
||||||
disabled={isLoading || props.disabled}
|
disabled={isLoading || props.disabled}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
|
||||||
const [addressResults, setAddressResults] = useState<GeoResult[]>([]);
|
const [addressResults, setAddressResults] = useState<GeoResult[]>([]);
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
const [showToast, setShowToast] = useState(false);
|
const [showToast, setShowToast] = useState(false);
|
||||||
|
|
||||||
// Default State or Initial Data
|
// Default State or Initial Data
|
||||||
const [formData, setFormData] = useState(initialData || {
|
const [formData, setFormData] = useState(initialData || {
|
||||||
name: '',
|
name: '',
|
||||||
|
|
@ -37,8 +37,8 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
|
||||||
});
|
});
|
||||||
|
|
||||||
const isClientRequest = user?.role === UserRole.EVENT_OWNER;
|
const isClientRequest = user?.role === UserRole.EVENT_OWNER;
|
||||||
const formTitle = initialData
|
const formTitle = initialData
|
||||||
? "Editar Evento"
|
? "Editar Evento"
|
||||||
: (isClientRequest ? "Solicitar Orçamento/Evento" : "Cadastrar Novo Evento");
|
: (isClientRequest ? "Solicitar Orçamento/Evento" : "Cadastrar Novo Evento");
|
||||||
const submitLabel = initialData ? "Salvar Alterações" : (isClientRequest ? "Enviar Solicitação" : "Criar 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);
|
setShowToast(true);
|
||||||
// Call original submit after small delay for visual effect or immediately
|
// Call original submit after small delay for visual effect or immediately
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onSubmit(formData);
|
onSubmit(formData);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow-xl overflow-hidden max-w-4xl mx-auto border border-gray-100 slide-up relative">
|
<div className="bg-white rounded-lg shadow-xl overflow-hidden max-w-4xl mx-auto border border-gray-100 slide-up relative">
|
||||||
|
|
||||||
{/* Success Toast */}
|
{/* Success Toast */}
|
||||||
{showToast && (
|
{showToast && (
|
||||||
<div className="absolute top-4 right-4 z-50 bg-brand-black text-white px-6 py-4 rounded shadow-2xl flex items-center space-x-3 fade-in">
|
<div className="absolute top-4 right-4 z-50 bg-brand-black text-white px-6 py-4 rounded shadow-2xl flex items-center space-x-3 fade-in">
|
||||||
<CheckCircle className="text-brand-gold h-6 w-6" />
|
<CheckCircle className="text-brand-gold h-6 w-6" />
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-bold text-sm">Sucesso!</h4>
|
<h4 className="font-bold text-sm">Sucesso!</h4>
|
||||||
<p className="text-xs text-gray-300">As informações foram salvas.</p>
|
<p className="text-xs text-gray-300">As informações foram salvas.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -123,19 +123,19 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-serif text-brand-black">{formTitle}</h2>
|
<h2 className="text-2xl font-serif text-brand-black">{formTitle}</h2>
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
{isClientRequest
|
{isClientRequest
|
||||||
? "Preencha os detalhes do seu sonho. Nossa equipe analisará em breve."
|
? "Preencha os detalhes do seu sonho. Nossa equipe analisará em breve."
|
||||||
: "Preencha as informações técnicas do evento."}
|
: "Preencha as informações técnicas do evento."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
{['details', 'location', 'briefing', 'files'].map((tab, idx) => (
|
{['details', 'location', 'briefing', 'files'].map((tab, idx) => (
|
||||||
<div key={tab} className={`flex flex-col items-center ${activeTab === tab ? 'opacity-100' : 'opacity-40'}`}>
|
<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'}`}>
|
<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}
|
{idx + 1}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -151,11 +151,10 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
|
||||||
<button
|
<button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={() => setActiveTab(item.id as any)}
|
onClick={() => setActiveTab(item.id as any)}
|
||||||
className={`w-full text-left px-4 py-3 rounded-sm text-sm font-medium transition-colors ${
|
className={`w-full text-left px-4 py-3 rounded-sm text-sm font-medium transition-colors ${activeTab === item.id
|
||||||
activeTab === item.id
|
? 'bg-white shadow-sm text-brand-gold border-l-4 border-brand-gold'
|
||||||
? 'bg-white shadow-sm text-brand-gold border-l-4 border-brand-gold'
|
|
||||||
: 'text-gray-500 hover:bg-gray-100'
|
: 'text-gray-500 hover:bg-gray-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -164,45 +163,77 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
|
||||||
|
|
||||||
{/* Form Content */}
|
{/* Form Content */}
|
||||||
<div className="col-span-3 p-8">
|
<div className="col-span-3 p-8">
|
||||||
|
|
||||||
{activeTab === 'details' && (
|
{activeTab === 'details' && (
|
||||||
<div className="space-y-6 fade-in">
|
<div className="space-y-6 fade-in">
|
||||||
<div className="grid grid-cols-1 gap-6">
|
<div className="grid grid-cols-1 gap-6">
|
||||||
<Input
|
<Input
|
||||||
label="Nome do Evento (Opcional)"
|
label="Nome do Evento (Opcional)"
|
||||||
placeholder="Ex: Casamento Silva & Souza"
|
placeholder="Ex: Casamento Silva & Souza"
|
||||||
value={formData.name}
|
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">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<Input
|
<Input
|
||||||
label="Data Pretendida"
|
label="Data Pretendida"
|
||||||
type="date"
|
type="date"
|
||||||
value={formData.date}
|
value={formData.date}
|
||||||
onChange={(e) => setFormData({...formData, date: e.target.value})}
|
onChange={(e) => setFormData({ ...formData, date: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="Horário Aproximado"
|
label="Horário Aproximado"
|
||||||
type="time"
|
type="time"
|
||||||
value={formData.time}
|
value={formData.time}
|
||||||
onChange={(e) => setFormData({...formData, time: e.target.value})}
|
onChange={(e) => setFormData({ ...formData, time: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
label="Tipo de Evento"
|
label="Tipo de Evento"
|
||||||
options={Object.values(EventType).map(t => ({ value: t, label: t }))}
|
options={Object.values(EventType).map(t => ({ value: t, label: t }))}
|
||||||
value={formData.type}
|
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
|
{/* Cover Image Upload */}
|
||||||
label="URL Imagem de Capa"
|
<div>
|
||||||
placeholder="https://..."
|
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
|
||||||
value={formData.coverImage}
|
Imagem de Capa
|
||||||
onChange={(e) => setFormData({...formData, coverImage: e.target.value})}
|
</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>
|
||||||
|
|
||||||
<div className="flex justify-end mt-8">
|
<div className="flex justify-end mt-8">
|
||||||
<Button onClick={() => setActiveTab('location')}>Próximo: Localização</Button>
|
<Button onClick={() => setActiveTab('location')}>Próximo: Localização</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -213,46 +244,46 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
|
||||||
<div className="space-y-6 fade-in">
|
<div className="space-y-6 fade-in">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
|
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
|
||||||
Busca Google Maps (Powered by Gemini)
|
Busca Google Maps (Powered by Gemini)
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<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"
|
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..."
|
placeholder="Digite o nome do local ou endereço..."
|
||||||
value={addressQuery}
|
value={addressQuery}
|
||||||
onChange={(e) => setAddressQuery(e.target.value)}
|
onChange={(e) => setAddressQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className="absolute right-3 top-2.5 text-gray-400">
|
<div className="absolute right-3 top-2.5 text-gray-400">
|
||||||
{isSearching ? (
|
{isSearching ? (
|
||||||
<div className="animate-spin h-5 w-5 border-2 border-brand-gold rounded-full border-t-transparent"></div>
|
<div className="animate-spin h-5 w-5 border-2 border-brand-gold rounded-full border-t-transparent"></div>
|
||||||
) : (
|
) : (
|
||||||
<Search size={20} />
|
<Search size={20} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{addressResults.length > 0 && (
|
{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">
|
<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) => (
|
{addressResults.map((addr, idx) => (
|
||||||
<li
|
<li
|
||||||
key={idx}
|
key={idx}
|
||||||
className="px-4 py-3 hover:bg-gray-50 cursor-pointer text-sm border-b border-gray-50 last:border-0"
|
className="px-4 py-3 hover:bg-gray-50 cursor-pointer text-sm border-b border-gray-50 last:border-0"
|
||||||
onClick={() => handleAddressSelect(addr)}
|
onClick={() => handleAddressSelect(addr)}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
<MapPin size={16} className="mt-0.5 mr-2 text-brand-gold flex-shrink-0"/>
|
<MapPin size={16} className="mt-0.5 mr-2 text-brand-gold flex-shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-800">{addr.description}</p>
|
<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>
|
<p className="text-xs text-gray-500 mt-0.5">{addr.city}, {addr.state}</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{addr.mapLink && (
|
</div>
|
||||||
<span className="flex items-center text-[10px] text-blue-600 bg-blue-50 px-2 py-1 rounded ml-2">
|
{addr.mapLink && (
|
||||||
<img src="https://www.google.com/images/branding/product/ico/maps15_bnuw3a_32dp.png" alt="Maps" className="w-3 h-3 mr-1"/>
|
<span className="flex items-center text-[10px] text-blue-600 bg-blue-50 px-2 py-1 rounded ml-2">
|
||||||
Maps
|
<img src="https://www.google.com/images/branding/product/ico/maps15_bnuw3a_32dp.png" alt="Maps" className="w-3 h-3 mr-1" />
|
||||||
</span>
|
Maps
|
||||||
)}
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
@ -264,28 +295,28 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<Input label="Rua" value={formData.address.street} readOnly />
|
<Input label="Rua" value={formData.address.street} readOnly />
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
label="Número"
|
label="Número"
|
||||||
placeholder="123"
|
placeholder="123"
|
||||||
value={formData.address.number}
|
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>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<Input label="Cidade" value={formData.address.city} readOnly />
|
<Input label="Cidade" value={formData.address.city} readOnly />
|
||||||
<Input label="Estado" value={formData.address.state} readOnly />
|
<Input label="Estado" value={formData.address.state} readOnly />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formData.address.mapLink && (
|
{formData.address.mapLink && (
|
||||||
<div className="bg-gray-50 p-3 rounded border border-gray-200 flex items-center justify-between">
|
<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">
|
<span className="text-xs text-gray-500 flex items-center">
|
||||||
<Check size={14} className="mr-1 text-green-500"/>
|
<Check size={14} className="mr-1 text-green-500" />
|
||||||
Localização verificada via Google Maps
|
Localização verificada via Google Maps
|
||||||
</span>
|
</span>
|
||||||
<a href={formData.address.mapLink} target="_blank" rel="noreferrer" className="text-xs text-brand-gold flex items-center hover:underline">
|
<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"/>
|
Ver no mapa <ExternalLink size={12} className="ml-1" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-between mt-8">
|
<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 className="space-y-6 fade-in">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
|
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
|
||||||
{isClientRequest ? "Conte-nos sobre o seu sonho" : "Briefing Técnico"}
|
{isClientRequest ? "Conte-nos sobre o seu sonho" : "Briefing Técnico"}
|
||||||
</label>
|
</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"
|
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..."}
|
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}
|
value={formData.briefing}
|
||||||
onChange={(e) => setFormData({...formData, briefing: e.target.value})}
|
onChange={(e) => setFormData({ ...formData, briefing: e.target.value })}
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center mb-2">
|
<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>
|
<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">
|
<button onClick={addContact} className="text-xs text-brand-gold font-bold hover:underline flex items-center">
|
||||||
<Plus size={14} className="mr-1"/> Adicionar
|
<Plus size={14} className="mr-1" /> Adicionar
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{formData.contacts.map((contact: any, idx: number) => (
|
{formData.contacts.map((contact: any, idx: number) => (
|
||||||
<div key={idx} className="flex space-x-2 items-start">
|
<div key={idx} className="flex space-x-2 items-start">
|
||||||
<Input
|
<Input
|
||||||
label={idx === 0 ? "Nome" : ""}
|
label={idx === 0 ? "Nome" : ""}
|
||||||
placeholder="Nome"
|
placeholder="Nome"
|
||||||
value={contact.name}
|
value={contact.name}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newContacts = [...formData.contacts];
|
const newContacts = [...formData.contacts];
|
||||||
newContacts[idx].name = e.target.value;
|
newContacts[idx].name = e.target.value;
|
||||||
setFormData({...formData, contacts: newContacts});
|
setFormData({ ...formData, contacts: newContacts });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label={idx === 0 ? "Papel" : ""}
|
label={idx === 0 ? "Papel" : ""}
|
||||||
placeholder="Ex: Cerimonialista"
|
placeholder="Ex: Cerimonialista"
|
||||||
value={contact.role}
|
value={contact.role}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newContacts = [...formData.contacts];
|
const newContacts = [...formData.contacts];
|
||||||
newContacts[idx].role = e.target.value;
|
newContacts[idx].role = e.target.value;
|
||||||
setFormData({...formData, contacts: newContacts});
|
setFormData({ ...formData, contacts: newContacts });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => removeContact(idx)}
|
onClick={() => removeContact(idx)}
|
||||||
className={`mt-1 p-2 text-gray-400 hover:text-red-500 ${idx === 0 ? 'mt-7' : ''}`}
|
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'files' && (
|
{activeTab === 'files' && (
|
||||||
<div className="space-y-6 fade-in">
|
<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">
|
<div className="border-2 border-dashed border-gray-300 rounded-lg p-10 text-center hover:bg-gray-50 transition-colors relative">
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
multiple
|
multiple
|
||||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||||
onChange={handleFileUpload}
|
onChange={handleFileUpload}
|
||||||
/>
|
/>
|
||||||
<Upload size={40} className="mx-auto text-gray-400 mb-4" />
|
<Upload size={40} className="mx-auto text-gray-400 mb-4" />
|
||||||
<p className="text-sm text-gray-600 font-medium">
|
<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>
|
||||||
<p className="text-xs text-gray-400 mt-1">PDF, JPG, PNG (Max 10MB)</p>
|
<p className="text-xs text-gray-400 mt-1">PDF, JPG, PNG (Max 10MB)</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formData.files.length > 0 && (
|
{formData.files.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h4 className="text-sm font-medium text-gray-700">Arquivos Selecionados:</h4>
|
<h4 className="text-sm font-medium text-gray-700">Arquivos Selecionados:</h4>
|
||||||
{formData.files.map((file: any, idx: number) => (
|
{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 key={idx} className="flex items-center justify-between p-3 bg-gray-50 rounded border border-gray-100">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<FileText size={18} className="text-brand-gold mr-3"/>
|
<FileText size={18} className="text-brand-gold mr-3" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium">{file.name}</p>
|
<p className="text-sm font-medium">{file.name}</p>
|
||||||
<p className="text-xs text-gray-400">{(file.size / 1024).toFixed(1)} KB</p>
|
<p className="text-xs text-gray-400">{(file.size / 1024).toFixed(1)} KB</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Check size={16} className="text-green-500" />
|
<Check size={16} className="text-green-500" />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-between mt-8">
|
<div className="flex justify-between mt-8">
|
||||||
|
|
|
||||||
|
|
@ -53,58 +53,47 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRoleLabel = () => {
|
const getRoleLabel = () => {
|
||||||
if (!user) return "";
|
if (!user) return "";
|
||||||
if (user.role === UserRole.BUSINESS_OWNER) return "Empresa";
|
if (user.role === UserRole.BUSINESS_OWNER) return "Empresa";
|
||||||
if (user.role === UserRole.EVENT_OWNER) return "Cliente";
|
if (user.role === UserRole.EVENT_OWNER) return "Cliente";
|
||||||
if (user.role === UserRole.PHOTOGRAPHER) return "Fotógrafo";
|
if (user.role === UserRole.PHOTOGRAPHER) return "Fotógrafo";
|
||||||
if (user.role === UserRole.SUPERADMIN) return "Super Admin";
|
if (user.role === UserRole.SUPERADMIN) return "Super Admin";
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
className={`fixed w-full z-50 transition-all duration-300 border-b ${
|
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'
|
||||||
isScrolled ? 'bg-white/95 backdrop-blur-md shadow-sm border-gray-100 py-2' : 'bg-white border-transparent py-4'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex justify-between items-center h-12">
|
<div className="flex justify-between items-center h-12">
|
||||||
|
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div
|
<div
|
||||||
className="flex-shrink-0 flex items-center cursor-pointer"
|
className="flex-shrink-0 flex items-center cursor-pointer"
|
||||||
onClick={() => onNavigate('home')}
|
onClick={() => onNavigate('home')}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="https://photum.com.br/wp-content/uploads/2019/09/logo-photum.png"
|
src="/logo.png"
|
||||||
alt="Photum Formaturas"
|
alt="Photum Formaturas"
|
||||||
className="h-10 md:h-12 w-auto object-contain transition-all hover:opacity-90"
|
className="h-24 md:h-25 w-auto object-contain transition-all hover:opacity-100"
|
||||||
onError={(e) => {
|
|
||||||
e.currentTarget.onerror = null;
|
|
||||||
e.currentTarget.style.display = 'none';
|
|
||||||
e.currentTarget.nextElementSibling?.classList.remove('hidden');
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{/* 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>
|
</div>
|
||||||
|
|
||||||
{/* Desktop Navigation */}
|
{/* Desktop Navigation */}
|
||||||
{user && (
|
{user && (
|
||||||
<div className="hidden md:flex items-center space-x-6">
|
<div className="hidden md:flex items-center space-x-6">
|
||||||
{getLinks().map((link) => (
|
{getLinks().map((link) => (
|
||||||
<button
|
<button
|
||||||
key={link.path}
|
key={link.path}
|
||||||
onClick={() => onNavigate(link.path)}
|
onClick={() => onNavigate(link.path)}
|
||||||
className={`text-sm font-medium tracking-wide uppercase hover:text-brand-gold transition-colors pb-1 ${
|
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'
|
||||||
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>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -112,16 +101,16 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
||||||
<div className="hidden md:flex items-center space-x-4">
|
<div className="hidden md:flex items-center space-x-4">
|
||||||
{user ? (
|
{user ? (
|
||||||
<div className="flex items-center space-x-4 pl-4 border-l border-gray-200">
|
<div className="flex items-center space-x-4 pl-4 border-l border-gray-200">
|
||||||
<div className="flex flex-col items-end mr-2">
|
<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-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>
|
<span className="text-[10px] uppercase tracking-wider text-brand-gold leading-tight">{getRoleLabel()}</span>
|
||||||
</div>
|
</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">
|
<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" />
|
<img src={user.avatar} alt="Avatar" className="w-full h-full object-cover" />
|
||||||
</div>
|
</div>
|
||||||
<button onClick={logout} className="text-gray-400 hover:text-red-500 transition-colors p-1" title="Sair">
|
<button onClick={logout} className="text-gray-400 hover:text-red-500 transition-colors p-1" title="Sair">
|
||||||
<LogOut size={18} />
|
<LogOut size={18} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button onClick={() => onNavigate('login')} size="sm">
|
<Button onClick={() => onNavigate('login')} size="sm">
|
||||||
|
|
@ -146,7 +135,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
||||||
{isMobileMenuOpen && (
|
{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="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">
|
<div className="px-4 py-4 space-y-3">
|
||||||
{user && getLinks().map((link) => (
|
{user && getLinks().map((link) => (
|
||||||
<button
|
<button
|
||||||
key={link.path}
|
key={link.path}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -158,24 +147,24 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
||||||
{link.name}
|
{link.name}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
{user ? (
|
{user ? (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<img src={user.avatar} className="w-8 h-8 rounded-full mr-2"/>
|
<img src={user.avatar} className="w-8 h-8 rounded-full mr-2" />
|
||||||
<div>
|
<div>
|
||||||
<span className="font-bold text-sm block">{user.name}</span>
|
<span className="font-bold text-sm block">{user.name}</span>
|
||||||
<span className="text-xs text-brand-gold">{getRoleLabel()}</span>
|
<span className="text-xs text-brand-gold">{getRoleLabel()}</span>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button variant="ghost" size="sm" onClick={logout}>Sair</Button>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<Button className="w-full" onClick={() => onNavigate('login')}>
|
<Button variant="ghost" size="sm" onClick={logout}>Sair</Button>
|
||||||
Acessar Painel
|
</div>
|
||||||
</Button>
|
) : (
|
||||||
)}
|
<Button className="w-full" onClick={() => onNavigate('login')}>
|
||||||
</div>
|
Acessar Painel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
18
index.html
18
index.html
|
|
@ -4,7 +4,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<link
|
<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"
|
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;
|
width: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::- webkit - scrollbar - track {
|
||||||
background: #f1f1f1;
|
background: #f1f1f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
:: -webkit - scrollbar - thumb {
|
||||||
background: #B9CF33;
|
background: #B9CF33;
|
||||||
border-radius: 4px;
|
border - radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
:: -webkit - scrollbar - thumb:hover {
|
||||||
background: #a5bd2e;
|
background: #a5bd2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-in {
|
.fade -in {
|
||||||
animation: fadeIn 0.5s ease-out forwards;
|
animation: fadeIn 0.5s ease- out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-up {
|
.slide - up {
|
||||||
animation: slideUp 0.6s ease-out forwards;
|
animation: slideUp 0.6s ease - out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
"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": [
|
"requestFramePermissions": [
|
||||||
"geolocation"
|
"geolocation"
|
||||||
|
|
|
||||||
7
package-lock.json
generated
7
package-lock.json
generated
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "photummanager",
|
"name": "photumformaturas",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "photummanager",
|
"name": "photumformaturas",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/genai": "^1.30.0",
|
"@google/genai": "^1.30.0",
|
||||||
|
|
@ -2022,7 +2022,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
|
|
@ -2585,4 +2584,4 @@
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "photummanager",
|
"name": "photumformaturas",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
@ -20,4 +20,4 @@
|
||||||
"typescript": "~5.8.2",
|
"typescript": "~5.8.2",
|
||||||
"vite": "^6.2.0"
|
"vite": "^6.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
190
pages/Albums.tsx
Normal file
190
pages/Albums.tsx
Normal 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
357
pages/Calendar.tsx
Normal 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
371
pages/Finance.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -3,9 +3,8 @@ import { Button } from '../components/Button';
|
||||||
import { Camera, Heart, Shield, Star } from 'lucide-react';
|
import { Camera, Heart, Shield, Star } from 'lucide-react';
|
||||||
|
|
||||||
const HERO_IMAGES = [
|
const HERO_IMAGES = [
|
||||||
"https://images.unsplash.com/photo-1511285560982-1351cdeb9821?ixlib=rb-1.2.1&auto=format&fit=crop&w=1920&q=80",
|
"/banner2.jpg",
|
||||||
"https://images.unsplash.com/photo-1519741497674-611481863552?ixlib=rb-1.2.1&auto=format&fit=crop&w=1920&q=80",
|
"/HOME_01.jpg"
|
||||||
"https://images.unsplash.com/photo-1472653431158-6364773b2710?ixlib=rb-1.2.1&auto=format&fit=crop&w=1920&q=80"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
interface HomeProps {
|
interface HomeProps {
|
||||||
|
|
@ -27,7 +26,7 @@ export const Home: React.FC<HomeProps> = ({ onEnter }) => {
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<div className="relative h-screen w-full overflow-hidden">
|
<div className="relative h-screen w-full overflow-hidden">
|
||||||
{HERO_IMAGES.map((img, idx) => (
|
{HERO_IMAGES.map((img, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
className={`absolute inset-0 transition-opacity duration-1000 ease-in-out ${idx === currentSlide ? 'opacity-100' : 'opacity-0'}`}
|
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 className="absolute inset-0 bg-black/40"></div>
|
||||||
</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">
|
<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">
|
<h1 className="text-4xl md:text-6xl lg:text-7xl font-serif text-white leading-tight">
|
||||||
Eternizando Momentos <br />
|
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.
|
Gestão completa para eventos inesquecíveis. Do planejamento à entrega do álbum perfeito.
|
||||||
</p>
|
</p>
|
||||||
<div className="pt-8 space-x-4">
|
<div className="pt-8 space-x-4">
|
||||||
<Button size="lg" variant="secondary" onClick={onEnter}>
|
<Button size="xl" variant="secondary" onClick={onEnter}>
|
||||||
Área do Cliente
|
Área do Cliente
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="lg" variant="outline" className="border-white text-white hover:bg-white hover:text-black">
|
<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 */}
|
{/* Carousel Dots */}
|
||||||
<div className="absolute bottom-10 left-0 right-0 flex justify-center space-x-3">
|
<div className="absolute bottom-10 left-0 right-0 flex justify-center space-x-3">
|
||||||
{HERO_IMAGES.map((_, idx) => (
|
{HERO_IMAGES.map((_, idx) => (
|
||||||
<button
|
<button
|
||||||
key={idx}
|
key={idx}
|
||||||
className={`w-2 h-2 rounded-full transition-all ${idx === currentSlide ? 'bg-brand-gold w-8' : 'bg-white/50'}`}
|
className={`w-2 h-2 rounded-full transition-all ${idx === currentSlide ? 'bg-brand-gold w-8' : 'bg-white/50'}`}
|
||||||
onClick={() => setCurrentSlide(idx)}
|
onClick={() => setCurrentSlide(idx)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Features Section */}
|
{/* Features Section */}
|
||||||
<section className="py-20 bg-white">
|
<section className="py-20 bg-white">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="text-center mb-16">
|
<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>
|
<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>
|
<h3 className="text-3xl md:text-4xl font-serif text-brand-black">Excelência em cada detalhe</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
|
<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: <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: <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." }
|
{ icon: <Heart size={32} />, title: "Atendimento Humanizado", desc: "Entendemos que seu evento é um sonho a ser realizado." }
|
||||||
].map((feature, idx) => (
|
].map((feature, idx) => (
|
||||||
<div key={idx} className="text-center group p-6 rounded-lg hover:bg-gray-50 transition-colors">
|
<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">
|
<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}
|
{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>
|
</div>
|
||||||
))}
|
<h4 className="text-xl font-medium mb-3">{feature.title}</h4>
|
||||||
</div>
|
<p className="text-gray-500 font-light leading-relaxed">{feature.desc}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Testimonials */}
|
{/* Testimonials */}
|
||||||
<section className="py-20 bg-brand-black text-white">
|
<section className="py-20 bg-brand-black text-white">
|
||||||
<div className="max-w-4xl mx-auto px-4 text-center">
|
<div className="max-w-4xl mx-auto px-4 text-center">
|
||||||
<Star className="text-brand-gold mx-auto mb-6" size={40} fill="#c5a059" />
|
<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">
|
<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."
|
"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>
|
</blockquote>
|
||||||
<cite className="not-italic">
|
<cite className="not-italic">
|
||||||
<span className="font-bold block text-brand-gold">Mariana & Pedro</span>
|
<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>
|
<span className="text-sm text-gray-400 uppercase tracking-widest">Casamento em Campos do Jordão</span>
|
||||||
</cite>
|
</cite>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
482
pages/Settings.tsx
Normal file
482
pages/Settings.tsx
Normal 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
562
pages/Team.tsx
Normal 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
BIN
public/HOME_01.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 711 KiB |
BIN
public/banner2.jpg
Normal file
BIN
public/banner2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 510 KiB |
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
Loading…
Reference in a new issue