feat: add photographer finance page and UI improvements

- Add photographer finance page at /meus-pagamentos with payment history table
- Remove university management page and related routes
- Update Finance and UserApproval pages with consistent spacing and typography
- Fix Dashboard background color to match other pages (bg-gray-50)
- Standardize navbar logo sizing across all pages
- Change institution field in course form from dropdown to text input
- Add year and semester fields for university graduation dates
- Improve header spacing on all pages to pt-20 sm:pt-24 md:pt-28 lg:pt-32
- Apply font-serif styling consistently across page headers
This commit is contained in:
João Vitor 2025-12-12 16:26:12 -03:00
parent b6deacc291
commit 7fc96d77d2
24 changed files with 2251 additions and 597 deletions

View file

@ -14,7 +14,8 @@ import { Login } from "./pages/Login";
import { Register } from "./pages/Register"; import { Register } from "./pages/Register";
import { ProfessionalRegister } from "./pages/ProfessionalRegister"; import { ProfessionalRegister } from "./pages/ProfessionalRegister";
import { TeamPage } from "./pages/Team"; import { TeamPage } from "./pages/Team";
import { FinancePage } from "./pages/Finance"; import Finance from "./pages/Finance";
import PhotographerFinance from "./pages/PhotographerFinance";
import { SettingsPage } from "./pages/Settings"; 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";
@ -189,30 +190,28 @@ const Footer: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<footer className="bg-gradient-to-br from-brand-purple to-brand-purple/90 text-brand-black py-12 sm:py-16 md:py-20"> <footer className="bg-gradient-to-br from-brand-purple to-brand-purple/90 text-brand-black py-8 sm:py-12 md:py-16">
<div className="w-full max-w-[1600px] mx-auto px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16"> <div className="w-full max-w-[1600px] mx-auto px-3 sm:px-6 md:px-8 lg:px-12 xl:px-16">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 sm:gap-8 md:gap-10 lg:gap-12 xl:gap-16 mb-8 sm:mb-12 md:mb-16"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6 sm:gap-8 md:gap-12 lg:gap-16 mb-6 sm:mb-10 md:mb-16">
{/* Logo e Descrição */} {/* Logo e Texto descritivo */}
<div className="lg:col-span-1 text-center sm:text-left"> <div className="text-center md:text-left">
<div className="flex flex-col items-center sm:items-start"> <img
<img src="/logo_preta.png"
src="/logo.png" alt="Photum Formaturas"
alt="Photum Formaturas" className="h-16 sm:h-20 md:h-24 mb-3 md:mb-6 mx-auto md:mx-20 object-contain"
className="h-24 sm:h-28 md:h-32 lg:h-36 mb-4 md:mb-6" />
/> <p className="text-brand-black/80 text-xs sm:text-sm md:text-base leading-relaxed px-2 sm:px-0">
<p className="text-brand-black/80 text-xs sm:text-sm md:text-base leading-relaxed"> Eternizando momentos únicos com excelência e profissionalismo
Eternizando momentos únicos com excelência e profissionalismo desde 2020.
desde 2020. </p>
</p>
</div>
</div> </div>
{/* Links Úteis */} {/* Links Úteis */}
<div> <div className="text-center">
<h4 className="font-bold text-brand-black mb-4 md:mb-6 uppercase tracking-wider text-sm sm:text-base md:text-lg"> <h4 className="font-bold text-brand-black mb-3 md:mb-4 uppercase tracking-wider text-xs sm:text-sm md:text-base">
Links Úteis Links Úteis
</h4> </h4>
<ul className="space-y-2 md:space-y-3 text-brand-black/70 text-sm sm:text-base md:text-lg"> <ul className="space-y-1.5 sm:space-y-2 md:space-y-3 text-brand-black/70 text-xs sm:text-sm md:text-base">
<li> <li>
<a <a
href="#" href="#"
@ -235,14 +234,14 @@ const Footer: React.FC = () => {
</div> </div>
{/* Contato */} {/* Contato */}
<div> <div className="text-center">
<h4 className="font-bold text-brand-black mb-4 md:mb-6 uppercase tracking-wider text-sm sm:text-base md:text-lg"> <h4 className="font-bold text-brand-black mb-3 md:mb-4 uppercase tracking-wider text-xs sm:text-sm md:text-base">
Contato Contato
</h4> </h4>
<ul className="space-y-3 md:space-y-4 text-brand-black/70 text-sm sm:text-base md:text-lg"> <ul className="space-y-2 sm:space-y-3 md:space-y-4 text-brand-black/70 text-xs sm:text-sm md:text-base">
<li className="flex items-center gap-2 md:gap-3"> <li className="flex items-center justify-center gap-1.5 sm:gap-2 md:gap-3">
<svg <svg
className="w-5 h-5 md:w-6 md:h-6 flex-shrink-0" className="w-4 h-4 sm:w-5 sm:h-5 md:w-6 md:h-6 flex-shrink-0"
fill="currentColor" fill="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
@ -255,9 +254,9 @@ const Footer: React.FC = () => {
contato@photum.com.br contato@photum.com.br
</a> </a>
</li> </li>
<li className="flex items-start gap-2 md:gap-3"> <li className="flex items-start justify-center gap-1.5 sm:gap-2 md:gap-3">
<svg <svg
className="w-5 h-5 md:w-6 md:h-6 flex-shrink-0 mt-0.5" className="w-4 h-4 sm:w-5 sm:h-5 md:w-6 md:h-6 flex-shrink-0 mt-0.5"
fill="currentColor" fill="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
@ -268,9 +267,9 @@ const Footer: React.FC = () => {
<p>(19) 3621 4621</p> <p>(19) 3621 4621</p>
</div> </div>
</li> </li>
<li className="flex items-center gap-2 md:gap-3"> <li className="flex items-center justify-center gap-1.5 sm:gap-2 md:gap-3">
<svg <svg
className="w-5 h-5 md:w-6 md:h-6 flex-shrink-0" className="w-4 h-4 sm:w-5 sm:h-5 md:w-6 md:h-6 flex-shrink-0"
fill="currentColor" fill="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
@ -278,13 +277,13 @@ const Footer: React.FC = () => {
</svg> </svg>
<span>Americana, SP</span> <span>Americana, SP</span>
</li> </li>
<li className="flex gap-4 md:gap-5 mt-4 md:mt-6"> <li className="flex justify-center gap-3 sm:gap-4 md:gap-5 mt-3 sm:mt-4 md:mt-6">
<a <a
href="#" href="#"
className="hover:text-brand-black transition-colors" className="hover:text-brand-black transition-colors"
> >
<svg <svg
className="w-7 h-7 md:w-8 md:h-8" className="w-6 h-6 sm:w-7 sm:h-7 md:w-8 md:h-8"
fill="currentColor" fill="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
@ -308,7 +307,7 @@ const Footer: React.FC = () => {
className="hover:text-brand-black transition-colors" className="hover:text-brand-black transition-colors"
> >
<svg <svg
className="w-7 h-7 md:w-8 md:h-8" className="w-6 h-6 sm:w-7 sm:h-7 md:w-8 md:h-8"
fill="currentColor" fill="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
@ -321,11 +320,11 @@ const Footer: React.FC = () => {
</div> </div>
{/* Bottom Bar */} {/* Bottom Bar */}
<div className="border-t border-black/20 pt-6 md:pt-8 flex flex-col md:flex-row justify-between items-center text-xs sm:text-sm md:text-base text-brand-black/60 gap-4"> <div className="border-t border-black/20 pt-4 sm:pt-6 md:pt-8 flex flex-col md:flex-row justify-between items-center text-[10px] sm:text-xs md:text-sm text-brand-black/60 gap-3 sm:gap-4">
<p> <p className="text-center px-2">
&copy; Todos os direitos reservados PHOTUM - CNPJ 27.708.950/0001-21 &copy; Todos os direitos reservados PHOTUM - CNPJ 27.708.950/0001-21
</p> </p>
<div className="flex flex-wrap justify-center gap-4 sm:gap-6 md:gap-8"> <div className="flex flex-wrap justify-center gap-2 sm:gap-4 md:gap-6">
<a <a
href="#" href="#"
onClick={() => navigate("/privacidade")} onClick={() => navigate("/privacidade")}
@ -470,7 +469,17 @@ const AppContent: React.FC = () => {
allowedRoles={[UserRole.SUPERADMIN, UserRole.BUSINESS_OWNER]} allowedRoles={[UserRole.SUPERADMIN, UserRole.BUSINESS_OWNER]}
> >
<PageWrapper> <PageWrapper>
<FinancePage /> <Finance />
</PageWrapper>
</ProtectedRoute>
}
/>
<Route
path="/meus-pagamentos"
element={
<ProtectedRoute allowedRoles={[UserRole.PHOTOGRAPHER]}>
<PageWrapper>
<PhotographerFinance />
</PageWrapper> </PageWrapper>
</ProtectedRoute> </ProtectedRoute>
} }

View file

@ -1,33 +1,36 @@
import React from 'react'; import React from "react";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost'; variant?: "primary" | "secondary" | "outline" | "ghost";
size?: 'sm' | 'md' | 'lg' | 'xl'; size?: "sm" | "md" | "lg" | "xl";
isLoading?: boolean; isLoading?: boolean;
} }
export const Button: React.FC<ButtonProps> = ({ export const Button: React.FC<ButtonProps> = ({
children, children,
variant = 'primary', variant = "primary",
size = 'md', size = "md",
isLoading, isLoading,
className = '', className = "",
...props ...props
}) => { }) => {
const baseStyles = "inline-flex items-center justify-center font-medium transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; const baseStyles =
"inline-flex items-center justify-center font-medium transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed";
const variants = { const variants = {
primary: "bg-[#492E61] text-white hover:bg-[#3a2450] focus:ring-[#492E61]", primary: "bg-[#492E61] text-white hover:bg-[#3a2450] focus:ring-[#492E61]",
secondary: "bg-[#B9CF32] text-white hover:bg-[#a5bd2e] focus:ring-[#B9CF32]", secondary:
outline: "border border-[#492E61] text-[#492E61] hover:bg-[#492E61]/5 focus:ring-[#492E61]", "bg-[#B9CF32] text-white hover:bg-[#a5bd2e] focus:ring-[#B9CF32]",
ghost: "text-[#492E61] hover:bg-[#492E61]/10 hover:text-[#3a2450]" outline:
"border border-[#492E61] text-[#492E61] hover:bg-[#492E61]/5 focus:ring-[#492E61]",
ghost: "text-[#492E61] hover:bg-[#492E61]/10 hover:text-[#3a2450]",
}; };
const sizes = { const sizes = {
sm: "text-xs px-3 py-1.5 rounded-md", sm: "text-[10px] sm:text-xs px-2 sm:px-3 py-1 sm:py-1.5 rounded-md",
md: "text-sm px-5 py-2.5 rounded-lg", md: "text-xs sm:text-sm px-3 sm:px-4 md:px-5 py-2 sm:py-2.5 rounded-lg",
lg: "text-base px-8 py-3 rounded-lg", lg: "text-sm sm:text-base px-5 sm:px-6 md:px-8 py-2.5 sm:py-3 rounded-lg",
xl: "text-lg px-10 py-4 rounded-xl font-semibold" xl: "text-base sm:text-lg px-6 sm:px-8 md:px-10 py-3 sm:py-4 rounded-xl font-semibold",
}; };
return ( return (
@ -37,9 +40,25 @@ export const Button: React.FC<ButtonProps> = ({
{...props} {...props}
> >
{isLoading ? ( {isLoading ? (
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-current" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> className="animate-spin -ml-1 mr-2 h-4 w-4 text-current"
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg> </svg>
) : null} ) : null}
{children} {children}

View file

@ -361,7 +361,7 @@ export const CourseForm: React.FC<CourseFormProps> = ({
{/* EF I / EF II */} {/* EF I / EF II */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
EF I / EF II* Cursos*
</label> </label>
<select <select
required required
@ -414,30 +414,21 @@ export const CourseForm: React.FC<CourseFormProps> = ({
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Instituição (Universidade)* Instituição (Universidade)*
</label> </label>
<select <input
type="text"
required required
value={instituicao} value={instituicao}
onChange={(e) => { onChange={(e) => {
setInstituicao(e.target.value); setInstituicao(e.target.value);
setError(""); setError("");
}} }}
disabled={isLoadingData || isBackendDown} placeholder="Digite o nome da universidade"
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold disabled:bg-gray-100 disabled:cursor-not-allowed" className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
> />
<option value="">Selecione uma universidade</option>
{universities.map((university) => (
<option key={university.id} value={university.id}>
{university.nome}
</option>
))}
</select>
{isBackendDown && ( {isBackendDown && (
<div className="mt-2 flex items-center gap-2 text-sm text-red-600"> <div className="mt-2 flex items-center gap-2 text-sm text-red-600">
<AlertTriangle size={16} /> <AlertTriangle size={16} />
<span> <span>Backend não está rodando.</span>
Backend não está rodando. Não é possível carregar
universidades.
</span>
</div> </div>
)} )}
{isLoadingData && !isBackendDown && ( {isLoadingData && !isBackendDown && (

View file

@ -1,40 +1,51 @@
import React, { useState } from 'react'; import React, { useState } from "react";
import { Eye, EyeOff } from 'lucide-react'; import { Eye, EyeOff } from "lucide-react";
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string; label: string;
error?: string; error?: string;
mask?: 'phone' | 'cnpj' | 'cep'; mask?: "phone" | "cnpj" | "cep";
} }
export const Input: React.FC<InputProps> = ({ label, error, className = '', type, mask, onChange, ...props }) => { export const Input: React.FC<InputProps> = ({
label,
error,
className = "",
type,
mask,
onChange,
...props
}) => {
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const isPassword = type === 'password'; const isPassword = type === "password";
const inputType = isPassword && showPassword ? 'text' : type; const inputType = isPassword && showPassword ? "text" : type;
const applyMask = (value: string, maskType?: 'phone' | 'cnpj' | 'cep') => { const applyMask = (value: string, maskType?: "phone" | "cnpj" | "cep") => {
if (!maskType) return value; if (!maskType) return value;
const numbers = value.replace(/\D/g, ''); const numbers = value.replace(/\D/g, "");
switch (maskType) { switch (maskType) {
case 'phone': case "phone":
// Limita a 11 dígitos (celular) // Limita a 11 dígitos (celular)
const phoneNumbers = numbers.slice(0, 11); const phoneNumbers = numbers.slice(0, 11);
if (phoneNumbers.length <= 10) { if (phoneNumbers.length <= 10) {
return phoneNumbers.replace(/(\d{2})(\d{4})(\d{0,4})/, '($1) $2-$3'); 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'); return phoneNumbers.replace(/(\d{2})(\d{5})(\d{0,4})/, "($1) $2-$3");
case 'cnpj': case "cnpj":
// Limita a 14 dígitos // Limita a 14 dígitos
const cnpjNumbers = numbers.slice(0, 14); 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'); return cnpjNumbers.replace(
/(\d{2})(\d{3})(\d{3})(\d{4})(\d{0,2})/,
"$1.$2.$3/$4-$5"
);
case 'cep': case "cep":
// Limita a 8 dígitos // Limita a 8 dígitos
const cepNumbers = numbers.slice(0, 8); const cepNumbers = numbers.slice(0, 8);
return cepNumbers.replace(/(\d{5})(\d{0,3})/, '$1-$2'); return cepNumbers.replace(/(\d{5})(\d{0,3})/, "$1-$2");
default: default:
return value; return value;
@ -53,23 +64,27 @@ export const Input: React.FC<InputProps> = ({ label, error, className = '', type
const getMaxLength = () => { const getMaxLength = () => {
if (!mask) return undefined; if (!mask) return undefined;
switch (mask) { switch (mask) {
case 'phone': return 15; // (00) 00000-0000 case "phone":
case 'cnpj': return 18; // 00.000.000/0000-00 return 15; // (00) 00000-0000
case 'cep': return 9; // 00000-000 case "cnpj":
default: return undefined; return 18; // 00.000.000/0000-00
case "cep":
return 9; // 00000-000
default:
return undefined;
} }
}; };
return ( return (
<div className="w-full"> <div className="w-full">
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs"> <label className="block text-[10px] sm:text-xs font-medium text-gray-700 mb-1 tracking-wide uppercase">
{label} {label}
</label> </label>
<div className="relative"> <div className="relative">
<input <input
className={`w-full px-4 py-2 border rounded-sm focus:outline-none focus:ring-1 focus:ring-brand-gold focus:border-brand-gold transition-colors className={`w-full px-2.5 sm:px-3 md:px-4 py-1.5 sm:py-2 text-xs sm:text-sm border rounded-sm focus:outline-none focus:ring-1 focus:ring-brand-gold focus:border-brand-gold transition-colors
${error ? 'border-red-500' : 'border-gray-300'} ${error ? "border-red-500" : "border-gray-300"}
${isPassword ? 'pr-10' : ''} ${isPassword ? "pr-9 sm:pr-10" : ""}
${className}`} ${className}`}
type={inputType} type={inputType}
onChange={handleChange} onChange={handleChange}
@ -80,13 +95,21 @@ export const Input: React.FC<InputProps> = ({ label, error, className = '', type
<button <button
type="button" type="button"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors" className="absolute right-2 sm:right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
> >
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />} {showPassword ? (
<EyeOff size={16} className="sm:w-[18px] sm:h-[18px]" />
) : (
<Eye size={16} className="sm:w-[18px] sm:h-[18px]" />
)}
</button> </button>
)} )}
</div> </div>
{error && <span className="text-xs text-red-500 mt-1">{error}</span>} {error && (
<span className="text-[10px] sm:text-xs text-red-500 mt-1">
{error}
</span>
)}
</div> </div>
); );
}; };
@ -97,7 +120,13 @@ interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
error?: string; error?: string;
} }
export const Select: React.FC<SelectProps> = ({ label, options, error, className = '', ...props }) => { export const Select: React.FC<SelectProps> = ({
label,
options,
error,
className = "",
...props
}) => {
return ( return (
<div className="w-full"> <div className="w-full">
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs"> <label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
@ -105,13 +134,17 @@ export const Select: React.FC<SelectProps> = ({ label, options, error, className
</label> </label>
<select <select
className={`w-full px-4 py-2 border rounded-sm focus:outline-none focus:ring-1 focus:ring-brand-gold focus:border-brand-gold transition-colors appearance-none bg-white className={`w-full px-4 py-2 border rounded-sm focus:outline-none focus:ring-1 focus:ring-brand-gold focus:border-brand-gold transition-colors appearance-none bg-white
${error ? 'border-red-500' : 'border-gray-300'} ${error ? "border-red-500" : "border-gray-300"}
${className}`} ${className}`}
{...props} {...props}
> >
<option value="" disabled>Selecione uma opção</option> <option value="" disabled>
{options.map(opt => ( Selecione uma opção
<option key={opt.value} value={opt.value}>{opt.label}</option> </option>
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))} ))}
</select> </select>
{error && <span className="text-xs text-red-500 mt-1">{error}</span>} {error && <span className="text-xs text-red-500 mt-1">{error}</span>}

View file

@ -72,7 +72,10 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
{ name: "Solicitar Evento", path: "solicitar-evento" }, { name: "Solicitar Evento", path: "solicitar-evento" },
]; ];
case UserRole.PHOTOGRAPHER: case UserRole.PHOTOGRAPHER:
return [{ name: "Eventos Designados", path: "painel" }]; return [
{ name: "Eventos Designados", path: "painel" },
{ name: "Meus Pagamentos", path: "meus-pagamentos" },
];
default: default:
return []; return [];
} }
@ -89,9 +92,9 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
return ( return (
<> <>
<nav className="fixed w-full z-50 bg-white shadow-sm py-3"> <nav className="fixed w-full z-50 bg-white shadow-sm py-2 sm:py-3">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-3 sm:px-4 lg:px-6">
<div className="flex justify-between items-center h-16"> <div className="flex justify-between items-center h-12 sm:h-14 md:h-16">
{/* Logo */} {/* Logo */}
<div <div
className="flex-shrink-0 flex items-center cursor-pointer" className="flex-shrink-0 flex items-center cursor-pointer"
@ -100,18 +103,18 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
<img <img
src="/logo.png" src="/logo.png"
alt="Photum Formaturas" alt="Photum Formaturas"
className="h-18 sm:h-30 w-auto max-w-[200px] object-contain mb-4" className="h-18 sm:h-30 w-auto max-w-[200px] object-contain mb-4 ml-3"
/> />
</div> </div>
{/* Desktop Navigation */} {/* Desktop Navigation */}
{user && ( {user && (
<div className="hidden md:flex items-center space-x-6"> <div className="hidden lg:flex items-center space-x-3 xl:space-x-6">
{getLinks().map((link) => ( {getLinks().map((link) => (
<button <button
key={link.path} key={link.path}
onClick={() => onNavigate(link.path)} onClick={() => onNavigate(link.path)}
className={`text-sm font-medium tracking-wide uppercase hover:text-brand-gold transition-colors pb-1 ${ className={`text-xs xl:text-sm font-medium tracking-wide uppercase hover:text-brand-gold transition-colors pb-1 ${
currentPage === link.path currentPage === link.path
? "text-brand-gold border-b-2 border-brand-gold" ? "text-brand-gold border-b-2 border-brand-gold"
: "text-gray-600 border-b-2 border-transparent" : "text-gray-600 border-b-2 border-transparent"
@ -124,14 +127,14 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
)} )}
{/* Right Side Actions */} {/* Right Side Actions */}
<div className="hidden md:flex items-center space-x-4"> <div className="hidden lg:flex items-center space-x-2 xl:space-x-4">
{user ? ( {user ? (
<div className="flex items-center gap-3"> <div className="flex items-center gap-2 xl:gap-3">
<div className="flex flex-col items-end mr-2"> <div className="flex flex-col items-end mr-1 xl:mr-2">
<span className="text-sm font-bold text-brand-black leading-tight"> <span className="text-xs xl:text-sm font-bold text-brand-black leading-tight">
{user.name} {user.name}
</span> </span>
<span className="text-[10px] uppercase tracking-wider text-brand-gold leading-tight"> <span className="text-[9px] xl:text-[10px] uppercase tracking-wider text-brand-gold leading-tight">
{getRoleLabel()} {getRoleLabel()}
</span> </span>
</div> </div>
@ -142,7 +145,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
onClick={() => onClick={() =>
setIsAccountDropdownOpen(!isAccountDropdownOpen) setIsAccountDropdownOpen(!isAccountDropdownOpen)
} }
className="h-9 w-9 rounded-full bg-gray-100 overflow-hidden border border-gray-200 ring-2 ring-transparent hover:ring-brand-gold transition-all cursor-pointer" className="h-8 w-8 xl:h-9 xl:w-9 rounded-full bg-gray-100 overflow-hidden border border-gray-200 ring-2 ring-transparent hover:ring-brand-gold transition-all cursor-pointer"
> >
<img <img
src={user.avatar} src={user.avatar}
@ -315,7 +318,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
</div> </div>
{/* Mobile Buttons */} {/* Mobile Buttons */}
<div className="md:hidden flex items-center gap-2"> <div className="lg:hidden flex items-center gap-2">
{user ? ( {user ? (
<> <>
<div className="relative"> <div className="relative">
@ -323,7 +326,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
onClick={() => onClick={() =>
setIsAccountDropdownOpen(!isAccountDropdownOpen) setIsAccountDropdownOpen(!isAccountDropdownOpen)
} }
className="h-10 w-10 rounded-full bg-gray-100 overflow-hidden border border-gray-200 ring-2 ring-transparent active:ring-brand-gold transition-all shadow-md" className="h-9 w-9 sm:h-10 sm:w-10 rounded-full bg-gray-100 overflow-hidden border border-gray-200 ring-2 ring-transparent active:ring-brand-gold transition-all shadow-md"
> >
<img <img
src={user.avatar} src={user.avatar}
@ -433,9 +436,13 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
{/* Menu Hamburguer - Apenas para usuários logados */} {/* Menu Hamburguer - Apenas para usuários logados */}
<button <button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)} onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="text-brand-black hover:text-brand-gold p-2" className="text-brand-black hover:text-brand-gold p-1.5 sm:p-2"
> >
{isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />} {isMobileMenuOpen ? (
<X size={22} className="sm:w-6 sm:h-6" />
) : (
<Menu size={22} className="sm:w-6 sm:h-6" />
)}
</button> </button>
</> </>
) : ( ) : (
@ -499,8 +506,8 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
{/* Mobile Menu */} {/* Mobile Menu */}
{isMobileMenuOpen && ( {isMobileMenuOpen && (
<div className="md:hidden absolute top-full left-0 w-full bg-white border-b border-gray-100 shadow-lg fade-in"> <div className="lg:hidden absolute top-full left-0 w-full bg-white border-b border-gray-100 shadow-lg fade-in">
<div className="px-4 py-4 space-y-3"> <div className="px-3 sm:px-4 py-3 sm:py-4 space-y-2 sm:space-y-3">
{user && {user &&
getLinks().map((link) => ( getLinks().map((link) => (
<button <button
@ -509,7 +516,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
onNavigate(link.path); onNavigate(link.path);
setIsMobileMenuOpen(false); setIsMobileMenuOpen(false);
}} }}
className="block w-full text-left text-base font-medium text-gray-700 hover:text-brand-gold py-2 border-b border-gray-50" className="block w-full text-left text-sm sm:text-base font-medium text-gray-700 hover:text-brand-gold py-2 border-b border-gray-50"
> >
{link.name} {link.name}
</button> </button>

View file

@ -6,6 +6,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PhotumFormaturas</title> <title>PhotumFormaturas</title>
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/logofav.ico" />
<link rel="shortcut icon" type="image/x-icon" href="/logofav.ico" />
<!-- Mapbox GL CSS (versão CDN confiável) --> <!-- Mapbox GL CSS (versão CDN confiável) -->
<link href='https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.css' rel='stylesheet' /> <link href='https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.css' rel='stylesheet' />

View file

@ -119,7 +119,7 @@ export const CourseManagement: React.FC = () => {
Empresa Empresa
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
EF I / EF II Cursos
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Observações Observações

View file

@ -211,16 +211,13 @@ export const Dashboard: React.FC<DashboardProps> = ({
const matchesDate = const matchesDate =
!advancedFilters.date || e.date === advancedFilters.date; !advancedFilters.date || e.date === advancedFilters.date;
const matchesFot = const matchesFot =
!advancedFilters.fotId || String((e as any).fotId || '').includes(advancedFilters.fotId); !advancedFilters.fotId ||
String((e as any).fotId || "").includes(advancedFilters.fotId);
const matchesType = const matchesType =
!advancedFilters.type || e.type === advancedFilters.type; !advancedFilters.type || e.type === advancedFilters.type;
return ( return (
matchesSearch && matchesSearch && matchesStatus && matchesDate && matchesFot && matchesType
matchesStatus &&
matchesDate &&
matchesFot &&
matchesType
); );
}); });
@ -292,10 +289,10 @@ export const Dashboard: React.FC<DashboardProps> = ({
if (user.role === UserRole.EVENT_OWNER) { if (user.role === UserRole.EVENT_OWNER) {
return ( return (
<div> <div>
<h1 className="text-3xl font-serif font-bold text-brand-black"> <h1 className="text-xl sm:text-2xl md:text-3xl font-serif font-bold text-brand-black">
Meus Eventos Meus Eventos
</h1> </h1>
<p className="text-gray-500 mt-1"> <p className="text-xs sm:text-sm text-gray-500 mt-0.5 sm:mt-1">
Acompanhe seus eventos ou solicite novos orçamentos. Acompanhe seus eventos ou solicite novos orçamentos.
</p> </p>
</div> </div>
@ -304,10 +301,10 @@ export const Dashboard: React.FC<DashboardProps> = ({
if (user.role === UserRole.PHOTOGRAPHER) { if (user.role === UserRole.PHOTOGRAPHER) {
return ( return (
<div> <div>
<h1 className="text-3xl font-serif font-bold text-brand-black"> <h1 className="text-xl sm:text-2xl md:text-3xl font-serif font-bold text-brand-black">
Eventos Designados Eventos Designados
</h1> </h1>
<p className="text-gray-500 mt-1"> <p className="text-xs sm:text-sm text-gray-500 mt-0.5 sm:mt-1">
Gerencie seus trabalhos e visualize detalhes. Gerencie seus trabalhos e visualize detalhes.
</p> </p>
</div> </div>
@ -315,10 +312,10 @@ export const Dashboard: React.FC<DashboardProps> = ({
} }
return ( return (
<div> <div>
<h1 className="text-3xl font-serif font-bold text-brand-black"> <h1 className="text-xl sm:text-2xl md:text-3xl font-serif font-bold text-brand-black">
Gestão Geral Gestão Geral
</h1> </h1>
<p className="text-gray-500 mt-1"> <p className="text-xs sm:text-sm text-gray-500 mt-0.5 sm:mt-1">
Controle total de eventos, aprovações e equipes. Controle total de eventos, aprovações e equipes.
</p> </p>
</div> </div>
@ -343,11 +340,11 @@ export const Dashboard: React.FC<DashboardProps> = ({
// --- MAIN RENDER --- // --- MAIN RENDER ---
return ( return (
<div className="min-h-screen bg-white pt-24 pb-12 px-4 sm:px-6 lg:px-8"> <div className="min-h-screen bg-gray-50 pt-20 sm:pt-24 md:pt-28 lg:pt-32 pb-8 sm:pb-12 px-3 sm:px-4 lg:px-6">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
{/* Header */} {/* Header */}
{view === "list" && ( {view === "list" && (
<div className="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4 fade-in"> <div className="flex flex-col md:flex-row md:items-center justify-between mb-6 sm:mb-8 gap-3 sm:gap-4 fade-in">
{renderRoleSpecificHeader()} {renderRoleSpecificHeader()}
{renderRoleSpecificActions()} {renderRoleSpecificActions()}
</div> </div>
@ -581,7 +578,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
FOT FOT
</td> </td>
<td className="px-4 py-3 text-sm text-gray-900 font-medium"> <td className="px-4 py-3 text-sm text-gray-900 font-medium">
{(selectedEvent as any).fotId || '-'} {(selectedEvent as any).fotId || "-"}
</td> </td>
</tr> </tr>
<tr className="hover:bg-gray-50"> <tr className="hover:bg-gray-50">
@ -589,7 +586,9 @@ export const Dashboard: React.FC<DashboardProps> = ({
Data Data
</td> </td>
<td className="px-4 py-3 text-sm text-gray-900"> <td className="px-4 py-3 text-sm text-gray-900">
{new Date(selectedEvent.date + "T00:00:00").toLocaleDateString("pt-BR")} {new Date(
selectedEvent.date + "T00:00:00"
).toLocaleDateString("pt-BR")}
</td> </td>
</tr> </tr>
<tr className="hover:bg-gray-50"> <tr className="hover:bg-gray-50">
@ -597,7 +596,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
Curso Curso
</td> </td>
<td className="px-4 py-3 text-sm text-gray-900"> <td className="px-4 py-3 text-sm text-gray-900">
{(selectedEvent as any).curso || '-'} {(selectedEvent as any).curso || "-"}
</td> </td>
</tr> </tr>
<tr className="hover:bg-gray-50"> <tr className="hover:bg-gray-50">
@ -605,7 +604,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
Instituição Instituição
</td> </td>
<td className="px-4 py-3 text-sm text-gray-900"> <td className="px-4 py-3 text-sm text-gray-900">
{(selectedEvent as any).instituicao || '-'} {(selectedEvent as any).instituicao || "-"}
</td> </td>
</tr> </tr>
<tr className="hover:bg-gray-50"> <tr className="hover:bg-gray-50">
@ -613,7 +612,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
Ano Formatura Ano Formatura
</td> </td>
<td className="px-4 py-3 text-sm text-gray-900"> <td className="px-4 py-3 text-sm text-gray-900">
{(selectedEvent as any).anoFormatura || '-'} {(selectedEvent as any).anoFormatura || "-"}
</td> </td>
</tr> </tr>
<tr className="hover:bg-gray-50"> <tr className="hover:bg-gray-50">
@ -621,7 +620,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
Empresa Empresa
</td> </td>
<td className="px-4 py-3 text-sm text-gray-900"> <td className="px-4 py-3 text-sm text-gray-900">
{(selectedEvent as any).empresa || '-'} {(selectedEvent as any).empresa || "-"}
</td> </td>
</tr> </tr>
<tr className="hover:bg-gray-50"> <tr className="hover:bg-gray-50">
@ -637,7 +636,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
Observações Observações
</td> </td>
<td className="px-4 py-3 text-sm text-gray-900"> <td className="px-4 py-3 text-sm text-gray-900">
{(selectedEvent as any).observacoes || '-'} {(selectedEvent as any).observacoes || "-"}
</td> </td>
</tr> </tr>
<tr className="hover:bg-gray-50"> <tr className="hover:bg-gray-50">
@ -645,7 +644,8 @@ export const Dashboard: React.FC<DashboardProps> = ({
Local Local
</td> </td>
<td className="px-4 py-3 text-sm text-gray-900"> <td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.address.street}, {selectedEvent.address.number} {selectedEvent.address.street},{" "}
{selectedEvent.address.number}
</td> </td>
</tr> </tr>
<tr className="hover:bg-gray-50"> <tr className="hover:bg-gray-50">
@ -653,8 +653,10 @@ export const Dashboard: React.FC<DashboardProps> = ({
Endereço Endereço
</td> </td>
<td className="px-4 py-3 text-sm text-gray-900"> <td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.address.city} - {selectedEvent.address.state} {selectedEvent.address.city} -{" "}
{selectedEvent.address.zip && ` | CEP: ${selectedEvent.address.zip}`} {selectedEvent.address.state}
{selectedEvent.address.zip &&
` | CEP: ${selectedEvent.address.zip}`}
</td> </td>
</tr> </tr>
<tr className="hover:bg-gray-50"> <tr className="hover:bg-gray-50">
@ -670,7 +672,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
Qtd Formandos Qtd Formandos
</td> </td>
<td className="px-4 py-3 text-sm text-gray-900"> <td className="px-4 py-3 text-sm text-gray-900">
{(selectedEvent as any).qtdFormandos || '-'} {(selectedEvent as any).qtdFormandos || "-"}
</td> </td>
</tr> </tr>
</tbody> </tbody>

File diff suppressed because it is too large Load diff

View file

@ -40,30 +40,30 @@ export const Home: React.FC<HomeProps> = ({ onEnter }) => {
{/* Círculos decorativos animados */} {/* Círculos decorativos animados */}
<div <div
className="absolute top-10 left-10 w-64 h-64 bg-white/20 rounded-full blur-3xl animate-float" className="absolute top-5 sm:top-10 left-5 sm:left-10 w-32 sm:w-48 md:w-64 h-32 sm:h-48 md:h-64 bg-white/20 rounded-full blur-2xl sm:blur-3xl animate-float"
style={{ animationDelay: "0s" }} style={{ animationDelay: "0s" }}
></div> ></div>
<div <div
className="absolute bottom-20 right-20 w-80 h-80 bg-white/20 rounded-full blur-3xl animate-float" className="absolute bottom-10 sm:bottom-20 right-10 sm:right-20 w-48 sm:w-64 md:w-80 h-48 sm:h-64 md:h-80 bg-white/20 rounded-full blur-2xl sm:blur-3xl animate-float"
style={{ animationDelay: "2s" }} style={{ animationDelay: "2s" }}
></div> ></div>
<div <div
className="absolute top-1/3 right-10 w-48 h-48 bg-white/20 rounded-full blur-2xl animate-float" className="absolute top-1/3 right-5 sm:right-10 w-32 sm:w-40 md:w-48 h-32 sm:h-40 md:h-48 bg-white/20 rounded-full blur-xl sm:blur-2xl animate-float"
style={{ animationDelay: "4s" }} style={{ animationDelay: "4s" }}
></div> ></div>
<div className="bg-white rounded-2xl shadow-2xl p-8 sm:p-12 max-w-md w-full mx-4 animate-fade-in-scale relative z-10"> <div className="bg-white rounded-xl sm:rounded-2xl shadow-2xl p-6 sm:p-8 md:p-12 max-w-[90%] sm:max-w-md w-full mx-3 sm:mx-4 animate-fade-in-scale relative z-10">
{/* Logo */} {/* Logo */}
<div className="flex justify-center mb-6"> <div className="flex justify-center mb-4 sm:mb-6">
<img <img
src="/logo_preta.png" src="/logo_preta.png"
alt="Photum Formaturas" alt="Photum Formaturas"
className="h-24 sm:h-32 w-auto object-contain" className="h-16 sm:h-20 md:h-24 lg:h-32 w-auto object-contain"
/> />
</div> </div>
{/* Botões */} {/* Botões */}
<div className="space-y-4"> <div className="space-y-3 sm:space-y-4">
<Button onClick={onEnter} className="w-full" size="lg"> <Button onClick={onEnter} className="w-full" size="lg">
Entrar Entrar
</Button> </Button>

View file

@ -1,9 +1,8 @@
import React, { useState } from "react";
import React, { useState } from 'react'; import { useAuth } from "../contexts/AuthContext";
import { useAuth } from '../contexts/AuthContext'; import { Button } from "../components/Button";
import { Button } from '../components/Button'; import { Input } from "../components/Input";
import { Input } from '../components/Input'; import { UserRole } from "../types";
import { UserRole } from '../types';
interface LoginProps { interface LoginProps {
onNavigate?: (page: string) => void; onNavigate?: (page: string) => void;
@ -11,16 +10,16 @@ interface LoginProps {
export const Login: React.FC<LoginProps> = ({ onNavigate }) => { export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
const { login, availableUsers } = useAuth(); const { login, availableUsers } = useAuth();
const [email, setEmail] = useState(''); const [email, setEmail] = useState("");
const [password, setPassword] = useState(''); const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState("");
const handleLogin = async (e: React.FormEvent) => { const handleLogin = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setIsLoading(true); setIsLoading(true);
setError(''); setError("");
try { try {
const success = await login(email, password); const success = await login(email, password);
@ -28,62 +27,79 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
// If real login throws, we catch it below. // If real login throws, we catch it below.
if (!success) { if (!success) {
// Fallback for mock if it returns false without throwing // Fallback for mock if it returns false without throwing
setError('Credenciais inválidas, tente novamente ou tente um dos e-mails de demonstração.'); setError(
"Credenciais inválidas, tente novamente ou tente um dos e-mails de demonstração."
);
} }
} catch (err: any) { } catch (err: any) {
setError(err.message || 'Erro ao realizar login'); setError(err.message || "Erro ao realizar login");
} }
setIsLoading(false); setIsLoading(false);
}; };
const fillCredentials = (userEmail: string) => { const fillCredentials = (userEmail: string) => {
setEmail(userEmail); setEmail(userEmail);
setPassword('123456'); // Dummy password to pass HTML5 validation setPassword("123456"); // Dummy password to pass HTML5 validation
}; };
const getRoleLabel = (role: UserRole) => { const getRoleLabel = (role: UserRole) => {
switch (role) { switch (role) {
case UserRole.SUPERADMIN: return "Superadmin"; case UserRole.SUPERADMIN:
case UserRole.BUSINESS_OWNER: return "Empresa"; return "Superadmin";
case UserRole.PHOTOGRAPHER: return "Fotógrafo"; case UserRole.BUSINESS_OWNER:
case UserRole.EVENT_OWNER: return "Cliente"; return "Empresa";
default: return role; case UserRole.PHOTOGRAPHER:
return "Fotógrafo";
case UserRole.EVENT_OWNER:
return "Cliente";
default:
return role;
} }
} };
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-50 to-white p-4 pt-24"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-50 to-white p-3 sm:p-4 pt-20 sm:pt-24">
<div className="w-full max-w-md fade-in relative z-10 space-y-6"> <div className="w-full max-w-md fade-in relative z-10 space-y-4 sm:space-y-6">
<div className="bg-white rounded-2xl shadow-xl border border-gray-100 p-6 sm:p-8"> <div className="bg-white rounded-xl sm:rounded-2xl shadow-xl border border-gray-100 p-5 sm:p-6 md:p-8">
{/* Logo dentro do card */} {/* Logo dentro do card */}
<div className="flex justify-center mb-4"> <div className="flex justify-center mb-3 sm:mb-4">
<img <img
src="/logo.png" src="/logo.png"
alt="Photum Formaturas" alt="Photum Formaturas"
className="h-18 sm:h-30 w-auto max-w-[200px] object-contain" className="h-14 sm:h-16 md:h-20 w-auto max-w-[150px] sm:max-w-[180px] object-contain"
/> />
</div> </div>
<div className="text-center"> <div className="text-center">
<span className="font-bold tracking-widest uppercase text-xs sm:text-sm" style={{ color: '#B9CF33' }}>Bem-vindo de volta</span> <span
<h2 className="mt-2 text-2xl sm:text-3xl font-serif font-bold text-gray-900">Acesse sua conta</h2> className="font-bold tracking-widest uppercase text-[10px] sm:text-xs"
<p className="mt-2 text-xs sm:text-sm text-gray-600"> style={{ color: "#B9CF33" }}
Não tem uma conta?{' '} >
Bem-vindo de volta
</span>
<h2 className="mt-1.5 sm:mt-2 text-xl sm:text-2xl md:text-3xl font-serif font-bold text-gray-900">
Acesse sua conta
</h2>
<p className="mt-1.5 sm:mt-2 text-xs sm:text-sm text-gray-600">
Não tem uma conta?{" "}
<button <button
type="button" type="button"
onClick={() => onNavigate?.('cadastro')} onClick={() => onNavigate?.("cadastro")}
className="font-medium hover:opacity-80 transition-opacity" className="font-medium hover:opacity-80 transition-opacity"
style={{ color: '#B9CF33' }} style={{ color: "#B9CF33" }}
> >
Cadastre-se Cadastre-se
</button> </button>
</p> </p>
</div> </div>
<form className="mt-6 sm:mt-8 space-y-4 sm:space-y-6" onSubmit={handleLogin}> <form
<div className="space-y-3 sm:space-y-4"> className="mt-5 sm:mt-6 md:mt-8 space-y-3 sm:space-y-4 md:space-y-6"
onSubmit={handleLogin}
>
<div className="space-y-2.5 sm:space-y-3 md:space-y-4">
<div> <div>
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-1.5 sm:mb-2"> <label className="block text-[10px] sm:text-xs md:text-sm font-medium text-gray-700 mb-1 sm:mb-1.5">
E-MAIL CORPORATIVO OU PESSOAL E-MAIL CORPORATIVO OU PESSOAL
</label> </label>
<input <input
@ -92,28 +108,32 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
placeholder="nome@exemplo.com" placeholder="nome@exemplo.com"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => 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" className="w-full px-2.5 sm:px-3 md:px-4 py-2 sm:py-2.5 md:py-3 text-xs sm:text-sm md:text-base border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:border-transparent transition-all"
style={{ focusRing: '2px solid #B9CF33' }} style={{ focusRing: "2px solid #B9CF33" }}
onFocus={(e) => e.target.style.borderColor = '#B9CF33'} onFocus={(e) => (e.target.style.borderColor = "#B9CF33")}
onBlur={(e) => e.target.style.borderColor = '#d1d5db'} onBlur={(e) => (e.target.style.borderColor = "#d1d5db")}
/> />
{error && <span className="text-xs text-red-500 mt-1 block">{error}</span>} {error && (
<span className="text-xs text-red-500 mt-1 block">
{error}
</span>
)}
</div> </div>
<div> <div>
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-1.5 sm:mb-2"> <label className="block text-[10px] sm:text-xs md:text-sm font-medium text-gray-700 mb-1 sm:mb-1.5">
SENHA SENHA
</label> </label>
<div className="relative"> <div className="relative">
<input <input
type={showPassword ? 'text' : 'password'} type={showPassword ? "text" : "password"}
placeholder="••••••••" placeholder="••••••••"
required required
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(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" className="w-full px-2.5 sm:px-3 md:px-4 py-2 sm:py-2.5 md:py-3 text-xs sm:text-sm md:text-base border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:border-transparent transition-all"
style={{ focusRing: '2px solid #5A4B81' }} style={{ focusRing: "2px solid #5A4B81" }}
onFocus={(e) => e.target.style.borderColor = '#5A4B81'} onFocus={(e) => (e.target.style.borderColor = "#5A4B81")}
onBlur={(e) => e.target.style.borderColor = '#d1d5db'} onBlur={(e) => (e.target.style.borderColor = "#d1d5db")}
/> />
<button <button
type="button" type="button"
@ -121,13 +141,38 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600" className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
> >
{showPassword ? ( {showPassword ? (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" /> className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
/>
</svg> </svg>
) : ( ) : (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> className="w-5 h-5"
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /> fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
/>
</svg> </svg>
)} )}
</button> </button>
@ -139,41 +184,63 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
type="submit" type="submit"
disabled={isLoading} disabled={isLoading}
className="w-full px-6 sm:px-10 py-3 sm:py-4 text-white font-bold text-base sm:text-lg rounded-lg transition-all duration-300 transform hover:scale-[1.02] hover:shadow-xl active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed" className="w-full px-6 sm:px-10 py-3 sm:py-4 text-white font-bold text-base sm:text-lg rounded-lg transition-all duration-300 transform hover:scale-[1.02] hover:shadow-xl active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
style={{ backgroundColor: '#4E345F' }} style={{ backgroundColor: "#4E345F" }}
> >
{isLoading ? 'Entrando...' : 'Entrar no Sistema'} {isLoading ? "Entrando..." : "Entrar no Sistema"}
</button> </button>
</form> </form>
</div> </div>
{/* Demo Users Quick Select */} {/* Demo Users Quick Select */}
<div className="bg-white rounded-2xl shadow-xl border border-gray-100 p-6"> <div className="bg-white rounded-2xl shadow-xl border border-gray-100 p-6">
<p className="text-[10px] sm:text-xs uppercase tracking-widest mb-3 sm:mb-4 text-center text-gray-400">Usuários de Demonstração (Clique para preencher)</p> <p className="text-[10px] sm:text-xs uppercase tracking-widest mb-3 sm:mb-4 text-center text-gray-400">
Usuários de Demonstração (Clique para preencher)
</p>
<div className="space-y-2"> <div className="space-y-2">
{availableUsers.map(user => ( {availableUsers.map((user) => (
<button <button
key={user.id} key={user.id}
onClick={() => fillCredentials(user.email)} onClick={() => fillCredentials(user.email)}
className="w-full flex items-center justify-between p-3 sm:p-4 border-2 rounded-xl hover:bg-gray-50 transition-all duration-300 text-left group transform hover:scale-[1.01] active:scale-[0.99]" className="w-full flex items-center justify-between p-3 sm:p-4 border-2 rounded-xl hover:bg-gray-50 transition-all duration-300 text-left group transform hover:scale-[1.01] active:scale-[0.99]"
style={{ borderColor: '#e5e7eb' }} style={{ borderColor: "#e5e7eb" }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#B9CF33'; e.currentTarget.style.borderColor = "#B9CF33";
e.currentTarget.style.boxShadow = '0 4px 12px rgba(185, 207, 51, 0.15)'; e.currentTarget.style.boxShadow =
"0 4px 12px rgba(185, 207, 51, 0.15)";
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.borderColor = '#e5e7eb'; e.currentTarget.style.borderColor = "#e5e7eb";
e.currentTarget.style.boxShadow = 'none'; e.currentTarget.style.boxShadow = "none";
}} }}
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<span className="text-sm sm:text-base font-bold text-gray-900 truncate">{user.name}</span> <span className="text-sm sm:text-base font-bold text-gray-900 truncate">
<span className="text-[10px] sm:text-xs uppercase tracking-wide font-semibold px-2 py-0.5 rounded-full whitespace-nowrap" style={{ backgroundColor: '#B9CF33', color: '#fff' }}>{getRoleLabel(user.role)}</span> {user.name}
</span>
<span
className="text-[10px] sm:text-xs uppercase tracking-wide font-semibold px-2 py-0.5 rounded-full whitespace-nowrap"
style={{ backgroundColor: "#B9CF33", color: "#fff" }}
>
{getRoleLabel(user.role)}
</span>
</div> </div>
<span className="text-xs sm:text-sm text-gray-500 truncate block">{user.email}</span> <span className="text-xs sm:text-sm text-gray-500 truncate block">
{user.email}
</span>
</div> </div>
<svg className="w-5 h-5 text-gray-400 group-hover:text-[#B9CF33] transition-colors flex-shrink-0 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /> className="w-5 h-5 text-gray-400 group-hover:text-[#B9CF33] transition-colors flex-shrink-0 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg> </svg>
</button> </button>
))} ))}

View file

@ -0,0 +1,401 @@
import React, { useState, useEffect, useMemo } from "react";
import {
Download,
ArrowUpDown,
ArrowUp,
ArrowDown,
AlertCircle,
} from "lucide-react";
interface PhotographerPayment {
id: string;
data: string;
nomeEvento: string;
tipoEvento: string;
empresa: string;
valorRecebido: number;
dataPagamento: string;
statusPagamento: "Pago" | "Pendente" | "Atrasado";
}
type SortField = keyof PhotographerPayment | null;
type SortDirection = "asc" | "desc" | null;
const PhotographerFinance: React.FC = () => {
const [payments, setPayments] = useState<PhotographerPayment[]>([
{
id: "1",
data: "2025-11-15",
nomeEvento: "Formatura Medicina UFPR 2025",
tipoEvento: "Formatura",
empresa: "PhotoPro Studio",
valorRecebido: 1500.0,
dataPagamento: "2025-11-20",
statusPagamento: "Pago",
},
{
id: "2",
data: "2025-11-18",
nomeEvento: "Formatura Direito PUC-PR 2025",
tipoEvento: "Formatura",
empresa: "PhotoPro Studio",
valorRecebido: 1200.0,
dataPagamento: "2025-11-25",
statusPagamento: "Pago",
},
{
id: "3",
data: "2025-12-01",
nomeEvento: "Formatura Engenharia UTFPR 2025",
tipoEvento: "Formatura",
empresa: "Lens & Art",
valorRecebido: 1800.0,
dataPagamento: "2025-12-15",
statusPagamento: "Pendente",
},
]);
const [sortField, setSortField] = useState<SortField>(null);
const [sortDirection, setSortDirection] = useState<SortDirection>(null);
const [apiError, setApiError] = useState<string>("");
// Load API data
useEffect(() => {
loadApiData();
}, []);
const loadApiData = async () => {
try {
// TODO: Implementar chamada real da API
// const response = await fetch("http://localhost:3000/api/photographer/payments");
// const data = await response.json();
// setPayments(data);
} catch (error) {
console.error("Erro ao carregar pagamentos:", error);
setApiError(
"Não foi possível carregar os dados da API. Usando dados de exemplo."
);
}
};
// Sorting logic
const handleSort = (field: keyof PhotographerPayment) => {
if (sortField === field) {
if (sortDirection === "asc") {
setSortDirection("desc");
} else if (sortDirection === "desc") {
setSortDirection(null);
setSortField(null);
}
} else {
setSortField(field);
setSortDirection("asc");
}
};
const getSortIcon = (field: keyof PhotographerPayment) => {
if (sortField !== field) {
return (
<ArrowUpDown
size={16}
className="opacity-0 group-hover:opacity-50 transition-opacity"
/>
);
}
if (sortDirection === "asc") {
return <ArrowUp size={16} className="text-brand-gold" />;
}
if (sortDirection === "desc") {
return <ArrowDown size={16} className="text-brand-gold" />;
}
return (
<ArrowUpDown
size={16}
className="opacity-0 group-hover:opacity-50 transition-opacity"
/>
);
};
const sortedPayments = useMemo(() => {
if (!sortField || !sortDirection) return payments;
return [...payments].sort((a, b) => {
const aValue = a[sortField];
const bValue = b[sortField];
if (aValue === null || aValue === undefined) return 1;
if (bValue === null || bValue === undefined) return -1;
let comparison = 0;
if (typeof aValue === "string" && typeof bValue === "string") {
comparison = aValue.localeCompare(bValue);
} else if (typeof aValue === "number" && typeof bValue === "number") {
comparison = aValue - bValue;
} else if (typeof aValue === "boolean" && typeof bValue === "boolean") {
comparison = aValue === bValue ? 0 : aValue ? 1 : -1;
}
return sortDirection === "asc" ? comparison : -comparison;
});
}, [payments, sortField, sortDirection]);
// Export to CSV
const handleExport = () => {
const headers = [
"Data Evento",
"Nome Evento",
"Tipo Evento",
"Empresa",
"Valor Recebido",
"Data Pagamento",
"Status",
];
const csvContent = [
headers.join(","),
...sortedPayments.map((p) =>
[
p.data,
`"${p.nomeEvento}"`,
p.tipoEvento,
p.empresa,
p.valorRecebido.toFixed(2),
p.dataPagamento,
p.statusPagamento,
].join(",")
),
].join("\n");
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
const link = document.createElement("a");
const url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", `meus_pagamentos_${Date.now()}.csv`);
link.style.visibility = "hidden";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// Calculate totals
const totalRecebido = sortedPayments.reduce(
(sum, p) => sum + p.valorRecebido,
0
);
const totalPago = sortedPayments
.filter((p) => p.statusPagamento === "Pago")
.reduce((sum, p) => sum + p.valorRecebido, 0);
const totalPendente = sortedPayments
.filter((p) => p.statusPagamento === "Pendente")
.reduce((sum, p) => sum + p.valorRecebido, 0);
const getStatusBadge = (status: string) => {
const statusColors = {
Pago: "bg-green-100 text-green-800",
Pendente: "bg-yellow-100 text-yellow-800",
Atrasado: "bg-red-100 text-red-800",
};
return (
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${
statusColors[status as keyof typeof statusColors] ||
"bg-gray-100 text-gray-800"
}`}
>
{status}
</span>
);
};
return (
<div className="min-h-screen bg-gray-50 pt-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Meus Pagamentos</h1>
<p className="mt-2 text-gray-600">
Visualize todos os pagamentos recebidos pelos eventos fotografados
</p>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="bg-white rounded-lg shadow p-6">
<p className="text-sm text-gray-600 mb-1">Total Recebido</p>
<p className="text-2xl font-bold text-gray-900">
R$ {totalRecebido.toFixed(2)}
</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<p className="text-sm text-gray-600 mb-1">Pagamentos Confirmados</p>
<p className="text-2xl font-bold text-green-600">
R$ {totalPago.toFixed(2)}
</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<p className="text-sm text-gray-600 mb-1">Pagamentos Pendentes</p>
<p className="text-2xl font-bold text-yellow-600">
R$ {totalPendente.toFixed(2)}
</p>
</div>
</div>
{/* Main Card */}
<div className="bg-white rounded-lg shadow">
{/* Actions Bar */}
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">
Histórico de Pagamentos
</h2>
<button
onClick={handleExport}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition-colors"
>
<Download size={20} />
Exportar
</button>
</div>
</div>
{apiError && (
<div className="mx-6 mt-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg flex items-start gap-3">
<AlertCircle
className="text-yellow-600 flex-shrink-0 mt-0.5"
size={20}
/>
<div className="flex-1">
<p className="text-sm font-medium text-yellow-800">Aviso</p>
<p className="text-sm text-yellow-700 mt-1">{apiError}</p>
</div>
</div>
)}
{/* Table */}
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-4 py-3 text-left">
<button
onClick={() => handleSort("data")}
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
>
Data Evento
{getSortIcon("data")}
</button>
</th>
<th className="px-4 py-3 text-left">
<button
onClick={() => handleSort("nomeEvento")}
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
>
Nome Evento
{getSortIcon("nomeEvento")}
</button>
</th>
<th className="px-4 py-3 text-left">
<button
onClick={() => handleSort("tipoEvento")}
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
>
Tipo Evento
{getSortIcon("tipoEvento")}
</button>
</th>
<th className="px-4 py-3 text-left">
<button
onClick={() => handleSort("empresa")}
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
>
Empresa
{getSortIcon("empresa")}
</button>
</th>
<th className="px-4 py-3 text-left">
<button
onClick={() => handleSort("valorRecebido")}
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
>
Valor Recebido
{getSortIcon("valorRecebido")}
</button>
</th>
<th className="px-4 py-3 text-left">
<button
onClick={() => handleSort("dataPagamento")}
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
>
Data Pagamento
{getSortIcon("dataPagamento")}
</button>
</th>
<th className="px-4 py-3 text-left">
<button
onClick={() => handleSort("statusPagamento")}
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
>
Status
{getSortIcon("statusPagamento")}
</button>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{sortedPayments.length === 0 ? (
<tr>
<td
colSpan={7}
className="px-4 py-8 text-center text-gray-500"
>
Nenhum pagamento encontrado
</td>
</tr>
) : (
sortedPayments.map((payment) => (
<tr
key={payment.id}
className="hover:bg-gray-50 transition-colors"
>
<td className="px-4 py-3 whitespace-nowrap text-sm">
{new Date(payment.data).toLocaleDateString("pt-BR")}
</td>
<td className="px-4 py-3 text-sm">
{payment.nomeEvento}
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">
{payment.tipoEvento}
</td>
<td className="px-4 py-3 text-sm">{payment.empresa}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm font-medium text-green-600">
R$ {payment.valorRecebido.toFixed(2)}
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">
{new Date(payment.dataPagamento).toLocaleDateString(
"pt-BR"
)}
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">
{getStatusBadge(payment.statusPagamento)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Footer */}
<div className="px-6 py-4 border-t border-gray-200 bg-gray-50">
<p className="text-sm text-gray-600">
Total de pagamentos: {sortedPayments.length}
</p>
</div>
</div>
</div>
</div>
);
};
export default PhotographerFinance;

View file

@ -157,15 +157,15 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
<div className="text-center"> <div className="text-center">
<span <span
className="font-bold tracking-widest uppercase text-xs sm:text-sm" className="font-bold tracking-widest uppercase text-[10px] sm:text-xs"
style={{ color: "#B9CF33" }} style={{ color: "#B9CF33" }}
> >
Comece agora Comece agora
</span> </span>
<h2 className="mt-2 text-2xl sm:text-3xl font-serif font-bold text-gray-900"> <h2 className="mt-1.5 sm:mt-2 text-xl sm:text-2xl md:text-3xl font-serif font-bold text-gray-900">
Crie sua conta Crie sua conta
</h2> </h2>
<p className="mt-2 text-xs sm:text-sm text-gray-600"> <p className="mt-1.5 sm:mt-2 text-xs sm:text-sm text-gray-600">
tem uma conta?{" "} tem uma conta?{" "}
<button <button
onClick={() => onNavigate("entrar")} onClick={() => onNavigate("entrar")}
@ -177,7 +177,28 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
</p> </p>
</div> </div>
<form className="mt-6 space-y-4" onSubmit={handleSubmit}> <form
className="mt-5 sm:mt-6 space-y-3 sm:space-y-4"
onSubmit={handleSubmit}
>
<div className="flex items-start bg-blue-50 border border-blue-200 rounded-lg p-2.5 sm:p-3 md:p-4 mb-3 sm:mb-4">
<div className="flex-1">
<p className="text-xs sm:text-sm text-gray-700">
<span className="font-medium text-xs sm:text-sm">
Você é um profissional?
</span>
</p>
<button
type="button"
onClick={() => onNavigate("cadastro-profissional")}
className="text-xs sm:text-sm mt-1 hover:opacity-80 transition-opacity underline font-medium"
style={{ color: "#B9CF33" }}
>
Clique aqui para se cadastrar como profissional
</button>
</div>
</div>
<div className="space-y-3"> <div className="space-y-3">
<Input <Input
label="Nome Completo" label="Nome Completo"
@ -208,11 +229,11 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
/> />
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-[10px] sm:text-xs md:text-sm font-medium text-gray-700 mb-1">
Empresa * Empresa *
</label> </label>
{isLoadingCompanies ? ( {isLoadingCompanies ? (
<p className="text-sm text-gray-500"> <p className="text-xs sm:text-sm text-gray-500">
Carregando empresas... Carregando empresas...
</p> </p>
) : ( ) : (
@ -220,7 +241,7 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
required required
value={formData.empresaId} value={formData.empresaId}
onChange={(e) => handleChange("empresaId", e.target.value)} onChange={(e) => handleChange("empresaId", e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent" className="w-full px-2.5 sm:px-3 md:px-4 py-1.5 sm:py-2 text-xs sm:text-sm border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent"
style={{ focusRing: "2px solid #B9CF33" }} style={{ focusRing: "2px solid #B9CF33" }}
> >
<option value="">Selecione uma empresa</option> <option value="">Selecione uma empresa</option>
@ -253,7 +274,7 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
} }
error={ error={
error && error &&
(error.includes("senha") || error.includes("coincidem")) (error.includes("senha") || error.includes("coincidem"))
? error ? error
: undefined : undefined
} }
@ -297,24 +318,6 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
)} )}
</div> </div>
<div className="flex items-start bg-gray-50 border border-gray-200 rounded-lg p-3 sm:p-4">
<div className="flex-1">
<p className="text-xs sm:text-sm text-gray-700">
<span className="font-medium text-xs sm:text-sm">
Você é um profissional?
</span>
</p>
<button
type="button"
onClick={() => onNavigate("cadastro-profissional")}
className="text-xs sm:text-sm mt-1 hover:opacity-80 transition-opacity underline font-medium"
style={{ color: "#B9CF33" }}
>
Clique aqui para se cadastrar como profissional
</button>
</div>
</div>
{error && {error &&
!error.includes("termos") && !error.includes("termos") &&
!error.includes("senha") && !error.includes("senha") &&

View file

@ -0,0 +1,490 @@
import React, { useState, useEffect } from 'react';
import { Download, Plus, ArrowUpDown, ArrowUp, ArrowDown, X, AlertCircle } from 'lucide-react';
interface University {
id: string;
empresa: string;
nomeUniversidade: string;
anoFormatura: number;
semestre: number;
cursos: string[];
}
const UniversityManagement: React.FC = () => {
const [universities, setUniversities] = useState<University[]>([
{
id: '1',
empresa: 'PhotoPro Studio',
nomeUniversidade: 'UFPR - Universidade Federal do Paraná',
anoFormatura: 2025,
semestre: 2,
cursos: ['Medicina', 'Direito', 'Engenharia Civil']
},
{
id: '2',
empresa: 'Lens & Art',
nomeUniversidade: 'PUC-PR - Pontifícia Universidade Católica do Paraná',
anoFormatura: 2025,
semestre: 1,
cursos: ['Administração', 'Arquitetura', 'Psicologia']
}
]);
const [showAddModal, setShowAddModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [selectedUniversity, setSelectedUniversity] = useState<University | null>(null);
const [sortConfig, setSortConfig] = useState<{ key: keyof University; direction: 'asc' | 'desc' } | null>(null);
// Estados para dados da API
const [cursos, setCursos] = useState<any[]>([]);
const [empresas, setEmpresas] = useState<any[]>([]);
const [apiError, setApiError] = useState<string>('');
const [loadingApi, setLoadingApi] = useState(false);
// Form state
const [formData, setFormData] = useState<Partial<University>>({
empresa: '',
nomeUniversidade: '',
anoFormatura: new Date().getFullYear(),
semestre: 1,
cursos: []
});
// Estados para seleção de cursos
const [selectedCursos, setSelectedCursos] = useState<string[]>([]);
const [cursoInput, setCursoInput] = useState('');
// Carregar dados da API
const loadApiData = async () => {
setLoadingApi(true);
setApiError('');
try {
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000';
// Carregar cursos
try {
const cursosRes = await fetch(`${API_BASE_URL}/api/cursos`);
if (cursosRes.ok) {
const cursosData = await cursosRes.json();
setCursos(cursosData);
}
} catch (error) {
console.error('Erro ao carregar cursos:', error);
}
// Carregar empresas
try {
const empRes = await fetch(`${API_BASE_URL}/api/empresas`);
if (empRes.ok) {
const empData = await empRes.json();
setEmpresas(empData);
}
} catch (error) {
console.error('Erro ao carregar empresas:', error);
}
if (cursos.length === 0 && empresas.length === 0) {
setApiError('Backend não está rodando. Alguns campos podem não estar disponíveis.');
}
} catch (error) {
setApiError('Backend não está rodando. Alguns campos podem não estar disponíveis.');
} finally {
setLoadingApi(false);
}
};
useEffect(() => {
if (showAddModal || showEditModal) {
loadApiData();
}
}, [showAddModal, showEditModal]);
// Ordenação
const handleSort = (key: keyof University) => {
if (sortConfig && sortConfig.key === key) {
if (sortConfig.direction === 'asc') {
setSortConfig({ key, direction: 'desc' });
} else {
setSortConfig(null);
}
} else {
setSortConfig({ key, direction: 'asc' });
}
};
const sortedUniversities = React.useMemo(() => {
let sortableUniversities = [...universities];
if (sortConfig !== null) {
sortableUniversities.sort((a, b) => {
const aValue = a[sortConfig.key];
const bValue = b[sortConfig.key];
if (aValue < bValue) {
return sortConfig.direction === 'asc' ? -1 : 1;
}
if (aValue > bValue) {
return sortConfig.direction === 'asc' ? 1 : -1;
}
return 0;
});
}
return sortableUniversities;
}, [universities, sortConfig]);
const getSortIcon = (key: keyof University) => {
if (sortConfig?.key !== key) {
return <ArrowUpDown size={14} className="opacity-0 group-hover:opacity-50 transition-opacity" />;
}
if (sortConfig.direction === 'asc') {
return <ArrowUp size={14} className="text-brand-gold" />;
}
return <ArrowDown size={14} className="text-brand-gold" />;
};
// Handlers
const handleAddUniversity = () => {
setFormData({
empresa: '',
nomeUniversidade: '',
semestre: 1,
anoFormatura: new Date().getFullYear(),
cursos: []
});
setSelectedCursos([]);
setShowAddModal(true);
};
const handleEditUniversity = (university: University) => {
setSelectedUniversity(university);
setFormData(university);
setSelectedCursos(university.cursos || []);
setShowEditModal(true);
};
const handleAddCurso = () => {
if (cursoInput && !selectedCursos.includes(cursoInput)) {
setSelectedCursos([...selectedCursos, cursoInput]);
setCursoInput('');
}
};
const handleRemoveCurso = (curso: string) => {
setSelectedCursos(selectedCursos.filter(c => c !== curso));
};
const handleSaveUniversity = () => {
const updatedData = { ...formData, cursos: selectedCursos };
if (showEditModal && selectedUniversity) {
setUniversities(universities.map(u =>
u.id === selectedUniversity.id
? { ...updatedData, id: selectedUniversity.id } as University
: u
));
setShowEditModal(false);
} else {
const newUniversity: University = {
...updatedData,
id: Date.now().toString()
} as University;
setUniversities([...universities, newUniversity]);
setShowAddModal(false);
}
setSelectedUniversity(null);
setSelectedCursos([]);
};
const handleExport = () => {
const headers = ['Empresa', 'Nome da Universidade', 'Ano Formatura', 'Cursos'];
const csvContent = [
headers.join(','),
...universities.map(u => [
u.empresa,
`"${u.nomeUniversidade}"`,
`${u.anoFormatura}.${u.semestre}`,
`"${u.cursos.join(', ')}"`
].join(','))
].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `universidades_${new Date().toISOString().split('T')[0]}.csv`;
link.click();
};
return (
<div className="min-h-screen bg-gray-50 pt-20 sm:pt-24 pb-8 sm:pb-12">
<div className="max-w-[95%] mx-auto px-3 sm:px-4">
{/* Header */}
<div className="mb-4 sm:mb-6 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-0">
<div>
<h1 className="text-xl sm:text-2xl md:text-3xl font-serif font-bold text-brand-black mb-1 sm:mb-2">
Gestão de Universidades
</h1>
<p className="text-xs sm:text-sm text-gray-600">
Cadastro e gerenciamento de universidades
</p>
</div>
<div className="flex gap-2 sm:gap-3 w-full sm:w-auto">
<button
onClick={handleExport}
className="flex items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-1.5 sm:py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors font-medium text-xs sm:text-sm flex-1 sm:flex-none justify-center"
>
<Download size={16} className="sm:w-[18px] sm:h-[18px]" />
<span className="hidden sm:inline">Exportar</span>
</button>
<button
onClick={handleAddUniversity}
className="flex items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-1.5 sm:py-2 bg-brand-gold text-white rounded-md hover:bg-[#a5bd2e] transition-colors font-medium text-xs sm:text-sm flex-1 sm:flex-none justify-center"
>
<Plus size={16} className="sm:w-[18px] sm:h-[18px]" />
<span className="hidden sm:inline">Cadastrar</span>
<span className="sm:hidden">Nova Universidade</span>
<span className="hidden sm:inline">Universidade</span>
</button>
</div>
</div>
{/* Tabela */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-x-auto">
<table className="w-full text-xs sm:text-sm min-w-[600px]">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
{[
{ key: 'empresa', label: 'Empresa' },
{ key: 'nomeUniversidade', label: 'Nome da Universidade' },
{ key: 'anoFormatura', label: 'Ano Formatura' },
{ key: 'cursos', label: 'Cursos' }
].map((column) => (
<th
key={column.key}
onClick={() => handleSort(column.key as keyof University)}
className="px-4 py-3 text-left font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors whitespace-nowrap group"
>
<div className="flex items-center gap-2">
{column.label}
{getSortIcon(column.key as keyof University)}
</div>
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{sortedUniversities.map((university) => (
<tr
key={university.id}
onClick={() => handleEditUniversity(university)}
className="hover:bg-gray-50 cursor-pointer transition-colors"
>
<td className="px-4 py-3 whitespace-nowrap">{university.empresa}</td>
<td className="px-4 py-3">{university.nomeUniversidade}</td>
<td className="px-4 py-3 whitespace-nowrap">{university.anoFormatura}.{university.semestre}</td>
<td className="px-4 py-3">
<div className="flex flex-wrap gap-1">
{university.cursos.map((curso, idx) => (
<span
key={idx}
className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs"
>
{curso}
</span>
))}
</div>
</td>
</tr>
))}
</tbody>
</table>
{sortedUniversities.length === 0 && (
<div className="text-center py-12 text-gray-500">
Nenhuma universidade cadastrada
</div>
)}
</div>
</div>
{/* Modal Adicionar/Editar */}
{(showAddModal || showEditModal) && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-white border-b border-gray-200 p-6 flex items-center justify-between">
<h2 className="text-2xl font-bold">
{showEditModal ? 'Editar Universidade' : 'Cadastrar Universidade'}
</h2>
<button
onClick={() => {
setShowAddModal(false);
setShowEditModal(false);
setSelectedUniversity(null);
setSelectedCursos([]);
}}
className="text-gray-500 hover:text-gray-700"
>
<X size={24} />
</button>
</div>
{apiError && (
<div className="mx-6 mt-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg flex items-start gap-3">
<AlertCircle className="text-yellow-600 flex-shrink-0 mt-0.5" size={20} />
<div className="flex-1">
<p className="text-sm font-medium text-yellow-800">Aviso</p>
<p className="text-sm text-yellow-700 mt-1">{apiError}</p>
</div>
</div>
)}
<div className="p-6">
<div className="grid grid-cols-1 gap-4">
{/* Empresa */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Empresa *
</label>
<select
value={formData.empresa}
onChange={(e) => setFormData({ ...formData, empresa: e.target.value })}
disabled={loadingApi}
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent disabled:bg-gray-50 disabled:text-gray-500"
>
<option value="">
{loadingApi ? 'Carregando...' : empresas.length > 0 ? 'Selecione uma empresa' : 'Nenhuma empresa disponível'}
</option>
{empresas.map((emp) => (
<option key={emp.id} value={emp.nome}>
{emp.nome}
</option>
))}
</select>
</div>
{/* Nome da Universidade */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Nome da Universidade *
</label>
<input
type="text"
value={formData.nomeUniversidade}
onChange={(e) => setFormData({ ...formData, nomeUniversidade: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent"
placeholder="Ex: UFPR - Universidade Federal do Paraná"
/>
</div>
{/* Ano Formatura */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Ano *
</label>
<input
type="number"
value={formData.anoFormatura}
onChange={(e) => setFormData({ ...formData, anoFormatura: parseInt(e.target.value) })}
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent"
placeholder="2025"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Semestre *
</label>
<select
value={formData.semestre}
onChange={(e) => setFormData({ ...formData, semestre: parseInt(e.target.value) })}
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent"
>
<option value={1}>1</option>
<option value={2}>2</option>
</select>
</div>
</div>
{/* Cursos */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Cursos *
</label>
<div className="flex gap-2 mb-2">
<select
value={cursoInput}
onChange={(e) => setCursoInput(e.target.value)}
disabled={loadingApi}
className="flex-1 px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent disabled:bg-gray-50 disabled:text-gray-500"
>
<option value="">
{loadingApi ? 'Carregando...' : cursos.length > 0 ? 'Selecione um curso' : 'Nenhum curso disponível'}
</option>
{cursos.map((curso) => (
<option key={curso.id} value={curso.nome}>
{curso.nome}
</option>
))}
</select>
<button
type="button"
onClick={handleAddCurso}
disabled={!cursoInput}
className="px-4 py-2 bg-brand-gold text-white rounded-md hover:bg-[#a5bd2e] transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
Adicionar
</button>
</div>
{/* Lista de cursos selecionados */}
<div className="flex flex-wrap gap-2 mt-3">
{selectedCursos.map((curso, idx) => (
<span
key={idx}
className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm flex items-center gap-2"
>
{curso}
<button
type="button"
onClick={() => handleRemoveCurso(curso)}
className="hover:text-blue-900"
>
<X size={14} />
</button>
</span>
))}
</div>
{selectedCursos.length === 0 && (
<p className="text-sm text-gray-500 mt-2">Nenhum curso selecionado</p>
)}
</div>
</div>
{/* Botões */}
<div className="flex gap-3 mt-8">
<button
onClick={() => {
setShowAddModal(false);
setShowEditModal(false);
setSelectedUniversity(null);
setSelectedCursos([]);
}}
className="flex-1 px-4 py-3 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 transition-colors font-medium"
>
Cancelar
</button>
<button
onClick={handleSaveUniversity}
className="flex-1 px-4 py-3 bg-brand-gold text-white rounded-md hover:bg-[#a5bd2e] transition-colors font-medium"
>
{showEditModal ? 'Salvar Alterações' : 'Cadastrar'}
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default UniversityManagement;

View file

@ -108,14 +108,14 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
).length; ).length;
return ( return (
<div className="min-h-screen bg-gray-50 pt-20 pb-8"> <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"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */} {/* Header */}
<div className="mb-8"> <div className="mb-6 sm:mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2"> <h1 className="text-2xl sm:text-3xl font-serif font-bold text-brand-black">
Aprovação de Cadastros Aprovação de Cadastros
</h1> </h1>
<p className="text-gray-600"> <p className="text-sm sm:text-base text-gray-600 mt-1">
Gerencie os cadastros pendentes de aprovação Gerencie os cadastros pendentes de aprovação
</p> </p>
</div> </div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

BIN
frontend/public/logofav.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB