diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png index b24fbd2..6882fd6 100644 Binary files a/public/apple-touch-icon.png and b/public/apple-touch-icon.png differ diff --git a/public/favicon-32.png b/public/favicon-32.png index f39bd4d..6b171bb 100644 Binary files a/public/favicon-32.png and b/public/favicon-32.png differ diff --git a/public/icon-192.png b/public/icon-192.png index 9093b4a..34fc8e9 100644 Binary files a/public/icon-192.png and b/public/icon-192.png differ diff --git a/public/icon-512-maskable.png b/public/icon-512-maskable.png index faab41e..af6fea9 100644 Binary files a/public/icon-512-maskable.png and b/public/icon-512-maskable.png differ diff --git a/public/icon-512.png b/public/icon-512.png index 18118d8..30da7ef 100644 Binary files a/public/icon-512.png and b/public/icon-512.png differ diff --git a/public/logo-localesp.png b/public/logo-localesp.png new file mode 100644 index 0000000..fb08011 Binary files /dev/null and b/public/logo-localesp.png differ diff --git a/public/logo-localesp@2x.png b/public/logo-localesp@2x.png new file mode 100644 index 0000000..7d2f121 Binary files /dev/null and b/public/logo-localesp@2x.png differ diff --git a/public/sw.js b/public/sw.js index 08e3309..7fe4d77 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,5 +1,15 @@ -const CACHE_NAME = "localesp-shell-v1"; -const SHELL_ASSETS = ["/", "/manifest.webmanifest", "/icon-192.png", "/icon-512.png"]; +const CACHE_NAME = "localesp-shell-v2"; +const SHELL_ASSETS = [ + "/", + "/manifest.webmanifest", + "/icon-192.png", + "/icon-512.png", + "/icon-512-maskable.png", + "/apple-touch-icon.png", + "/favicon-32.png", + "/logo-localesp.png", + "/logo-localesp@2x.png", +]; self.addEventListener("install", (event) => { event.waitUntil( diff --git a/src/AdminPanel.jsx b/src/AdminPanel.jsx index 0d88281..ed6dc69 100644 --- a/src/AdminPanel.jsx +++ b/src/AdminPanel.jsx @@ -7,14 +7,8 @@ import { suscribirLocales, deleteLocal, obtenerPalabrasFiltradas, guardarPalabrasFiltradas, } from "./firebase.js"; - -const CATEGORIAS = { - "Restauración": { emoji:"🍽️", color:"#8B0000", bg:"#FFF0F0" }, - "Pequeño comercio": { emoji:"🛍️", color:"#1A5276", bg:"#EBF5FB" }, - "Peluquería y estética": { emoji:"✂️", color:"#76448A", bg:"#F5EEF8" }, - "Servicios del hogar": { emoji:"🔧", color:"#1E8449", bg:"#EAFAF1" }, - "Otros": { emoji:"📌", color:"#784212", bg:"#FDF2E9" }, -}; +import { obtenerCategoria } from "./constants/categorias.js"; +import StarRating from "./components/StarRating.jsx"; const S = { page: { minHeight:"100vh", background:"#F2EDE8", fontFamily:"-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" }, @@ -85,21 +79,21 @@ function PantallaAuth() { // ── Tarjeta propuesta pendiente ─────────────────────────────────────────────── function TarjetaPropuesta({ p, onAprobar, onRechazar, busy }) { - const cat = CATEGORIAS[p.categoria] || {}; + const cat = obtenerCategoria(p.categoria); const fecha = new Date(p.fechaPropuesta).toLocaleDateString("es-ES", { day:"numeric", month:"short", hour:"2-digit", minute:"2-digit" }); return ( -
+
{p.nombre} - {cat.emoji && {cat.emoji} {p.categoria}} + {p.categoria && {cat.emoji} {p.categoria}} {p.subcategoria && {p.subcategoria}}
-
+
📍 {p.provincia} {p.direccion && 🗺 {p.direccion}} - {"★".repeat(p.puntuacion||0)}{"☆".repeat(5-(p.puntuacion||0))} + Enviado: {fecha}
{p.descripcion &&

"{p.descripcion}"

} @@ -128,10 +122,14 @@ function PanelAdmin({ email }) { useEffect(() => { setCargando(true); - const u1 = suscribirPendientes(l => { setPendientes(l); setCargando(false); }, () => setCargando(false)); - const u2 = suscribirLocales(l => setPublicados(l), () => {}); - obtenerPalabrasFiltradas().then(p => setPalabras(p)); - return () => { u1(); u2(); }; + let cancelado = false; + let limpiarPendientes, limpiarPublicados; + suscribirPendientes(l => { if (!cancelado) { setPendientes(l); setCargando(false); } }, () => !cancelado && setCargando(false)) + .then(fn => { if (cancelado) fn?.(); else limpiarPendientes = fn; }); + suscribirLocales(l => !cancelado && setPublicados(l), () => {}) + .then(fn => { if (cancelado) fn?.(); else limpiarPublicados = fn; }); + obtenerPalabrasFiltradas().then(p => !cancelado && setPalabras(p)); + return () => { cancelado = true; limpiarPendientes?.(); limpiarPublicados?.(); }; }, []); const aprobar = async (p) => { setBusy(true); try { await aprobarPropuesta(p); } catch(e) { alert("Error: "+e.message); } setBusy(false); }; @@ -214,7 +212,7 @@ function PanelAdmin({ email }) {
{l.nombre} - {CATEGORIAS[l.categoria] && {CATEGORIAS[l.categoria].emoji} {l.categoria}} + {l.categoria && {obtenerCategoria(l.categoria).emoji} {l.categoria}} 📍 {l.provincia} · {new Date(l.fecha).toLocaleDateString("es-ES",{day:"numeric",month:"short",year:"numeric"})}
{l.direccion &&

{l.direccion}

} diff --git a/src/App.jsx b/src/App.jsx index d475524..7862c13 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,266 +1,18 @@ -import { useState, useEffect, useRef, useCallback } from "react"; -import { suscribirLocales, enviarPropuesta, obtenerPalabrasFiltradas, contienepalabrasProhibidas } from "./firebase.js"; +import { useEffect, useRef, useState } from "react"; +import { contienepalabrasProhibidas, enviarPropuesta, obtenerPalabrasFiltradas, suscribirLocales } from "./firebase.js"; +import { CATEGORIAS, COLORES_MAPA, NOMBRES_CATEGORIAS, obtenerCategoria } from "./constants/categorias.js"; +import { COORDS_PROVINCIAS, PROVINCIAS, CENTRO_ESPANA } from "./constants/provincias.js"; +import { inputStyle } from "./styles/shared.js"; +import StarRating from "./components/StarRating.jsx"; +import LocalCard from "./components/LocalCard.jsx"; +import CampoUbicacion from "./components/CampoUbicacion.jsx"; +import BannerMovil from "./components/BannerMovil.jsx"; -const CATEGORIAS = { - "Restauración": { - emoji: "🍽️", color: "#8B0000", bgLight: "#FFF0F0", textColor: "#8B0000", - subcategorias: ["Tapas y raciones","Paella y arroces","Pintxos","Asador / Carne a la brasa","Mariscos y pescados","Bocadillos y montaditos","Menú del día","Cocina vasca","Cocina catalana","Cocina andaluza","Cocina gallega","Cocina madrileña","Cocina mediterránea","Pizzería","Hamburguesería","Comida rápida","Cocina internacional","Cafetería / Desayunos","Heladería","Pastelería"] - }, - "Pequeño comercio": { - emoji: "🛍️", color: "#1A5276", bgLight: "#EBF5FB", textColor: "#1A5276", - subcategorias: ["Alimentación / Ultramarinos","Frutería","Carnicería","Pescadería","Panadería","Farmacia","Papelería / Librería","Floristería","Joyería / Relojería","Zapatería","Ropa y moda","Juguetería","Ferretería","Bazar / Todo a 100","Estanco","Quiosco","Óptica","Ortopedia","Tienda de mascotas","Electrodomésticos"] - }, - "Peluquería y estética": { - emoji: "✂️", color: "#76448A", bgLight: "#F5EEF8", textColor: "#76448A", - subcategorias: ["Peluquería señora","Peluquería caballero","Peluquería unisex","Barbería","Centro de estética","Uñas / Manicura","Depilación","Masajes y spa","Tatuajes y piercings","Centro de bronceado","Micropigmentación"] - }, - "Servicios del hogar": { - emoji: "🔧", color: "#1E8449", bgLight: "#EAFAF1", textColor: "#1E8449", - subcategorias: ["Cerrajería","Fontanería / Plomería","Electricidad","Reformas y construcción","Pintura","Carpintería","Cristalería","Climatización / Aire acondicionado","Mudanzas","Limpieza","Jardinería","Instalación solar","Alarmas y seguridad","Reparación electrodomésticos"] - }, - "Otros": { - emoji: "📌", color: "#784212", bgLight: "#FDF2E9", textColor: "#784212", - subcategorias: ["Taller mecánico","Lavado de coches","Academia / Clases","Gestoría / Asesoría","Inmobiliaria","Agencia de viajes","Fotografía","Informática / Reparación móviles","Copistería / Imprenta","Veterinaria","Gimnasio / Fitness","Centro médico / Clínica","Fisioterapia","Psicología","Lavandería","Tintorería","Otro"] - } +const FORM_VACIO = { + nombre: "", provincia: "", categoria: "", subcategoria: "", + direccion: "", enlaceGoogleMaps: "", lat: null, lng: null, + puntuacion: 0, descripcion: "", }; -const NOMBRES_CATEGORIAS = Object.keys(CATEGORIAS); - -const PROVINCIAS = [ - "Álava","Albacete","Alicante","Almería","Asturias","Ávila","Badajoz","Barcelona","Burgos","Cáceres", - "Cádiz","Cantabria","Castellón","Ciudad Real","Córdoba","Cuenca","Gerona","Granada","Guadalajara","Guipúzcoa", - "Huelva","Huesca","Islas Baleares","Jaén","La Coruña","La Rioja","Las Palmas","León","Lérida","Lugo", - "Madrid","Málaga","Murcia","Navarra","Orense","Palencia","Pontevedra","Salamanca","Santa Cruz de Tenerife","Segovia", - "Sevilla","Soria","Tarragona","Teruel","Toledo","Valencia","Valladolid","Vizcaya","Zamora","Zaragoza" -]; - -const COORDS_PROVINCIAS = { - "Álava":[42.8467,-2.6726],"Albacete":[38.9942,-1.8585],"Alicante":[38.3452,-0.4815], - "Almería":[36.834,-2.4637],"Asturias":[43.3614,-5.8593],"Ávila":[40.6566,-4.6814], - "Badajoz":[38.8794,-6.9707],"Barcelona":[41.3851,2.1734],"Burgos":[42.344,-3.6969], - "Cáceres":[39.4753,-6.3724],"Cádiz":[36.5271,-6.2886],"Cantabria":[43.4623,-3.8099], - "Castellón":[39.9864,-0.0513],"Ciudad Real":[38.9848,-3.9274],"Córdoba":[37.8882,-4.7794], - "Cuenca":[40.0704,-2.1374],"Gerona":[41.9794,2.8214],"Granada":[37.1773,-3.5986], - "Guadalajara":[40.6328,-3.1614],"Guipúzcoa":[43.3183,-1.9812],"Huelva":[37.2614,-6.9447], - "Huesca":[42.1401,-0.4089],"Islas Baleares":[39.5696,2.6502],"Jaén":[37.7796,-3.7849], - "La Coruña":[43.3623,-8.4115],"La Rioja":[42.4627,-2.4449],"Las Palmas":[28.1235,-15.4366], - "León":[42.5987,-5.5671],"Lérida":[41.6178,0.62],"Lugo":[43.0097,-7.5567], - "Madrid":[40.4168,-3.7038],"Málaga":[36.7213,-4.4214],"Murcia":[37.9922,-1.1307], - "Navarra":[42.8169,-1.6432],"Orense":[42.3362,-7.8639],"Palencia":[42.0097,-4.5288], - "Pontevedra":[42.4333,-8.65],"Salamanca":[40.9701,-5.6635],"Santa Cruz de Tenerife":[28.4636,-16.2518], - "Segovia":[40.9429,-4.1088],"Sevilla":[37.3891,-5.9845],"Soria":[41.764,-2.464], - "Tarragona":[41.1189,1.2445],"Teruel":[40.344,-1.1065],"Toledo":[39.8628,-4.0273], - "Valencia":[39.4699,-0.3763],"Valladolid":[41.6523,-4.7245],"Vizcaya":[43.263,-2.935], - "Zamora":[41.5034,-5.7446],"Zaragoza":[41.6488,-0.8891], -}; - -const COLORES_MAPA = { - "Restauración":"#8B0000","Pequeño comercio":"#1A5276", - "Peluquería y estética":"#76448A","Servicios del hogar":"#1E8449","Otros":"#784212", -}; - -function parsearEnlaceGoogleMaps(url) { - try { - let m = url.match(/@(-?\d+\.\d+),(-?\d+\.\d+)/); - if (m) return { lat: parseFloat(m[1]), lng: parseFloat(m[2]) }; - m = url.match(/!3d(-?\d+\.\d+)!4d(-?\d+\.\d+)/); - if (m) return { lat: parseFloat(m[1]), lng: parseFloat(m[2]) }; - m = url.match(/[?&]q=(-?\d+\.\d+),(-?\d+\.\d+)/); - if (m) return { lat: parseFloat(m[1]), lng: parseFloat(m[2]) }; - } catch {} - return null; -} - -async function geocodificarDireccion(direccion) { - const query = encodeURIComponent(direccion + ", España"); - const url = `https://nominatim.openstreetmap.org/search?q=${query}&format=json&limit=1&countrycodes=es`; - const res = await fetch(url, { headers: { "Accept-Language": "es", "User-Agent": "LocalesEspanoles/1.0" } }); - const data = await res.json(); - if (data?.length) return { lat: parseFloat(data[0].lat), lng: parseFloat(data[0].lon), displayName: data[0].display_name }; - return null; -} - -function StarRating({ value, onChange, readOnly = false }) { - const [hover, setHover] = useState(0); - return ( -
- {[1,2,3,4,5].map(star => ( - !readOnly && onChange && onChange(star)} - onMouseEnter={() => !readOnly && setHover(star)} - onMouseLeave={() => !readOnly && setHover(0)} - style={{ fontSize: 22, cursor: readOnly ? "default" : "pointer", color: star <= (hover || value) ? "#D4AF37" : "#ccc", transition: "color 0.15s", userSelect: "none" }} - >★ - ))} -
- ); -} - -function CategoriaBadge({ categoria, subcategoria }) { - const cat = CATEGORIAS[categoria]; - if (!cat) return null; - return ( -
- - {cat.emoji} {categoria} - - {subcategoria && ( - - {subcategoria} - - )} -
- ); -} - -function LocalCard({ local }) { - const gmUrl = local.enlaceGoogleMaps || (local.direccion - ? `https://www.google.com/maps/search/${encodeURIComponent(local.nombre + " " + local.direccion)}` - : null); - - return ( -
e.currentTarget.style.borderColor = "var(--color-border-secondary)"} - onMouseLeave={e => e.currentTarget.style.borderColor = "var(--color-border-tertiary)"} - > -
-
-

{local.nombre}

-

{local.provincia}

-
-
- - {local.direccion && ( -
- - {local.direccion} -
- )} - {gmUrl && ( - - - Ver en Google Maps - - )} - - {local.descripcion && ( -

"{local.descripcion}"

- )} -

- Publicado {new Date(local.fecha).toLocaleDateString("es-ES", { day: "numeric", month: "long", year: "numeric" })} -

-
- ); -} - -function CampoUbicacion({ form, setForm }) { - const [modoInput, setModoInput] = useState("direccion"); - const [geocodificando, setGeocodificando] = useState(false); - const [resultadoGeo, setResultadoGeo] = useState(null); - const debounceRef = useRef(null); - - const inputStyle = { - width: "100%", boxSizing: "border-box", padding: "8px 12px", - borderRadius: "var(--border-radius-md)", border: "0.5px solid var(--color-border-secondary)", - background: "var(--color-background-primary)", color: "var(--color-text-primary)", fontSize: 14, outline: "none" - }; - - const handleDireccionChange = (val) => { - setForm(f => ({ ...f, direccion: val, lat: null, lng: null, enlaceGoogleMaps: "" })); - setResultadoGeo(null); - clearTimeout(debounceRef.current); - if (val.trim().length < 8) return; - debounceRef.current = setTimeout(async () => { - setGeocodificando(true); - try { - const geo = await geocodificarDireccion(val); - if (geo) { - setForm(f => ({ ...f, lat: geo.lat, lng: geo.lng })); - setResultadoGeo({ ok: true, texto: geo.displayName }); - } else { - setResultadoGeo({ ok: false, texto: "No se encontró la dirección. Se usará el centro de la provincia." }); - } - } catch { - setResultadoGeo({ ok: false, texto: "Error al geocodificar. Se usará el centro de la provincia." }); - } - setGeocodificando(false); - }, 900); - }; - - const handleEnlaceChange = (val) => { - setForm(f => ({ ...f, enlaceGoogleMaps: val, lat: null, lng: null, direccion: "" })); - setResultadoGeo(null); - if (!val.trim()) return; - const coords = parsearEnlaceGoogleMaps(val); - if (coords) { - setForm(f => ({ ...f, lat: coords.lat, lng: coords.lng })); - setResultadoGeo({ ok: true, texto: `Coordenadas detectadas: ${coords.lat.toFixed(5)}, ${coords.lng.toFixed(5)}` }); - } else { - setResultadoGeo({ ok: false, texto: "No se pudieron extraer coordenadas. Copia la URL completa desde Google Maps." }); - } - }; - - return ( -
-
- -
- {[{ id: "direccion", label: "📍 Dirección" }, { id: "enlace", label: "🔗 Google Maps" }].map(opt => ( - - ))} -
-
- {modoInput === "direccion" ? ( - handleDireccionChange(e.target.value)} /> - ) : ( - handleEnlaceChange(e.target.value)} /> - )} - {geocodificando && ( -
- Buscando dirección... -
- )} - {resultadoGeo && !geocodificando && ( -
- - {resultadoGeo.texto} -
- )} -
- ); -} - -// ── Banner descarga app móvil ───────────────────────────────────────────────── -function BannerMovil() { - const [visible, setVisible] = useState(true); - if (!visible) return null; - return ( -
- -
- 📱 -
-

App para móvil

-

Sincronizada en tiempo real

-
-
- - 📲 Abrir app móvil - -
- ); -} // ── App principal ───────────────────────────────────────────────────────────── export default function App() { @@ -281,28 +33,29 @@ export default function App() { const mapInstanceRef = useRef(null); const markersRef = useRef([]); - const [form, setForm] = useState({ - nombre: "", provincia: "", categoria: "", subcategoria: "", - direccion: "", enlaceGoogleMaps: "", lat: null, lng: null, - puntuacion: 0, descripcion: "" - }); + const [form, setForm] = useState(FORM_VACIO); - // Carga locales aprobados en tiempo real + // Carga locales aprobados en tiempo real. + // suscribirLocales es async (hace fetch antes de devolver la función de + // limpieza), así que su valor de retorno inmediato es una Promise, no la + // función en sí: hay que esperarla antes de poder cancelar el intervalo. useEffect(() => { setLoading(true); - const unsub = suscribirLocales( - lista => { setLocales(lista); setLoading(false); }, - () => { setLocales([]); setLoading(false); } - ); - return () => unsub(); + let cancelado = false; + let limpiar; + suscribirLocales( + (lista) => { if (!cancelado) { setLocales(lista); setLoading(false); } }, + () => { if (!cancelado) { setLocales([]); setLoading(false); } }, + ).then((fn) => { if (cancelado) fn?.(); else limpiar = fn; }); + return () => { cancelado = true; limpiar?.(); }; }, []); - // Carga filtro de palabras desde Firestore + // Carga filtro de palabras desde el backend useEffect(() => { - obtenerPalabrasFiltradas().then(p => setPalabrasProhibidas(p)); + obtenerPalabrasFiltradas().then((p) => setPalabrasProhibidas(p)); }, []); - const resetForm = () => setForm({ nombre: "", provincia: "", categoria: "", subcategoria: "", direccion: "", enlaceGoogleMaps: "", lat: null, lng: null, puntuacion: 0, descripcion: "" }); + const resetForm = () => setForm(FORM_VACIO); const enviarLocal = async () => { setErrorFiltro(""); @@ -313,11 +66,11 @@ export default function App() { return; } - let lat = form.lat, lng = form.lng; + let { lat, lng } = form; if (!lat || !lng) { - const fb = COORDS_PROVINCIAS[form.provincia] || [40.4, -3.7]; - lat = fb[0] + (Math.random() - 0.5) * 0.04; - lng = fb[1] + (Math.random() - 0.5) * 0.04; + const fallback = COORDS_PROVINCIAS[form.provincia] || CENTRO_ESPANA; + lat = fallback[0] + (Math.random() - 0.5) * 0.04; + lng = fallback[1] + (Math.random() - 0.5) * 0.04; } setSaving(true); @@ -333,16 +86,18 @@ export default function App() { resetForm(); setMostrarForm(false); setTimeout(() => setEnviado(false), 5000); - } catch(e) { console.error(e); } + } catch (e) { + console.error(e); + } setSaving(false); }; const subcatsFiltro = filtroCategoria ? CATEGORIAS[filtroCategoria]?.subcategorias || [] : []; const localesFiltrados = locales - .filter(l => { + .filter((l) => { const q = busqueda.toLowerCase(); - return (!q || l.nombre.toLowerCase().includes(q) || l.provincia.toLowerCase().includes(q) || (l.categoria||"").toLowerCase().includes(q) || (l.subcategoria||"").toLowerCase().includes(q) || (l.direccion||"").toLowerCase().includes(q)) + return (!q || l.nombre.toLowerCase().includes(q) || l.provincia.toLowerCase().includes(q) || (l.categoria || "").toLowerCase().includes(q) || (l.subcategoria || "").toLowerCase().includes(q) || (l.direccion || "").toLowerCase().includes(q)) && (!filtroCategoria || l.categoria === filtroCategoria) && (!filtroSubcat || l.subcategoria === filtroSubcat) && (!filtroProvincia || l.provincia === filtroProvincia); @@ -359,9 +114,17 @@ export default function App() { const timer = setTimeout(() => { if (!mapRef.current) return; if (!window.L) { - const link = document.createElement("link"); link.rel = "stylesheet"; link.href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"; document.head.appendChild(link); - const script = document.createElement("script"); script.src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"; script.onload = () => initMap(); document.head.appendChild(script); - } else { initMap(); } + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"; + document.head.appendChild(link); + const script = document.createElement("script"); + script.src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"; + script.onload = () => initMap(); + document.head.appendChild(script); + } else { + initMap(); + } }, 100); return () => clearTimeout(timer); }, [vista]); @@ -383,44 +146,47 @@ export default function App() { const L = window.L; const map = mapObj || mapInstanceRef.current; if (!L || !map) return; - markersRef.current.forEach(m => m.remove()); + markersRef.current.forEach((m) => m.remove()); markersRef.current = []; - locales.forEach(local => { + locales.forEach((local) => { if (!local.lat || !local.lng) return; - const color = COLORES_MAPA[local.categoria] || "#555"; - const cat = CATEGORIAS[local.categoria]; - const emoji = cat?.emoji || "📌"; - const stars = "★".repeat(local.puntuacion) + "☆".repeat(5 - local.puntuacion); + const cat = obtenerCategoria(local.categoria); + const color = COLORES_MAPA[local.categoria] || cat.color; + const puntuacion = Math.min(5, Math.max(0, Math.round(Number(local.puntuacion) || 0))); + const stars = "★".repeat(puntuacion) + "☆".repeat(5 - puntuacion); const labelDir = local.direccion ? ` · ${local.direccion.split(",")[0]}` : ""; const marker = L.marker([local.lat, local.lng], { - icon: L.divIcon({ className: "", html: `
${emoji} ${local.nombre}${labelDir}
`, iconAnchor: [0, 32] }) + icon: L.divIcon({ className: "", html: `
${cat.emoji} ${local.nombre}${labelDir}
`, iconAnchor: [0, 32] }), }).addTo(map); - const gmLink = local.enlaceGoogleMaps || (local.direccion ? `https://www.google.com/maps/search/${encodeURIComponent(local.nombre + " " + local.direccion)}` : null); - marker.bindPopup(`
${local.nombre}
${local.provincia}
${emoji} ${local.categoria}${local.subcategoria?`${local.subcategoria}`:""}${local.direccion?`
📍 ${local.direccion}`:""}${gmLink?`
Ver en Google Maps ↗`:"" }
${stars}${local.descripcion?`"${local.descripcion}"`:""}
`); + const gmLink = local.enlaceGoogleMaps || (local.direccion ? `https://www.google.com/maps/search/${encodeURIComponent(`${local.nombre} ${local.direccion}`)}` : null); + marker.bindPopup(`
${local.nombre}
${local.provincia}
${cat.emoji} ${local.categoria}${local.subcategoria ? `${local.subcategoria}` : ""}${local.direccion ? `
📍 ${local.direccion}` : ""}${gmLink ? `
Ver en Google Maps ↗` : ""}
${stars}${local.descripcion ? `"${local.descripcion}"` : ""}
`); markersRef.current.push(marker); }); } - const conteosCat = NOMBRES_CATEGORIAS.reduce((acc, c) => { acc[c] = locales.filter(l => l.categoria === c).length; return acc; }, {}); - const provincias = [...new Set(locales.map(l => l.provincia))].sort(); + const conteosCat = NOMBRES_CATEGORIAS.reduce((acc, c) => { acc[c] = locales.filter((l) => l.categoria === c).length; return acc; }, {}); + const provincias = [...new Set(locales.map((l) => l.provincia))].sort(); const mediaGlobal = locales.length ? (locales.reduce((s, l) => s + l.puntuacion, 0) / locales.length).toFixed(1) : "—"; - const inputStyle = { width: "100%", boxSizing: "border-box", padding: "8px 12px", borderRadius: "var(--border-radius-md)", border: "0.5px solid var(--color-border-secondary)", background: "var(--color-background-primary)", color: "var(--color-text-primary)", fontSize: 14, outline: "none" }; const tieneUbicacion = !!(form.direccion || form.enlaceGoogleMaps); const formValido = form.nombre && form.provincia && form.categoria && form.puntuacion > 0 && tieneUbicacion; return (
-

Directorio de locales españoles

- {/* Header */}
-
- 🇪🇸 -

Locales Españoles

-
-

Directorio colaborativo de negocios por toda España

+

+ LocalESP · Locales Españoles +

+

Directorio colaborativo de negocios por toda España

@@ -473,45 +240,45 @@ export default function App() {
- - setForm(f => ({ ...f, nombre: e.target.value }))} /> + + setForm((f) => ({ ...f, nombre: e.target.value }))} />
- - setForm((f) => ({ ...f, provincia: e.target.value }))}> - {PROVINCIAS.map(p => )} + {PROVINCIAS.map((p) => )}
- - setForm((f) => ({ ...f, categoria: e.target.value, subcategoria: "" }))}> - {NOMBRES_CATEGORIAS.map(c => )} + {NOMBRES_CATEGORIAS.map((c) => )}
- - setForm((f) => ({ ...f, subcategoria: e.target.value }))} disabled={!form.categoria}> - {form.categoria && CATEGORIAS[form.categoria]?.subcategorias.map(s => )} + {form.categoria && CATEGORIAS[form.categoria]?.subcategorias.map((s) => )}
- -