Migración de esquema DB y soporte PWA:
deploy-branch / deploy (push) Successful in 2m16s
deploy-branch / teardown (push) Skipped

- Mapeo campos en base de datos: ubicacion→provincia, telefono→subcategoria,
  email→direccion, web→enlaceGoogleMaps y añadido puntuacion.
- Migración suave (ensureColumn) para añadir las nuevas columnas sin perder
  datos existentes si la DB ya existía con el esquema antiguo.

- Actualización de rutas del servidor para guardar/leer los nuevos campos y
  convertir el NULL a valor nulo por defecto en puntuacion.

- Soporte PWA: añadido meta tags (manifest, theme-color, apple-touch-icon) y
  registro de service worker para instalación del app y shell offline.
This commit is contained in:
2026-08-01 19:27:57 +02:00
parent d1fec59dcb
commit a3fd928073
5 changed files with 57 additions and 19 deletions
+8
View File
@@ -7,6 +7,14 @@
<meta name="description" content="Directorio colaborativo de negocios por toda España" /> <meta name="description" content="Directorio colaborativo de negocios por toda España" />
<!-- No indexar el admin --> <!-- No indexar el admin -->
<meta name="robots" content="index, follow" /> <meta name="robots" content="index, follow" />
<!-- PWA -->
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#8B0000" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="LocalESP" />
<!-- Tabler Icons --> <!-- Tabler Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3.11.0/dist/tabler-icons.min.css" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3.11.0/dist/tabler-icons.min.css" />
<style> <style>
+2 -1
View File
@@ -2,7 +2,8 @@
command = "npm run build" command = "npm run build"
publish = "dist" publish = "dist"
# SPA: todas las rutas van al index.html (React gestiona /admin y /movil) # SPA: todas las rutas van al index.html (React gestiona /admin; el resto es
# la PWA principal, instalable en móvil)
[[redirects]] [[redirects]]
from = "/*" from = "/*"
to = "/index.html" to = "/index.html"
+29 -8
View File
@@ -11,10 +11,11 @@ db.exec(`
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
nombre TEXT NOT NULL, nombre TEXT NOT NULL,
descripcion TEXT, descripcion TEXT,
ubicacion TEXT, provincia TEXT,
telefono TEXT, subcategoria TEXT,
email TEXT, direccion TEXT,
web TEXT, enlaceGoogleMaps TEXT,
puntuacion INTEGER DEFAULT 0,
categoria TEXT, categoria TEXT,
fecha TEXT DEFAULT CURRENT_TIMESTAMP, fecha TEXT DEFAULT CURRENT_TIMESTAMP,
lat REAL, lat REAL,
@@ -25,10 +26,11 @@ db.exec(`
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
nombre TEXT NOT NULL, nombre TEXT NOT NULL,
descripcion TEXT, descripcion TEXT,
ubicacion TEXT, provincia TEXT,
telefono TEXT, subcategoria TEXT,
email TEXT, direccion TEXT,
web TEXT, enlaceGoogleMaps TEXT,
puntuacion INTEGER DEFAULT 0,
categoria TEXT, categoria TEXT,
estado TEXT DEFAULT 'pendiente', estado TEXT DEFAULT 'pendiente',
fechaPropuesta TEXT DEFAULT CURRENT_TIMESTAMP, fechaPropuesta TEXT DEFAULT CURRENT_TIMESTAMP,
@@ -49,4 +51,23 @@ db.exec(`
); );
`); `);
// Migración suave: si la base de datos ya existía con el esquema antiguo
// (sin provincia/subcategoria/direccion/enlaceGoogleMaps/puntuacion),
// añadimos las columnas que falten sin borrar nada, porque
// CREATE TABLE IF NOT EXISTS no modifica tablas ya creadas.
function ensureColumn(table, column, definition) {
const cols = db.prepare(`PRAGMA table_info(${table})`).all().map(c => c.name);
if (!cols.includes(column)) {
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
}
}
for (const table of ["locales", "pendientes"]) {
ensureColumn(table, "provincia", "TEXT");
ensureColumn(table, "subcategoria", "TEXT");
ensureColumn(table, "direccion", "TEXT");
ensureColumn(table, "enlaceGoogleMaps", "TEXT");
ensureColumn(table, "puntuacion", "INTEGER DEFAULT 0");
}
export default db; export default db;
+8 -8
View File
@@ -24,16 +24,16 @@ app.get("/api/pendientes", (_req, res) => {
// Enviar propuesta // Enviar propuesta
app.post("/api/propuestas", (req, res) => { app.post("/api/propuestas", (req, res) => {
const { nombre, descripcion, ubicacion, telefono, email, web, categoria, lat, lng } = req.body; const { nombre, descripcion, provincia, subcategoria, direccion, enlaceGoogleMaps, puntuacion, categoria, lat, lng } = req.body;
const id = randomUUID(); const id = randomUUID();
const fechaPropuesta = new Date().toISOString(); const fechaPropuesta = new Date().toISOString();
db.prepare(` db.prepare(`
INSERT INTO pendientes (id, nombre, descripcion, ubicacion, telefono, email, web, categoria, estado, fechaPropuesta, lat, lng) INSERT INTO pendientes (id, nombre, descripcion, provincia, subcategoria, direccion, enlaceGoogleMaps, puntuacion, categoria, estado, fechaPropuesta, lat, lng)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pendiente', ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pendiente', ?, ?, ?)
`).run(id, nombre, descripcion, ubicacion, telefono, email, web, categoria, fechaPropuesta, lat, lng); `).run(id, nombre, descripcion, provincia, subcategoria, direccion, enlaceGoogleMaps, puntuacion ?? 0, categoria, fechaPropuesta, lat, lng);
res.status(201).json({ id, nombre, descripcion, ubicacion, telefono, email, web, categoria, estado: "pendiente", fechaPropuesta, lat, lng }); res.status(201).json({ id, nombre, descripcion, provincia, subcategoria, direccion, enlaceGoogleMaps, puntuacion: puntuacion ?? 0, categoria, estado: "pendiente", fechaPropuesta, lat, lng });
}); });
// Aprobar propuesta // Aprobar propuesta
@@ -47,9 +47,9 @@ app.post("/api/aprobar/:id", (req, res) => {
const fecha = new Date().toISOString(); const fecha = new Date().toISOString();
db.prepare(` db.prepare(`
INSERT INTO locales (id, nombre, descripcion, ubicacion, telefono, email, web, categoria, fecha, lat, lng) INSERT INTO locales (id, nombre, descripcion, provincia, subcategoria, direccion, enlaceGoogleMaps, puntuacion, categoria, fecha, lat, lng)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(localeId, propuesta.nombre, propuesta.descripcion, propuesta.ubicacion, propuesta.telefono, propuesta.email, propuesta.web, propuesta.categoria, fecha, propuesta.lat, propuesta.lng); `).run(localeId, propuesta.nombre, propuesta.descripcion, propuesta.provincia, propuesta.subcategoria, propuesta.direccion, propuesta.enlaceGoogleMaps, propuesta.puntuacion ?? 0, propuesta.categoria, fecha, propuesta.lat, propuesta.lng);
db.prepare("DELETE FROM pendientes WHERE id = ?").run(id); db.prepare("DELETE FROM pendientes WHERE id = ?").run(id);
+10 -2
View File
@@ -1,7 +1,6 @@
import { StrictMode } from "react"; import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import App from "./App.jsx"; import App from "./App.jsx";
import AppMovil from "./AppMovil.jsx";
import AdminPanel from "./AdminPanel.jsx"; import AdminPanel from "./AdminPanel.jsx";
// Enrutador mínimo sin dependencias extra // Enrutador mínimo sin dependencias extra
@@ -9,10 +8,19 @@ const ruta = window.location.pathname.replace(/\/$/, "");
function Root() { function Root() {
if (ruta === "/admin") return <AdminPanel />; if (ruta === "/admin") return <AdminPanel />;
if (ruta === "/movil") return <AppMovil />;
return <App />; return <App />;
} }
createRoot(document.getElementById("root")).render( createRoot(document.getElementById("root")).render(
<StrictMode><Root /></StrictMode> <StrictMode><Root /></StrictMode>
); );
// Registro del service worker: convierte la web en una PWA instalable
// (icono en pantalla de inicio, apertura en modo standalone, shell offline).
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker.register("/sw.js").catch((err) => {
console.error("Error registrando el service worker:", err);
});
});
}