feat(ui): notificaciones toast y mapa que sobrevive al cambio de pestaña
deploy-branch / deploy (push) Successful in 2m41s
deploy-branch / teardown (push) Skipped

- Añade sistema de avisos tipo snackbar (Toast.jsx + useToast.js) con
  auto-cierre, animación de entrada y estilos info/éxito/error.
- LocalCard: feedback visual al añadir/quitar un local de favoritos.
- AdminPanel: sustituye los alert() por toasts al aprobar, rechazar,
  eliminar locales, descartar avisos y guardar el filtro de palabras;
  además saluda con un toast tras iniciar sesión o crear cuenta.
- App: el contenedor del mapa Leaflet queda siempre montado (oculto con
  CSS) y se llama a invalidateSize() al volver a la pestaña de mapa;
  así el mapa ya no se rompe al navegar entre vistas (antes obligaba
  a recargar la página).
This commit is contained in:
2026-08-15 20:37:04 +02:00
parent b36867225c
commit d48b74cbd4
5 changed files with 109 additions and 29 deletions
+26 -7
View File
@@ -11,6 +11,10 @@ import {
import { obtenerCategoria } from "./constants/categorias.js";
import { enlaceGoogleMapsDesdeLocal, enlaceWazeDesdeLocal } from "./utils/geo.js";
import StarRating from "./components/StarRating.jsx";
import Toast from "./components/Toast.jsx";
import useToast from "./hooks/useToast.js";
const CLAVE_TOAST_LOGIN = "localesp_mostrar_toast_login";
const S = {
page: { minHeight:"100vh", background:"#F2EDE8", fontFamily:"-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" },
@@ -41,7 +45,10 @@ function PantallaAuth() {
if (!email || !pass) { setErr("Rellena todos los campos."); return; }
if (modo === "registro" && pass !== pass2) { setErr("Las contraseñas no coinciden."); return; }
setBusy(true);
try { modo === "login" ? await loginAdmin(email, pass) : await registrarAdmin(email, pass); }
try {
modo === "login" ? await loginAdmin(email, pass) : await registrarAdmin(email, pass);
try { sessionStorage.setItem(CLAVE_TOAST_LOGIN, modo === "login" ? "1" : "0"); } catch { /* sessionStorage no disponible */ }
}
catch(e) { setErr(errMsg(e.code)); }
setBusy(false);
};
@@ -127,9 +134,20 @@ function PanelAdmin({ email }) {
const [busy, setBusy] = useState(false);
const [guardando, setGuardando] = useState(false);
const [buscador, setBuscador] = useState("");
const { toast, mostrarToast } = useToast();
const cargarReportes = () => obtenerReportes().then(setReportes).catch(() => {});
useEffect(() => {
try {
const marca = sessionStorage.getItem(CLAVE_TOAST_LOGIN);
if (marca !== null) {
mostrarToast(marca === "1" ? `Sesión iniciada como ${email}` : "Cuenta de administrador creada", "exito");
sessionStorage.removeItem(CLAVE_TOAST_LOGIN);
}
} catch { /* sessionStorage no disponible */ }
}, []);
useEffect(() => {
setCargando(true);
let cancelado = false;
@@ -143,10 +161,10 @@ function PanelAdmin({ email }) {
return () => { cancelado = true; limpiarPendientes?.(); limpiarPublicados?.(); };
}, []);
const aprobar = async (p) => { setBusy(true); try { await aprobarPropuesta(p); } catch(e) { alert("Error: "+e.message); } setBusy(false); };
const rechazar = async (id) => { if (!confirm("¿Rechazar y eliminar esta propuesta?")) return; try { await rechazarPropuesta(id); } catch(e) { alert("Error: "+e.message); } };
const eliminar = async (id) => { if (!confirm("¿Eliminar este local publicado?")) return; try { await deleteLocal(id); cargarReportes(); } catch(e) { alert("Error: "+e.message); } };
const descartar = async (id) => { try { await descartarReporte(id); setReportes(r => r.filter(x => x.id !== id)); } catch(e) { alert("Error: "+e.message); } };
const aprobar = async (p) => { setBusy(true); try { await aprobarPropuesta(p); mostrarToast(`✅ "${p.nombre}" aprobado y publicado`, "exito"); } catch(e) { mostrarToast("Error: "+e.message, "error"); } setBusy(false); };
const rechazar = async (id) => { if (!confirm("¿Rechazar y eliminar esta propuesta?")) return; try { await rechazarPropuesta(id); mostrarToast("Propuesta rechazada", "info"); } catch(e) { mostrarToast("Error: "+e.message, "error"); } };
const eliminar = async (id) => { if (!confirm("¿Eliminar este local publicado?")) return; try { await deleteLocal(id); cargarReportes(); mostrarToast("Local eliminado", "info"); } catch(e) { mostrarToast("Error: "+e.message, "error"); } };
const descartar = async (id) => { try { await descartarReporte(id); setReportes(r => r.filter(x => x.id !== id)); mostrarToast("Aviso descartado", "info"); } catch(e) { mostrarToast("Error: "+e.message, "error"); } };
const addPalabra = () => {
const p = nuevaP.trim().toLowerCase();
@@ -155,8 +173,8 @@ function PanelAdmin({ email }) {
};
const guardarFiltro = async () => {
setGuardando(true);
try { await guardarPalabrasFiltradas(palabras); alert(" Filtro guardado."); }
catch(e) { alert("Error: "+e.message); }
try { await guardarPalabrasFiltradas(palabras); mostrarToast("💾 Filtro de palabras guardado", "exito"); }
catch(e) { mostrarToast("Error: "+e.message, "error"); }
setGuardando(false);
};
@@ -318,6 +336,7 @@ function PanelAdmin({ email }) {
</div>
)}
</div>
<Toast toast={toast} />
</div>
);
}