diff --git a/App.tsx b/App.tsx deleted file mode 100644 index 4cf001f..0000000 --- a/App.tsx +++ /dev/null @@ -1,114 +0,0 @@ - -import React, { useState, useEffect } from 'react'; -import { Navbar } from './components/Navbar'; -import { Home } from './pages/Home'; -import { Dashboard } from './pages/Dashboard'; -import { Login } from './pages/Login'; -import { AuthProvider, useAuth } from './contexts/AuthContext'; -import { DataProvider } from './contexts/DataContext'; -import { Construction } from 'lucide-react'; // Placeholder icon - -const AppContent: React.FC = () => { - const { user } = useAuth(); - const [currentPage, setCurrentPage] = useState('home'); - - useEffect(() => { - if (user && currentPage === 'login') { - setCurrentPage('dashboard'); - } - }, [user, currentPage]); - - // Simple Router Logic - const renderPage = () => { - if (currentPage === 'home') return setCurrentPage(user ? 'dashboard' : 'login')} />; - if (currentPage === 'login') return user ? : ; - - // Protected Routes Check - if (!user) return ; - - switch (currentPage) { - case 'dashboard': - case 'events': - return ; - - case 'request-event': - return ; - - case 'uploads': - return ; - - // Placeholder routes for future implementation - case 'team': - case 'finance': - case 'settings': - case 'albums': - case 'calendar': - return ( -
-
-
- -
-

- {currentPage === 'team' ? 'Equipe & Fotógrafos' : - currentPage === 'finance' ? 'Financeiro' : - currentPage === 'calendar' ? 'Agenda' : - currentPage} -

-

- Esta funcionalidade está em desenvolvimento e estará disponível em breve no seu painel. -

- -
-
- ); - - default: - // Fallback - return ; - } - }; - - return ( -
- -
- {renderPage()} -
- - {/* Footer only on Home */} - {currentPage === 'home' && ( - - )} -
- ); -}; - -function App() { - return ( - - - - - - ); -} - -export default App; diff --git a/README.md b/README.md deleted file mode 100644 index 244e08c..0000000 --- a/README.md +++ /dev/null @@ -1,20 +0,0 @@ -
-GHBanner -
- -# Run and deploy your AI Studio appp - -This contains everything you need to run your app locally. - -View your app in AI Studio: https://ai.studio/apps/drive/1Rd4siG8Ot2v0r3XhTNfIYrylHVUvYJmm - -## Run Locally - -**Prerequisites:** Node.js - - -1. Install dependencies: - `npm install` -2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key -3. Run the app: - `npm run dev` diff --git a/backend/.gitkeep b/backend/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/components/EventForm.tsx b/components/EventForm.tsx deleted file mode 100644 index aae8dda..0000000 --- a/components/EventForm.tsx +++ /dev/null @@ -1,405 +0,0 @@ - -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 { searchLocationWithGemini, GeoResult } from '../services/genaiService'; -import { useAuth } from '../contexts/AuthContext'; -import { UserRole } from '../types'; - -interface EventFormProps { - onCancel: () => void; - onSubmit: (data: any) => void; - initialData?: any; -} - -export const EventForm: React.FC = ({ onCancel, onSubmit, initialData }) => { - const { user } = useAuth(); - 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); - - // Default State or Initial Data - const [formData, setFormData] = useState(initialData || { - name: '', - date: '', - time: '', - type: '', - status: EventStatus.PLANNING, - address: { street: '', number: '', city: '', state: '', zip: '', mapLink: '' } as Address, - 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 - }); - - const isClientRequest = user?.role === UserRole.EVENT_OWNER; - const formTitle = initialData - ? "Editar Evento" - : (isClientRequest ? "Solicitar Orçamento/Evento" : "Cadastrar Novo Evento"); - const submitLabel = initialData ? "Salvar Alterações" : (isClientRequest ? "Enviar Solicitação" : "Criar Evento"); - - // Address Autocomplete Logic using Gemini - useEffect(() => { - const timer = setTimeout(async () => { - if (addressQuery.length > 4) { // Increased threshold to avoid spamming API - setIsSearching(true); - const results = await searchLocationWithGemini(addressQuery); - setAddressResults(results); - setIsSearching(false); - } else { - setAddressResults([]); - } - }, 800); // Increased debounce for API efficiency - return () => clearTimeout(timer); - }, [addressQuery]); - - const handleAddressSelect = (addr: GeoResult) => { - setFormData((prev: any) => ({ - ...prev, - address: { - street: addr.street, - number: addr.number, - city: addr.city, - state: addr.state, - zip: addr.zip, - mapLink: addr.mapLink - } - })); - setAddressQuery(''); - setAddressResults([]); - }; - - const addContact = () => { - setFormData((prev: any) => ({ - ...prev, - contacts: [...prev.contacts, { name: '', role: '', phone: '' }] - })); - }; - - const removeContact = (index: number) => { - const newContacts = [...formData.contacts]; - newContacts.splice(index, 1); - setFormData((prev: any) => ({ ...prev, contacts: newContacts })); - }; - - const handleFileUpload = (e: React.ChangeEvent) => { - if (e.target.files) { - setFormData((prev: any) => ({ - ...prev, - files: [...prev.files, ...Array.from(e.target.files || [])] - })); - } - }; - - const handleSubmit = () => { - // Show toast - setShowToast(true); - // Call original submit after small delay for visual effect or immediately - setTimeout(() => { - onSubmit(formData); - }, 1000); - }; - - return ( -
- - {/* Success Toast */} - {showToast && ( -
- -
-

Sucesso!

-

As informações foram salvas.

-
-
- )} - - {/* Form Header */} -
-
-

{formTitle}

-

- {isClientRequest - ? "Preencha os detalhes do seu sonho. Nossa equipe analisará em breve." - : "Preencha as informações técnicas do evento."} -

-
-
- {['details', 'location', 'briefing', 'files'].map((tab, idx) => ( -
- - {idx + 1} - -
- ))} -
-
- -
- {/* Sidebar Navigation for Form */} -
- {[ - { id: 'details', label: 'Detalhes & Data' }, - { id: 'location', label: 'Localização' }, - { id: 'briefing', label: isClientRequest ? 'Seus Desejos' : 'Briefing & Equipe' }, - { id: 'files', label: 'Inspirações' } - ].map(item => ( - - ))} -
- - {/* Form Content */} -
- - {activeTab === 'details' && ( -
-
- setFormData({...formData, name: e.target.value})} - /> -
- setFormData({...formData, date: e.target.value})} - /> - setFormData({...formData, time: e.target.value})} - /> -
- setFormData({...formData, coverImage: e.target.value})} - /> -
- -
- -
-
- )} - - {activeTab === 'location' && ( -
-
- -
- setAddressQuery(e.target.value)} - /> -
- {isSearching ? ( -
- ) : ( - - )} -
-
- - {addressResults.length > 0 && ( -
    - {addressResults.map((addr, idx) => ( -
  • handleAddressSelect(addr)} - > -
    -
    - -
    -

    {addr.description}

    -

    {addr.city}, {addr.state}

    -
    -
    - {addr.mapLink && ( - - Maps - Maps - - )} -
    -
  • - ))} -
- )} -
- -
-
- -
- setFormData({...formData, address: {...formData.address, number: e.target.value}})} - /> -
-
- - -
- - {formData.address.mapLink && ( -
- - - Localização verificada via Google Maps - - - Ver no mapa - -
- )} - -
- - -
-
- )} - - {activeTab === 'briefing' && ( -
-
- - -
- -
-
- - -
-
- {formData.contacts.map((contact: any, idx: number) => ( -
- { - const newContacts = [...formData.contacts]; - newContacts[idx].name = e.target.value; - setFormData({...formData, contacts: newContacts}); - }} - /> - { - const newContacts = [...formData.contacts]; - newContacts[idx].role = e.target.value; - setFormData({...formData, contacts: newContacts}); - }} - /> - -
- ))} -
-
- -
- - -
-
- )} - - {activeTab === 'files' && ( -
-
- - -

- {isClientRequest ? "Anexe referências visuais (Moodboard)" : "Anexe contratos e cronogramas"} -

-

PDF, JPG, PNG (Max 10MB)

-
- - {formData.files.length > 0 && ( -
-

Arquivos Selecionados:

- {formData.files.map((file: any, idx: number) => ( -
-
- -
-

{file.name}

-

{(file.size / 1024).toFixed(1)} KB

-
-
- -
- ))} -
- )} - -
- - -
-
- )} - -
-
-
- ); -}; \ No newline at end of file diff --git a/components/Input.tsx b/components/Input.tsx deleted file mode 100644 index 0a998d4..0000000 --- a/components/Input.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; - -interface InputProps extends React.InputHTMLAttributes { - label: string; - error?: string; -} - -export const Input: React.FC = ({ label, error, className = '', ...props }) => { - return ( -
- - - {error && {error}} -
- ); -}; - -interface SelectProps extends React.SelectHTMLAttributes { - label: string; - options: { value: string; label: string }[]; - error?: string; -} - -export const Select: React.FC = ({ label, options, error, className = '', ...props }) => { - return ( -
- - - {error && {error}} -
- ); -}; \ No newline at end of file diff --git a/components/Navbar.tsx b/components/Navbar.tsx deleted file mode 100644 index bb43a27..0000000 --- a/components/Navbar.tsx +++ /dev/null @@ -1,184 +0,0 @@ - -import React, { useState, useEffect } from 'react'; -import { UserRole } from '../types'; -import { useAuth } from '../contexts/AuthContext'; -import { Menu, X, LogOut } from 'lucide-react'; -import { Button } from './Button'; - -interface NavbarProps { - onNavigate: (page: string) => void; - currentPage: string; -} - -export const Navbar: React.FC = ({ onNavigate, currentPage }) => { - const { user, logout } = useAuth(); - const [isScrolled, setIsScrolled] = useState(false); - const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); - - useEffect(() => { - const handleScroll = () => { - setIsScrolled(window.scrollY > 20); - }; - window.addEventListener('scroll', handleScroll); - return () => window.removeEventListener('scroll', handleScroll); - }, []); - - const getLinks = () => { - if (!user) return []; - - switch (user.role) { - case UserRole.SUPERADMIN: - case UserRole.BUSINESS_OWNER: - return [ - { name: 'Gestão de Eventos', path: 'dashboard' }, - { name: 'Equipe & Fotógrafos', path: 'team' }, - { name: 'Financeiro', path: 'finance' }, - { name: 'Configurações', path: 'settings' } - ]; - case UserRole.EVENT_OWNER: - return [ - { name: 'Meus Eventos', path: 'dashboard' }, - { name: 'Solicitar Evento', path: 'request-event' }, - { name: 'Álbuns Entregues', path: 'albums' } - ]; - case UserRole.PHOTOGRAPHER: - return [ - { name: 'Eventos Designados', path: 'dashboard' }, - { name: 'Meus Uploads', path: 'uploads' }, - { name: 'Agenda', path: 'calendar' } - ]; - default: - return []; - } - }; - - const getRoleLabel = () => { - if (!user) return ""; - if (user.role === UserRole.BUSINESS_OWNER) return "Empresa"; - if (user.role === UserRole.EVENT_OWNER) return "Cliente"; - if (user.role === UserRole.PHOTOGRAPHER) return "Fotógrafo"; - if (user.role === UserRole.SUPERADMIN) return "Super Admin"; - return ""; - }; - - return ( - - ); -}; diff --git a/contexts/DataContext.tsx b/contexts/DataContext.tsx deleted file mode 100644 index 60e003b..0000000 --- a/contexts/DataContext.tsx +++ /dev/null @@ -1,123 +0,0 @@ - -import React, { createContext, useContext, useState, ReactNode } from 'react'; -import { EventData, EventStatus, EventType, Attachment } from '../types'; - -// Initial Mock Data -const INITIAL_EVENTS: EventData[] = [ - { - id: '1', - name: 'Casamento Juliana & Marcos', - date: '2024-10-15', - time: '16:00', - type: EventType.WEDDING, - status: EventStatus.CONFIRMED, - address: { - street: 'Av. das Hortênsias', - number: '1200', - city: 'Gramado', - state: 'RS', - zip: '95670-000' - }, - briefing: 'Cerimônia ao pôr do sol. Foco em fotos espontâneas dos noivos e pais.', - coverImage: 'https://picsum.photos/id/1059/800/400', - contacts: [{ id: 'c1', name: 'Cerimonial Silva', role: 'Cerimonialista', phone: '9999-9999', email: 'c@teste.com'}], - checklist: [], - attachments: [ - { name: 'Ensaio 1', size: '2mb', type: 'image/jpeg', url: 'https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=400&q=80' }, - { name: 'Ensaio 2', size: '2mb', type: 'image/jpeg', url: 'https://images.unsplash.com/photo-1511285560982-1351cdeb9821?auto=format&fit=crop&w=400&q=80' } - ], - ownerId: 'client-1', - photographerIds: ['photographer-1'] - }, - { - id: '2', - name: 'Conferência Tech Innovators', - date: '2024-11-05', - time: '08:00', - type: EventType.CORPORATE, - status: EventStatus.PENDING_APPROVAL, - address: { - street: 'Rua Olimpíadas', - number: '205', - city: 'São Paulo', - state: 'SP', - zip: '04551-000' - }, - briefing: 'Cobrir palestras principais e networking no coffee break.', - coverImage: 'https://picsum.photos/id/3/800/400', - contacts: [], - checklist: [], - attachments: [], - ownerId: 'client-2', // Other client - photographerIds: [] - } -]; - -interface DataContextType { - events: EventData[]; - 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; -} - -const DataContext = createContext(undefined); - -export const DataProvider: React.FC<{ children: ReactNode }> = ({ children }) => { - const [events, setEvents] = useState(INITIAL_EVENTS); - - const addEvent = (event: EventData) => { - setEvents(prev => [event, ...prev]); - }; - - const updateEventStatus = (id: string, status: EventStatus) => { - setEvents(prev => prev.map(e => e.id === id ? { ...e, status } : e)); - }; - - const assignPhotographer = (eventId: string, photographerId: string) => { - setEvents(prev => prev.map(e => { - if (e.id === eventId) { - const current = e.photographerIds || []; - if (!current.includes(photographerId)) { - return { ...e, photographerIds: [...current, photographerId] }; - } - } - return e; - })); - }; - - const getEventsByRole = (userId: string, role: string) => { - if (role === 'SUPERADMIN' || role === 'BUSINESS_OWNER') { - return events; - } - if (role === 'EVENT_OWNER') { - return events.filter(e => e.ownerId === userId); - } - if (role === 'PHOTOGRAPHER') { - return events.filter(e => e.photographerIds.includes(userId)); - } - return []; - }; - - const addAttachment = (eventId: string, attachment: Attachment) => { - setEvents(prev => prev.map(e => { - if (e.id === eventId) { - return { ...e, attachments: [...e.attachments, attachment] }; - } - return e; - })); - }; - - return ( - - {children} - - ); -}; - -export const useData = () => { - const context = useContext(DataContext); - if (!context) throw new Error('useData must be used within a DataProvider'); - return context; -}; \ No newline at end of file diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..0229e78 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,12 @@ +# Configuração da API do Mapbox +# Obtenha sua chave gratuita em: https://account.mapbox.com/ + +# Para usar: +# 1. Copie este arquivo para .env.local +# 2. Substitua YOUR_MAPBOX_TOKEN_HERE pela sua chave real +# 3. Atualize o arquivo services/mapboxService.ts com sua chave + +VITE_MAPBOX_TOKEN=YOUR_MAPBOX_TOKEN_HERE + +# Nota: A chave já está configurada diretamente no código para demonstração +# Em produção, use variáveis de ambiente como acima diff --git a/.gitignore b/frontend/.gitignore similarity index 100% rename from .gitignore rename to frontend/.gitignore diff --git a/frontend/App.tsx b/frontend/App.tsx new file mode 100644 index 0000000..71cbef8 --- /dev/null +++ b/frontend/App.tsx @@ -0,0 +1,266 @@ +import React, { useState, useEffect } from "react"; +import { Navbar } from "./components/Navbar"; +import { Home } from "./pages/Home"; +import { Dashboard } from "./pages/Dashboard"; +import { Login } from "./pages/Login"; +import { Register } from "./pages/Register"; +import { CalendarPage } from "./pages/Calendar"; +import { TeamPage } from "./pages/Team"; +import { FinancePage } from "./pages/Finance"; +import { SettingsPage } from "./pages/Settings"; +import { InspirationPage } from "./pages/Inspiration"; +import { PrivacyPolicy } from "./pages/PrivacyPolicy"; +import { TermsOfUse } from "./pages/TermsOfUse"; +import { LGPD } from "./pages/LGPD"; +import { AuthProvider, useAuth } from "./contexts/AuthContext"; +import { DataProvider } from "./contexts/DataContext"; +import { Construction } from "lucide-react"; + +const AppContent: React.FC = () => { + const { user } = useAuth(); + const [currentPage, setCurrentPage] = useState("home"); + + useEffect(() => { + if (user && currentPage === "login") { + setCurrentPage("dashboard"); + } + }, [user, currentPage]); + + const renderPage = () => { + if (currentPage === "home") + return ( + setCurrentPage(user ? "dashboard" : "login")} /> + ); + if (currentPage === "login") + return user ? : ; + if (currentPage === "register") + return user ? : ; + if (currentPage === "privacy") + return ; + if (currentPage === "terms") + return ; + if (currentPage === "lgpd") return ; + + if (!user) return ; + + switch (currentPage) { + case "dashboard": + case "events": + return ; + + case "request-event": + return ; + + case "inspiration": + return ; + + case "calendar": + return ; + + case "team": + return ; + + case "finance": + return ; + + case "settings": + return ; + + default: + return ; + } + }; + + return ( +
+ +
{renderPage()}
+ + {currentPage === "home" && ( + + )} +
+ ); +}; + +function App() { + return ( + + + + + + ); +} + +export default App; diff --git a/frontend/MAPBOX_SETUP.md b/frontend/MAPBOX_SETUP.md new file mode 100644 index 0000000..981ab88 --- /dev/null +++ b/frontend/MAPBOX_SETUP.md @@ -0,0 +1,79 @@ +# 🗺️ Configuração do Mapbox + +## Token do Mapbox Inválido + +O sistema está configurado para usar a API do Mapbox, mas o token atual é inválido ou expirado. + +## Como Obter um Token Válido (GRATUITO) + +### 1. Crie uma conta no Mapbox + +- Acesse: **https://account.mapbox.com/** +- Clique em **"Sign up"** (ou faça login se já tiver conta) +- É **100% gratuito** para até 50.000 requisições/mês + +### 2. Acesse a página de Tokens + +- Após fazer login, vá para: **https://account.mapbox.com/access-tokens/** +- Ou clique no menu em **"Access tokens"** + +### 3. Crie um novo token + +- Clique em **"Create a token"** +- Dê um nome (ex: "Photum Forms") +- Selecione os escopos necessários (deixe os padrões marcados) +- Clique em **"Create token"** + +### 4. Copie o token + +- O token será algo como: `pk.eyJ1IjoiZXhhbXBsZSIsImEiOiJja...` +- **COPIE O TOKEN COMPLETO** + +### 5. Configure no Projeto + +Abra o arquivo **`services/mapboxService.ts`** e substitua o token na linha 26: + +```typescript +const MAPBOX_TOKEN = "SEU_TOKEN_AQUI"; // Cole o token do Mapbox aqui +``` + +### 6. Salve e recarregue + +Após salvar o arquivo, o Vite recarregará automaticamente e o mapa funcionará! + +--- + +## Verificação + +Se tudo estiver correto, você verá: + +- ✅ "Inicializando mapa Mapbox..." no console +- ✅ "Mapa criado com sucesso" no console +- ✅ Mapa interativo carregado na tela de criação de eventos + +Se houver erro: + +- ❌ Verifique se copiou o token completo (incluindo `pk.`) +- ❌ Verifique se não há espaços extras antes/depois do token +- ❌ Certifique-se de que o token não expirou + +--- + +## Recursos do Mapbox no Sistema + +- 🔍 Busca de endereços com autocomplete +- 📍 Mapa interativo com pin arrastável +- 🌍 Geocoding e Reverse Geocoding +- 🗺️ Integração com Google Maps para compartilhamento + +## Limites Gratuitos + +O plano gratuito do Mapbox inclui: + +- 50.000 requisições de geocoding/mês +- 50.000 carregamentos de mapa/mês +- Mais que suficiente para este projeto! + +--- + +**Precisa de ajuda?** Acesse a documentação: https://docs.mapbox.com/api/ diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..e0e54ed --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,397 @@ +# 📸 Photum Manager + +Sistema de gestão completa para eventos fotográficos premium. Plataforma moderna para gerenciamento de eventos, fotógrafos, clientes e entregas, com foco em excelência e atendimento humanizado. + +--- + +## 🎯 Visão Geral + +O **Photum Manager** é uma aplicação web desenvolvida em React + TypeScript que centraliza todo o fluxo de trabalho de uma empresa de fotografia de eventos, desde a solicitação inicial do cliente até a entrega final do álbum. + +### Principais Funcionalidades + +- ✅ **Gestão Multi-perfil**: Suporte para 4 tipos de usuários (Superadmin, Empresa, Fotógrafo, Cliente) +- ✅ **Controle de Eventos**: CRUD completo de eventos com status workflow +- ✅ **Dashboard Personalizado**: Interface adaptada por perfil de usuário +- ✅ **Sistema de Aprovações**: Workflow de aprovação de eventos para clientes +- ✅ **Gestão de Equipe**: Atribuição de fotógrafos aos eventos +- ✅ **Upload de Fotos**: Sistema de anexos e galeria (em desenvolvimento) +- ✅ **Integração com IA**: Google GenAI para funcionalidades avançadas + +--- + +## 🚀 Como Executar o Projeto + +### Pré-requisitos + +- **Node.js** (versão 16 ou superior) +- **npm** ou **yarn** + +### Instalação + +1. **Clone o repositório**: + ```bash + git clone https://github.com/rede5/photum-frontend.git + cd photum-frontend + ``` + +2. **Instale as dependências**: + ```bash + npm install + ``` + +3. **Configure a API do Gemini** (opcional): + - Crie um arquivo `.env.local` na raiz do projeto + - Adicione sua chave de API: + ``` + GEMINI_API_KEY=sua_chave_aqui + ``` + +4. **Execute o projeto em modo desenvolvimento**: + ```bash + npm run dev + ``` + +5. **Acesse no navegador**: + ``` + http://localhost:5173 + ``` + +### Build para Produção + +```bash +npm run build +npm run preview +``` + +--- + +## 🗺️ Rotas e Navegação + +O sistema utiliza um **roteamento customizado baseado em estados** (sem React Router). As rotas disponíveis são: + +| Rota | Descrição | Acesso | +|------|-----------|--------| +| `/` (`home`) | Landing page institucional | Público | +| `/login` | Tela de autenticação | Público | +| `/dashboard` | Dashboard principal (Lista de eventos) | Autenticado | +| `/events` | Mesma view de dashboard | Autenticado | +| `/request-event` | Formulário de criação de evento | Autenticado | +| `/uploads` | Gerenciamento de uploads de fotos | Autenticado | +| `/team` | Gestão de equipe e fotógrafos | 🚧 Em desenvolvimento | +| `/finance` | Módulo financeiro | 🚧 Em desenvolvimento | +| `/calendar` | Agenda de eventos | 🚧 Em desenvolvimento | +| `/settings` | Configurações | 🚧 Em desenvolvimento | +| `/albums` | Álbuns de fotos | 🚧 Em desenvolvimento | + +### Navegação no Sistema + +A navegação acontece através do componente `` que chama a função `onNavigate(pageName)` passando o nome da rota desejada. Rotas protegidas redirecionam automaticamente para `/login` se o usuário não estiver autenticado. + +--- + +## 🔐 Tela de Login e Usuários de Demonstração + +### Como Acessar + +1. Na página inicial, clique em **"Área do Cliente"** +2. Você será redirecionado para a **tela de login** (`/login`) + +### Interface de Login + +A tela de login apresenta: +- **Layout dividido**: + - Lado esquerdo com imagem institucional + - Lado direito com formulário de login +- **Campos**: + - E-mail corporativo ou pessoal + - Senha (desabilitada no modo demo) +- **Botão**: "Entrar no Sistema" + +### Usuários de Demonstração + +Não é necessário senha. Basta clicar em um dos cartões de usuário demo ou digitar o e-mail: + +| Perfil | Nome | E-mail | Descrição | +|--------|------|--------|-----------| +| **Superadmin** | Dev Admin | `admin@photum.com` | Acesso total ao sistema | +| **Empresa** | Carlos CEO | `empresa@photum.com` | Dono da empresa de fotografia | +| **Fotógrafo** | Ana Lente | `foto@photum.com` | Profissional fotógrafo | +| **Cliente** | Juliana Noiva | `cliente@photum.com` | Cliente final (dono do evento) | + +### Fluxo de Login + +``` +1. Usuário clica em "Área do Cliente" na Home + ↓ +2. Sistema redireciona para /login + ↓ +3. Usuário seleciona um perfil demo OU digita e-mail manualmente + ↓ +4. Sistema valida o e-mail com mock de usuários + ↓ +5. Se válido: redireciona para /dashboard + Se inválido: exibe mensagem de erro +``` + +### Exemplo de Código (Login) + +```tsx +// Login simplificado (sem senha) +const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + const success = await login(email); // Busca no mock de usuários + if (!success) { + setError('Usuário não encontrado'); + } +}; +``` + +--- + +## 👥 Perfis de Usuário e Permissões + +### 1. Superadmin (`SUPERADMIN`) +- **Acesso**: Total ao sistema +- **Funcionalidades**: + - Visualiza todos os eventos + - Gerencia todos os usuários + - Acesso a configurações avançadas + +### 2. Dono da Empresa (`BUSINESS_OWNER`) +- **Acesso**: Administrativo +- **Funcionalidades**: + - Cria e edita eventos + - Atribui fotógrafos + - Aprova solicitações de clientes + - Gerencia equipe + +### 3. Fotógrafo (`PHOTOGRAPHER`) +- **Acesso**: Operacional +- **Funcionalidades**: + - Visualiza eventos atribuídos + - Atualiza status de eventos + - Faz upload de fotos + - Acessa detalhes de contatos + +### 4. Cliente (`EVENT_OWNER`) +- **Acesso**: Restrito aos próprios eventos +- **Funcionalidades**: + - Solicita novos eventos + - Acompanha status do seu evento + - Visualiza fotos do evento + - Eventos criados entram em status "Aguardando Aprovação" + +--- + +## 📊 Status de Eventos + +Os eventos seguem um workflow com os seguintes status: + +| Status | Descrição | Cor | +|--------|-----------|-----| +| `Aguardando Aprovação` | Cliente criou, aguarda aprovação da empresa | Cinza | +| `Em Planejamento` | Aprovado, em fase de planejamento | Azul | +| `Confirmado` | Evento confirmado e agendado | Verde | +| `Em Execução` | Evento acontecendo | Roxo | +| `Entregue` | Fotos entregues ao cliente | Verde escuro | +| `Arquivado` | Evento finalizado e arquivado | Cinza escuro | + +--- + +## 🛠️ Tecnologias Utilizadas + +### Core +- **React 19.2.0** - Biblioteca JavaScript para UI +- **TypeScript 5.8.2** - Superset tipado do JavaScript +- **Vite 6.2.0** - Build tool e dev server ultra-rápido + +### UI/UX +- **Lucide React** - Ícones modernos e customizáveis +- **TailwindCSS** - Framework CSS utility-first (configurado via classes) + +### IA e APIs +- **@google/genai 1.30.0** - Integração com Google Gemini AI + +### Build Tools +- **@vitejs/plugin-react 5.0.0** - Plugin oficial do Vite para React +- **@types/node 22.14.0** - Tipos TypeScript para Node.js + +--- + +## 📁 Estrutura do Projeto + +``` +photum-forms/ +├── components/ # Componentes reutilizáveis +│ ├── Button.tsx # Botão customizado +│ ├── EventCard.tsx # Card de evento +│ ├── EventForm.tsx # Formulário de evento +│ ├── Input.tsx # Input customizado +│ └── Navbar.tsx # Barra de navegação +├── contexts/ # Context API +│ ├── AuthContext.tsx # Autenticação e usuários +│ └── DataContext.tsx # Gerenciamento de eventos +├── pages/ # Páginas principais +│ ├── Dashboard.tsx # Dashboard principal +│ ├── Home.tsx # Landing page +│ └── Login.tsx # Tela de login +├── services/ # Serviços e APIs +│ ├── genaiService.ts # Integração Google GenAI +│ └── mockGeoService.ts # Mock de geolocalização +├── App.tsx # Componente raiz + roteamento +├── constants.ts # Constantes globais +├── types.ts # Tipos TypeScript +├── index.tsx # Entry point +├── package.json # Dependências +├── tsconfig.json # Config TypeScript +└── vite.config.ts # Config Vite +``` + +--- + +## 🎨 Temas e Design System + +### Cores da Marca + +```css +/* Cores principais */ +--brand-black: #000000 +--brand-gold: #c5a059 +--brand-white: #ffffff + +/* Status Colors (definidas em constants.ts) */ +Aguardando: #6b7280 (gray) +Planejamento: #3b82f6 (blue) +Confirmado: #10b981 (green) +Em Execução: #8b5cf6 (purple) +Entregue: #059669 (emerald) +Arquivado: #4b5563 (gray-dark) +``` + +### Tipografia + +- **Títulos**: Font Serif (elegante e sofisticada) +- **Corpo**: Font Sans (moderna e legível) +- **Tamanhos**: Sistema responsivo com classes Tailwind + +--- + +## 🔄 Workflows do Sistema + +### Workflow de Criação de Evento (Cliente) + +``` +1. Cliente faz login + ↓ +2. Acessa "Solicitar Evento" + ↓ +3. Preenche formulário (nome, tipo, data, endereço, contatos) + ↓ +4. Evento criado com status "Aguardando Aprovação" + ↓ +5. Empresa visualiza solicitação + ↓ +6. Empresa aprova → Status muda para "Confirmado" + ↓ +7. Empresa atribui fotógrafo + ↓ +8. Fotógrafo acessa evento e realiza trabalho + ↓ +9. Status atualizado conforme workflow + ↓ +10. Cliente visualiza fotos e álbum final +``` + +### Workflow de Gestão (Empresa) + +``` +1. Empresa cria evento diretamente (status: "Em Planejamento") + ↓ +2. Adiciona informações detalhadas + ↓ +3. Atribui fotógrafos à equipe + ↓ +4. Confirma evento (status: "Confirmado") + ↓ +5. Acompanha execução + ↓ +6. Marca como entregue após conclusão +``` + +--- + +## 🧪 Dados de Teste (Mock) + +O sistema utiliza dados mockados para demonstração: + +### Usuários Mock +- 4 usuários pré-cadastrados (ver seção de Login) + +### Eventos Mock +- Eventos de exemplo criados no `DataContext.tsx` +- Incluem diferentes tipos: Casamentos, Corporativos, Aniversários +- Variados status para demonstrar o workflow + +--- + +## 🚧 Funcionalidades em Desenvolvimento + +As seguintes rotas exibem tela de "Em construção": +- `/team` - Gestão de Equipe e Fotógrafos +- `/finance` - Módulo Financeiro +- `/calendar` - Agenda Completa +- `/settings` - Configurações do Sistema +- `/albums` - Galeria de Álbuns + +--- + +## 📝 Scripts Disponíveis + +| Comando | Descrição | +|---------|-----------| +| `npm run dev` | Inicia servidor de desenvolvimento (Vite) | +| `npm run build` | Gera build de produção otimizado | +| `npm run preview` | Preview do build de produção | + +--- + +## 🤝 Contribuindo + +Este é um projeto em desenvolvimento ativo. Contribuições são bem-vindas! + +### Branch Strategy +- **main**: Produção +- **dev**: Desenvolvimento ativo (branch atual) + +--- + +## 📄 Licença + +Projeto privado - Todos os direitos reservados © 2024 PhotumFormaturas + +--- + +## 📞 Suporte + +Para dúvidas ou suporte: +- **Repositório**: [github.com/rede5/photum-frontend](https://github.com/rede5/photum-frontend) +- **Issues**: Abra uma issue no GitHub + +--- + +## 🎯 Roadmap Futuro + +- [ ] Implementar módulo de Agenda/Calendário +- [ ] Sistema completo de Upload de Fotos +- [ ] Módulo Financeiro (Pagamentos e Orçamentos) +- [ ] Gestão de Equipe completa +- [ ] Sistema de Notificações em tempo real +- [ ] App Mobile (React Native) +- [ ] Integração com APIs de pagamento +- [ ] Assinatura digital de contratos +- [ ] Exportação de relatórios PDF + +--- + +**Desenvolvido com ❤️ para eternizar momentos únicos** ✨📸 diff --git a/components/Button.tsx b/frontend/components/Button.tsx similarity index 80% rename from components/Button.tsx rename to frontend/components/Button.tsx index 05e04c6..8c47f63 100644 --- a/components/Button.tsx +++ b/frontend/components/Button.tsx @@ -2,23 +2,23 @@ import React from 'react'; interface ButtonProps extends React.ButtonHTMLAttributes { variant?: 'primary' | 'secondary' | 'outline' | 'ghost'; - size?: 'sm' | 'md' | 'lg'; + size?: 'sm' | 'md' | 'lg' | 'xl'; isLoading?: boolean; } -export const Button: React.FC = ({ - children, - variant = 'primary', - size = 'md', +export const Button: React.FC = ({ + children, + variant = 'primary', + size = 'md', isLoading, - className = '', - ...props + className = '', + ...props }) => { const baseStyles = "inline-flex items-center justify-center font-medium transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; - + const variants = { primary: "bg-brand-black text-white hover:bg-gray-800 focus:ring-brand-black", - secondary: "bg-brand-gold text-white hover:bg-amber-600 focus:ring-brand-gold", + secondary: "bg-[#B9CF32] text-white hover:bg-[#a5bd2e] focus:ring-[#B9CF32]", outline: "border border-brand-black text-brand-black hover:bg-gray-50 focus:ring-brand-black", ghost: "text-brand-black hover:bg-gray-100 hover:text-gray-900" }; @@ -26,11 +26,12 @@ export const Button: React.FC = ({ const sizes = { sm: "text-xs px-3 py-1.5 rounded-sm", md: "text-sm px-5 py-2.5 rounded-sm", - lg: "text-base px-8 py-3 rounded-sm" + lg: "text-base px-8 py-3 rounded-sm", + xl: "text-lg px-10 py-4 rounded-md font-semibold" }; return ( - + ))} + + + {/* Form Content */} +
+ {activeTab === "details" && ( +
+
+ + setFormData({ ...formData, name: e.target.value }) + } + /> +
+ + setFormData({ ...formData, date: e.target.value }) + } + /> + + setFormData({ ...formData, time: e.target.value }) + } + /> +
+ + setFormData({ + ...formData, + institutionId: e.target.value, + }) + } + required + > + + {userInstitutions.map((inst) => ( + + ))} + + + + + {formData.institutionId && ( +
+ + + Universidade selecionada com sucesso + +
+ )} +
+ )} +
+ + {/* Cover Image Upload */} +
+ +
+ { + if (e.target.files && e.target.files[0]) { + const file = e.target.files[0]; + const imageUrl = URL.createObjectURL(file); + setFormData({ ...formData, coverImage: imageUrl }); + } + }} + /> +
+ + {formData.coverImage && + !formData.coverImage.startsWith("http") + ? "Imagem selecionada" + : formData.coverImage + ? "Imagem atual (URL)" + : "Clique para selecionar..."} + +
+ +
+
+
+ {formData.coverImage && ( +
+ Preview +
+ Visualização da Capa +
+
+ )} +
+
+ +
+ +
+ + )} + + {activeTab === "location" && ( +
+
+ +
+ setAddressQuery(e.target.value)} + /> +
+ {isSearching ? ( +
+ ) : ( + + )} +
+
+ + {addressResults.length > 0 && ( +
    + {addressResults.map((addr, idx) => ( +
  • handleAddressSelect(addr)} + > +
    +
    + +
    +

    + {addr.description} +

    +

    + {addr.city}, {addr.state} +

    +
    +
    + {addr.mapLink && ( + + Maps + Maps + + )} +
    +
  • + ))} +
+ )} +
+ +
+
+ { + setFormData({ + ...formData, + address: { + ...formData.address, + street: e.target.value, + }, + }); + }} + onBlur={handleManualAddressChange} + placeholder="Digite o nome da rua" + /> +
+ { + const value = e.target.value; + setFormData({ + ...formData, + address: { ...formData.address, number: value }, + }); + }} + onBlur={handleManualAddressChange} + type="text" + inputMode="numeric" + /> +
+
+ { + setFormData({ + ...formData, + address: { ...formData.address, city: e.target.value }, + }); + }} + onBlur={handleManualAddressChange} + placeholder="Digite a cidade" + /> + { + const value = e.target.value.toUpperCase().slice(0, 2); + setFormData({ + ...formData, + address: { ...formData.address, state: value }, + }); + }} + onBlur={handleManualAddressChange} + placeholder="SP" + maxLength={2} + /> +
+ + {/* Mapa Interativo */} +
+ + +
+ + {formData.address.mapLink && ( +
+ + + Localização verificada via Mapbox + + + Ver no mapa + +
+ )} + +
+ + +
+
+ )} + + {activeTab === "briefing" && ( +
+
+ + +
+ +
+
+ + +
+
+ {formData.contacts.map((contact: any, idx: number) => ( +
+ { + const newContacts = [...formData.contacts]; + newContacts[idx].name = e.target.value; + setFormData({ ...formData, contacts: newContacts }); + }} + /> + { + const newContacts = [...formData.contacts]; + newContacts[idx].role = e.target.value; + setFormData({ ...formData, contacts: newContacts }); + }} + /> + +
+ ))} +
+
+ +
+ + +
+
+ )} + + {activeTab === "files" && ( +
+
+ + +

+ {isClientRequest + ? "Anexe referências visuais (Moodboard)" + : "Anexe contratos e cronogramas"} +

+

+ PDF, JPG, PNG (Max 10MB) +

+
+ + {formData.files.length > 0 && ( +
+

+ Arquivos Selecionados: +

+ {formData.files.map((file: any, idx: number) => ( +
+
+ +
+

{file.name}

+

+ {(file.size / 1024).toFixed(1)} KB +

+
+
+ +
+ ))} +
+ )} + +
+ + +
+
+ )} + + + + ); +}; diff --git a/frontend/components/Input.tsx b/frontend/components/Input.tsx new file mode 100644 index 0000000..ab85651 --- /dev/null +++ b/frontend/components/Input.tsx @@ -0,0 +1,120 @@ +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 = '', 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 ( +
+ +
+ + {isPassword && ( + + )} +
+ {error && {error}} +
+ ); +}; + +interface SelectProps extends React.SelectHTMLAttributes { + label: string; + options: { value: string; label: string }[]; + error?: string; +} + +export const Select: React.FC = ({ label, options, error, className = '', ...props }) => { + return ( +
+ + + {error && {error}} +
+ ); +}; \ No newline at end of file diff --git a/frontend/components/InstitutionForm.tsx b/frontend/components/InstitutionForm.tsx new file mode 100644 index 0000000..32fcac6 --- /dev/null +++ b/frontend/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 +

+
+
+ +
+ +
+ + {/* Informações Básicas */} +
+

+ Informações Básicas +

+ + handleChange('name', e.target.value)} + required + /> + +
+ handleChange('cnpj', e.target.value)} + mask="cnpj" + /> +
+ +
+ handleChange('phone', e.target.value)} + mask="phone" + required + /> + + handleChange('email', e.target.value)} + required + /> +
+ +
+ + + +
+ + {/* Right side - WhatsApp Card */} +
+
+
+ {/* WhatsApp Icon */} +
+ + + +
+ + {/* Text */} +
+

Tire suas dúvidas

+

+ Faça orçamento direto no nosso WhatsApp +

+
+ + {/* WhatsApp Button */} + + + + + Falar no WhatsApp + + + {/* Additional Info */} +
+
+ Resposta rápida garantida +
+
+
+
+
+
+ + + ); +}; \ No newline at end of file diff --git a/frontend/pages/Inspiration.tsx b/frontend/pages/Inspiration.tsx new file mode 100644 index 0000000..d28376f --- /dev/null +++ b/frontend/pages/Inspiration.tsx @@ -0,0 +1,206 @@ +import React, { useState } from "react"; +import { Heart, Search, Filter } from "lucide-react"; +import { Construction } from "lucide-react"; + +const MOCK_GALLERIES = [ + { + id: 1, + title: "Formatura Medicina UNICAMP 2024", + category: "Medicina", + images: [ + "https://images.unsplash.com/photo-1523050854058-8df90110c9f1?w=800", + "https://images.unsplash.com/photo-1541339907198-e08756dedf3f?w=800", + "https://images.unsplash.com/photo-1523240795612-9a054b0db644?w=800", + ], + likes: 234, + }, + { + id: 2, + title: "Engenharia Civil - USP 2024", + category: "Engenharia", + images: [ + "https://images.unsplash.com/photo-1513542789411-b6a5d4f31634?w=800", + "https://images.unsplash.com/photo-1511632765486-a01980e01a18?w=800", + "https://images.unsplash.com/photo-1523050854058-8df90110c9f1?w=800", + ], + likes: 189, + }, + { + id: 3, + title: "Direito PUC 2023", + category: "Direito", + images: [ + "https://images.unsplash.com/photo-1519389950473-47ba0277781c?w=800", + "https://images.unsplash.com/photo-1521737711867-e3b97375f902?w=800", + "https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=800", + ], + likes: 312, + }, + { + id: 4, + title: "Arquitetura UNESP 2024", + category: "Arquitetura", + images: [ + "https://images.unsplash.com/photo-1528605248644-14dd04022da1?w=800", + "https://images.unsplash.com/photo-1523050854058-8df90110c9f1?w=800", + "https://images.unsplash.com/photo-1519337265831-281ec6cc8514?w=800", + ], + likes: 278, + }, +]; + +const CATEGORIES = [ + "Todas", + "Medicina", + "Engenharia", + "Direito", + "Arquitetura", + "Administração", +]; + +export const InspirationPage: React.FC = () => { + const [searchTerm, setSearchTerm] = useState(""); + const [selectedCategory, setSelectedCategory] = useState("Todas"); + + const filteredGalleries = MOCK_GALLERIES.filter((gallery) => { + const matchesSearch = gallery.title + .toLowerCase() + .includes(searchTerm.toLowerCase()); + const matchesCategory = + selectedCategory === "Todas" || gallery.category === selectedCategory; + return matchesSearch && matchesCategory; + }); + + return ( +
+
+ {/* Header */} +
+

+ Galeria de Inspiração +

+

+ Explore álbuns de formaturas anteriores e inspire-se para criar o + seu evento perfeito +

+
+ + {/* Search and Filter */} +
+ {/* Search Bar */} +
+ + setSearchTerm(e.target.value)} + /> +
+ + {/* Category Filter */} +
+ {CATEGORIES.map((category) => ( + + ))} +
+
+ + {/* Gallery Grid */} + {filteredGalleries.length > 0 ? ( +
+ {filteredGalleries.map((gallery) => ( +
+ {/* Main Image */} +
+ {gallery.title} +
+ + {/* Category Badge */} +
+ + {gallery.category} + +
+
+ + {/* Content */} +
+

+ {gallery.title} +

+ + {/* Thumbnail Preview */} +
+ {gallery.images.slice(1, 3).map((img, idx) => ( +
+ +
+ ))} +
+ +12 +
+
+ + {/* Footer */} +
+
+ + + {gallery.likes} curtidas + +
+ +
+
+
+ ))} +
+ ) : ( +
+

Nenhuma galeria encontrada

+
+ )} + + {/* Coming Soon Banner */} +
+ +

+ Em Breve: Mais Funcionalidades +

+

+ Estamos trabalhando para trazer mais galerias, filtros avançados e a + possibilidade de salvar seus favoritos! +

+
+
+
+ ); +}; diff --git a/frontend/pages/LGPD.tsx b/frontend/pages/LGPD.tsx new file mode 100644 index 0000000..83c5f8f --- /dev/null +++ b/frontend/pages/LGPD.tsx @@ -0,0 +1,269 @@ +import React, { useState } from 'react'; + +interface LGPDProps { + onNavigate: (page: string) => void; +} + +interface Section { + id: number; + title: string; + icon: string; + content: React.ReactNode; +} + +export const LGPD: React.FC = ({ onNavigate }) => { + const [openSection, setOpenSection] = useState(null); + + const sections: Section[] = [ + { + id: 1, + title: 'Compromisso com a LGPD', + icon: '', + content: ( +

+ A Photum Formaturas está comprometida em proteger seus dados pessoais e cumprir todas as + disposições da Lei Geral de Proteção de Dados (Lei nº 13.709/2018). Esta página explica + como tratamos seus dados em conformidade com a LGPD. +

+ ) + }, + { + id: 2, + title: 'Princípios da LGPD', + icon: '', + content: ( + <> +

Nosso tratamento de dados segue os seguintes princípios:

+
    +
  • Finalidade: Tratamento para propósitos específicos e legítimos
  • +
  • Adequação: Compatível com as finalidades informadas
  • +
  • Necessidade: Limitado ao mínimo necessário
  • +
  • Transparência: Informações claras e acessíveis
  • +
  • Segurança: Medidas técnicas e administrativas adequadas
  • +
+ + ) + }, + { + id: 3, + title: 'Seus Direitos como Titular', + icon: '', + content: ( + <> +

A LGPD garante diversos direitos sobre seus dados pessoais:

+
    +
  • Confirmação da existência de tratamento
  • +
  • Acesso aos dados
  • +
  • Correção de dados incompletos ou desatualizados
  • +
  • Anonimização, bloqueio ou eliminação de dados desnecessários
  • +
  • Portabilidade dos dados a outro fornecedor
  • +
  • Eliminação dos dados tratados com seu consentimento
  • +
  • Informação sobre o compartilhamento de dados
  • +
  • Revogação do consentimento
  • +
+ + ) + }, + { + id: 4, + title: 'Bases Legais para Tratamento', + icon: '', + content: ( + <> +

Tratamos seus dados com base nas seguintes hipóteses legais:

+
    +
  • Consentimento do titular
  • +
  • Cumprimento de obrigação legal ou regulatória
  • +
  • Execução de contrato ou procedimentos preliminares
  • +
  • Exercício regular de direitos
  • +
  • Legítimos interesses do controlador
  • +
+ + ) + }, + { + id: 5, + title: 'Compartilhamento de Dados', + icon: '', + content: ( + <> +

Seus dados podem ser compartilhados com:

+
    +
  • Prestadores de serviços de pagamento
  • +
  • Provedores de infraestrutura tecnológica
  • +
  • Autoridades governamentais (quando exigido por lei)
  • +
  • Parceiros comerciais (com seu consentimento)
  • +
+

+ Todos os terceiros são obrigados a manter a confidencialidade e segurança de seus dados. +

+ + ) + }, + { + id: 6, + title: 'Segurança dos Dados', + icon: '', + content: ( + <> +

Implementamos medidas de segurança técnicas e organizacionais:

+
    +
  • Criptografia de dados sensíveis
  • +
  • Controles de acesso rigorosos
  • +
  • Monitoramento contínuo de segurança
  • +
  • Treinamento regular da equipe
  • +
  • Plano de resposta a incidentes
  • +
+ + ) + }, + { + id: 7, + title: 'Retenção de Dados', + icon: '', + content: ( +

+ Mantemos seus dados pessoais apenas pelo tempo necessário para cumprir as finalidades para + as quais foram coletados, incluindo requisitos legais, contratuais, de resolução de disputas + e aplicação de nossos acordos. Após esse período, os dados são eliminados ou anonimizados de + forma segura. +

+ ) + }, + { + id: 8, + title: 'Encarregado de Proteção de Dados (DPO)', + icon: '', + content: ( + <> +

+ Nosso Encarregado de Proteção de Dados está disponível para esclarecer dúvidas e receber + solicitações relacionadas aos seus direitos: +

+
+

Contato do DPO:

+

+ + lgpd@photum.com.br + +

+
+ + ) + }, + { + id: 9, + title: 'Autoridade Nacional de Proteção de Dados (ANPD)', + icon: '', + content: ( + <> +

+ Você tem o direito de apresentar reclamação à Autoridade Nacional de Proteção de Dados: +

+
+

ANPD:

+

+ Website:{' '} + + www.gov.br/anpd + +

+
+ + ) + } + ]; + + return ( +
+
+ + +
+

+ Lei Geral de Proteção de Dados +

+

Transparência e segurança no tratamento dos seus dados

+
+ +
+ {sections.map((section, index) => ( +
+ + +
+
+
+ {section.content} +
+
+
+
+ ))} +
+ +
+

+ Última atualização: Janeiro de 2025 +

+
+
+ + +
+ ); +}; diff --git a/frontend/pages/Login.tsx b/frontend/pages/Login.tsx new file mode 100644 index 0000000..5c4d76b --- /dev/null +++ b/frontend/pages/Login.tsx @@ -0,0 +1,160 @@ + +import React, { useState } from 'react'; +import { useAuth } from '../contexts/AuthContext'; +import { Button } from '../components/Button'; +import { Input } from '../components/Input'; +import { UserRole } from '../types'; + +interface LoginProps { + onNavigate?: (page: string) => void; +} + +export const Login: React.FC = ({ onNavigate }) => { + const { login, availableUsers } = useAuth(); + const [email, setEmail] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setError(''); + + const success = await login(email); + if (!success) { + setError('Usuário não encontrado. Tente um dos e-mails de demonstração.'); + } + setIsLoading(false); + }; + + const fillCredentials = (userEmail: string) => { + setEmail(userEmail); + }; + + const getRoleLabel = (role: UserRole) => { + switch(role) { + case UserRole.SUPERADMIN: return "Superadmin"; + case UserRole.BUSINESS_OWNER: return "Empresa"; + case UserRole.PHOTOGRAPHER: return "Fotógrafo"; + case UserRole.EVENT_OWNER: return "Cliente"; + default: return role; + } + } + + return ( +
+ {/* Left Side - Image */} +
+ Photum Login +
+
+

Photum Formaturas

+

Gestão de eventos premium para quem não abre mão da excelência.

+
+
+
+ + {/* Right Side - Form */} +
+
+
+ Bem-vindo de volta +

Acesse sua conta

+

+ Não tem uma conta?{' '} + +

+
+ + +
+
+ + setEmail(e.target.value)} + className="w-full px-3 sm:px-4 py-2.5 sm:py-3 text-sm sm:text-base border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:border-transparent transition-all" + style={{focusRing: '2px solid #B9CF33'}} + onFocus={(e) => e.target.style.borderColor = '#B9CF33'} + onBlur={(e) => e.target.style.borderColor = '#d1d5db'} + /> + {error && {error}} +
+
+ +
+ + +
+
+
+ + + + + {/* Demo Users Quick Select */} +
+

Usuários de Demonstração (Clique para preencher)

+
+ {availableUsers.map(user => ( + + ))} +
+
+
+
+
+ ); +}; diff --git a/frontend/pages/PrivacyPolicy.tsx b/frontend/pages/PrivacyPolicy.tsx new file mode 100644 index 0000000..988ed82 --- /dev/null +++ b/frontend/pages/PrivacyPolicy.tsx @@ -0,0 +1,201 @@ +import React, { useState } from 'react'; + +interface PrivacyPolicyProps { + onNavigate: (page: string) => void; +} + +interface Section { + id: number; + title: string; + icon: string; + content: React.ReactNode; +} + +export const PrivacyPolicy: React.FC = ({ onNavigate }) => { + const [openSection, setOpenSection] = useState(null); + + const sections: Section[] = [ + { + id: 1, + title: 'Informações que Coletamos', + icon: '', + content: ( + <> +

+ A Photum Formaturas coleta informações que você nos fornece diretamente ao criar uma conta, + solicitar serviços ou entrar em contato conosco. Isso pode incluir: +

+
    +
  • Nome completo
  • +
  • Endereço de e-mail
  • +
  • Número de telefone
  • +
  • Informações sobre o evento (formatura)
  • +
  • Dados de pagamento (processados de forma segura)
  • +
+ + ) + }, + { + id: 2, + title: 'Como Usamos suas Informações', + icon: '', + content: ( + <> +

Utilizamos as informações coletadas para:

+
    +
  • Fornecer e gerenciar nossos serviços de fotografia
  • +
  • Processar pagamentos e enviar confirmações
  • +
  • Comunicar sobre seus eventos e serviços contratados
  • +
  • Melhorar nossos serviços e experiência do cliente
  • +
  • Enviar atualizações e informações relevantes
  • +
+ + ) + }, + { + id: 3, + title: 'Proteção de Dados', + icon: '', + content: ( +

+ Implementamos medidas de segurança técnicas e organizacionais para proteger suas informações + pessoais contra acesso não autorizado, alteração, divulgação ou destruição. +

+ ) + }, + { + id: 4, + title: 'Compartilhamento de Informações', + icon: '', + content: ( +

+ Não vendemos, alugamos ou compartilhamos suas informações pessoais com terceiros, exceto + quando necessário para fornecer nossos serviços ou quando exigido por lei. +

+ ) + }, + { + id: 5, + title: 'Seus Direitos', + icon: '', + content: ( + <> +

Você tem o direito de:

+
    +
  • Acessar suas informações pessoais
  • +
  • Corrigir dados incorretos
  • +
  • Solicitar a exclusão de seus dados
  • +
  • Retirar seu consentimento a qualquer momento
  • +
  • Solicitar a portabilidade de seus dados
  • +
+ + ) + }, + { + id: 6, + title: 'Contato', + icon: '', + content: ( +

+ Para questões sobre esta Política de Privacidade, entre em contato conosco em:{' '} + + contato@photum.com.br + +

+ ) + } + ]; + + return ( +
+
+ + +
+

+ Política de Privacidade +

+

Sua privacidade é nossa prioridade

+
+ +
+ {sections.map((section, index) => ( +
+ + +
+
+
+ {section.content} +
+
+
+
+ ))} +
+ +
+

+ Última atualização: Janeiro de 2025 +

+
+
+ + +
+ ); +}; diff --git a/frontend/pages/Register.tsx b/frontend/pages/Register.tsx new file mode 100644 index 0000000..fd28c8e --- /dev/null +++ b/frontend/pages/Register.tsx @@ -0,0 +1,277 @@ + +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 = ({ onNavigate }) => { + const { addInstitution } = useData(); + const [formData, setFormData] = useState({ + name: '', + email: '', + phone: '', + password: '', + 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 | boolean) => { + setFormData(prev => ({ ...prev, [field]: value })); + setError(''); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + 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'); + setIsLoading(false); + return; + } + + if (formData.password.length < 6) { + setError('A senha deve ter no mínimo 6 caracteres'); + setIsLoading(false); + 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); + setSuccess(true); + setTimeout(() => { + onNavigate('login'); + }, 2000); + }, 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 ( +
+ { + // 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} + /> +
+ ); + } + + if (success) { + return ( +
+
+
+ + + +
+

Cadastro realizado com sucesso!

+

Redirecionando para o login...

+
+
+ ); + } + + return ( +
+ {/* Left Side - Image */} +
+ Photum Cadastro +
+
+

Faça parte da Photum

+

+ Eternize seus momentos especiais com a melhor plataforma de gestão de eventos fotográficos. +

+
+
+
+ + {/* Right Side - Form */} +
+
+
+ Comece agora +

Crie sua conta

+

+ Já tem uma conta?{' '} + +

+
+ +
+
+ handleChange('name', e.target.value)} + /> + + handleChange('email', e.target.value)} + /> + + handleChange('phone', e.target.value)} + mask="phone" + /> + + handleChange('password', e.target.value)} + /> + + handleChange('confirmPassword', e.target.value)} + error={error && (error.includes('senha') || error.includes('coincidem')) ? error : undefined} + /> +
+ +
+
+ setAgreedToTerms(e.target.checked)} + className="mt-0.5 sm:mt-1 h-4 w-4 flex-shrink-0 border-gray-300 rounded focus:ring-2" + style={{accentColor: '#B9CF33'}} + /> + +
+ {error && error.includes('termos') && ( + {error} + )} +
+ +
+ setFormData(prev => ({ ...prev, wantsToAddInstitution: e.target.checked }))} + className="mt-0.5 sm:mt-1 h-4 w-4 flex-shrink-0 border-gray-300 rounded focus:ring-2" + style={{accentColor: '#B9CF33'}} + /> + +
+ + +
+
+
+
+ ); +}; diff --git a/frontend/pages/Settings.tsx b/frontend/pages/Settings.tsx new file mode 100644 index 0000000..9e592cb --- /dev/null +++ b/frontend/pages/Settings.tsx @@ -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 ( +
+
+ {/* Header */} +
+

+ Configurações +

+

+ Gerencie suas preferências e informações da conta +

+
+ +
+ {/* Sidebar */} +
+
+ +
+
+ + {/* Content */} +
+
+ {/* Profile Tab */} + {activeTab === 'profile' && ( +
+

Informações do Perfil

+ +
+
+
+ Avatar + +
+
+

{profileData.name}

+

{profileData.email}

+ +
+
+
+ +
+
+ +
+ + 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" + /> +
+
+ +
+ +
+ + 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" + /> +
+
+ +
+ +
+ + 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" + /> +
+
+ +
+ +
+ + 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" + /> +
+
+ +
+ +