© 2025 PhotumFormaturas. Todos os direitos reservados.
-
- Política de Privacidade
-
-
- Termos de Uso
-
-
- Instagram
-
+
diff --git a/components/EventForm.tsx b/components/EventForm.tsx
index 641ccd0..5004aa2 100644
--- a/components/EventForm.tsx
+++ b/components/EventForm.tsx
@@ -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
= ({ 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([]);
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 = ({ 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 = ({ 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 = ({ 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 (
+
+ setShowInstitutionForm(false)}
+ onSubmit={handleInstitutionSubmit}
+ userId={user?.id || ''}
+ />
+
+ );
+ }
+
return (
@@ -194,6 +237,70 @@ export const EventForm: React.FC
= ({ onCancel, onSubmit, initia
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
/>
+ {/* Institution Selection - OBRIGATÓRIO */}
+
+
+
+ {userInstitutions.length === 0 ? (
+
+
+
+
+
+ Nenhuma universidade cadastrada
+
+
+ Você precisa cadastrar uma universidade antes de criar um evento.
+ Trabalhamos exclusivamente com eventos fotográficos em universidades.
+
+
+
+
+
+ ) : (
+
+
+
+
+
+ {formData.institutionId && (
+
+
+ Universidade selecionada com sucesso
+
+ )}
+
+ )}
+
+
{/* Cover Image Upload */}
diff --git a/components/Input.tsx b/components/Input.tsx
index 0a998d4..ab85651 100644
--- a/components/Input.tsx
+++ b/components/Input.tsx
@@ -1,22 +1,91 @@
-import React from 'react';
+import React, { useState } from 'react';
+import { Eye, EyeOff } from 'lucide-react';
interface InputProps extends React.InputHTMLAttributes
{
label: string;
error?: string;
+ mask?: 'phone' | 'cnpj' | 'cep';
}
-export const Input: React.FC = ({ label, error, className = '', ...props }) => {
+export const Input: React.FC = ({ 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) => {
+ 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 (
);
diff --git a/components/InstitutionForm.tsx b/components/InstitutionForm.tsx
new file mode 100644
index 0000000..32fcac6
--- /dev/null
+++ b/components/InstitutionForm.tsx
@@ -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) => 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 = ({
+ onCancel,
+ onSubmit,
+ initialData,
+ userId
+}) => {
+ const [formData, setFormData] = useState>(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 (
+
+
+ {/* Success Toast */}
+ {showToast && (
+
+
+
+
Sucesso!
+
Universidade cadastrada com sucesso.
+
+
+ )}
+
+ {/* Form Header */}
+
+
+
+
+
+ {initialData ? 'Editar Universidade' : 'Cadastrar Universidade'}
+
+
+ Registre a universidade onde os eventos fotográficos serão realizados
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/contexts/DataContext.tsx b/contexts/DataContext.tsx
index 60e003b..4134503 100644
--- a/contexts/DataContext.tsx
+++ b/contexts/DataContext.tsx
@@ -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) => void;
+ getInstitutionsByUserId: (userId: string) => Institution[];
+ getInstitutionById: (id: string) => Institution | undefined;
}
const DataContext = createContext(undefined);
export const DataProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [events, setEvents] = useState(INITIAL_EVENTS);
+ const [institutions, setInstitutions] = useState(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) => {
+ 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 (
-
+
{children}
);
diff --git a/package-lock.json b/package-lock.json
index 0295287..50aa41d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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"
}
}
-}
\ No newline at end of file
+}
diff --git a/package.json b/package.json
index 2036e4c..3486521 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
-}
\ No newline at end of file
+}
diff --git a/pages/Dashboard.tsx b/pages/Dashboard.tsx
index 6ce0f58..7fcf0a7 100644
--- a/pages/Dashboard.tsx
+++ b/pages/Dashboard.tsx
@@ -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 = ({ 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(null);
@@ -298,6 +298,49 @@ export const Dashboard: React.FC = ({ initialView = 'list' }) =>
)}
+ {/* Institution Information */}
+ {selectedEvent.institutionId && (() => {
+ const institution = getInstitutionById(selectedEvent.institutionId);
+ if (institution) {
+ return (
+
+
+
+
+
+
+
{institution.name}
+
{institution.type}
+
+
+
+
Contato
+
{institution.phone}
+
{institution.email}
+
+
+ {institution.address && (
+
+
Endereço
+
{institution.address.street}, {institution.address.number}
+
{institution.address.city} - {institution.address.state}
+
+ )}
+
+
+ {institution.description && (
+
+ {institution.description}
+
+ )}
+
+
+
+ );
+ }
+ return null;
+ })()}
+
Sobre o Evento
{selectedEvent.briefing || "Sem briefing detalhado."}
diff --git a/pages/Home.tsx b/pages/Home.tsx
index 58edd4c..1217266 100644
--- a/pages/Home.tsx
+++ b/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 = ({ onEnter }) => {