Prototipo YB01 #1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user