feat: directorio más fiable y más fácil de usar
El objetivo de estos cambios es, por un lado, mantener los datos
actualizados y, por otro, mejorar la experiencia para quienes lo usan:
- Fiabilidad: los locales se pueden confirmar ("Sigue aquí") y
reportar (cerrado, datos incorrectos...), y se avisa de duplicados
antes de publicar, para que el contenido no se quede obsoleto ni
repetido.
- Usabilidad: autocompletado al proponer, favoritos, "cerca de mí",
compartir fichas, enlaces a Waze y una política de privacidad
visible, para que sea más sencillo aportar y encontrar locales.
This commit is contained in:
+56
-4
@@ -5,9 +5,11 @@ import {
|
||||
escucharAuth, loginAdmin, registrarAdmin, cerrarSesion,
|
||||
suscribirPendientes, aprobarPropuesta, rechazarPropuesta,
|
||||
suscribirLocales, deleteLocal,
|
||||
obtenerReportes, descartarReporte,
|
||||
obtenerPalabrasFiltradas, guardarPalabrasFiltradas,
|
||||
} from "./firebase.js";
|
||||
import { obtenerCategoria } from "./constants/categorias.js";
|
||||
import { enlaceGoogleMapsDesdeLocal, enlaceWazeDesdeLocal } from "./utils/geo.js";
|
||||
import StarRating from "./components/StarRating.jsx";
|
||||
|
||||
const S = {
|
||||
@@ -81,6 +83,8 @@ function PantallaAuth() {
|
||||
function TarjetaPropuesta({ p, onAprobar, onRechazar, busy }) {
|
||||
const cat = obtenerCategoria(p.categoria);
|
||||
const fecha = new Date(p.fechaPropuesta).toLocaleDateString("es-ES", { day:"numeric", month:"short", hour:"2-digit", minute:"2-digit" });
|
||||
const gmUrl = p.enlaceGoogleMaps || enlaceGoogleMapsDesdeLocal(p);
|
||||
const wazeUrl = enlaceWazeDesdeLocal(p);
|
||||
return (
|
||||
<div style={{ ...S.card, borderLeft:`4px solid ${cat.color}`, marginBottom:10 }}>
|
||||
<div style={{ display:"flex", justifyContent:"space-between", alignItems:"flex-start", flexWrap:"wrap", gap:12 }}>
|
||||
@@ -92,12 +96,15 @@ function TarjetaPropuesta({ p, onAprobar, onRechazar, busy }) {
|
||||
</div>
|
||||
<div style={{ display:"flex", flexWrap:"wrap", alignItems:"center", gap:16, fontSize:13, color:"#666" }}>
|
||||
<span>📍 {p.provincia}</span>
|
||||
{p.direccion && <span>🗺 {p.direccion}</span>}
|
||||
{p.direccion && (gmUrl ? <a href={gmUrl} target="_blank" rel="noopener noreferrer" style={{ color:"#666", textDecoration:"underline" }}>🗺 {p.direccion}</a> : <span>🗺 {p.direccion}</span>)}
|
||||
<StarRating value={p.puntuacion} readOnly size={14} />
|
||||
<span style={{ color:"#AAA" }}>Enviado: {fecha}</span>
|
||||
</div>
|
||||
{p.descripcion && <p style={{ margin:"8px 0 0", fontSize:13, color:"#666", fontStyle:"italic", background:"#FAFAFA", padding:"8px 12px", borderRadius:8 }}>"{p.descripcion}"</p>}
|
||||
{p.enlaceGoogleMaps && <a href={p.enlaceGoogleMaps} target="_blank" rel="noopener noreferrer" style={{ display:"inline-block", marginTop:6, fontSize:12, color:"#1A73E8" }}>Ver en Google Maps ↗</a>}
|
||||
<div style={{ display:"flex", gap:10 }}>
|
||||
{gmUrl && <a href={gmUrl} target="_blank" rel="noopener noreferrer" style={{ display:"inline-block", marginTop:6, fontSize:12, color:"#1A73E8" }}>Ver en Google Maps ↗</a>}
|
||||
{wazeUrl && <a href={wazeUrl} target="_blank" rel="noopener noreferrer" style={{ display:"inline-block", marginTop:6, fontSize:12, color:"#0A9FD9" }}>Ver en Waze ↗</a>}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display:"flex", gap:8, flexShrink:0 }}>
|
||||
<button onClick={() => onAprobar(p)} disabled={busy} style={{ ...S.btn("#1E8449"), padding:"8px 16px", fontSize:13 }}>✓ Aprobar</button>
|
||||
@@ -113,6 +120,7 @@ function PanelAdmin({ email }) {
|
||||
const [tab, setTab] = useState("pendientes");
|
||||
const [pendientes, setPendientes] = useState([]);
|
||||
const [publicados, setPublicados] = useState([]);
|
||||
const [reportes, setReportes] = useState([]);
|
||||
const [palabras, setPalabras] = useState([]);
|
||||
const [nuevaP, setNuevaP] = useState("");
|
||||
const [cargando, setCargando] = useState(true);
|
||||
@@ -120,6 +128,8 @@ function PanelAdmin({ email }) {
|
||||
const [guardando, setGuardando] = useState(false);
|
||||
const [buscador, setBuscador] = useState("");
|
||||
|
||||
const cargarReportes = () => obtenerReportes().then(setReportes).catch(() => {});
|
||||
|
||||
useEffect(() => {
|
||||
setCargando(true);
|
||||
let cancelado = false;
|
||||
@@ -129,12 +139,14 @@ function PanelAdmin({ email }) {
|
||||
suscribirLocales(l => !cancelado && setPublicados(l), () => {})
|
||||
.then(fn => { if (cancelado) fn?.(); else limpiarPublicados = fn; });
|
||||
obtenerPalabrasFiltradas().then(p => !cancelado && setPalabras(p));
|
||||
cargarReportes();
|
||||
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); } 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 addPalabra = () => {
|
||||
const p = nuevaP.trim().toLowerCase();
|
||||
@@ -155,6 +167,7 @@ function PanelAdmin({ email }) {
|
||||
const TABS = [
|
||||
{ id:"pendientes", l:`Pendientes (${pendientes.length})` },
|
||||
{ id:"publicados", l:`Publicados (${publicados.length})` },
|
||||
{ id:"reportes", l:`Reportes${reportes.length ? ` (${reportes.length})` : ""}` },
|
||||
{ id:"filtro", l:"Filtro de palabras" },
|
||||
];
|
||||
|
||||
@@ -215,7 +228,12 @@ function PanelAdmin({ email }) {
|
||||
{l.categoria && <span style={S.badge(obtenerCategoria(l.categoria).textColor, obtenerCategoria(l.categoria).bgLight)}>{obtenerCategoria(l.categoria).emoji} {l.categoria}</span>}
|
||||
<span style={{ fontSize:12, color:"#AAA" }}>📍 {l.provincia} · {new Date(l.fecha).toLocaleDateString("es-ES",{day:"numeric",month:"short",year:"numeric"})}</span>
|
||||
</div>
|
||||
{l.direccion && <p style={{ margin:"3px 0 0", fontSize:13, color:"#888" }}>{l.direccion}</p>}
|
||||
{l.direccion && (() => {
|
||||
const url = l.enlaceGoogleMaps || enlaceGoogleMapsDesdeLocal(l);
|
||||
return url
|
||||
? <a href={url} target="_blank" rel="noopener noreferrer" style={{ margin:"3px 0 0", fontSize:13, color:"#888", textDecoration:"underline", display:"block" }}>{l.direccion}</a>
|
||||
: <p style={{ margin:"3px 0 0", fontSize:13, color:"#888" }}>{l.direccion}</p>;
|
||||
})()}
|
||||
</div>
|
||||
<button onClick={() => eliminar(l.id)} style={{ background:"none", border:"1px solid #FADADD", borderRadius:8, padding:"6px 14px", color:"#C0392B", cursor:"pointer", fontSize:12, fontWeight:600, flexShrink:0 }}>Eliminar</button>
|
||||
</div>
|
||||
@@ -225,6 +243,40 @@ function PanelAdmin({ email }) {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── Reportes ── */}
|
||||
{tab === "reportes" && (
|
||||
reportes.length === 0 ? (
|
||||
<div style={{ textAlign:"center", padding:"4rem", color:"#AAA" }}>
|
||||
<div style={{ fontSize:52, marginBottom:12 }}>🚩</div>
|
||||
<p style={{ margin:0, fontSize:16, fontWeight:600, color:"#888" }}>Sin reportes pendientes</p>
|
||||
<p style={{ margin:"6px 0 0", fontSize:13 }}>Cuando alguien reporte un local (cerrado, datos incorrectos...), aparecerá aquí.</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p style={{ margin:"0 0 16px", fontSize:13, color:"#888" }}>{reportes.length} reporte{reportes.length!==1?"s":""} sin revisar.</p>
|
||||
{reportes.map(r => (
|
||||
<div key={r.id} style={{ ...S.card, borderLeft:"4px solid #C0392B", marginBottom:10 }}>
|
||||
<div style={{ display:"flex", justifyContent:"space-between", alignItems:"flex-start", flexWrap:"wrap", gap:12 }}>
|
||||
<div style={{ flex:1 }}>
|
||||
<div style={{ display:"flex", alignItems:"center", gap:8, flexWrap:"wrap", marginBottom:6 }}>
|
||||
<span style={{ fontWeight:700, fontSize:16 }}>{r.localNombre || "(local ya eliminado)"}</span>
|
||||
<span style={S.badge("#C0392B", "#FDEDEC")}>{{ cerrado:"Ha cerrado", datos_incorrectos:"Datos incorrectos", inapropiado:"Contenido inapropiado", otro:"Otro motivo" }[r.motivo] || r.motivo}</span>
|
||||
</div>
|
||||
<p style={{ margin:0, fontSize:13, color:"#666" }}>📍 {r.localProvincia} {r.localDireccion ? `· ${r.localDireccion}` : ""}</p>
|
||||
{r.comentario && <p style={{ margin:"8px 0 0", fontSize:13, color:"#666", fontStyle:"italic", background:"#FAFAFA", padding:"8px 12px", borderRadius:8 }}>"{r.comentario}"</p>}
|
||||
<p style={{ margin:"6px 0 0", fontSize:11, color:"#AAA" }}>Reportado: {new Date(r.fecha).toLocaleDateString("es-ES", { day:"numeric", month:"short", hour:"2-digit", minute:"2-digit" })}</p>
|
||||
</div>
|
||||
<div style={{ display:"flex", gap:8, flexShrink:0 }}>
|
||||
{r.localId && <button onClick={() => eliminar(r.localId)} style={{ ...S.btn("#C0392B"), padding:"8px 16px", fontSize:13 }}>🗑 Eliminar local</button>}
|
||||
<button onClick={() => descartar(r.id)} style={{ ...S.btn("#EEE","#666"), padding:"8px 16px", fontSize:13 }}>Descartar aviso</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* ── Filtro de palabras ── */}
|
||||
{tab === "filtro" && (
|
||||
<div style={{ maxWidth:620 }}>
|
||||
|
||||
Reference in New Issue
Block a user