feat(login): remodela a tela de login/cadastro com as funcionalidades atuais
- novo painel showcase (marketplace de profissionais por proximidade, locação de salas, agenda anti-overbooking, financeiro/GTO, RX, busca por proximidade) - chips de perfis incl. biomédico(a); visual escuro moderno; splash mais rápido - mantém login, recuperar/redefinir senha e cadastro via OnboardingChat - App.tsx: handler onAuthSuccess extraído (reuso pós-login) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+113
-354
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Mail, Lock, User, ArrowRight, ShieldCheck, Loader2, Eye, EyeOff,
|
||||
CheckCircle2, Stethoscope, Calendar, Users, BarChart3, Zap,
|
||||
Building2, ChevronLeft, Briefcase, KeyRound, RotateCcw
|
||||
Mail, Lock, ArrowRight, ShieldCheck, Loader2, Eye, EyeOff, CheckCircle2,
|
||||
Stethoscope, Calendar, BarChart3, ChevronLeft, KeyRound, RotateCcw,
|
||||
UserSearch, DoorOpen, ScanLine, Sparkles, MapPin, Building2, Users, FlaskConical
|
||||
} from 'lucide-react';
|
||||
import { HybridBackend } from '../services/backend.ts';
|
||||
import { APP_VERSION } from '../constants.ts';
|
||||
@@ -14,63 +14,24 @@ interface LoginViewProps {
|
||||
|
||||
const API_URL = (window as any).__API_URL__ || '';
|
||||
|
||||
type UserRole = 'donoconsultorio' | 'donoclinica' | 'dentista' | 'protetico' | 'paciente';
|
||||
|
||||
const UF_LIST = ['AC','AL','AM','AP','BA','CE','DF','ES','GO','MA','MG','MS','MT','PA','PB','PE','PI','PR','RJ','RN','RO','RR','RS','SC','SE','SP','TO'];
|
||||
|
||||
const ROLE_OPTIONS: { role: UserRole; icon: React.ElementType; title: string; desc: string; badge: string; price: string; priceColor: string }[] = [
|
||||
{
|
||||
role: 'donoconsultorio',
|
||||
icon: Briefcase,
|
||||
title: 'Consultório',
|
||||
desc: 'Prática solo ou 1 cadeira — ideal para dentistas autônomos',
|
||||
badge: 'Mais acessível',
|
||||
price: 'R$49,90/mês',
|
||||
priceColor: 'text-emerald-600',
|
||||
},
|
||||
{
|
||||
role: 'donoclinica',
|
||||
icon: Building2,
|
||||
title: 'Clínica',
|
||||
desc: 'Multi-cadeira com equipe, gestão completa e financeiro',
|
||||
badge: 'Completo',
|
||||
price: 'R$499,00/mês',
|
||||
priceColor: 'text-blue-600',
|
||||
},
|
||||
{
|
||||
role: 'dentista',
|
||||
icon: Stethoscope,
|
||||
title: 'Dentista',
|
||||
desc: 'Acesse a clínica ou consultório pelo qual foi convidado',
|
||||
badge: '',
|
||||
price: 'R$49,90/mês',
|
||||
priceColor: 'text-indigo-600',
|
||||
},
|
||||
{
|
||||
role: 'protetico',
|
||||
icon: Zap,
|
||||
title: 'Protético',
|
||||
desc: 'Laboratório de prótese — acesse pelo convite recebido',
|
||||
badge: '',
|
||||
price: 'R$49,90/mês',
|
||||
priceColor: 'text-violet-600',
|
||||
},
|
||||
{
|
||||
role: 'paciente',
|
||||
icon: Users,
|
||||
title: 'Paciente',
|
||||
desc: 'Acompanhe seus tratamentos e histórico clínico',
|
||||
badge: '',
|
||||
price: 'Gratuito',
|
||||
priceColor: 'text-green-600',
|
||||
},
|
||||
// Funcionalidades atuais da plataforma (destaque no painel da esquerda).
|
||||
const FEATURES = [
|
||||
{ icon: UserSearch, label: 'Marketplace de Profissionais', desc: 'Encontre dentistas, biomédicos e protéticos por proximidade' },
|
||||
{ icon: DoorOpen, label: 'Locação de Salas', desc: 'Alugue ou disponibilize salas com reserva anti-conflito' },
|
||||
{ icon: Calendar, label: 'Agenda Anti-overbooking', desc: 'Horários do profissional sincronizados entre unidades' },
|
||||
{ icon: BarChart3, label: 'Financeiro & GTO', desc: 'Faturamento, guias e contratos num lugar só' },
|
||||
{ icon: ScanLine, label: 'RX / Radiografias', desc: 'Imagens integradas ao prontuário do paciente' },
|
||||
{ icon: MapPin, label: 'Busca por Proximidade', desc: 'Profissionais e salas mais perto de você primeiro' },
|
||||
];
|
||||
|
||||
const FEATURES = [
|
||||
{ icon: Calendar, label: 'Agenda Inteligente', desc: 'Gestão de horários com lembretes automáticos' },
|
||||
{ icon: Users, label: 'CRM de Pacientes', desc: 'Histórico completo e acompanhamento de tratamentos' },
|
||||
{ icon: BarChart3, label: 'Financeiro Avançado', desc: 'Faturamento, GTO e contratos integrados' },
|
||||
{ icon: Zap, label: 'Ortodontia Avançada', desc: 'Controle de tratamentos e radiografias' },
|
||||
// Perfis suportados (chips informativos — o cadastro real é guiado no chat).
|
||||
const ROLE_CHIPS = [
|
||||
{ icon: Building2, label: 'Clínica' },
|
||||
{ icon: Stethoscope, label: 'Consultório' },
|
||||
{ icon: Stethoscope, label: 'Dentista' },
|
||||
{ icon: Sparkles, label: 'Biomédico(a)' },
|
||||
{ icon: FlaskConical, label: 'Protético' },
|
||||
{ icon: Users, label: 'Paciente' },
|
||||
];
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
@@ -80,55 +41,43 @@ const Field: React.FC<{ label: string; children: React.ReactNode }> = ({ label,
|
||||
<div className="relative">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const InputIcon: React.FC<{ icon: React.ReactNode }> = ({ icon }) => (
|
||||
<span className="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none z-10">{icon}</span>
|
||||
);
|
||||
|
||||
const EyeToggle: React.FC<{ show: boolean; onToggle: () => void }> = ({ show, onToggle }) => (
|
||||
<button type="button" onClick={onToggle}
|
||||
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors z-10">
|
||||
{show ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
);
|
||||
|
||||
const ErrorBanner: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||
<div className="p-3 bg-red-50 border border-red-100 rounded-xl text-red-600 text-xs font-bold text-center uppercase tracking-wide">
|
||||
{children}
|
||||
</div>
|
||||
<div className="p-3 bg-red-50 border border-red-100 rounded-xl text-red-600 text-xs font-bold text-center uppercase tracking-wide">{children}</div>
|
||||
);
|
||||
const InputCls = 'w-full pl-10 pr-4 py-3.5 bg-gray-50 border border-gray-200 rounded-xl text-sm font-medium text-gray-900 placeholder:text-gray-400 outline-none focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 transition-all';
|
||||
|
||||
// ── Component ─────────────────────────────────────────────────────────────────
|
||||
export const LoginView: React.FC<LoginViewProps> = ({ onLoginSuccess }) => {
|
||||
const [tab, setTab] = useState<'login' | 'cadastro'>('login');
|
||||
const [authMode, setAuthMode] = useState<'login' | 'forgot' | 'reset'>('login');
|
||||
// cadastro steps: 'role' | 'form'
|
||||
const [cadStep, setCadStep] = useState<'role' | 'form'>('role');
|
||||
const [selectedRole, setSelectedRole] = useState<UserRole | null>(null);
|
||||
const [isClinica, setIsClinica] = useState(false);
|
||||
const [tab, setTab] = useState<'login' | 'cadastro'>('login');
|
||||
const [authMode, setAuthMode] = useState<'login' | 'forgot' | 'reset'>('login');
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [nome, setNome] = useState('');
|
||||
const [nomeEstab, setNomeEstab] = useState('');
|
||||
const [confirmPass, setConfirmPass] = useState('');
|
||||
const [showPass, setShowPass] = useState(false);
|
||||
const [showCfm, setShowCfm] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
const [initializing, setInit] = useState(true);
|
||||
const [leaving, setLeaving] = useState(false);
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPass, setShowPass] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
const [initializing, setInit] = useState(true);
|
||||
const [leaving, setLeaving] = useState(false);
|
||||
|
||||
// forgot/reset state
|
||||
const [forgotEmail, setForgotEmail] = useState('');
|
||||
const [resetToken, setResetToken] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmNew, setConfirmNew] = useState('');
|
||||
const [showNewPass, setShowNewPass] = useState(false);
|
||||
// forgot/reset
|
||||
const [forgotEmail, setForgotEmail] = useState('');
|
||||
const [resetToken, setResetToken] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmNew, setConfirmNew] = useState('');
|
||||
const [showNewPass, setShowNewPass] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => setInit(false), 2000);
|
||||
const t = setTimeout(() => setInit(false), 1200);
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const token = params.get('reset_token');
|
||||
if (token) { setResetToken(token); setAuthMode('reset'); }
|
||||
@@ -137,10 +86,8 @@ export const LoginView: React.FC<LoginViewProps> = ({ onLoginSuccess }) => {
|
||||
|
||||
const switchTab = (next: 'login' | 'cadastro') => {
|
||||
if (next === tab || leaving) return;
|
||||
setLeaving(true);
|
||||
setError(''); setSuccess('');
|
||||
setCadStep('role'); setSelectedRole(null); setIsClinica(false);
|
||||
setTimeout(() => { setTab(next); setLeaving(false); }, 220);
|
||||
setLeaving(true); setError(''); setSuccess('');
|
||||
setTimeout(() => { setTab(next); setLeaving(false); }, 200);
|
||||
};
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
@@ -148,42 +95,10 @@ export const LoginView: React.FC<LoginViewProps> = ({ onLoginSuccess }) => {
|
||||
setLoading(true); setError('');
|
||||
try {
|
||||
const result = await HybridBackend.login(email, password);
|
||||
if (result.ok) {
|
||||
onLoginSuccess(result.noWorkspace ? { noWorkspace: true, userRole: result.userRole } : undefined);
|
||||
} else {
|
||||
setError('Credenciais inválidas. Verifique e-mail e senha.');
|
||||
}
|
||||
} catch {
|
||||
setError('Erro de conexão. Tente novamente.');
|
||||
} finally { setLoading(false); }
|
||||
};
|
||||
|
||||
const handleCadastro = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!selectedRole) { setError('Selecione um perfil.'); return; }
|
||||
if (password !== confirmPass) { setError('As senhas não coincidem.'); return; }
|
||||
if (password.length < 6) { setError('A senha deve ter no mínimo 6 caracteres.'); return; }
|
||||
setLoading(true); setError('');
|
||||
try {
|
||||
const effectiveRole = selectedRole === 'donoconsultorio' && isClinica ? 'donoclinica' : selectedRole;
|
||||
const res = await fetch(`${API_URL}/api/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ nome, email, senha: password, role: effectiveRole, workspaceNome: nomeEstab.trim() || undefined }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.success) { setError(data.message || 'Erro ao criar conta.'); return; }
|
||||
localStorage.setItem('SCOREODONTO_AUTH_TOKEN', data.token);
|
||||
localStorage.setItem('SCOREODONTO_USER_DATA', JSON.stringify(data.user));
|
||||
localStorage.setItem('SCOREODONTO_WORKSPACES', JSON.stringify(data.workspaces || []));
|
||||
if (data.workspaces?.length) {
|
||||
localStorage.setItem('SCOREODONTO_ACTIVE_WORKSPACE', JSON.stringify(data.workspaces[0]));
|
||||
}
|
||||
setSuccess('Conta criada! Redirecionando...');
|
||||
setTimeout(() => onLoginSuccess(data.noWorkspace ? { noWorkspace: true, userRole: data.userRole } : undefined), 1400);
|
||||
} catch {
|
||||
setError('Erro de conexão. Tente novamente.');
|
||||
} finally { setLoading(false); }
|
||||
if (result.ok) onLoginSuccess(result.noWorkspace ? { noWorkspace: true, userRole: result.userRole } : undefined);
|
||||
else setError('Credenciais inválidas. Verifique e-mail e senha.');
|
||||
} catch { setError('Erro de conexão. Tente novamente.'); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
const handleForgotPassword = async (e: React.FormEvent) => {
|
||||
@@ -216,184 +131,163 @@ export const LoginView: React.FC<LoginViewProps> = ({ onLoginSuccess }) => {
|
||||
setSuccess('Senha redefinida! Redirecionando para o login...');
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
setTimeout(() => { setAuthMode('login'); setSuccess(''); setNewPassword(''); setConfirmNew(''); }, 2200);
|
||||
} else {
|
||||
setError(data.error || 'Link inválido ou expirado. Solicite um novo.');
|
||||
}
|
||||
} else setError(data.error || 'Link inválido ou expirado. Solicite um novo.');
|
||||
} catch { setError('Erro de conexão. Tente novamente.'); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
/* ─── Loading splash ─── */
|
||||
/* ─── Splash ─── */
|
||||
if (initializing) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC] flex flex-col items-center justify-center gap-5">
|
||||
<div className="w-16 h-16 bg-gradient-to-tr from-blue-600 to-indigo-600 rounded-2xl flex items-center justify-center shadow-xl shadow-blue-200">
|
||||
<div className="h-dvh bg-[#0B1220] flex flex-col items-center justify-center gap-5">
|
||||
<div className="w-16 h-16 bg-gradient-to-tr from-blue-500 to-indigo-500 rounded-2xl flex items-center justify-center shadow-xl shadow-blue-900/40">
|
||||
<Stethoscope className="text-white" size={30} />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-black text-gray-900 tracking-tighter uppercase italic">
|
||||
Score<span className="text-blue-600">Odonto</span>
|
||||
</p>
|
||||
<p className="text-gray-400 text-xs font-bold uppercase tracking-widest mt-1">Inicializando sistema...</p>
|
||||
<p className="text-2xl font-black text-white tracking-tighter uppercase italic">Score<span className="text-blue-400">Odonto</span></p>
|
||||
<div className="flex gap-1.5">
|
||||
{[0, 1, 2].map(i => <div key={i} className="w-2 h-2 rounded-full bg-blue-400" style={{ animation: `b 1.2s ease-in-out ${i * 0.2}s infinite` }} />)}
|
||||
</div>
|
||||
<div className="flex gap-1.5 mt-1">
|
||||
{[0, 1, 2].map(i => (
|
||||
<div key={i} className="w-2 h-2 rounded-full bg-blue-400"
|
||||
style={{ animation: `bounce 1.2s ease-in-out ${i * 0.2}s infinite` }} />
|
||||
))}
|
||||
</div>
|
||||
<span className="text-gray-400 text-[10px] font-mono bg-gray-100 px-2 py-1 rounded">{APP_VERSION}</span>
|
||||
<style>{`@keyframes bounce{0%,80%,100%{transform:translateY(0)}40%{transform:translateY(-8px)}}`}</style>
|
||||
<style>{`@keyframes b{0%,80%,100%{transform:translateY(0);opacity:.5}40%{transform:translateY(-8px);opacity:1}}`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Main layout ─── */
|
||||
/* ─── Layout ─── */
|
||||
return (
|
||||
<div className="h-dvh overflow-hidden bg-[#F8FAFC] flex flex-col lg:flex-row">
|
||||
<div className="h-dvh overflow-hidden bg-white flex flex-col lg:flex-row">
|
||||
<style>{`
|
||||
@keyframes fadeUp { from{opacity:0;transform:translateY(10px)} to{opacity:1;transform:translateY(0)} }
|
||||
@keyframes fadeOut { from{opacity:1;transform:translateY(0)} to{opacity:0;transform:translateY(-6px)} }
|
||||
@keyframes spinAnim { to{transform:rotate(360deg)} }
|
||||
.form-enter { animation: fadeUp .3s ease forwards; }
|
||||
.form-leave { animation: fadeOut .22s ease forwards; }
|
||||
.spin { animation: spinAnim 1s linear infinite; }
|
||||
@keyframes fadeUp { from{opacity:0;transform:translateY(10px)} to{opacity:1;transform:translateY(0)} }
|
||||
@keyframes fadeOut{ from{opacity:1;transform:translateY(0)} to{opacity:0;transform:translateY(-6px)} }
|
||||
@keyframes spin { to{transform:rotate(360deg)} }
|
||||
@keyframes float { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-10px)} }
|
||||
.fe{animation:fadeUp .3s ease forwards}.fl{animation:fadeOut .2s ease forwards}.sp{animation:spin 1s linear infinite}
|
||||
`}</style>
|
||||
|
||||
{/* ── LEFT BRAND PANEL ── */}
|
||||
<div className="hidden lg:flex flex-col justify-between flex-1 h-full bg-gradient-to-br from-blue-600 to-indigo-700 p-12 xl:p-16 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-80 h-80 bg-white/10 rounded-full -translate-y-1/2 translate-x-1/2 blur-3xl pointer-events-none" />
|
||||
<div className="absolute bottom-0 left-0 w-64 h-64 bg-black/10 rounded-full translate-y-1/2 -translate-x-1/2 blur-3xl pointer-events-none" />
|
||||
{/* ── LEFT — showcase ── */}
|
||||
<div className="hidden lg:flex flex-col justify-between flex-1 h-full relative overflow-hidden p-12 xl:p-16
|
||||
bg-gradient-to-br from-[#0B1220] via-[#0e1a33] to-[#1e3a8a] text-white">
|
||||
<div className="absolute -top-24 -right-24 w-96 h-96 bg-blue-500/20 rounded-full blur-3xl pointer-events-none" style={{ animation: 'float 8s ease-in-out infinite' }} />
|
||||
<div className="absolute -bottom-24 -left-24 w-80 h-80 bg-indigo-500/20 rounded-full blur-3xl pointer-events-none" />
|
||||
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-3 relative z-10">
|
||||
<div className="w-11 h-11 bg-white/20 backdrop-blur-sm rounded-xl flex items-center justify-center border border-white/30 shadow-lg">
|
||||
<div className="w-11 h-11 bg-white/10 backdrop-blur-sm rounded-xl flex items-center justify-center border border-white/20 shadow-lg">
|
||||
<Stethoscope className="text-white" size={22} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white font-black text-xl tracking-tighter uppercase italic leading-none">
|
||||
Score<span className="text-blue-200">Odonto</span>
|
||||
</p>
|
||||
<p className="text-blue-200 text-[10px] font-black uppercase tracking-widest">CRM Odontológico</p>
|
||||
<p className="font-black text-xl tracking-tighter uppercase italic leading-none">Score<span className="text-blue-300">Odonto</span></p>
|
||||
<p className="text-blue-300/70 text-[10px] font-black uppercase tracking-widest">Plataforma Odontológica</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Headline */}
|
||||
<div className="relative z-10 space-y-6">
|
||||
{/* Headline + features */}
|
||||
<div className="relative z-10 space-y-7">
|
||||
<div>
|
||||
<h2 className="text-4xl xl:text-5xl font-black text-white tracking-tighter leading-none mb-4">
|
||||
O Futuro da Sua<br />Clínica é{' '}
|
||||
<span className="text-blue-200 italic">Digital</span>
|
||||
<span className="inline-flex items-center gap-1.5 text-[10px] font-black uppercase tracking-widest bg-blue-500/20 text-blue-200 border border-blue-400/30 px-3 py-1 rounded-full mb-4">
|
||||
<Sparkles size={11} /> Novo: marketplace + locação de salas
|
||||
</span>
|
||||
<h2 className="text-4xl xl:text-5xl font-black tracking-tighter leading-[1.05]">
|
||||
Gestão, conexão e<br /><span className="text-blue-300 italic">crescimento</span> da sua prática
|
||||
</h2>
|
||||
<p className="text-blue-100 text-base font-medium leading-relaxed max-w-sm">
|
||||
Gestão financeira, agenda avançada e controle de tratamentos — tudo em uma plataforma elegante.
|
||||
<p className="text-blue-100/70 text-base font-medium leading-relaxed max-w-md mt-4">
|
||||
Do prontuário ao marketplace: encontre profissionais e salas por proximidade, gerencie a agenda sem choques e o financeiro num só lugar.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{FEATURES.map(({ icon: Icon, label, desc }) => (
|
||||
<div key={label} className="bg-white/10 backdrop-blur-sm rounded-2xl p-4 border border-white/20">
|
||||
<Icon size={20} className="text-blue-200 mb-2" />
|
||||
<p className="text-white font-black text-xs uppercase tracking-tight leading-none mb-1">{label}</p>
|
||||
<p className="text-blue-200 text-[10px] font-medium leading-snug">{desc}</p>
|
||||
<div key={label} className="bg-white/5 backdrop-blur-sm rounded-2xl p-4 border border-white/10 hover:bg-white/10 transition-colors">
|
||||
<Icon size={20} className="text-blue-300 mb-2" />
|
||||
<p className="font-black text-xs uppercase tracking-tight leading-none mb-1">{label}</p>
|
||||
<p className="text-blue-200/60 text-[10px] font-medium leading-snug">{desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{ROLE_CHIPS.map(({ icon: Icon, label }) => (
|
||||
<span key={label} className="inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wide bg-white/5 border border-white/10 text-blue-100/80 px-2.5 py-1 rounded-lg">
|
||||
<Icon size={12} className="text-blue-300" /> {label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="relative z-10 flex items-center gap-2 text-blue-300/60 text-[10px] font-black uppercase tracking-widest">
|
||||
<div className="relative z-10 flex items-center gap-2 text-blue-300/50 text-[10px] font-black uppercase tracking-widest">
|
||||
<ShieldCheck size={12} /> Dados criptografados · LGPD · {APP_VERSION}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── RIGHT FORM PANEL ── */}
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-6 lg:p-12 overflow-hidden">
|
||||
{/* ── RIGHT — auth ── */}
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-6 lg:p-12 overflow-y-auto">
|
||||
{/* Mobile logo */}
|
||||
<div className="lg:hidden flex items-center gap-2 mb-8">
|
||||
<div className="w-9 h-9 bg-gradient-to-tr from-blue-600 to-indigo-600 rounded-xl flex items-center justify-center shadow-md shadow-blue-200">
|
||||
<Stethoscope size={18} className="text-white" />
|
||||
</div>
|
||||
<span className="text-xl font-black text-gray-900 tracking-tighter uppercase italic">
|
||||
Score<span className="text-blue-600">Odonto</span>
|
||||
</span>
|
||||
<span className="text-xl font-black text-gray-900 tracking-tighter uppercase italic">Score<span className="text-blue-600">Odonto</span></span>
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-md">
|
||||
{/* Reset password mode */}
|
||||
{authMode === 'reset' ? (
|
||||
<div className="bg-white rounded-[2rem] shadow-[0_40px_80px_-20px_rgba(0,0,0,0.1)] border border-gray-100 overflow-hidden">
|
||||
<div className="bg-white rounded-[2rem] shadow-[0_40px_80px_-20px_rgba(0,0,0,0.12)] border border-gray-100 overflow-hidden">
|
||||
<div className="h-1.5 bg-gradient-to-r from-blue-500 to-indigo-500" />
|
||||
<div className="p-8">
|
||||
<button onClick={() => setAuthMode('login')} className="flex items-center gap-1.5 text-xs font-black text-gray-400 hover:text-gray-600 uppercase tracking-widest mb-6 transition-colors">
|
||||
<ChevronLeft size={14} /> Voltar ao login
|
||||
</button>
|
||||
<div className="text-center mb-6">
|
||||
<div className="w-12 h-12 bg-blue-50 rounded-2xl flex items-center justify-center mx-auto mb-3">
|
||||
<KeyRound size={22} className="text-blue-600" />
|
||||
</div>
|
||||
<div className="w-12 h-12 bg-blue-50 rounded-2xl flex items-center justify-center mx-auto mb-3"><KeyRound size={22} className="text-blue-600" /></div>
|
||||
<h2 className="text-xl font-black text-gray-900 uppercase tracking-tight">Nova Senha</h2>
|
||||
<p className="text-xs text-gray-500 font-medium mt-1">Defina sua nova senha de acesso</p>
|
||||
</div>
|
||||
<form onSubmit={handleResetPassword} className="space-y-4">
|
||||
<Field label="Nova Senha">
|
||||
<InputIcon icon={<Lock size={16} />} />
|
||||
<input type={showNewPass ? 'text' : 'password'} required value={newPassword} onChange={e => setNewPassword(e.target.value)}
|
||||
className="w-full pl-10 pr-10 py-3.5 bg-gray-50 border border-gray-200 rounded-xl text-sm font-medium outline-none focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 transition-all"
|
||||
placeholder="••••••" autoComplete="new-password" />
|
||||
<input type={showNewPass ? 'text' : 'password'} required value={newPassword} onChange={e => setNewPassword(e.target.value)} className={InputCls} placeholder="••••••" autoComplete="new-password" />
|
||||
<EyeToggle show={showNewPass} onToggle={() => setShowNewPass(p => !p)} />
|
||||
</Field>
|
||||
<Field label="Confirmar Senha">
|
||||
<InputIcon icon={<Lock size={16} />} />
|
||||
<input type="password" required value={confirmNew} onChange={e => setConfirmNew(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-3.5 bg-gray-50 border border-gray-200 rounded-xl text-sm font-medium outline-none focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 transition-all"
|
||||
placeholder="••••••" autoComplete="new-password" />
|
||||
<input type="password" required value={confirmNew} onChange={e => setConfirmNew(e.target.value)} className={InputCls} placeholder="••••••" autoComplete="new-password" />
|
||||
</Field>
|
||||
{error && <ErrorBanner>{error}</ErrorBanner>}
|
||||
{success && <div className="p-3 bg-green-50 border border-green-100 rounded-xl text-green-700 text-xs font-bold text-center uppercase">{success}</div>}
|
||||
<button type="submit" disabled={loading}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 disabled:opacity-60 text-white py-4 rounded-xl text-sm font-black uppercase tracking-widest shadow-lg shadow-blue-200 transition-all flex items-center justify-center gap-2">
|
||||
{loading ? <><Loader2 size={18} className="spin" /> Salvando...</> : <>Redefinir Senha <ArrowRight size={18} /></>}
|
||||
<button type="submit" disabled={loading} className="w-full bg-blue-600 hover:bg-blue-700 disabled:opacity-60 text-white py-4 rounded-xl text-sm font-black uppercase tracking-widest shadow-lg shadow-blue-200 transition-all flex items-center justify-center gap-2">
|
||||
{loading ? <><Loader2 size={18} className="sp" /> Salvando...</> : <>Redefinir Senha <ArrowRight size={18} /></>}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
) : authMode === 'forgot' ? (
|
||||
<div className="bg-white rounded-[2rem] shadow-[0_40px_80px_-20px_rgba(0,0,0,0.1)] border border-gray-100 overflow-hidden">
|
||||
<div className="bg-white rounded-[2rem] shadow-[0_40px_80px_-20px_rgba(0,0,0,0.12)] border border-gray-100 overflow-hidden">
|
||||
<div className="h-1.5 bg-gradient-to-r from-blue-500 to-indigo-500" />
|
||||
<div className="p-8">
|
||||
<button onClick={() => { setAuthMode('login'); setError(''); setSuccess(''); }} className="flex items-center gap-1.5 text-xs font-black text-gray-400 hover:text-gray-600 uppercase tracking-widest mb-6 transition-colors">
|
||||
<ChevronLeft size={14} /> Voltar ao login
|
||||
</button>
|
||||
<div className="text-center mb-6">
|
||||
<div className="w-12 h-12 bg-blue-50 rounded-2xl flex items-center justify-center mx-auto mb-3">
|
||||
<RotateCcw size={22} className="text-blue-600" />
|
||||
</div>
|
||||
<div className="w-12 h-12 bg-blue-50 rounded-2xl flex items-center justify-center mx-auto mb-3"><RotateCcw size={22} className="text-blue-600" /></div>
|
||||
<h2 className="text-xl font-black text-gray-900 uppercase tracking-tight">Recuperar Senha</h2>
|
||||
<p className="text-xs text-gray-500 font-medium mt-1">Informe o e-mail cadastrado</p>
|
||||
</div>
|
||||
<form onSubmit={handleForgotPassword} className="space-y-4">
|
||||
<Field label="E-mail Cadastrado">
|
||||
<InputIcon icon={<Mail size={16} />} />
|
||||
<input type="email" required value={forgotEmail} onChange={e => setForgotEmail(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-3.5 bg-gray-50 border border-gray-200 rounded-xl text-sm font-medium outline-none focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 transition-all"
|
||||
placeholder="seu@email.com" autoComplete="email" />
|
||||
<input type="email" required value={forgotEmail} onChange={e => setForgotEmail(e.target.value)} className={InputCls} placeholder="seu@email.com" autoComplete="email" />
|
||||
</Field>
|
||||
{error && <ErrorBanner>{error}</ErrorBanner>}
|
||||
{success && <div className="p-3 bg-green-50 border border-green-100 rounded-xl text-green-700 text-xs font-bold text-center uppercase">{success}</div>}
|
||||
<button type="submit" disabled={loading}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 disabled:opacity-60 text-white py-4 rounded-xl text-sm font-black uppercase tracking-widest shadow-lg shadow-blue-200 transition-all flex items-center justify-center gap-2">
|
||||
{loading ? <><Loader2 size={18} className="spin" /> Enviando...</> : <>Enviar Instruções <ArrowRight size={18} /></>}
|
||||
<button type="submit" disabled={loading} className="w-full bg-blue-600 hover:bg-blue-700 disabled:opacity-60 text-white py-4 rounded-xl text-sm font-black uppercase tracking-widest shadow-lg shadow-blue-200 transition-all flex items-center justify-center gap-2">
|
||||
{loading ? <><Loader2 size={18} className="sp" /> Enviando...</> : <>Enviar Instruções <ArrowRight size={18} /></>}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* ── Login / Cadastro ── */
|
||||
<div className="bg-white rounded-[2rem] shadow-[0_40px_80px_-20px_rgba(0,0,0,0.1)] border border-gray-100 overflow-hidden">
|
||||
<div className="bg-white rounded-[2rem] shadow-[0_40px_80px_-20px_rgba(0,0,0,0.12)] border border-gray-100 overflow-hidden">
|
||||
<div className="h-1.5 bg-gradient-to-r from-blue-500 to-indigo-500" />
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-gray-100">
|
||||
{(['login', 'cadastro'] as const).map(t => (
|
||||
<button key={t} type="button" onClick={() => switchTab(t)}
|
||||
@@ -402,45 +296,33 @@ export const LoginView: React.FC<LoginViewProps> = ({ onLoginSuccess }) => {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-8">
|
||||
<div className={leaving ? 'form-leave' : 'form-enter'}>
|
||||
<div className={leaving ? 'fl' : 'fe'}>
|
||||
{tab === 'cadastro' ? (
|
||||
<OnboardingChat onDone={onLoginSuccess} onBackToLogin={() => switchTab('login')} />
|
||||
) : success ? (
|
||||
<div className="flex flex-col items-center gap-4 py-6">
|
||||
<div className="w-16 h-16 bg-green-50 rounded-[1.5rem] flex items-center justify-center">
|
||||
<CheckCircle2 size={32} className="text-green-500" />
|
||||
</div>
|
||||
<p className="text-gray-900 font-black text-lg uppercase tracking-tight">Conta criada!</p>
|
||||
<div className="w-16 h-16 bg-green-50 rounded-[1.5rem] flex items-center justify-center"><CheckCircle2 size={32} className="text-green-500" /></div>
|
||||
<p className="text-gray-900 font-black text-lg uppercase tracking-tight">Bem-vindo!</p>
|
||||
<p className="text-gray-500 text-sm">Redirecionando para o sistema...</p>
|
||||
<Loader2 size={20} className="text-blue-600 spin" />
|
||||
<Loader2 size={20} className="text-blue-600 sp" />
|
||||
</div>
|
||||
) : tab === 'login' ? (
|
||||
/* ── LOGIN ── */
|
||||
) : (
|
||||
<form onSubmit={handleLogin} className="space-y-5">
|
||||
<Field label="E-mail">
|
||||
<InputIcon icon={<Mail size={16} />} />
|
||||
<input type="email" required value={email} onChange={e => setEmail(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-3.5 bg-gray-50 border border-gray-200 rounded-xl text-sm font-medium text-gray-900 placeholder:text-gray-400 outline-none focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 transition-all"
|
||||
placeholder="seu@email.com" autoComplete="email" autoFocus />
|
||||
<input type="email" required value={email} onChange={e => setEmail(e.target.value)} className={InputCls} placeholder="seu@email.com" autoComplete="email" autoFocus />
|
||||
</Field>
|
||||
|
||||
<Field label="Senha">
|
||||
<InputIcon icon={<Lock size={16} />} />
|
||||
<input type={showPass ? 'text' : 'password'} required value={password} onChange={e => setPassword(e.target.value)}
|
||||
className="w-full pl-10 pr-10 py-3.5 bg-gray-50 border border-gray-200 rounded-xl text-sm font-medium text-gray-900 placeholder:text-gray-400 outline-none focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 transition-all"
|
||||
placeholder="••••••" autoComplete="current-password" />
|
||||
<input type={showPass ? 'text' : 'password'} required value={password} onChange={e => setPassword(e.target.value)} className={InputCls.replace('pr-4', 'pr-10')} placeholder="••••••" autoComplete="current-password" />
|
||||
<EyeToggle show={showPass} onToggle={() => setShowPass(p => !p)} />
|
||||
</Field>
|
||||
|
||||
{error && <ErrorBanner>{error}</ErrorBanner>}
|
||||
|
||||
<button type="submit" disabled={loading}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 disabled:opacity-60 disabled:cursor-not-allowed text-white py-4 rounded-2xl text-sm font-black uppercase tracking-widest shadow-lg shadow-blue-200 hover:shadow-xl hover:shadow-blue-300 hover:-translate-y-0.5 active:translate-y-0 transition-all flex items-center justify-center gap-2">
|
||||
{loading ? <><Loader2 size={18} className="spin" /> Verificando...</> : <>Entrar <ArrowRight size={18} /></>}
|
||||
{loading ? <><Loader2 size={18} className="sp" /> Verificando...</> : <>Entrar <ArrowRight size={18} /></>}
|
||||
</button>
|
||||
|
||||
<div className="text-center">
|
||||
<button type="button" onClick={() => { setAuthMode('forgot'); setError(''); setSuccess(''); }}
|
||||
className="text-xs text-gray-400 hover:text-blue-600 font-bold uppercase tracking-widest transition-colors">
|
||||
@@ -448,135 +330,12 @@ export const LoginView: React.FC<LoginViewProps> = ({ onLoginSuccess }) => {
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
) : cadStep === 'role' ? (
|
||||
/* ── STEP 0: Role selection ── */
|
||||
<div className="space-y-2.5">
|
||||
{/* Todos os perfis, cada um com seu preço */}
|
||||
{ROLE_OPTIONS.map(({ role, icon: Icon, title, desc, badge, price, priceColor }) => (
|
||||
<button
|
||||
key={role}
|
||||
type="button"
|
||||
onClick={() => { setSelectedRole(role); setCadStep('form'); setError(''); }}
|
||||
className="w-full flex items-center gap-4 p-3.5 bg-gray-50 hover:bg-blue-50 border-2 border-transparent hover:border-blue-200 rounded-2xl text-left transition-all group"
|
||||
>
|
||||
<div className="w-10 h-10 bg-white group-hover:bg-blue-100 border border-gray-200 group-hover:border-blue-200 rounded-xl flex items-center justify-center shrink-0 transition-colors shadow-sm">
|
||||
<Icon size={18} className="text-gray-500 group-hover:text-blue-600 transition-colors" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-black text-gray-900 uppercase tracking-tight">{title}</span>
|
||||
{badge && (
|
||||
<span className="text-[9px] font-black bg-blue-100 text-blue-600 px-2 py-0.5 rounded-full uppercase tracking-wider">
|
||||
{badge}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 font-medium mt-0.5 truncate">{desc}</p>
|
||||
</div>
|
||||
<span className={`text-xs font-black shrink-0 ${priceColor}`}>{price}</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
<p className="text-center text-xs text-gray-400 font-medium pt-2">
|
||||
Já tem conta?{' '}
|
||||
<button type="button" onClick={() => switchTab('login')}
|
||||
className="text-blue-600 font-black hover:underline">
|
||||
Entrar agora
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
/* ── STEP 1: Form ── */
|
||||
<form onSubmit={handleCadastro} className="space-y-4">
|
||||
<button type="button" onClick={() => { setCadStep('role'); setError(''); }}
|
||||
className="flex items-center gap-1.5 text-xs font-black text-gray-400 hover:text-gray-600 uppercase tracking-widest transition-colors mb-1">
|
||||
<ChevronLeft size={14} />
|
||||
Perfil: {ROLE_OPTIONS.find(r => r.role === selectedRole)?.title}
|
||||
</button>
|
||||
|
||||
<Field label="Nome completo">
|
||||
<InputIcon icon={<User size={16} />} />
|
||||
<input type="text" required value={nome} onChange={e => setNome(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-3.5 bg-gray-50 border border-gray-200 rounded-xl text-sm font-medium text-gray-900 placeholder:text-gray-400 outline-none focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 transition-all"
|
||||
placeholder="Dr. João Silva" autoComplete="name" />
|
||||
</Field>
|
||||
|
||||
{(selectedRole === 'dentista' || selectedRole === 'protetico') && (
|
||||
<Field label={selectedRole === 'protetico' ? 'Nome do Laboratório' : 'Nome do Consultório'}>
|
||||
<InputIcon icon={<Building2 size={16} />} />
|
||||
<input type="text" value={nomeEstab} onChange={e => setNomeEstab(e.target.value.toUpperCase())}
|
||||
className="w-full pl-10 pr-4 py-3.5 bg-gray-50 border border-gray-200 rounded-xl text-sm font-medium text-gray-900 placeholder:text-gray-400 outline-none focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 transition-all uppercase placeholder:normal-case"
|
||||
placeholder={selectedRole === 'protetico' ? 'Ex: Laboratório Sorriso' : 'Ex: Consultório Sorriso'} />
|
||||
<p className="text-[10px] text-gray-400 mt-1 ml-1">
|
||||
É o nome da sua unidade (não o seu). Em branco, usaremos{' '}
|
||||
<strong>{selectedRole === 'protetico' ? `Laboratório de ${nome || '...'}` : `Consultório de ${nome || '...'}`}</strong>.
|
||||
</p>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field label="E-mail">
|
||||
<InputIcon icon={<Mail size={16} />} />
|
||||
<input type="email" required value={email} onChange={e => setEmail(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-3.5 bg-gray-50 border border-gray-200 rounded-xl text-sm font-medium text-gray-900 placeholder:text-gray-400 outline-none focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 transition-all"
|
||||
placeholder="seu@email.com" autoComplete="email" />
|
||||
</Field>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Senha">
|
||||
<InputIcon icon={<Lock size={16} />} />
|
||||
<input type={showPass ? 'text' : 'password'} required value={password} onChange={e => setPassword(e.target.value)}
|
||||
className="w-full pl-10 pr-10 py-3.5 bg-gray-50 border border-gray-200 rounded-xl text-sm font-medium text-gray-900 placeholder:text-gray-400 outline-none focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 transition-all"
|
||||
placeholder="••••••" autoComplete="new-password" />
|
||||
<EyeToggle show={showPass} onToggle={() => setShowPass(p => !p)} />
|
||||
</Field>
|
||||
<Field label="Confirmar">
|
||||
<InputIcon icon={<Lock size={16} />} />
|
||||
<input type={showCfm ? 'text' : 'password'} required value={confirmPass} onChange={e => setConfirmPass(e.target.value)}
|
||||
className="w-full pl-10 pr-10 py-3.5 bg-gray-50 border border-gray-200 rounded-xl text-sm font-medium text-gray-900 placeholder:text-gray-400 outline-none focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 transition-all"
|
||||
placeholder="••••••" autoComplete="new-password" />
|
||||
<EyeToggle show={showCfm} onToggle={() => setShowCfm(p => !p)} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
{/* Password strength */}
|
||||
{password.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
{[3, 6, 9, 12].map((threshold, i) => (
|
||||
<div key={i} className="flex-1 h-1 rounded-full transition-colors duration-300"
|
||||
style={{ background: password.length >= threshold ? (password.length >= 10 ? '#22C55E' : password.length >= 6 ? '#F59E0B' : '#EF4444') : '#E5E7EB' }} />
|
||||
))}
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-gray-400 ml-1">
|
||||
{password.length < 6 ? 'Fraca' : password.length < 10 ? 'Boa' : 'Forte'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <ErrorBanner>{error}</ErrorBanner>}
|
||||
|
||||
<button type="submit" disabled={loading}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 disabled:opacity-60 disabled:cursor-not-allowed text-white py-4 rounded-2xl text-sm font-black uppercase tracking-widest shadow-lg shadow-blue-200 hover:shadow-xl hover:shadow-blue-300 hover:-translate-y-0.5 active:translate-y-0 transition-all flex items-center justify-center gap-2 mt-2">
|
||||
{loading
|
||||
? <><Loader2 size={18} className="spin" /> Criando conta...</>
|
||||
: <><span>Criar Conta Grátis</span><ArrowRight size={18} /></>
|
||||
}
|
||||
</button>
|
||||
|
||||
<p className="text-center text-[11px] text-gray-400 font-medium leading-relaxed">
|
||||
Ao criar conta você concorda com os{' '}
|
||||
<span className="text-blue-600 font-bold cursor-pointer hover:underline">Termos de Uso</span>{' '}
|
||||
e{' '}
|
||||
<span className="text-blue-600 font-bold cursor-pointer hover:underline">Política de Privacidade</span>.
|
||||
</p>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-8 py-4 bg-gray-50 border-t border-gray-100 flex items-center justify-center gap-2">
|
||||
<ShieldCheck size={12} className="text-gray-400" />
|
||||
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">
|
||||
Ambiente Seguro · {APP_VERSION}
|
||||
</span>
|
||||
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Ambiente Seguro · {APP_VERSION}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user