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:
+114
-15
@@ -1,5 +1,12 @@
|
||||
import { useState } from "react";
|
||||
import CategoriaBadge from "./CategoriaBadge.jsx";
|
||||
import StarRating from "./StarRating.jsx";
|
||||
import ReportarLocal from "./ReportarLocal.jsx";
|
||||
import useFavoritos from "../hooks/useFavoritos.js";
|
||||
import { confirmarLocal } from "../firebase.js";
|
||||
import { enlaceGoogleMapsDesdeLocal, enlaceWazeDesdeLocal, formatearDistancia } from "../utils/geo.js";
|
||||
|
||||
const MESES_PARA_CADUCAR = 6;
|
||||
|
||||
function formatearFecha(fecha) {
|
||||
if (!fecha) return null;
|
||||
@@ -8,22 +15,78 @@ function formatearFecha(fecha) {
|
||||
return date.toLocaleDateString("es-ES", { day: "numeric", month: "long", year: "numeric" });
|
||||
}
|
||||
|
||||
export default function LocalCard({ local }) {
|
||||
const gmUrl = local.enlaceGoogleMaps || (local.direccion
|
||||
? `https://www.google.com/maps/search/${encodeURIComponent(`${local.nombre} ${local.direccion}`)}`
|
||||
: null);
|
||||
function mesesDesde(fecha) {
|
||||
if (!fecha) return null;
|
||||
const date = new Date(fecha);
|
||||
if (Number.isNaN(date.getTime())) return null;
|
||||
return (Date.now() - date.getTime()) / (1000 * 60 * 60 * 24 * 30);
|
||||
}
|
||||
|
||||
export default function LocalCard({ local, distanciaKm }) {
|
||||
const { esFavorito, alternarFavorito } = useFavoritos();
|
||||
const [confirmando, setConfirmando] = useState(false);
|
||||
const [confirmado, setConfirmado] = useState(false);
|
||||
const [fechaConfirmacion, setFechaConfirmacion] = useState(local.fechaConfirmacion || local.fecha);
|
||||
|
||||
const gmUrl = local.enlaceGoogleMaps || enlaceGoogleMapsDesdeLocal(local);
|
||||
const wazeUrl = enlaceWazeDesdeLocal(local);
|
||||
const fechaFormateada = formatearFecha(local.fecha);
|
||||
const meses = mesesDesde(fechaConfirmacion);
|
||||
const sinConfirmarReciente = meses != null && meses >= MESES_PARA_CADUCAR;
|
||||
const favorito = esFavorito(local.id);
|
||||
|
||||
const handleConfirmar = async () => {
|
||||
setConfirmando(true);
|
||||
try {
|
||||
const res = await confirmarLocal(local.id);
|
||||
setFechaConfirmacion(res?.fechaConfirmacion || new Date().toISOString());
|
||||
setConfirmado(true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
setConfirmando(false);
|
||||
};
|
||||
|
||||
const compartir = async () => {
|
||||
const url = `${window.location.origin}${window.location.pathname}?local=${local.id}`;
|
||||
const texto = `${local.nombre}${local.provincia ? ` · ${local.provincia}` : ""}`;
|
||||
if (navigator.share) {
|
||||
try { await navigator.share({ title: local.nombre, text: texto, url }); } catch { /* usuario canceló */ }
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
window.open(`https://wa.me/?text=${encodeURIComponent(`${texto} ${url}`)}`, "_blank", "noopener,noreferrer");
|
||||
} catch {
|
||||
window.open(`https://wa.me/?text=${encodeURIComponent(`${texto} ${url}`)}`, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`local-${local.id}`}
|
||||
style={{ background: "var(--color-background-primary)", border: "0.5px solid var(--color-border-tertiary)", borderRadius: "var(--border-radius-lg)", padding: "1rem 1.25rem", display: "flex", flexDirection: "column", gap: 8, transition: "border-color 0.2s" }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.borderColor = "var(--color-border-secondary)"; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.borderColor = "var(--color-border-tertiary)"; }}
|
||||
>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 8 }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<p style={{ fontWeight: 500, fontSize: 16, margin: 0 }}>{local.nombre}</p>
|
||||
{local.provincia && <p style={{ fontSize: 13, color: "var(--color-text-secondary)", margin: "2px 0 0" }}>{local.provincia}</p>}
|
||||
<p style={{ fontSize: 13, color: "var(--color-text-secondary)", margin: "2px 0 0" }}>
|
||||
{local.provincia}
|
||||
{distanciaKm != null && <span style={{ color: "var(--color-text-tertiary)" }}> · {formatearDistancia(distanciaKm)}</span>}
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 4, flexShrink: 0 }}>
|
||||
<button type="button" onClick={compartir} title="Compartir"
|
||||
style={{ background: "none", border: "none", cursor: "pointer", fontSize: 16, color: "var(--color-text-tertiary)", padding: 4 }}>
|
||||
<i className="ti ti-share-2" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" onClick={() => alternarFavorito(local.id)} title={favorito ? "Quitar de favoritos" : "Guardar en favoritos"}
|
||||
aria-pressed={favorito}
|
||||
style={{ background: "none", border: "none", cursor: "pointer", fontSize: 16, color: favorito ? "#8B0000" : "var(--color-text-tertiary)", padding: 4 }}>
|
||||
<i className={favorito ? "ti ti-heart-filled" : "ti ti-heart"} aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,16 +95,34 @@ export default function LocalCard({ local }) {
|
||||
{local.direccion && (
|
||||
<div style={{ display: "flex", alignItems: "flex-start", gap: 6 }}>
|
||||
<i className="ti ti-map-pin" aria-hidden="true" style={{ fontSize: 14, color: "var(--color-text-tertiary)", marginTop: 1, flexShrink: 0 }}></i>
|
||||
<span style={{ fontSize: 13, color: "var(--color-text-secondary)" }}>{local.direccion}</span>
|
||||
{gmUrl ? (
|
||||
<a href={gmUrl} target="_blank" rel="noopener noreferrer" title="Abrir dirección en Google Maps"
|
||||
style={{ fontSize: 13, color: "var(--color-text-secondary)", textDecoration: "underline", textDecorationStyle: "dotted", textUnderlineOffset: 2 }}>
|
||||
{local.direccion}
|
||||
</a>
|
||||
) : (
|
||||
<span style={{ fontSize: 13, color: "var(--color-text-secondary)" }}>{local.direccion}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{gmUrl && (
|
||||
<a href={gmUrl} target="_blank" rel="noopener noreferrer"
|
||||
style={{ display: "inline-flex", alignItems: "center", gap: 5, fontSize: 12, color: "#1A73E8", textDecoration: "none", width: "fit-content", background: "#E8F0FE", padding: "4px 10px", borderRadius: "var(--border-radius-md)", fontWeight: 500 }}>
|
||||
<i className="ti ti-brand-google-maps" aria-hidden="true" style={{ fontSize: 14 }}></i>
|
||||
Ver en Google Maps
|
||||
</a>
|
||||
{(gmUrl || wazeUrl) && (
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||
{gmUrl && (
|
||||
<a href={gmUrl} target="_blank" rel="noopener noreferrer"
|
||||
style={{ display: "inline-flex", alignItems: "center", gap: 5, fontSize: 12, color: "#1A73E8", textDecoration: "none", width: "fit-content", background: "#E8F0FE", padding: "4px 10px", borderRadius: "var(--border-radius-md)", fontWeight: 500 }}>
|
||||
<i className="ti ti-brand-google-maps" aria-hidden="true" style={{ fontSize: 14 }}></i>
|
||||
Google Maps
|
||||
</a>
|
||||
)}
|
||||
{wazeUrl && (
|
||||
<a href={wazeUrl} target="_blank" rel="noopener noreferrer"
|
||||
style={{ display: "inline-flex", alignItems: "center", gap: 5, fontSize: 12, color: "#33CCFF", textDecoration: "none", width: "fit-content", background: "#E6FAFF", padding: "4px 10px", borderRadius: "var(--border-radius-md)", fontWeight: 500 }}>
|
||||
<i className="ti ti-brand-waze" aria-hidden="true" style={{ fontSize: 14 }}></i>
|
||||
Waze
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<StarRating value={local.puntuacion} readOnly />
|
||||
@@ -50,9 +131,27 @@ export default function LocalCard({ local }) {
|
||||
<p style={{ fontSize: 13, color: "var(--color-text-secondary)", margin: 0, fontStyle: "italic" }}>"{local.descripcion}"</p>
|
||||
)}
|
||||
|
||||
{fechaFormateada && (
|
||||
<p style={{ fontSize: 11, color: "var(--color-text-tertiary)", margin: 0 }}>Publicado {fechaFormateada}</p>
|
||||
{sinConfirmarReciente && !confirmado && (
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, flexWrap: "wrap", background: "#FDF2E9", border: "1px solid #F5CBA7", borderRadius: "var(--border-radius-md)", padding: "6px 10px" }}>
|
||||
<span style={{ fontSize: 12, color: "#784212", display: "flex", alignItems: "center", gap: 5 }}>
|
||||
<i className="ti ti-clock-exclamation" aria-hidden="true"></i> Sin confirmar recientemente
|
||||
</span>
|
||||
<button type="button" onClick={handleConfirmar} disabled={confirmando}
|
||||
style={{ fontSize: 12, background: "#1E8449", color: "white", border: "none", borderRadius: "var(--border-radius-md)", padding: "4px 10px", cursor: "pointer", fontWeight: 500 }}>
|
||||
{confirmando ? "..." : "Sigue aquí ✅"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{confirmado && (
|
||||
<p style={{ fontSize: 12, color: "#1E8449", margin: 0 }}>✅ Gracias por confirmar que sigue activo.</p>
|
||||
)}
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
|
||||
{fechaFormateada && (
|
||||
<p style={{ fontSize: 11, color: "var(--color-text-tertiary)", margin: 0 }}>Publicado {fechaFormateada}</p>
|
||||
)}
|
||||
<ReportarLocal localId={local.id} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user