feat(routing): switch from hash routing to HTML5 History API
Remove all window.location.hash assignments and hashchange events. Navigate with history.pushState; listen on popstate. Fix base href to '/' so assets resolve correctly on deep paths. Update magic link URL. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -264,7 +264,7 @@ app.post('/api/dentistas/magic-link', async (req, res) => {
|
||||
try {
|
||||
const token = Math.random().toString(36).substring(2, 15);
|
||||
const appUrl = process.env.APP_URL || `http://localhost:${process.env.FRONTEND_PORT || 3000}`;
|
||||
const magicLink = `${appUrl}/#/cadastro-dentista?token=${token}&clinica=${clinicaId}&nome=${encodeURIComponent(dentistName)}`;
|
||||
const magicLink = `${appUrl}/cadastro-dentista?token=${token}&clinica=${clinicaId}&nome=${encodeURIComponent(dentistName)}`;
|
||||
res.json({ success: true, link: magicLink });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
+6
-21
@@ -97,29 +97,16 @@ const VIEW_TO_ROUTE: Record<ViewKey, string> = {
|
||||
'cadastro-dentista': '/cadastro-dentista'
|
||||
};
|
||||
|
||||
/** Get the hash path, e.g. "/#/pacientes" → "/pacientes" */
|
||||
function getHashPath(): string {
|
||||
const hash = window.location.hash;
|
||||
if (hash.startsWith('#/')) return hash.slice(1); // "#/pacientes" → "/pacientes"
|
||||
if (hash === '#' || hash === '') return '/';
|
||||
return '/';
|
||||
}
|
||||
|
||||
/** Resolve current URL to a ViewKey */
|
||||
function resolveViewFromUrl(): ViewKey {
|
||||
// Legacy support: ?p=contato
|
||||
if (window.location.search.includes('p=contato')) return 'public';
|
||||
// Legacy support: #update_db (old non-slash hash)
|
||||
if (window.location.hash === '#update_db') return 'update';
|
||||
|
||||
const path = getHashPath();
|
||||
const path = window.location.pathname || '/';
|
||||
return ROUTE_MAP[path] ?? 'dashboard';
|
||||
}
|
||||
|
||||
/** Push a new hash-based URL without reloading the page */
|
||||
/** Push a new URL without reloading the page */
|
||||
function navigate(view: ViewKey) {
|
||||
const route = VIEW_TO_ROUTE[view];
|
||||
window.location.hash = route;
|
||||
history.pushState({}, '', route);
|
||||
}
|
||||
|
||||
// ------- App -------
|
||||
@@ -200,8 +187,8 @@ const App: React.FC = () => {
|
||||
|
||||
setCurrentView(fromUrl);
|
||||
};
|
||||
window.addEventListener('hashchange', onHashChange);
|
||||
return () => window.removeEventListener('hashchange', onHashChange);
|
||||
window.addEventListener('popstate', onHashChange);
|
||||
return () => window.removeEventListener('popstate', onHashChange);
|
||||
}, []);
|
||||
|
||||
// Inject dynamic brand color
|
||||
@@ -210,9 +197,7 @@ const App: React.FC = () => {
|
||||
document.documentElement.style.setProperty('--brand-color-soft', `${clinColor}11`);
|
||||
}, [clinColor]);
|
||||
|
||||
// On mount: always sync URL to match the actual current view.
|
||||
// This handles auth redirects (e.g. reload at /#/login while authenticated shows dashboard
|
||||
// but URL would stay /#/login without this call).
|
||||
// On mount: always sync URL to match the actual current view (handles auth redirects).
|
||||
useEffect(() => {
|
||||
navigate(currentView);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -148,7 +148,7 @@ export const NotificationCenter: React.FC = () => {
|
||||
<div className="p-4 bg-white border-t border-slate-100 text-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
window.location.hash = '/notificacoes';
|
||||
history.pushState({}, '', '/notificacoes');
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className="text-xs font-bold text-slate-500 hover:text-blue-600 transition-colors tracking-widest uppercase"
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
<base href="./">
|
||||
<base href="/">
|
||||
|
||||
|
||||
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
|
||||
|
||||
@@ -76,7 +76,7 @@ export const HybridBackend = {
|
||||
localStorage.removeItem('SCOREODONTO_USER_DATA');
|
||||
localStorage.removeItem('SCOREODONTO_WORKSPACES');
|
||||
localStorage.removeItem('SCOREODONTO_ACTIVE_WORKSPACE');
|
||||
window.location.hash = '/';
|
||||
window.location.href = '/';
|
||||
window.location.reload();
|
||||
},
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ export const Dashboard: React.FC = () => {
|
||||
<div className="flex items-center gap-3">
|
||||
{(currentRole === 'admin' || currentRole === 'donoclinica' || currentRole === 'funcionario') && (
|
||||
<button
|
||||
onClick={() => window.location.hash = '#/relatorios'}
|
||||
onClick={() => history.pushState({}, '', '/relatorios')}
|
||||
className="bg-white text-gray-900 border border-gray-200 px-6 py-3 rounded-2xl text-xs font-black uppercase tracking-widest hover:bg-gray-50 transition-all flex items-center gap-2 shadow-sm"
|
||||
>
|
||||
<Clock size={16} /> Relatórios
|
||||
@@ -136,7 +136,7 @@ export const Dashboard: React.FC = () => {
|
||||
)}
|
||||
{(currentRole === 'admin' || currentRole === 'donoclinica' || currentRole === 'funcionario') && (
|
||||
<button
|
||||
onClick={() => window.location.hash = '#/pacientes'}
|
||||
onClick={() => history.pushState({}, '', '/pacientes')}
|
||||
className="bg-blue-600 text-white px-6 py-3 rounded-2xl text-xs font-black uppercase tracking-widest hover:bg-blue-700 hover:shadow-xl hover:shadow-blue-200 transition-all flex items-center gap-2 shadow-lg shadow-blue-100"
|
||||
>
|
||||
<Plus size={16} /> Novo Paciente
|
||||
@@ -155,7 +155,7 @@ export const Dashboard: React.FC = () => {
|
||||
trend="+12% este mês"
|
||||
isPositive={true}
|
||||
color="blue"
|
||||
onClick={() => window.location.hash = '#/pacientes'}
|
||||
onClick={() => history.pushState({}, '', '/pacientes')}
|
||||
/>
|
||||
{showFinancials && (
|
||||
<StatCard
|
||||
@@ -165,7 +165,7 @@ export const Dashboard: React.FC = () => {
|
||||
trend="+5.4% vs mês ant."
|
||||
isPositive={true}
|
||||
color="green"
|
||||
onClick={() => window.location.hash = '#/financeiro'}
|
||||
onClick={() => history.pushState({}, '', '/financeiro')}
|
||||
/>
|
||||
)}
|
||||
<StatCard
|
||||
@@ -175,7 +175,7 @@ export const Dashboard: React.FC = () => {
|
||||
trend="82% de ocupação"
|
||||
isPositive={true}
|
||||
color="purple"
|
||||
onClick={() => window.location.hash = '#/agenda'}
|
||||
onClick={() => history.pushState({}, '', '/agenda')}
|
||||
/>
|
||||
{showFinancials && (
|
||||
<StatCard
|
||||
@@ -185,7 +185,7 @@ export const Dashboard: React.FC = () => {
|
||||
trend="-2% vs ontem"
|
||||
isPositive={false}
|
||||
color="amber"
|
||||
onClick={() => window.location.hash = '#/leads'}
|
||||
onClick={() => history.pushState({}, '', '/leads')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -253,7 +253,7 @@ export const Dashboard: React.FC = () => {
|
||||
{recent.length > 0 ? recent.map((app: any) => (
|
||||
<div
|
||||
key={app.id}
|
||||
onClick={() => window.location.hash = '#/agenda'}
|
||||
onClick={() => history.pushState({}, '', '/agenda')}
|
||||
className="flex items-center gap-4 group cursor-pointer"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-2xl bg-white/10 flex items-center justify-center text-blue-400 group-hover:bg-blue-600 group-hover:text-white transition-all duration-300 flex-shrink-0">
|
||||
@@ -276,7 +276,7 @@ export const Dashboard: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => window.location.hash = '#/agenda'}
|
||||
onClick={() => history.pushState({}, '', '/agenda')}
|
||||
className="w-full mt-8 bg-white/5 border border-white/10 text-white py-4 rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-white/10 transition-all flex items-center justify-center gap-2 group"
|
||||
>
|
||||
Ver Agenda Completa <ArrowRight size={14} className="group-hover:translate-x-1 transition-transform" />
|
||||
|
||||
@@ -8,7 +8,7 @@ export const DentistRegisterView: React.FC = () => {
|
||||
const toast = useToast();
|
||||
|
||||
// Get data from URL
|
||||
const query = new URLSearchParams(window.location.hash.split('?')[1]);
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
const clinicaId = query.get('clinica');
|
||||
const tempName = query.get('nome');
|
||||
const token = query.get('token');
|
||||
@@ -232,7 +232,7 @@ export const DentistRegisterView: React.FC = () => {
|
||||
<p className="text-gray-500 font-medium mb-10">Seu perfil foi criado e você já está vinculado à unidade <br /> <span className="text-blue-600 font-bold">{clinicaId}</span>.</p>
|
||||
|
||||
<button
|
||||
onClick={() => window.location.hash = '/login'}
|
||||
onClick={() => { history.pushState({}, '', '/login'); window.location.reload(); }}
|
||||
className="w-full bg-gray-900 text-white py-5 rounded-2xl font-black uppercase tracking-widest hover:bg-blue-600 shadow-2xl transition-all"
|
||||
>
|
||||
Ir para o Login
|
||||
|
||||
Reference in New Issue
Block a user