fix(login): render standalone views edge-to-edge e esconde scrollbar do carrossel de onboarding
- App.tsx: wrappers de padding (p-4/md:p-8) e max-w-7xl/mx-auto agora só se aplicam quando não é tela standalone; login/landing/public renderizam full-bleed - OnboardingChat.tsx: classe ob-noscroll esconde a barra de rolagem do slide intro (mantém scroll), substituindo a custom-scrollbar indefinida no fluxo de login Inclui também demais alterações pendentes do working tree (snapshot do estado atual de dev). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Mail, LogOut, Stethoscope, Clock, CheckCircle2, ArrowRight, MapPin, Users, Wrench, ChevronDown, ChevronUp, Loader2 } from 'lucide-react';
|
||||
import { HybridBackend } from '../services/backend.ts';
|
||||
import { APP_VERSION } from '../constants.ts';
|
||||
|
||||
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 API_URL = (window as any).__API_URL__ || '';
|
||||
|
||||
const ROLE_CONFIG: Record<string, { label: string; icon: React.ElementType; color: string; desc: string }> = {
|
||||
dentista: { label: 'Dentista', icon: Stethoscope, color: 'blue', desc: 'dentista disponível para contratação' },
|
||||
protetico: { label: 'Protético', icon: Wrench, color: 'purple', desc: 'protético disponível para laboratório' },
|
||||
};
|
||||
|
||||
export const WaitingInviteView: React.FC = () => {
|
||||
const userData = localStorage.getItem('SCOREODONTO_USER_DATA');
|
||||
const user = userData ? JSON.parse(userData) : null;
|
||||
const role = user?.role ?? 'dentista';
|
||||
const cfg = ROLE_CONFIG[role] ?? ROLE_CONFIG['dentista'];
|
||||
const email = user?.email ?? '';
|
||||
const RoleIcon = cfg.icon;
|
||||
|
||||
const [showDiretorio, setShowDiretorio] = useState(false);
|
||||
const [estado, setEstado] = useState('');
|
||||
const [cidade, setCidade] = useState('');
|
||||
const [bairro, setBairro] = useState('');
|
||||
const [especialidade, setEspecialidade] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [saveError, setSaveError] = useState('');
|
||||
|
||||
const steps = [
|
||||
{ icon: CheckCircle2, label: 'Conta criada', done: true },
|
||||
{ icon: Mail, label: 'Aguardando convite', done: false },
|
||||
{ icon: ArrowRight, label: 'Acesso à clínica', done: false },
|
||||
];
|
||||
|
||||
const handleSaveDiretorio = async () => {
|
||||
if (!estado || !cidade) { setSaveError('Estado e cidade são obrigatórios.'); return; }
|
||||
setSaving(true); setSaveError('');
|
||||
try {
|
||||
const token = localStorage.getItem('SCOREODONTO_AUTH_TOKEN');
|
||||
const res = await fetch(`${API_URL}/api/usuarios/localidade`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ estado, cidade, bairro, especialidade, disponivel_diretorio: true }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.success) { setSaveError(data.message || 'Erro ao salvar.'); }
|
||||
else setSaved(true);
|
||||
} catch { setSaveError('Erro de conexão.'); }
|
||||
finally { setSaving(false); }
|
||||
};
|
||||
|
||||
const colorMap: Record<string, string> = {
|
||||
blue: 'from-blue-600 to-indigo-600',
|
||||
purple: 'from-purple-600 to-violet-600',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC] flex flex-col items-center justify-center px-4 py-12">
|
||||
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-2 mb-10">
|
||||
<div className={`w-9 h-9 bg-gradient-to-tr ${colorMap[cfg.color]} rounded-xl flex items-center justify-center shadow-md`}>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Card */}
|
||||
<div className="w-full max-w-md bg-white rounded-3xl border border-gray-100 shadow-[0_20px_60px_rgba(0,0,0,0.07)] overflow-hidden">
|
||||
<div className={`h-1 bg-gradient-to-r ${colorMap[cfg.color]}`} />
|
||||
|
||||
<div className="px-8 py-10 text-center">
|
||||
{/* Icon */}
|
||||
<div className={`w-20 h-20 bg-${cfg.color}-50 border border-${cfg.color}-100 rounded-3xl flex items-center justify-center mx-auto mb-6`}>
|
||||
<RoleIcon size={36} className={`text-${cfg.color}-600`} />
|
||||
</div>
|
||||
|
||||
<h1 className="text-2xl font-black text-gray-900 tracking-tighter mb-2">
|
||||
Conta criada com sucesso!
|
||||
</h1>
|
||||
<p className="text-gray-500 text-sm font-medium leading-relaxed mb-8">
|
||||
Seu perfil de <strong className="text-gray-700">{cfg.label}</strong> foi registrado.
|
||||
{role === 'protetico'
|
||||
? ' Para acessar o sistema, você precisa ser convidado por um laboratório ou clínica.'
|
||||
: ' Para acessar o sistema, você precisa ser convidado pelo administrador de uma clínica ou consultório.'}
|
||||
</p>
|
||||
|
||||
{/* Steps */}
|
||||
<div className="flex items-center justify-center gap-0 mb-8">
|
||||
{steps.map((step, i) => {
|
||||
const Icon = step.icon;
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
<div className="flex flex-col items-center gap-1.5">
|
||||
<div className={`w-10 h-10 rounded-2xl flex items-center justify-center ${
|
||||
step.done
|
||||
? 'bg-green-100 text-green-600'
|
||||
: i === 1
|
||||
? `bg-${cfg.color}-100 text-${cfg.color}-600 ring-2 ring-${cfg.color}-200 ring-offset-1`
|
||||
: 'bg-gray-100 text-gray-400'
|
||||
}`}>
|
||||
<Icon size={18} />
|
||||
</div>
|
||||
<span className={`text-[9px] font-black uppercase tracking-widest ${
|
||||
step.done ? 'text-green-600' : i === 1 ? `text-${cfg.color}-600` : 'text-gray-400'
|
||||
}`}>{step.label}</span>
|
||||
</div>
|
||||
{i < steps.length - 1 && (
|
||||
<div className={`w-12 h-px mb-5 ${step.done ? 'bg-green-200' : 'bg-gray-200'}`} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Info box */}
|
||||
<div className="bg-blue-50 border border-blue-100 rounded-2xl p-4 text-left mb-4">
|
||||
<p className="text-[10px] font-black text-blue-600 uppercase tracking-widest mb-1">
|
||||
O que fazer agora?
|
||||
</p>
|
||||
<ul className="space-y-1.5 text-sm text-blue-800 font-medium">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-blue-400 mt-0.5">1.</span>
|
||||
Informe seu e-mail <strong>{email}</strong> ao administrador
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-blue-400 mt-0.5">2.</span>
|
||||
Aguarde o convite ser enviado
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-blue-400 mt-0.5">3.</span>
|
||||
Acesse o link do convite para completar o vínculo
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Diretório de profissionais */}
|
||||
<div className="border border-purple-100 bg-purple-50 rounded-2xl overflow-hidden mb-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowDiretorio(v => !v)}
|
||||
className="w-full flex items-center justify-between gap-3 p-4 text-left"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-purple-100 rounded-xl flex items-center justify-center">
|
||||
<Users size={16} className="text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-black text-purple-800 uppercase tracking-wide">Apareça no Diretório</p>
|
||||
<p className="text-[10px] text-purple-600 font-medium">Clínicas encontram você por localização</p>
|
||||
</div>
|
||||
</div>
|
||||
{showDiretorio ? <ChevronUp size={16} className="text-purple-400" /> : <ChevronDown size={16} className="text-purple-400" />}
|
||||
</button>
|
||||
|
||||
{showDiretorio && (
|
||||
<div className="px-4 pb-4 space-y-3 border-t border-purple-100 pt-3">
|
||||
{saved ? (
|
||||
<div className="flex items-center gap-2 text-green-700 bg-green-50 rounded-xl p-3">
|
||||
<CheckCircle2 size={18} />
|
||||
<span className="text-xs font-black uppercase">Perfil publicado no diretório!</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-[11px] text-purple-700 font-medium">
|
||||
Preencha sua localização para que clínicas e consultórios possam te contratar como {cfg.desc}.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<select
|
||||
value={estado}
|
||||
onChange={e => setEstado(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-white border border-purple-200 rounded-lg text-xs font-bold text-gray-700 outline-none focus:border-purple-400"
|
||||
>
|
||||
<option value="">Estado *</option>
|
||||
{UF_LIST.map(uf => <option key={uf}>{uf}</option>)}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
value={cidade}
|
||||
onChange={e => setCidade(e.target.value.toUpperCase())}
|
||||
placeholder="Cidade *"
|
||||
className="w-full px-3 py-2 bg-white border border-purple-200 rounded-lg text-xs font-bold text-gray-700 outline-none focus:border-purple-400 uppercase"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={bairro}
|
||||
onChange={e => setBairro(e.target.value.toUpperCase())}
|
||||
placeholder="Bairro"
|
||||
className="w-full px-3 py-2 bg-white border border-purple-200 rounded-lg text-xs font-bold text-gray-700 outline-none focus:border-purple-400 uppercase"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={especialidade}
|
||||
onChange={e => setEspecialidade(e.target.value.toUpperCase())}
|
||||
placeholder={role === 'protetico' ? 'Especialidade (ex: METAL)' : 'Especialidade (ex: ORTO)'}
|
||||
className="w-full px-3 py-2 bg-white border border-purple-200 rounded-lg text-xs font-bold text-gray-700 outline-none focus:border-purple-400 uppercase"
|
||||
/>
|
||||
</div>
|
||||
{saveError && <p className="text-xs text-red-600 font-bold">{saveError}</p>}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSaveDiretorio}
|
||||
disabled={saving}
|
||||
className="w-full bg-purple-600 hover:bg-purple-700 text-white py-2.5 rounded-xl text-xs font-black uppercase tracking-widest flex items-center justify-center gap-2 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{saving ? <Loader2 size={14} className="animate-spin" /> : <MapPin size={14} />}
|
||||
Publicar no Diretório
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => HybridBackend.logout()}
|
||||
className="flex items-center gap-2 mx-auto text-xs font-black text-gray-400 hover:text-red-500 uppercase tracking-widest transition-colors"
|
||||
>
|
||||
<LogOut size={14} />
|
||||
Sair da conta
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="px-8 py-4 bg-gray-50 border-t border-gray-100 flex items-center justify-center">
|
||||
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">{APP_VERSION}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user