- login.html: página de acceso con diseño Quiet Wealth (sin popup del browser) - api/login.php: endpoint POST para iniciar sesión, GET ?action=logout para cerrarla - auth.php: valida sesión PHP ($_SESSION) en lugar de HTTP Basic Auth - ui.js: apiFetch() intercepta 401 y redirige a login.html automáticamente - Todos los fetch() en dashboard, add y historial migrados a apiFetch() - Eliminado docker/htpasswd y auth_basic de NGINX Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
78 lines
3.5 KiB
JavaScript
78 lines
3.5 KiB
JavaScript
// Wrapper de fetch que redirige al login si la sesión expiró
|
|
window.apiFetch = async function (url, options = {}) {
|
|
const r = await fetch(url, options);
|
|
if (r.status === 401) {
|
|
window.location.href = 'login.html';
|
|
return null;
|
|
}
|
|
return r;
|
|
};
|
|
|
|
(function () {
|
|
const MODAL_HTML = `
|
|
<div id="rm-modal-overlay" class="fixed inset-0 z-[100] flex items-end justify-center p-6 bg-black/50 backdrop-blur-sm hidden">
|
|
<div class="w-full max-w-sm bg-white rounded-[2rem] p-6 shadow-2xl space-y-3">
|
|
<h3 id="rm-modal-title" class="text-base font-bold text-on-surface"></h3>
|
|
<p id="rm-modal-message" class="text-sm text-on-surface-variant leading-relaxed"></p>
|
|
<div id="rm-modal-actions" class="flex gap-3 pt-2"></div>
|
|
</div>
|
|
</div>
|
|
<div id="rm-toast" class="fixed bottom-28 left-1/2 -translate-x-1/2 z-[200] px-5 py-3 rounded-full text-sm font-semibold shadow-xl opacity-0 transition-all duration-300 pointer-events-none whitespace-nowrap"></div>
|
|
`;
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
document.body.insertAdjacentHTML('beforeend', MODAL_HTML);
|
|
});
|
|
|
|
function show(title, message, buttons) {
|
|
document.getElementById('rm-modal-title').textContent = title;
|
|
document.getElementById('rm-modal-message').textContent = message;
|
|
const actions = document.getElementById('rm-modal-actions');
|
|
actions.innerHTML = '';
|
|
buttons.forEach(({ label, style, action }) => {
|
|
const b = document.createElement('button');
|
|
b.textContent = label;
|
|
const styles = {
|
|
primary: 'flex-1 py-3 bg-primary text-white rounded-xl font-bold active:scale-95 transition-transform',
|
|
danger: 'flex-1 py-3 bg-[#ba1a1a] text-white rounded-xl font-bold active:scale-95 transition-transform',
|
|
secondary: 'flex-1 py-3 bg-[#eceeed] text-[#191c1c] rounded-xl font-semibold active:scale-95 transition-transform',
|
|
};
|
|
b.className = styles[style] || styles.secondary;
|
|
b.onclick = action;
|
|
actions.appendChild(b);
|
|
});
|
|
document.getElementById('rm-modal-overlay').classList.remove('hidden');
|
|
}
|
|
|
|
function hide() {
|
|
document.getElementById('rm-modal-overlay').classList.add('hidden');
|
|
}
|
|
|
|
window.rmModal = {
|
|
alert(message, title = 'Aviso') {
|
|
return new Promise(resolve => {
|
|
show(title, message, [
|
|
{ label: 'Aceptar', style: 'primary', action: () => { hide(); resolve(); } }
|
|
]);
|
|
});
|
|
},
|
|
confirm(message, title = '¿Confirmar?') {
|
|
return new Promise(resolve => {
|
|
show(title, message, [
|
|
{ label: 'Cancelar', style: 'secondary', action: () => { hide(); resolve(false); } },
|
|
{ label: 'Eliminar', style: 'danger', action: () => { hide(); resolve(true); } }
|
|
]);
|
|
});
|
|
},
|
|
toast(message, type = 'success') {
|
|
const el = document.getElementById('rm-toast');
|
|
el.textContent = message;
|
|
el.className = `fixed bottom-28 left-1/2 -translate-x-1/2 z-[200] px-5 py-3 rounded-full text-sm font-semibold shadow-xl transition-all duration-300 pointer-events-none whitespace-nowrap ${
|
|
type === 'error' ? 'bg-[#ba1a1a] text-white' : 'bg-[#005050] text-white'
|
|
}`;
|
|
el.style.opacity = '1';
|
|
setTimeout(() => { el.style.opacity = '0'; }, 2500);
|
|
}
|
|
};
|
|
})();
|