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:
VPS 4 Builder
2026-05-14 04:44:53 +02:00
parent 435176c0c3
commit 2f3c0420e3
7 changed files with 20 additions and 35 deletions
+1 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -16,7 +16,7 @@
})();
</script>
<base href="./">
<base href="/">
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
+1 -1
View File
@@ -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();
},
+8 -8
View File
@@ -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" />
+2 -2
View File
@@ -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ê 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