Merge pull request #31 from rede5/feature/ui-improvements-and-access-codes

feat: Adicionar sistema de código de acesso, upload de foto de perfil…
This commit is contained in:
João Vitor 2025-12-18 15:13:59 -03:00 committed by GitHub
commit 3a0a2041a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 633 additions and 45 deletions

View file

@ -20,6 +20,7 @@ import { SettingsPage } from "./pages/Settings";
import { CourseManagement } from "./pages/CourseManagement"; import { CourseManagement } from "./pages/CourseManagement";
import { InspirationPage } from "./pages/Inspiration"; import { InspirationPage } from "./pages/Inspiration";
import { UserApproval } from "./pages/UserApproval"; import { UserApproval } from "./pages/UserApproval";
import { AccessCodeManagement } from "./pages/AccessCodeManagement";
import { PrivacyPolicy } from "./pages/PrivacyPolicy"; import { PrivacyPolicy } from "./pages/PrivacyPolicy";
import { TermsOfUse } from "./pages/TermsOfUse"; import { TermsOfUse } from "./pages/TermsOfUse";
import { LGPD } from "./pages/LGPD"; import { LGPD } from "./pages/LGPD";
@ -520,6 +521,18 @@ const AppContent: React.FC = () => {
</ProtectedRoute> </ProtectedRoute>
} }
/> />
<Route
path="/codigos-acesso"
element={
<ProtectedRoute
allowedRoles={[UserRole.SUPERADMIN, UserRole.BUSINESS_OWNER]}
>
<PageWrapper>
<AccessCodeManagement onNavigate={(page) => {}} />
</PageWrapper>
</ProtectedRoute>
}
/>
{/* Rota padrão - redireciona para home */} {/* Rota padrão - redireciona para home */}
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />

View file

@ -287,25 +287,6 @@ export const FotForm: React.FC<FotFormProps> = ({ onCancel, onSubmit, token, exi
</select> </select>
</div> </div>
{/* Gastos Captação */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Gastos Captação
</label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">R$</span>
<input
type="number"
min="0"
step="0.01"
value={formData.gastos_captacao}
onChange={(e) => setFormData({ ...formData, gastos_captacao: 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"
placeholder="0,00"
/>
</div>
</div>
{/* Pre Venda Checkbox */} {/* Pre Venda Checkbox */}
<div className="flex items-center h-full pt-6"> <div className="flex items-center h-full pt-6">
<label className="flex items-center space-x-3 cursor-pointer"> <label className="flex items-center space-x-3 cursor-pointer">

View file

@ -31,6 +31,8 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
phone: "", phone: "",
avatar: user?.avatar || "", avatar: user?.avatar || "",
}); });
const [avatarFile, setAvatarFile] = useState<File | null>(null);
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
const handleScroll = () => { const handleScroll = () => {
@ -64,6 +66,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
{ name: "Equipe", path: "equipe" }, { name: "Equipe", path: "equipe" },
{ name: "Cadastro de FOT", path: "cursos" }, { name: "Cadastro de FOT", path: "cursos" },
{ name: "Aprovação de Cadastros", path: "aprovacao-cadastros" }, { name: "Aprovação de Cadastros", path: "aprovacao-cadastros" },
{ name: "Códigos de Acesso", path: "codigos-acesso" },
{ name: "Financeiro", path: "financeiro" }, { name: "Financeiro", path: "financeiro" },
]; ];
case UserRole.EVENT_OWNER: case UserRole.EVENT_OWNER:
@ -99,6 +102,32 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
)}&background=random&color=fff&size=128`; )}&background=random&color=fff&size=128`;
}; };
const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
// Validate file size (max 5MB)
if (file.size > 5 * 1024 * 1024) {
alert("A imagem deve ter no máximo 5MB");
return;
}
// Validate file type
if (!file.type.startsWith('image/')) {
alert("Por favor, selecione uma imagem válida");
return;
}
setAvatarFile(file);
// Create preview URL
const reader = new FileReader();
reader.onloadend = () => {
setAvatarPreview(reader.result as string);
};
reader.readAsDataURL(file);
}
};
return ( return (
<> <>
<nav className="fixed w-full z-50 bg-white shadow-sm py-2 sm:py-3"> <nav className="fixed w-full z-50 bg-white shadow-sm py-2 sm:py-3">
@ -261,6 +290,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
</div> </div>
</div> </div>
) : ( ) : (
!['entrar', 'cadastro', 'cadastro-profissional'].includes(currentPage) && (
<div className="relative"> <div className="relative">
<button <button
onClick={() => onClick={() =>
@ -322,6 +352,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
</div> </div>
)} )}
</div> </div>
)
)} )}
</div> </div>
@ -454,6 +485,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
</button> </button>
</> </>
) : ( ) : (
!['entrar', 'cadastro', 'cadastro-profissional'].includes(currentPage) && (
<div className="relative"> <div className="relative">
<button <button
onClick={() => onClick={() =>
@ -507,6 +539,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
</div> </div>
)} )}
</div> </div>
)
)} )}
</div> </div>
</div> </div>
@ -649,13 +682,23 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
</button> </button>
<div className="w-24 h-24 mx-auto mb-4 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center border-4 border-white/30 overflow-hidden relative group"> <div className="w-24 h-24 mx-auto mb-4 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center border-4 border-white/30 overflow-hidden relative group">
<img <img
src={getAvatarSrc(profileData)} src={avatarPreview || getAvatarSrc(profileData)}
alt="Avatar" alt="Avatar"
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center cursor-pointer"> <label
htmlFor="avatar-upload"
className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center cursor-pointer"
>
<Camera size={28} className="text-white" /> <Camera size={28} className="text-white" />
</div> </label>
<input
id="avatar-upload"
type="file"
accept="image/*"
onChange={handleAvatarChange}
className="hidden"
/>
</div> </div>
<h2 className="text-2xl font-bold text-white mb-1"> <h2 className="text-2xl font-bold text-white mb-1">
Editar Perfil Editar Perfil

View file

@ -13,6 +13,7 @@ export interface ProfessionalData {
email: string; email: string;
senha: string; senha: string;
confirmarSenha: string; confirmarSenha: string;
avatar?: File | null;
funcaoId: string; funcaoId: string;
cep: string; cep: string;
rua: string; rua: string;
@ -25,11 +26,13 @@ export interface ProfessionalData {
cpfCnpj: string; cpfCnpj: string;
banco: string; banco: string;
agencia: string; agencia: string;
contaPix: string; conta: string;
pix: string;
carroDisponivel: string; carroDisponivel: string;
possuiEstudio: string; possuiEstudio: string;
qtdEstudios: string; qtdEstudios: string;
tipoCartao: string; tipoCartao: string;
equipamentos: string;
observacao: string; observacao: string;
} }
@ -43,11 +46,13 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
const [isLoadingFunctions, setIsLoadingFunctions] = useState(false); const [isLoadingFunctions, setIsLoadingFunctions] = useState(false);
const [functionsError, setFunctionsError] = useState(false); const [functionsError, setFunctionsError] = useState(false);
const [isLoadingCep, setIsLoadingCep] = useState(false); const [isLoadingCep, setIsLoadingCep] = useState(false);
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
const [formData, setFormData] = useState<ProfessionalData>({ const [formData, setFormData] = useState<ProfessionalData>({
nome: "", nome: "",
email: "", email: "",
senha: "", senha: "",
confirmarSenha: "", confirmarSenha: "",
avatar: null,
funcaoId: "", funcaoId: "",
cep: "", cep: "",
rua: "", rua: "",
@ -60,11 +65,13 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
cpfCnpj: "", cpfCnpj: "",
banco: "", banco: "",
agencia: "", agencia: "",
contaPix: "", conta: "",
pix: "",
carroDisponivel: "nao", carroDisponivel: "nao",
possuiEstudio: "nao", possuiEstudio: "nao",
qtdEstudios: "0", qtdEstudios: "0",
tipoCartao: "", tipoCartao: "",
equipamentos: "",
observacao: "", observacao: "",
}); });
@ -91,6 +98,19 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
setFormData((prev) => ({ ...prev, [field]: value })); setFormData((prev) => ({ ...prev, [field]: value }));
}; };
const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setFormData((prev) => ({ ...prev, avatar: file }));
// Create preview URL
const reader = new FileReader();
reader.onloadend = () => {
setAvatarPreview(reader.result as string);
};
reader.readAsDataURL(file);
}
};
const handleCepBlur = async () => { const handleCepBlur = async () => {
const cep = formData.cep.replace(/\D/g, ""); const cep = formData.cep.replace(/\D/g, "");
if (cep.length !== 8) return; if (cep.length !== 8) return;
@ -214,6 +234,57 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
onChange={(e) => handleChange("nome", e.target.value)} onChange={(e) => handleChange("nome", e.target.value)}
/> />
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Foto de Perfil
</label>
<div className="flex items-center gap-4">
{avatarPreview ? (
<div className="relative w-24 h-24 rounded-full overflow-hidden border-4 border-[#B9CF33] shadow-lg">
<img
src={avatarPreview}
alt="Preview"
className="w-full h-full object-cover"
/>
<button
type="button"
onClick={() => {
setAvatarPreview(null);
setFormData((prev) => ({ ...prev, avatar: null }));
}}
className="absolute top-0 right-0 bg-red-500 text-white rounded-full p-1 hover:bg-red-600 transition-colors"
title="Remover foto"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
) : (
<div className="w-24 h-24 rounded-full bg-gray-100 flex items-center justify-center border-2 border-dashed border-gray-300">
<svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
)}
<div className="flex-1">
<label className="cursor-pointer inline-flex items-center px-4 py-2 bg-[#B9CF33] text-white rounded-lg hover:opacity-90 transition-opacity">
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Escolher Foto
<input
type="file"
accept="image/*"
onChange={handleAvatarChange}
className="hidden"
/>
</label>
<p className="text-xs text-gray-500 mt-1">PNG, JPG ou JPEG até 5MB</p>
</div>
</div>
</div>
<Input <Input
label="E-mail *" label="E-mail *"
type="email" type="email"
@ -419,30 +490,38 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
/> />
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Input <Input
label="Banco *" label="Banco"
type="text" type="text"
required
value={formData.banco} value={formData.banco}
onChange={(e) => handleChange("banco", e.target.value)} onChange={(e) => handleChange("banco", e.target.value)}
/> />
<Input <Input
label="Agência *" label="Agência"
type="text" type="text"
required
value={formData.agencia} value={formData.agencia}
onChange={(e) => { onChange={(e) => {
const value = e.target.value.replace(/\D/g, ""); const value = e.target.value.replace(/\D/g, "");
handleChange("agencia", value); handleChange("agencia", value);
}} }}
/> />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Input <Input
label="Conta / Pix *" label="Conta"
type="text"
value={formData.conta}
onChange={(e) => handleChange("conta", e.target.value)}
/>
<Input
label="PIX *"
type="text" type="text"
required required
value={formData.contaPix} placeholder="E-mail, telefone, CPF ou chave aleatória"
onChange={(e) => handleChange("contaPix", e.target.value)} value={formData.pix}
onChange={(e) => handleChange("pix", e.target.value)}
/> />
</div> </div>
</div> </div>
@ -503,6 +582,19 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
/> />
)} )}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Equipamentos
</label>
<textarea
value={formData.equipamentos}
onChange={(e) => handleChange("equipamentos", e.target.value)}
rows={3}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent resize-none"
placeholder="Descreva os equipamentos que você possui (câmeras, lentes, flashes, etc.)"
/>
</div>
<Input <Input
label="Tipo de Cartão (Cartão de Memória) *" label="Tipo de Cartão (Cartão de Memória) *"
type="text" type="text"

View file

@ -116,15 +116,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
}; };
const login = async (email: string, password?: string) => { const login = async (email: string, password?: string) => {
// 1. Check for Demo/Mock users first - REMOVED to force API usage // 1. Try Real API first
// const mockUser = MOCK_USERS.find(u => u.email === email);
// if (mockUser) {
// await new Promise(resolve => setTimeout(resolve, 800)); // Simulate delay
// setUser({ ...mockUser, ativo: true });
// return true;
// }
// 2. Try Real API
try { try {
const response = await fetch(`${import.meta.env.VITE_API_URL}/auth/login`, { const response = await fetch(`${import.meta.env.VITE_API_URL}/auth/login`, {
method: 'POST', method: 'POST',
@ -166,6 +158,16 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
return true; return true;
} catch (err) { } catch (err) {
console.error('Login error:', err); console.error('Login error:', err);
// 2. Fallback to Demo/Mock users if API fails
const mockUser = MOCK_USERS.find(u => u.email === email);
if (mockUser) {
console.log('Using demo user:', mockUser.email);
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate delay
setUser({ ...mockUser, ativo: true });
return true;
}
throw err; throw err;
} }
}; };

View file

@ -0,0 +1,293 @@
import React, { useState } from "react";
import { Button } from "../components/Button";
import { Copy, Trash2, Plus, Calendar, Key, Check } from "lucide-react";
interface AccessCodeManagementProps {
onNavigate: (page: string) => void;
}
interface AccessCode {
id: string;
code: string;
expiresIn: number; // dias
createdAt: Date;
expiresAt: Date;
isActive: boolean;
}
export const AccessCodeManagement: React.FC<AccessCodeManagementProps> = ({
onNavigate,
}) => {
const [accessCodes, setAccessCodes] = useState<AccessCode[]>([
{
id: "1",
code: "PHOTUM2025",
expiresIn: 30,
createdAt: new Date(),
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
isActive: true,
},
]);
const [showCreateModal, setShowCreateModal] = useState(false);
const [newCodeDays, setNewCodeDays] = useState("7");
const [copiedCode, setCopiedCode] = useState<string | null>(null);
const generateCode = () => {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let code = "";
for (let i = 0; i < 10; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length));
}
return code;
};
const handleCreateCode = () => {
const days = parseInt(newCodeDays);
if (isNaN(days) || days < 1 || days > 365) {
alert("Por favor, insira um número válido de dias entre 1 e 365");
return;
}
const newCode: AccessCode = {
id: Date.now().toString(),
code: generateCode(),
expiresIn: days,
createdAt: new Date(),
expiresAt: new Date(Date.now() + days * 24 * 60 * 60 * 1000),
isActive: true,
};
setAccessCodes([newCode, ...accessCodes]);
setShowCreateModal(false);
setNewCodeDays("7");
};
const handleDeleteCode = (id: string) => {
if (confirm("Tem certeza que deseja excluir este código de acesso?")) {
setAccessCodes(accessCodes.filter((code) => code.id !== id));
}
};
const handleCopyCode = (code: string) => {
navigator.clipboard.writeText(code);
setCopiedCode(code);
setTimeout(() => setCopiedCode(null), 2000);
};
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat("pt-BR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(date);
};
const getDaysRemaining = (expiresAt: Date) => {
const now = new Date();
const diffTime = expiresAt.getTime() - now.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays;
};
const isExpired = (expiresAt: Date) => {
return new Date() > expiresAt;
};
return (
<div className="min-h-screen bg-gray-50 pt-20 sm:pt-24 md:pt-28 lg:pt-32 pb-8 sm:pb-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mb-6 sm:mb-8">
<h1 className="text-2xl sm:text-3xl font-serif font-bold text-brand-black mb-2">
Gerenciar Códigos de Acesso
</h1>
<p className="text-sm sm:text-base text-gray-600">
Crie e gerencie códigos de acesso temporários para novos cadastros
</p>
</div>
{/* Botão Criar Código */}
<div className="mb-6">
<Button
onClick={() => setShowCreateModal(true)}
className="flex items-center gap-2"
>
<Plus size={20} />
Gerar Novo Código
</Button>
</div>
{/* Tabela de Códigos */}
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
{accessCodes.length === 0 ? (
<div className="p-12 text-center">
<Key size={48} className="mx-auto text-gray-300 mb-4" />
<p className="text-gray-500 text-lg mb-2">
Nenhum código de acesso criado
</p>
<p className="text-gray-400 text-sm">
Clique em "Gerar Novo Código" para criar um código de acesso
</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-gray-50 border-b border-gray-200">
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
Código
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
Validade (dias)
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
Data de Criação
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
Data de Expiração
</th>
<th className="px-6 py-4 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider">
Ações
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{accessCodes.map((code) => {
const expired = isExpired(code.expiresAt);
const daysRemaining = getDaysRemaining(code.expiresAt);
return (
<tr
key={code.id}
className={`hover:bg-gray-50 transition-colors ${
expired ? "bg-red-50/30" : ""
}`}
>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`inline-flex px-3 py-1 rounded-full text-xs font-semibold ${
expired
? "bg-red-100 text-red-700"
: daysRemaining <= 3
? "bg-yellow-100 text-yellow-700"
: "bg-green-100 text-green-700"
}`}
>
{expired
? "Expirado"
: daysRemaining === 1
? "1 dia"
: `${daysRemaining} dias`}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center gap-2">
<code className="text-lg font-bold text-[#492E61] tracking-wider">
{code.code}
</code>
<button
onClick={() => handleCopyCode(code.code)}
className="p-1.5 hover:bg-gray-100 rounded transition-colors"
title="Copiar código"
>
{copiedCode === code.code ? (
<Check size={16} className="text-green-500" />
) : (
<Copy size={16} className="text-gray-400" />
)}
</button>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700">
{code.expiresIn} {code.expiresIn === 1 ? "dia" : "dias"}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
{formatDate(code.createdAt)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
{formatDate(code.expiresAt)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-center">
<button
onClick={() => handleDeleteCode(code.id)}
className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-red-50 text-red-600 hover:bg-red-100 rounded-lg transition-colors text-sm font-medium"
title="Excluir código"
>
<Trash2 size={16} />
Excluir
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
</div>
{/* Modal de Criar Código */}
{showCreateModal && (
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"
onClick={() => setShowCreateModal(false)}
>
<div
className="bg-white rounded-2xl shadow-2xl max-w-md w-full p-6 sm:p-8"
onClick={(e) => e.stopPropagation()}
>
<h2 className="text-2xl font-bold text-gray-900 mb-6">
Gerar Novo Código de Acesso
</h2>
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
Validade do Código (em dias) *
</label>
<input
type="number"
min="1"
max="365"
value={newCodeDays}
onChange={(e) => setNewCodeDays(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#492E61] focus:border-transparent"
placeholder="Ex: 7"
/>
<p className="text-sm text-gray-500 mt-2">
O código será válido por {newCodeDays} dia(s) a partir da
criação
</p>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<p className="text-sm text-blue-800">
<strong>Dica:</strong> O código será gerado automaticamente e
poderá ser compartilhado com os usuários que desejam se
cadastrar.
</p>
</div>
<div className="flex gap-3">
<Button
variant="outline"
onClick={() => setShowCreateModal(false)}
className="flex-1"
>
Cancelar
</Button>
<Button onClick={handleCreateCode} className="flex-1">
Gerar Código
</Button>
</div>
</div>
</div>
)}
</div>
);
};

View file

@ -1,11 +1,49 @@
import React from "react"; import React, { useState } from "react";
import { Button } from "../components/Button"; import { Button } from "../components/Button";
import { X } from "lucide-react";
interface HomeProps { interface HomeProps {
onEnter: () => void; onEnter: () => void;
} }
export const Home: React.FC<HomeProps> = ({ onEnter }) => { export const Home: React.FC<HomeProps> = ({ onEnter }) => {
const [showAccessCodeModal, setShowAccessCodeModal] = useState(false);
const [accessCode, setAccessCode] = useState("");
const [showProfessionalPrompt, setShowProfessionalPrompt] = useState(false);
const [codeError, setCodeError] = useState("");
// Código mockado - em produção, verificar com o backend
const MOCK_ACCESS_CODE = "PHOTUM2025";
const handleRegisterClick = () => {
setShowAccessCodeModal(true);
setAccessCode("");
setCodeError("");
};
const handleVerifyCode = () => {
if (accessCode.trim() === "") {
setCodeError("Por favor, digite o código de acesso");
return;
}
if (accessCode.toUpperCase() === MOCK_ACCESS_CODE) {
setShowAccessCodeModal(false);
setShowProfessionalPrompt(true);
} else {
setCodeError("Código de acesso inválido ou expirado");
}
};
const handleProfessionalChoice = (isProfessional: boolean) => {
setShowProfessionalPrompt(false);
if (isProfessional) {
window.location.href = "/cadastro-profissional";
} else {
window.location.href = "/cadastro";
}
};
return ( return (
<div <div
className="min-h-screen flex items-center justify-center relative overflow-hidden" className="min-h-screen flex items-center justify-center relative overflow-hidden"
@ -69,7 +107,7 @@ export const Home: React.FC<HomeProps> = ({ onEnter }) => {
</Button> </Button>
<Button <Button
onClick={() => (window.location.href = "/cadastro")} onClick={handleRegisterClick}
variant="outline" variant="outline"
className="w-full" className="w-full"
size="lg" size="lg"
@ -78,6 +116,132 @@ export const Home: React.FC<HomeProps> = ({ onEnter }) => {
</Button> </Button>
</div> </div>
</div> </div>
{/* Modal de Código de Acesso */}
{showAccessCodeModal && (
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"
onClick={() => setShowAccessCodeModal(false)}
>
<div
className="bg-white rounded-2xl shadow-2xl max-w-md w-full p-6 sm:p-8 animate-fade-in-scale"
onClick={(e) => e.stopPropagation()}
>
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-gray-900">
Código de Acesso
</h2>
<button
onClick={() => setShowAccessCodeModal(false)}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<X size={24} />
</button>
</div>
<p className="text-gray-600 mb-6">
Digite o código de acesso fornecido pela empresa para continuar
com o cadastro.
</p>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Código de Acesso *
</label>
<input
type="text"
value={accessCode}
onChange={(e) => {
setAccessCode(e.target.value.toUpperCase());
setCodeError("");
}}
onKeyPress={(e) => e.key === "Enter" && handleVerifyCode()}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent uppercase"
placeholder="Digite o código"
autoFocus
/>
{codeError && (
<p className="text-red-500 text-sm mt-2">{codeError}</p>
)}
</div>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<p className="text-sm text-yellow-800">
<strong>Atenção:</strong> O código de acesso é fornecido pela
empresa e tem validade temporária.
</p>
</div>
<div className="flex gap-3">
<Button
variant="outline"
onClick={() => setShowAccessCodeModal(false)}
className="flex-1"
>
Cancelar
</Button>
<Button onClick={handleVerifyCode} className="flex-1">
Verificar
</Button>
</div>
</div>
</div>
)}
{/* Modal de Escolha de Tipo de Cadastro */}
{showProfessionalPrompt && (
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"
onClick={() => setShowProfessionalPrompt(false)}
>
<div
className="bg-white rounded-2xl shadow-2xl max-w-md w-full p-6 sm:p-8 animate-fade-in-scale"
onClick={(e) => e.stopPropagation()}
>
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-gray-900">
Tipo de Cadastro
</h2>
<button
onClick={() => setShowProfessionalPrompt(false)}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<X size={24} />
</button>
</div>
<p className="text-gray-600 mb-6">
Você é um profissional (fotógrafo/cinegrafista) ou um cliente?
</p>
<div className="space-y-3">
<button
onClick={() => handleProfessionalChoice(true)}
className="w-full p-4 border-2 border-[#492E61] bg-[#492E61]/5 hover:bg-[#492E61]/10 rounded-xl transition-colors text-left group"
>
<h3 className="font-semibold text-[#492E61] mb-1">
Sou Profissional
</h3>
<p className="text-sm text-gray-600">
Cadastro para fotógrafos e cinegrafistas
</p>
</button>
<button
onClick={() => handleProfessionalChoice(false)}
className="w-full p-4 border-2 border-[#B9CF33] bg-[#B9CF33]/5 hover:bg-[#B9CF33]/10 rounded-xl transition-colors text-left group"
>
<h3 className="font-semibold text-[#492E61] mb-1">
Sou Cliente
</h3>
<p className="text-sm text-gray-600">
Cadastro para solicitar serviços
</p>
</button>
</div>
</div>
</div>
)}
</div> </div>
); );
}; };

View file

@ -41,10 +41,10 @@ export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
banco: professionalData.banco, banco: professionalData.banco,
carro_disponivel: professionalData.carroDisponivel === "sim", carro_disponivel: professionalData.carroDisponivel === "sim",
cidade: professionalData.cidade, cidade: professionalData.cidade,
conta_pix: professionalData.contaPix, conta_pix: professionalData.pix, // Usando o campo PIX separado
cpf_cnpj_titular: professionalData.cpfCnpj, cpf_cnpj_titular: professionalData.cpfCnpj,
endereco: `${professionalData.cep}, ${professionalData.rua}, ${professionalData.numero} - ${professionalData.bairro}`, endereco: `${professionalData.cep}, ${professionalData.rua}, ${professionalData.numero} - ${professionalData.bairro}`,
equipamentos: "", // Campo não está no form explícito, talvez observação ou outro? equipamentos: professionalData.equipamentos,
extra_por_equipamento: false, // Default extra_por_equipamento: false, // Default
funcao_profissional_id: professionalData.funcaoId, funcao_profissional_id: professionalData.funcaoId,
observacao: professionalData.observacao, observacao: professionalData.observacao,