atualização
This commit is contained in:
parent
9940f4f967
commit
44ed329b68
14 changed files with 969 additions and 97 deletions
127
App.tsx
127
App.tsx
|
|
@ -70,19 +70,120 @@ const AppContent: React.FC = () => {
|
|||
<main>{renderPage()}</main>
|
||||
|
||||
{currentPage === "home" && (
|
||||
<footer className="bg-white border-t border-gray-100 py-12">
|
||||
<div className="max-w-7xl mx-auto px-4 flex flex-col md:flex-row justify-between items-center text-sm text-gray-500">
|
||||
<p>© 2025 PhotumFormaturas. Todos os direitos reservados.</p>
|
||||
<div className="flex space-x-6 mt-4 md:mt-0">
|
||||
<a href="#" className="hover:text-brand-black">
|
||||
Política de Privacidade
|
||||
</a>
|
||||
<a href="#" className="hover:text-brand-black">
|
||||
Termos de Uso
|
||||
</a>
|
||||
<a href="#" className="hover:text-brand-black">
|
||||
Instagram
|
||||
</a>
|
||||
<footer className="bg-brand-black text-white py-20">
|
||||
<div className="w-full max-w-[1400px] mx-auto px-8 sm:px-12 lg:px-20">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-16 lg:gap-24 mb-20">
|
||||
{/* Logo e Descrição */}
|
||||
<div className="lg:col-span-1">
|
||||
<h3 className="text-2xl font-serif font-bold mb-4" style={{color: '#B9CF33'}}>
|
||||
Photum Formaturas
|
||||
</h3>
|
||||
<p className="text-gray-400 text-sm leading-relaxed">
|
||||
Eternizando momentos únicos com excelência e profissionalismo desde 2020.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Serviços */}
|
||||
<div>
|
||||
<h4 className="font-bold text-white mb-4 uppercase tracking-wider text-sm">Serviços</h4>
|
||||
<ul className="space-y-3 text-gray-400 text-sm">
|
||||
<li>
|
||||
<a href="#" className="hover:text-brand-gold transition-colors">
|
||||
Fotografia de Eventos
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="hover:text-brand-gold transition-colors">
|
||||
Álbuns Personalizados
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="hover:text-brand-gold transition-colors">
|
||||
Gestão de Formaturas
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="hover:text-brand-gold transition-colors">
|
||||
Cobertura Completa
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Links Úteis */}
|
||||
<div>
|
||||
<h4 className="font-bold text-white mb-4 uppercase tracking-wider text-sm">Links Úteis</h4>
|
||||
<ul className="space-y-3 text-gray-400 text-sm">
|
||||
<li>
|
||||
<a href="#" onClick={() => setCurrentPage('login')} className="hover:text-brand-gold transition-colors">
|
||||
Área do Cliente
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" onClick={() => setCurrentPage('register')} className="hover:text-brand-gold transition-colors">
|
||||
Cadastre sua Formatura
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="hover:text-brand-gold transition-colors">
|
||||
Portfólio
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="hover:text-brand-gold transition-colors">
|
||||
FAQ
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Contato */}
|
||||
<div>
|
||||
<h4 className="font-bold text-white mb-4 uppercase tracking-wider text-sm">Contato</h4>
|
||||
<ul className="space-y-3 text-gray-400 text-sm">
|
||||
<li className="flex items-center gap-2">
|
||||
<span>📧</span>
|
||||
<a href="mailto:contato@photum.com.br" className="hover:text-brand-gold transition-colors">
|
||||
contato@photum.com.br
|
||||
</a>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span>📱</span>
|
||||
<span>(11) 99999-9999</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span>📍</span>
|
||||
<span>São Paulo, SP</span>
|
||||
</li>
|
||||
<li className="flex gap-4 mt-4">
|
||||
<a href="#" className="hover:text-brand-gold transition-colors text-xl">
|
||||
📷
|
||||
</a>
|
||||
<a href="#" className="hover:text-brand-gold transition-colors text-xl">
|
||||
👥
|
||||
</a>
|
||||
<a href="#" className="hover:text-brand-gold transition-colors text-xl">
|
||||
▶️
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Bar */}
|
||||
<div className="border-t border-gray-800 pt-8 flex flex-col md:flex-row justify-between items-center text-sm text-gray-500">
|
||||
<p>© 2025 PhotumFormaturas. Todos os direitos reservados.</p>
|
||||
<div className="flex space-x-8 mt-4 md:mt-0">
|
||||
<a href="#" className="hover:text-brand-gold transition-colors">
|
||||
Política de Privacidade
|
||||
</a>
|
||||
<a href="#" className="hover:text-brand-gold transition-colors">
|
||||
Termos de Uso
|
||||
</a>
|
||||
<a href="#" className="hover:text-brand-gold transition-colors">
|
||||
LGPD
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ import React, { useState, useEffect } from 'react';
|
|||
import { EventType, EventStatus, Address } from '../types';
|
||||
import { Input, Select } from './Input';
|
||||
import { Button } from './Button';
|
||||
import { MapPin, Upload, Plus, X, Check, FileText, ExternalLink, Search, CheckCircle } from 'lucide-react';
|
||||
import { MapPin, Upload, Plus, X, Check, FileText, ExternalLink, Search, CheckCircle, Building2, AlertCircle } from 'lucide-react';
|
||||
import { searchLocationWithGemini, GeoResult } from '../services/genaiService';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useData } from '../contexts/DataContext';
|
||||
import { UserRole } from '../types';
|
||||
import { InstitutionForm } from './InstitutionForm';
|
||||
|
||||
interface EventFormProps {
|
||||
onCancel: () => void;
|
||||
|
|
@ -16,11 +18,21 @@ interface EventFormProps {
|
|||
|
||||
export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initialData }) => {
|
||||
const { user } = useAuth();
|
||||
const { institutions, getInstitutionsByUserId, addInstitution } = useData();
|
||||
const [activeTab, setActiveTab] = useState<'details' | 'location' | 'briefing' | 'files'>('details');
|
||||
const [addressQuery, setAddressQuery] = useState('');
|
||||
const [addressResults, setAddressResults] = useState<GeoResult[]>([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const [showInstitutionForm, setShowInstitutionForm] = useState(false);
|
||||
|
||||
// Get institutions based on user role
|
||||
// Business owners and admins see all institutions, clients see only their own
|
||||
const userInstitutions = user
|
||||
? (user.role === UserRole.BUSINESS_OWNER || user.role === UserRole.SUPERADMIN
|
||||
? institutions
|
||||
: getInstitutionsByUserId(user.id))
|
||||
: [];
|
||||
|
||||
// Default State or Initial Data
|
||||
const [formData, setFormData] = useState(initialData || {
|
||||
|
|
@ -33,7 +45,8 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
|
|||
briefing: '',
|
||||
contacts: [{ name: '', role: '', phone: '' }],
|
||||
files: [] as File[],
|
||||
coverImage: 'https://images.unsplash.com/photo-1511795409834-ef04bbd61622?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=80' // Default
|
||||
coverImage: 'https://images.unsplash.com/photo-1511795409834-ef04bbd61622?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=80', // Default
|
||||
institutionId: ''
|
||||
});
|
||||
|
||||
const isClientRequest = user?.role === UserRole.EVENT_OWNER;
|
||||
|
|
@ -96,6 +109,12 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
|
|||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
// Validate institution selection
|
||||
if (!formData.institutionId) {
|
||||
alert('Por favor, selecione uma instituição antes de continuar.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show toast
|
||||
setShowToast(true);
|
||||
// Call original submit after small delay for visual effect or immediately
|
||||
|
|
@ -104,6 +123,30 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
|
|||
}, 1000);
|
||||
};
|
||||
|
||||
const handleInstitutionSubmit = (institutionData: any) => {
|
||||
const newInstitution = {
|
||||
...institutionData,
|
||||
id: `inst-${Date.now()}`,
|
||||
ownerId: user?.id || ''
|
||||
};
|
||||
addInstitution(newInstitution);
|
||||
setFormData(prev => ({ ...prev, institutionId: newInstitution.id }));
|
||||
setShowInstitutionForm(false);
|
||||
};
|
||||
|
||||
// Show institution form modal
|
||||
if (showInstitutionForm) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<InstitutionForm
|
||||
onCancel={() => setShowInstitutionForm(false)}
|
||||
onSubmit={handleInstitutionSubmit}
|
||||
userId={user?.id || ''}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-xl overflow-hidden max-w-4xl mx-auto border border-gray-100 slide-up relative">
|
||||
|
||||
|
|
@ -194,6 +237,70 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
|
|||
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
|
||||
/>
|
||||
|
||||
{/* Institution Selection - OBRIGATÓRIO */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
|
||||
Universidade* <span className="text-brand-gold">(Obrigatório)</span>
|
||||
</label>
|
||||
|
||||
{userInstitutions.length === 0 ? (
|
||||
<div className="border-2 border-dashed border-amber-300 bg-amber-50 rounded-sm p-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<AlertCircle className="text-amber-600 flex-shrink-0 mt-0.5" size={20} />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-amber-900 mb-2">
|
||||
Nenhuma universidade cadastrada
|
||||
</p>
|
||||
<p className="text-xs text-amber-700 mb-3">
|
||||
Você precisa cadastrar uma universidade antes de criar um evento.
|
||||
Trabalhamos exclusivamente com eventos fotográficos em universidades.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowInstitutionForm(true)}
|
||||
className="text-xs font-bold text-amber-900 hover:text-amber-700 underline flex items-center"
|
||||
>
|
||||
<Plus size={14} className="mr-1" />
|
||||
Cadastrar minha primeira universidade
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<select
|
||||
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"
|
||||
value={formData.institutionId}
|
||||
onChange={(e) => setFormData({ ...formData, institutionId: e.target.value })}
|
||||
required
|
||||
>
|
||||
<option value="">Selecione uma universidade</option>
|
||||
{userInstitutions.map(inst => (
|
||||
<option key={inst.id} value={inst.id}>
|
||||
{inst.name} - {inst.type}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowInstitutionForm(true)}
|
||||
className="text-xs text-brand-gold hover:underline flex items-center"
|
||||
>
|
||||
<Plus size={12} className="mr-1" />
|
||||
Cadastrar nova universidade
|
||||
</button>
|
||||
|
||||
{formData.institutionId && (
|
||||
<div className="bg-green-50 border border-green-200 rounded-sm p-3 flex items-center">
|
||||
<Check size={16} className="text-green-600 mr-2" />
|
||||
<span className="text-xs text-green-800">Universidade selecionada com sucesso</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Cover Image Upload */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
|
||||
|
|
@ -299,7 +406,12 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
|
|||
label="Número"
|
||||
placeholder="123"
|
||||
value={formData.address.number}
|
||||
onChange={(e) => setFormData({ ...formData, address: { ...formData.address, number: e.target.value } })}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.replace(/\D/g, '');
|
||||
setFormData({ ...formData, address: { ...formData.address, number: value } });
|
||||
}}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
|
|
|
|||
|
|
@ -1,22 +1,91 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Eye, EyeOff } from 'lucide-react';
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label: string;
|
||||
error?: string;
|
||||
mask?: 'phone' | 'cnpj' | 'cep';
|
||||
}
|
||||
|
||||
export const Input: React.FC<InputProps> = ({ label, error, className = '', ...props }) => {
|
||||
export const Input: React.FC<InputProps> = ({ label, error, className = '', type, mask, onChange, ...props }) => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const isPassword = type === 'password';
|
||||
const inputType = isPassword && showPassword ? 'text' : type;
|
||||
|
||||
const applyMask = (value: string, maskType?: 'phone' | 'cnpj' | 'cep') => {
|
||||
if (!maskType) return value;
|
||||
|
||||
const numbers = value.replace(/\D/g, '');
|
||||
|
||||
switch (maskType) {
|
||||
case 'phone':
|
||||
// Limita a 11 dígitos (celular)
|
||||
const phoneNumbers = numbers.slice(0, 11);
|
||||
if (phoneNumbers.length <= 10) {
|
||||
return phoneNumbers.replace(/(\d{2})(\d{4})(\d{0,4})/, '($1) $2-$3');
|
||||
}
|
||||
return phoneNumbers.replace(/(\d{2})(\d{5})(\d{0,4})/, '($1) $2-$3');
|
||||
|
||||
case 'cnpj':
|
||||
// Limita a 14 dígitos
|
||||
const cnpjNumbers = numbers.slice(0, 14);
|
||||
return cnpjNumbers.replace(/(\d{2})(\d{3})(\d{3})(\d{4})(\d{0,2})/, '$1.$2.$3/$4-$5');
|
||||
|
||||
case 'cep':
|
||||
// Limita a 8 dígitos
|
||||
const cepNumbers = numbers.slice(0, 8);
|
||||
return cepNumbers.replace(/(\d{5})(\d{0,3})/, '$1-$2');
|
||||
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (mask) {
|
||||
const maskedValue = applyMask(e.target.value, mask);
|
||||
e.target.value = maskedValue;
|
||||
}
|
||||
onChange?.(e);
|
||||
};
|
||||
|
||||
// Define maxLength baseado na máscara
|
||||
const getMaxLength = () => {
|
||||
if (!mask) return undefined;
|
||||
switch (mask) {
|
||||
case 'phone': return 15; // (00) 00000-0000
|
||||
case 'cnpj': return 18; // 00.000.000/0000-00
|
||||
case 'cep': return 9; // 00000-000
|
||||
default: return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
className={`w-full px-4 py-2 border rounded-sm focus:outline-none focus:ring-1 focus:ring-brand-gold focus:border-brand-gold transition-colors
|
||||
${error ? 'border-red-500' : 'border-gray-300'}
|
||||
${className}`}
|
||||
{...props}
|
||||
/>
|
||||
<div className="relative">
|
||||
<input
|
||||
className={`w-full px-4 py-2 border rounded-sm focus:outline-none focus:ring-1 focus:ring-brand-gold focus:border-brand-gold transition-colors
|
||||
${error ? 'border-red-500' : 'border-gray-300'}
|
||||
${isPassword ? 'pr-10' : ''}
|
||||
${className}`}
|
||||
type={inputType}
|
||||
onChange={handleChange}
|
||||
maxLength={getMaxLength()}
|
||||
{...props}
|
||||
/>
|
||||
{isPassword && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{error && <span className="text-xs text-red-500 mt-1">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
248
components/InstitutionForm.tsx
Normal file
248
components/InstitutionForm.tsx
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
|
||||
import React, { useState } from 'react';
|
||||
import { Institution, Address } from '../types';
|
||||
import { Input, Select } from './Input';
|
||||
import { Button } from './Button';
|
||||
import { Building2, X, Check } from 'lucide-react';
|
||||
|
||||
interface InstitutionFormProps {
|
||||
onCancel: () => void;
|
||||
onSubmit: (data: Partial<Institution>) => void;
|
||||
initialData?: Institution;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
const INSTITUTION_TYPES = [
|
||||
'Universidade Pública',
|
||||
'Universidade Privada',
|
||||
'Faculdade',
|
||||
'Instituto Federal',
|
||||
'Centro Universitário',
|
||||
'Campus Universitário'
|
||||
];
|
||||
|
||||
export const InstitutionForm: React.FC<InstitutionFormProps> = ({
|
||||
onCancel,
|
||||
onSubmit,
|
||||
initialData,
|
||||
userId
|
||||
}) => {
|
||||
const [formData, setFormData] = useState<Partial<Institution>>(initialData || {
|
||||
name: '',
|
||||
type: '',
|
||||
cnpj: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
description: '',
|
||||
ownerId: userId,
|
||||
address: {
|
||||
street: '',
|
||||
number: '',
|
||||
city: '',
|
||||
state: '',
|
||||
zip: ''
|
||||
}
|
||||
});
|
||||
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const [stateError, setStateError] = useState('');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setShowToast(true);
|
||||
setTimeout(() => {
|
||||
onSubmit(formData);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const handleChange = (field: keyof Institution, value: any) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleAddressChange = (field: keyof Address, value: string) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
address: {
|
||||
...prev.address!,
|
||||
[field]: value
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-xl overflow-hidden max-w-2xl mx-auto border border-gray-100 slide-up relative">
|
||||
|
||||
{/* Success Toast */}
|
||||
{showToast && (
|
||||
<div className="absolute top-4 right-4 z-50 bg-brand-black text-white px-6 py-4 rounded shadow-2xl flex items-center space-x-3 fade-in">
|
||||
<Check className="text-brand-gold h-6 w-6" />
|
||||
<div>
|
||||
<h4 className="font-bold text-sm">Sucesso!</h4>
|
||||
<p className="text-xs text-gray-300">Universidade cadastrada com sucesso.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Form Header */}
|
||||
<div className="bg-gray-50 border-b px-8 py-6 flex justify-between items-center">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Building2 className="text-brand-gold h-8 w-8" />
|
||||
<div>
|
||||
<h2 className="text-2xl font-serif text-brand-black">
|
||||
{initialData ? 'Editar Universidade' : 'Cadastrar Universidade'}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Registre a universidade onde os eventos fotográficos serão realizados
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="p-2 hover:bg-gray-200 rounded-full transition-colors"
|
||||
>
|
||||
<X size={20} className="text-gray-600" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-8 space-y-6">
|
||||
|
||||
{/* Informações Básicas */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-semibold text-gray-700 tracking-wide uppercase">
|
||||
Informações Básicas
|
||||
</h3>
|
||||
|
||||
<Input
|
||||
label="Nome da Universidade*"
|
||||
placeholder="Ex: Universidade Federal do Rio Grande do Sul"
|
||||
value={formData.name || ''}
|
||||
onChange={(e) => handleChange('name', e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Select
|
||||
label="Tipo de Universidade*"
|
||||
options={INSTITUTION_TYPES.map(t => ({ value: t, label: t }))}
|
||||
value={formData.type || ''}
|
||||
onChange={(e) => handleChange('type', e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="CNPJ (Opcional)"
|
||||
placeholder="00.000.000/0000-00"
|
||||
value={formData.cnpj || ''}
|
||||
onChange={(e) => handleChange('cnpj', e.target.value)}
|
||||
mask="cnpj"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="Telefone*"
|
||||
type="tel"
|
||||
placeholder="(00) 00000-0000"
|
||||
value={formData.phone || ''}
|
||||
onChange={(e) => handleChange('phone', e.target.value)}
|
||||
mask="phone"
|
||||
required
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="E-mail*"
|
||||
type="email"
|
||||
placeholder="contato@instituicao.com"
|
||||
value={formData.email || ''}
|
||||
onChange={(e) => handleChange('email', e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
|
||||
Descrição (Opcional)
|
||||
</label>
|
||||
<textarea
|
||||
className="w-full border border-gray-300 rounded-sm p-3 focus:outline-none focus:border-brand-gold h-24 text-sm"
|
||||
placeholder="Ex: Campus principal, informações sobre o campus, áreas para eventos..."
|
||||
value={formData.description || ''}
|
||||
onChange={(e) => handleChange('description', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Endereço */}
|
||||
<div className="space-y-4 border-t pt-6">
|
||||
<h3 className="text-sm font-semibold text-gray-700 tracking-wide uppercase">
|
||||
Endereço (Opcional)
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-2">
|
||||
<Input
|
||||
label="Rua"
|
||||
placeholder="Nome da rua"
|
||||
value={formData.address?.street || ''}
|
||||
onChange={(e) => handleAddressChange('street', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
label="Número"
|
||||
placeholder="123"
|
||||
value={formData.address?.number || ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.replace(/\D/g, '');
|
||||
handleAddressChange('number', value);
|
||||
}}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Input
|
||||
label="Cidade"
|
||||
placeholder="Cidade"
|
||||
value={formData.address?.city || ''}
|
||||
onChange={(e) => handleAddressChange('city', e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Estado"
|
||||
placeholder="UF"
|
||||
value={formData.address?.state || ''}
|
||||
onChange={(e) => {
|
||||
const hasNumbers = /[0-9]/.test(e.target.value);
|
||||
if (hasNumbers) {
|
||||
setStateError('O campo Estado aceita apenas letras');
|
||||
setTimeout(() => setStateError(''), 3000);
|
||||
}
|
||||
const value = e.target.value.replace(/[0-9]/g, '').toUpperCase();
|
||||
handleAddressChange('state', value);
|
||||
}}
|
||||
maxLength={2}
|
||||
error={stateError}
|
||||
/>
|
||||
<Input
|
||||
label="CEP"
|
||||
placeholder="00000-000"
|
||||
value={formData.address?.zip || ''}
|
||||
onChange={(e) => handleAddressChange('zip', e.target.value)}
|
||||
mask="cep"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end space-x-3 pt-6 border-t">
|
||||
<Button variant="outline" onClick={onCancel} type="button">
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" variant="secondary">
|
||||
{initialData ? 'Salvar Alterações' : 'Cadastrar Universidade'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,8 +1,27 @@
|
|||
|
||||
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
||||
import { EventData, EventStatus, EventType, Attachment } from '../types';
|
||||
import { EventData, EventStatus, EventType, Attachment, Institution } from '../types';
|
||||
|
||||
// Initial Mock Data
|
||||
const INITIAL_INSTITUTIONS: Institution[] = [
|
||||
{
|
||||
id: 'inst-1',
|
||||
name: 'Universidade Federal do Rio Grande do Sul',
|
||||
type: 'Universidade Pública',
|
||||
phone: '(51) 3308-3333',
|
||||
email: 'eventos@ufrgs.br',
|
||||
address: {
|
||||
street: 'Av. Paulo Gama',
|
||||
number: '110',
|
||||
city: 'Porto Alegre',
|
||||
state: 'RS',
|
||||
zip: '90040-060'
|
||||
},
|
||||
description: 'Campus Central - Principais eventos realizados no Salão de Atos',
|
||||
ownerId: 'client-1'
|
||||
}
|
||||
];
|
||||
|
||||
const INITIAL_EVENTS: EventData[] = [
|
||||
{
|
||||
id: '1',
|
||||
|
|
@ -27,7 +46,8 @@ const INITIAL_EVENTS: EventData[] = [
|
|||
{ name: 'Ensaio 2', size: '2mb', type: 'image/jpeg', url: 'https://images.unsplash.com/photo-1511285560982-1351cdeb9821?auto=format&fit=crop&w=400&q=80' }
|
||||
],
|
||||
ownerId: 'client-1',
|
||||
photographerIds: ['photographer-1']
|
||||
photographerIds: ['photographer-1'],
|
||||
institutionId: 'inst-1'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
|
|
@ -55,17 +75,23 @@ const INITIAL_EVENTS: EventData[] = [
|
|||
|
||||
interface DataContextType {
|
||||
events: EventData[];
|
||||
institutions: Institution[];
|
||||
addEvent: (event: EventData) => void;
|
||||
updateEventStatus: (id: string, status: EventStatus) => void;
|
||||
assignPhotographer: (eventId: string, photographerId: string) => void;
|
||||
getEventsByRole: (userId: string, role: string) => EventData[];
|
||||
addAttachment: (eventId: string, attachment: Attachment) => void;
|
||||
addInstitution: (institution: Institution) => void;
|
||||
updateInstitution: (id: string, institution: Partial<Institution>) => void;
|
||||
getInstitutionsByUserId: (userId: string) => Institution[];
|
||||
getInstitutionById: (id: string) => Institution | undefined;
|
||||
}
|
||||
|
||||
const DataContext = createContext<DataContextType | undefined>(undefined);
|
||||
|
||||
export const DataProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [events, setEvents] = useState<EventData[]>(INITIAL_EVENTS);
|
||||
const [institutions, setInstitutions] = useState<Institution[]>(INITIAL_INSTITUTIONS);
|
||||
|
||||
const addEvent = (event: EventData) => {
|
||||
setEvents(prev => [event, ...prev]);
|
||||
|
|
@ -109,8 +135,38 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||
}));
|
||||
};
|
||||
|
||||
const addInstitution = (institution: Institution) => {
|
||||
setInstitutions(prev => [...prev, institution]);
|
||||
};
|
||||
|
||||
const updateInstitution = (id: string, updatedData: Partial<Institution>) => {
|
||||
setInstitutions(prev => prev.map(inst =>
|
||||
inst.id === id ? { ...inst, ...updatedData } : inst
|
||||
));
|
||||
};
|
||||
|
||||
const getInstitutionsByUserId = (userId: string) => {
|
||||
return institutions.filter(inst => inst.ownerId === userId);
|
||||
};
|
||||
|
||||
const getInstitutionById = (id: string) => {
|
||||
return institutions.find(inst => inst.id === id);
|
||||
};
|
||||
|
||||
return (
|
||||
<DataContext.Provider value={{ events, addEvent, updateEventStatus, assignPhotographer, getEventsByRole, addAttachment }}>
|
||||
<DataContext.Provider value={{
|
||||
events,
|
||||
institutions,
|
||||
addEvent,
|
||||
updateEventStatus,
|
||||
assignPhotographer,
|
||||
getEventsByRole,
|
||||
addAttachment,
|
||||
addInstitution,
|
||||
updateInstitution,
|
||||
getInstitutionsByUserId,
|
||||
getInstitutionById
|
||||
}}>
|
||||
{children}
|
||||
</DataContext.Provider>
|
||||
);
|
||||
|
|
|
|||
63
package-lock.json
generated
63
package-lock.json
generated
|
|
@ -11,7 +11,8 @@
|
|||
"@google/genai": "^1.30.0",
|
||||
"lucide-react": "^0.554.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.9.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
|
|
@ -1416,6 +1417,19 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
|
@ -2046,6 +2060,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
|
|
@ -2063,6 +2078,44 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.9.6",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz",
|
||||
"integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.9.6",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.6.tgz",
|
||||
"integrity": "sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.9.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "5.0.10",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
|
||||
|
|
@ -2156,6 +2209,12 @@
|
|||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
|
@ -2584,4 +2643,4 @@
|
|||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@
|
|||
"@google/genai": "^1.30.0",
|
||||
"lucide-react": "^0.554.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.9.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
|
|
@ -20,4 +21,4 @@
|
|||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { UserRole, EventData, EventStatus, EventType } from '../types';
|
|||
import { EventCard } from '../components/EventCard';
|
||||
import { EventForm } from '../components/EventForm';
|
||||
import { Button } from '../components/Button';
|
||||
import { PlusCircle, Search, CheckCircle, Clock, Upload, Edit, Users, Map, Image as ImageIcon } from 'lucide-react';
|
||||
import { PlusCircle, Search, CheckCircle, Clock, Upload, Edit, Users, Map, Image as ImageIcon, Building2 } from 'lucide-react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useData } from '../contexts/DataContext';
|
||||
import { STATUS_COLORS } from '../constants';
|
||||
|
|
@ -15,7 +15,7 @@ interface DashboardProps {
|
|||
|
||||
export const Dashboard: React.FC<DashboardProps> = ({ initialView = 'list' }) => {
|
||||
const { user } = useAuth();
|
||||
const { events, getEventsByRole, addEvent, updateEventStatus, assignPhotographer, addAttachment } = useData();
|
||||
const { events, getEventsByRole, addEvent, updateEventStatus, assignPhotographer, addAttachment, getInstitutionById } = useData();
|
||||
const [view, setView] = useState<'list' | 'create' | 'edit' | 'details' | 'uploads'>(initialView);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedEvent, setSelectedEvent] = useState<EventData | null>(null);
|
||||
|
|
@ -298,6 +298,49 @@ export const Dashboard: React.FC<DashboardProps> = ({ initialView = 'list' }) =>
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Institution Information */}
|
||||
{selectedEvent.institutionId && (() => {
|
||||
const institution = getInstitutionById(selectedEvent.institutionId);
|
||||
if (institution) {
|
||||
return (
|
||||
<section className="bg-gradient-to-br from-brand-gold/10 to-transparent border border-brand-gold/30 rounded-sm p-6">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="bg-brand-gold/20 p-3 rounded-full">
|
||||
<Building2 className="text-brand-gold" size={24} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-bold text-brand-black mb-1">{institution.name}</h3>
|
||||
<p className="text-sm text-brand-gold uppercase tracking-wide font-medium mb-3">{institution.type}</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<p className="text-gray-500 text-xs uppercase tracking-wide">Contato</p>
|
||||
<p className="text-gray-700 font-medium">{institution.phone}</p>
|
||||
<p className="text-gray-600">{institution.email}</p>
|
||||
</div>
|
||||
|
||||
{institution.address && (
|
||||
<div>
|
||||
<p className="text-gray-500 text-xs uppercase tracking-wide">Endereço</p>
|
||||
<p className="text-gray-700">{institution.address.street}, {institution.address.number}</p>
|
||||
<p className="text-gray-600">{institution.address.city} - {institution.address.state}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{institution.description && (
|
||||
<p className="text-gray-600 text-sm mt-3 italic border-t border-brand-gold/20 pt-3">
|
||||
{institution.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
|
||||
<section>
|
||||
<h3 className="text-lg font-bold border-b pb-2 mb-4 text-brand-black">Sobre o Evento</h3>
|
||||
<p className="text-gray-600 leading-relaxed whitespace-pre-wrap">{selectedEvent.briefing || "Sem briefing detalhado."}</p>
|
||||
|
|
|
|||
179
pages/Home.tsx
179
pages/Home.tsx
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Camera, Heart, Shield, Star } from 'lucide-react';
|
||||
import { Camera, Heart, Shield, Star, BookOpen } from 'lucide-react';
|
||||
|
||||
const HERO_IMAGES = [
|
||||
"/banner2.jpg",
|
||||
|
|
@ -59,59 +59,150 @@ export const Home: React.FC<HomeProps> = ({ onEnter }) => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features Section */}
|
||||
<section className="py-20 bg-white">
|
||||
{/* Albums Section */}
|
||||
<section className="py-24 relative overflow-hidden" style={{ backgroundColor: '#ffffff' }}>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-sm font-bold tracking-widest text-brand-gold uppercase mb-2">Por que nós?</h2>
|
||||
<h3 className="text-3xl md:text-4xl font-serif text-brand-black">Excelência em cada detalhe</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
|
||||
<div className="text-center group">
|
||||
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-gray-100 text-gray-700 mb-6 transition-all">
|
||||
<Camera size={40} strokeWidth={1.5} />
|
||||
<div className="flex flex-col lg:flex-row items-center gap-16">
|
||||
{/* Left side - Icon and Text */}
|
||||
<div className="lg:w-1/2 text-left fade-in">
|
||||
<div className="inline-flex items-center justify-center w-36 h-36 mb-8 transform transition-transform duration-300 hover:scale-110 hover:rotate-3">
|
||||
<img src="/HOME_17.png" alt="Álbuns" className="w-full h-full object-contain drop-shadow-2xl" />
|
||||
</div>
|
||||
|
||||
<h2 className="text-5xl md:text-6xl font-bold mb-8 leading-tight" style={{color: '#B9CF33'}}>
|
||||
ÁLBUNS<br/>PERSONALIZADOS
|
||||
</h2>
|
||||
|
||||
<div className="space-y-3 text-gray-700">
|
||||
<p className="text-lg leading-relaxed">
|
||||
Escolha a cor, tamanho, tecido, acabamento, modelo,
|
||||
</p>
|
||||
<p className="text-lg leading-relaxed">
|
||||
laminação, tipo de impressão e muito mais!
|
||||
</p>
|
||||
<p className="text-xl font-semibold text-brand-black mt-4">
|
||||
Tenha seu álbum exclusivo e de acordo com o seu gosto.
|
||||
</p>
|
||||
</div>
|
||||
<h4 className="text-xl font-bold mb-3 text-brand-black">Qualidade Impecável</h4>
|
||||
<p className="text-gray-600 leading-relaxed">
|
||||
Equipamentos de última geração e profissionais premiados.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center group">
|
||||
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-gray-100 text-gray-700 mb-6 transition-all">
|
||||
<Shield size={40} strokeWidth={1.5} />
|
||||
{/* Right side - CTA */}
|
||||
<div className="lg:w-2/8 flex flex-col items-center lg:items-start justify-center ml-40 slide-up">
|
||||
<div className="bg-white rounded-lg shadow-2xl p-10 border-t-4 transform transition-all duration-300 hover:shadow-xl hover:-translate-y-2" style={{ borderColor: '#B9CF33' }}>
|
||||
<div className="flex justify-center mb-6">
|
||||
<div className="w-29 h-29 transform transition-transform duration-300 hover:scale-110">
|
||||
<img src="/logo.png" alt="Cadastrar" className="w-full h-full object-contain drop-shadow-lg" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-700 text-xl mb-5 text-center font-medium leading-relaxed">
|
||||
Faça parte da Photum e cadastre sua formatura.
|
||||
</p>
|
||||
<button
|
||||
onClick={onEnter}
|
||||
className="w-full px-10 py-5 text-white font-bold text-lg rounded-md transition-all duration-300 transform hover:scale-105 hover:shadow-2xl active:scale-95"
|
||||
style={{ backgroundColor: '#B9CF33' }}
|
||||
>
|
||||
Cadastrar Formatura
|
||||
</button>
|
||||
</div>
|
||||
<h4 className="text-xl font-bold mb-3 text-brand-black">Segurança Total</h4>
|
||||
<p className="text-gray-600 leading-relaxed">
|
||||
Backup duplo em nuvem e contratos transparentes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center group">
|
||||
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-gray-100 text-gray-700 mb-6 transition-all">
|
||||
<Heart size={40} strokeWidth={1.5} />
|
||||
</div>
|
||||
<h4 className="text-xl font-bold mb-3 text-brand-black">Atendimento Humanizado</h4>
|
||||
<p className="text-gray-600 leading-relaxed">
|
||||
Entendemos que seu evento é um sonho a ser realizado.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decorative elements */}
|
||||
<div className="absolute top-10 right-10 w-32 h-32 rounded-full opacity-10" style={{ backgroundColor: '#B9CF33' }}></div>
|
||||
<div className="absolute bottom-10 left-10 w-24 h-24 rounded-full opacity-10" style={{ backgroundColor: '#C2388B' }}></div>
|
||||
</section>
|
||||
|
||||
{/* Testimonials */}
|
||||
<section className="py-20 bg-brand-black text-white">
|
||||
<div className="max-w-4xl mx-auto px-4 text-center">
|
||||
<Star className="text-brand-gold mx-auto mb-6" size={40} fill="#c5a059" />
|
||||
<blockquote className="text-2xl md:text-3xl font-serif italic leading-relaxed mb-8">
|
||||
"A equipe do Photum superou todas as expectativas. O sistema de acompanhamento nos deixou tranquilos durante todo o processo e as fotos ficaram incríveis."
|
||||
</blockquote>
|
||||
<cite className="not-italic">
|
||||
<span className="font-bold block text-brand-gold">Mariana & Pedro</span>
|
||||
<span className="text-sm text-gray-400 uppercase tracking-widest">Casamento em Campos do Jordão</span>
|
||||
</cite>
|
||||
{/* Contact Section */}
|
||||
<section className="py-24 relative overflow-hidden" style={{ backgroundColor: '#492E61' }}>
|
||||
<div className="max-w-7xl mx-auto px-8 sm:px-12 lg:px-20">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4 tracking-wide">ENTRE EM CONTATO</h2>
|
||||
<p className="text-white/90 text-lg">Envie sua mensagem, ligue ou faça uma visita em nossa empresa!</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-20 items-start max-w-6xl mx-auto">
|
||||
{/* Left side - Contact Info */}
|
||||
<div className="space-y-6 text-white">
|
||||
<div className="group">
|
||||
<div className="flex items-start gap-4 p-3 rounded-lg transition-all duration-300 hover:bg-white/10">
|
||||
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-white/20 flex items-center justify-center">
|
||||
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold mb-1">Rua Bom Recreio, 305</p>
|
||||
<p className="text-white/80 text-sm">Jd. Boer II • Americana SP</p>
|
||||
<p className="text-white/80 text-sm mb-1">CEP 13477-720</p>
|
||||
<a
|
||||
href="https://goo.gl/maps/4EwukztUUXP2"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs underline hover:text-white/70 transition-colors inline-flex items-center gap-1"
|
||||
>
|
||||
Ver no mapa →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="group">
|
||||
<div className="flex items-start gap-4 p-3 rounded-lg transition-all duration-300 hover:bg-white/10">
|
||||
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-white/20 flex items-center justify-center">
|
||||
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p>(19) 3405.5024</p>
|
||||
<p>(19) 3621.4621</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="group">
|
||||
<div className="flex items-start gap-4 p-3 rounded-lg transition-all duration-300 hover:bg-white/10">
|
||||
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-white/20 flex items-center justify-center">
|
||||
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<a href="mailto:contato@photum.com.br" className="hover:text-white/70 transition-colors">
|
||||
contato@photum.com.br
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right side - Contact Form */}
|
||||
<div className="space-y-5 bg-white/5 p-6 rounded-xl backdrop-blur-sm">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Nome"
|
||||
className="w-full px-3 py-3 bg-transparent border-b-2 border-white/30 text-white placeholder-white/60 focus:border-white focus:outline-none transition-colors"
|
||||
/>
|
||||
<input
|
||||
type="tel"
|
||||
placeholder="Telefone"
|
||||
className="w-full px-3 py-3 bg-transparent border-b-2 border-white/30 text-white placeholder-white/60 focus:border-white focus:outline-none transition-colors"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="Mensagem"
|
||||
rows={4}
|
||||
className="w-full px-3 py-3 bg-transparent border-b-2 border-white/30 text-white placeholder-white/60 focus:border-white focus:outline-none transition-colors resize-none"
|
||||
></textarea>
|
||||
<button
|
||||
className="w-full px-8 py-3 bg-white text-brand-black font-bold rounded-lg hover:bg-white/90 transition-all transform hover:scale-[1.02] active:scale-95 shadow-lg"
|
||||
>
|
||||
enviar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,24 +2,31 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Button } from '../components/Button';
|
||||
import { Input } from '../components/Input';
|
||||
import { InstitutionForm } from '../components/InstitutionForm';
|
||||
import { useData } from '../contexts/DataContext';
|
||||
|
||||
interface RegisterProps {
|
||||
onNavigate: (page: string) => void;
|
||||
}
|
||||
|
||||
export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
||||
const { addInstitution } = useData();
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
confirmPassword: '',
|
||||
wantsToAddInstitution: false
|
||||
});
|
||||
const [agreedToTerms, setAgreedToTerms] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [showInstitutionForm, setShowInstitutionForm] = useState(false);
|
||||
const [tempUserId] = useState(`user-${Date.now()}`);
|
||||
|
||||
const handleChange = (field: string, value: string) => {
|
||||
const handleChange = (field: string, value: string | boolean) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
setError('');
|
||||
};
|
||||
|
|
@ -29,6 +36,13 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
|||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
// Validação do checkbox de termos
|
||||
if (!agreedToTerms) {
|
||||
setError('Você precisa concordar com os termos de uso para continuar');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validações
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
setError('As senhas não coincidem');
|
||||
|
|
@ -42,6 +56,13 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
|||
return;
|
||||
}
|
||||
|
||||
// If user wants to add institution, show form
|
||||
if (formData.wantsToAddInstitution) {
|
||||
setIsLoading(false);
|
||||
setShowInstitutionForm(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Simular registro (conta será criada como Cliente/EVENT_OWNER automaticamente)
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
|
|
@ -52,6 +73,44 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
|||
}, 1500);
|
||||
};
|
||||
|
||||
const handleInstitutionSubmit = (institutionData: any) => {
|
||||
const newInstitution = {
|
||||
...institutionData,
|
||||
id: `inst-${Date.now()}`,
|
||||
ownerId: tempUserId
|
||||
};
|
||||
addInstitution(newInstitution);
|
||||
setShowInstitutionForm(false);
|
||||
|
||||
// Complete registration
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
setSuccess(true);
|
||||
setTimeout(() => {
|
||||
onNavigate('login');
|
||||
}, 2000);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
// Show institution form modal
|
||||
if (showInstitutionForm) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<InstitutionForm
|
||||
onCancel={() => {
|
||||
// Apenas fecha o modal e volta para o formulário
|
||||
setShowInstitutionForm(false);
|
||||
// Desmarca a opção de cadastrar universidade
|
||||
setFormData(prev => ({ ...prev, wantsToAddInstitution: false }));
|
||||
}}
|
||||
onSubmit={handleInstitutionSubmit}
|
||||
userId={tempUserId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-white">
|
||||
|
|
@ -73,7 +132,7 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
|||
{/* Left Side - Image */}
|
||||
<div className="hidden lg:block lg:w-1/2 relative overflow-hidden">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1511285560929-80b456fea0bc?ixlib=rb-1.2.1&auto=format&fit=crop&w=1920&q=80"
|
||||
src="https://images.unsplash.com/photo-1541339907198-e08756dedf3f?fm=jpg&q=60&w=3000&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
||||
alt="Photum Cadastro"
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
|
|
@ -131,6 +190,7 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
|||
placeholder="(00) 00000-0000"
|
||||
value={formData.phone}
|
||||
onChange={(e) => handleChange('phone', e.target.value)}
|
||||
mask="phone"
|
||||
/>
|
||||
|
||||
<Input
|
||||
|
|
@ -149,36 +209,54 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
|||
placeholder="••••••••"
|
||||
value={formData.confirmPassword}
|
||||
onChange={(e) => handleChange('confirmPassword', e.target.value)}
|
||||
error={error}
|
||||
error={error && (error.includes('senha') || error.includes('coincidem')) ? error : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start">
|
||||
<div>
|
||||
<div className="flex items-start">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={agreedToTerms}
|
||||
onChange={(e) => setAgreedToTerms(e.target.checked)}
|
||||
className="mt-1 h-4 w-4 text-brand-gold focus:ring-brand-gold border-gray-300 rounded"
|
||||
/>
|
||||
<label className="ml-2 text-sm text-gray-600">
|
||||
Concordo com os{' '}
|
||||
<a href="#" className="text-brand-gold hover:text-brand-gold/80">
|
||||
termos de uso
|
||||
</a>{' '}
|
||||
e{' '}
|
||||
<a href="#" className="text-brand-gold hover:text-brand-gold/80">
|
||||
política de privacidade
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
{error && error.includes('termos') && (
|
||||
<span className="text-xs text-red-500 mt-1 block ml-6">{error}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-start bg-gray-50 border border-gray-200 rounded-sm p-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
required
|
||||
checked={formData.wantsToAddInstitution}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, wantsToAddInstitution: e.target.checked }))}
|
||||
className="mt-1 h-4 w-4 text-brand-gold focus:ring-brand-gold border-gray-300 rounded"
|
||||
/>
|
||||
<label className="ml-2 text-sm text-gray-600">
|
||||
Concordo com os{' '}
|
||||
<a href="#" className="text-brand-gold hover:text-brand-gold/80">
|
||||
termos de uso
|
||||
</a>{' '}
|
||||
e{' '}
|
||||
<a href="#" className="text-brand-gold hover:text-brand-gold/80">
|
||||
política de privacidade
|
||||
</a>
|
||||
<label className="ml-2 text-sm text-gray-700">
|
||||
<span className="font-medium">Cadastrar universidade agora (Opcional)</span>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Você pode cadastrar sua universidade durante o cadastro ou posteriormente no sistema.
|
||||
Trabalhamos exclusivamente com eventos fotográficos em universidades.
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full" size="lg" isLoading={isLoading}>
|
||||
Criar Conta
|
||||
{formData.wantsToAddInstitution ? 'Continuar para Universidade' : 'Criar Conta'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="text-center text-xs text-gray-500 pt-4">
|
||||
Ao criar uma conta, você concorda em receber atualizações e novidades sobre nossos serviços.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
BIN
public/logo23.png
Normal file
BIN
public/logo23.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
BIN
public/logofav.png
Normal file
BIN
public/logofav.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
14
types.ts
14
types.ts
|
|
@ -29,6 +29,19 @@ export interface User {
|
|||
email: string;
|
||||
role: UserRole;
|
||||
avatar?: string;
|
||||
institutionId?: string; // Instituição vinculada ao usuário
|
||||
}
|
||||
|
||||
export interface Institution {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string; // Ex: Universidade Pública, Universidade Privada, Faculdade, etc.
|
||||
cnpj?: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
address?: Address;
|
||||
description?: string;
|
||||
ownerId: string; // ID do usuário que criou a instituição
|
||||
}
|
||||
|
||||
export interface Address {
|
||||
|
|
@ -79,4 +92,5 @@ export interface EventData {
|
|||
attachments: Attachment[];
|
||||
ownerId: string; // ID do cliente dono do evento
|
||||
photographerIds: string[]; // IDs dos fotógrafos designados
|
||||
institutionId?: string; // ID da instituição vinculada (obrigatório)
|
||||
}
|
||||
Loading…
Reference in a new issue