aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/routes
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2026-03-16 13:56:06 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2026-03-16 13:56:15 +0100
commit8942cf3c705bbc78a6b3317599658e9bb86dd31b (patch)
treec02c432dad7b31fa11160f16c221dfac45255920 /src/frontend/app/routes
parent3ce586243a49f34b36d0fe4099bbfb2631610f11 (diff)
Add legal document shenanigans
Closes #147
Diffstat (limited to 'src/frontend/app/routes')
-rw-r--r--src/frontend/app/routes/privacy.tsx375
-rw-r--r--src/frontend/app/routes/settings.tsx82
2 files changed, 456 insertions, 1 deletions
diff --git a/src/frontend/app/routes/privacy.tsx b/src/frontend/app/routes/privacy.tsx
new file mode 100644
index 0000000..2998223
--- /dev/null
+++ b/src/frontend/app/routes/privacy.tsx
@@ -0,0 +1,375 @@
+import { usePageTitle } from "~/contexts/PageTitleContext";
+import "../tailwind-full.css";
+
+export default function Privacy() {
+ usePageTitle("Política de privacidad");
+
+ return (
+ <div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
+ <h1 className="text-3xl font-bold mb-2 text-text">
+ Política de privacidad
+ </h1>
+ <p className="text-sm text-muted mb-8">
+ Última actualización: 16 de marzo de 2026
+ </p>
+
+ {/* 1. Responsable */}
+ <section className="mb-8">
+ <h2 className="text-2xl font-semibold mb-4 border-b border-border pb-2 text-text">
+ 1. Responsable del tratamiento
+ </h2>
+ <p className="text-text opacity-90 leading-relaxed">
+ El responsable del tratamiento de los datos personales recogidos a
+ través de esta aplicación es:
+ </p>
+ <ul className="mt-3 space-y-1 text-text opacity-90 list-none ml-0">
+ <li>
+ <strong>Nombre:</strong> Ariel Costas Guerrero
+ </li>
+ <li>
+ <strong>Correo de contacto:</strong>{" "}
+ <a
+ href="mailto:privacidad@enmarcha.app"
+ className="text-blue-600 dark:text-blue-400 hover:underline"
+ >
+ privacidad@enmarcha.app
+ </a>
+ </li>
+ </ul>
+ </section>
+
+ {/* 2. Datos tratados */}
+ <section className="mb-8">
+ <h2 className="text-2xl font-semibold mb-4 border-b border-border pb-2 text-text">
+ 2. Datos que recogemos y por qué
+ </h2>
+ <p className="text-text opacity-90 mb-4">
+ Esta aplicación es un servicio de consulta de transporte público. No
+ se crean cuentas de usuario ni se realiza ningún tipo de seguimiento
+ publicitario.
+ </p>
+
+ <div className="overflow-x-auto">
+ <table className="w-full text-sm text-text border-collapse bg-surface rounded shadow overflow-hidden">
+ <thead>
+ <tr className="border-b border-border">
+ <th className="text-left p-3 font-semibold">Actividad</th>
+ <th className="text-left p-3 font-semibold">Datos</th>
+ <th className="text-left p-3 font-semibold">Base jurídica</th>
+ <th className="text-left p-3 font-semibold">Conservación</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr className="border-b border-border">
+ <td className="p-3">Consulta de llegadas a una parada</td>
+ <td className="p-3">Código de parada (no personal)</td>
+ <td className="p-3">
+ Interés legítimo (prestación del servicio)
+ </td>
+ <td className="p-3">No se almacena</td>
+ </tr>
+ <tr className="border-b border-border bg-surface/40">
+ <td className="p-3">Planificación de rutas</td>
+ <td className="p-3">
+ Coordenadas de origen y destino, hora deseada
+ </td>
+ <td className="p-3">
+ Interés legítimo (prestación del servicio)
+ </td>
+ <td className="p-3">2 horas (caché local en tu dispositivo)</td>
+ </tr>
+ <tr className="border-b border-border">
+ <td className="p-3">
+ Geocodificación / geocodificación inversa
+ </td>
+ <td className="p-3">Coordenadas o texto de búsqueda</td>
+ <td className="p-3">
+ Interés legítimo (prestación del servicio)
+ </td>
+ <td className="p-3">No se almacena en servidor</td>
+ </tr>
+ <tr className="border-b border-border bg-surface/40">
+ <td className="p-3">
+ Paradas favoritas y nombres personalizados
+ </td>
+ <td className="p-3">
+ Identificadores de parada, nombres que tú asignas
+ </td>
+ <td className="p-3">
+ Acción del usuario (guardado voluntario)
+ </td>
+ <td className="p-3">Hasta que los borres tú</td>
+ </tr>
+ <tr className="border-b border-border">
+ <td className="p-3">Paradas y lugares recientes</td>
+ <td className="p-3">
+ Códigos de parada, coordenadas de búsquedas recientes
+ </td>
+ <td className="p-3">Interés legítimo (facilitar el uso)</td>
+ <td className="p-3">Persistente hasta que los borres tú</td>
+ </tr>
+ <tr className="border-b border-border bg-surface/40">
+ <td className="p-3">Ubicaciones de casa y trabajo</td>
+ <td className="p-3">Dirección descriptiva y coordenadas</td>
+ <td className="p-3">
+ Acción del usuario (configurado voluntariamente)
+ </td>
+ <td className="p-3">Hasta que los borres tú</td>
+ </tr>
+ <tr className="border-b border-border">
+ <td className="p-3">Posición GPS del mapa</td>
+ <td className="p-3">Coordenadas GPS de tu dispositivo</td>
+ <td className="p-3">
+ Consentimiento (permiso de geolocalización del navegador)
+ </td>
+ <td className="p-3">
+ 30 días (caché local; se descarta automáticamente)
+ </td>
+ </tr>
+ <tr className="border-b border-border bg-surface/40">
+ <td className="p-3">Registros operativos del servidor</td>
+ <td className="p-3">
+ Dirección IP anonimizada, identificadores de parada/ruta
+ </td>
+ <td className="p-3">
+ Interés legítimo (operación y seguridad)
+ </td>
+ <td className="p-3">Rotación estándar de logs del servidor</td>
+ </tr>
+ <tr className="border-b border-border">
+ <td className="p-3">Telemetría de rendimiento</td>
+ <td className="p-3">
+ Identificadores de parada/ruta (sin coordenadas)
+ </td>
+ <td className="p-3">Interés legítimo (mejora del servicio)</td>
+ <td className="p-3">Según configuración del colector OTLP</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </section>
+
+ {/* 3. Geolocalización */}
+ <section className="mb-8">
+ <h2 className="text-2xl font-semibold mb-4 border-b border-border pb-2 text-text">
+ 3. Geolocalización
+ </h2>
+ <p className="text-text opacity-90 leading-relaxed">
+ La aplicación puede solicitar acceso a tu ubicación GPS a través del
+ permiso de geolocalización del navegador. Este acceso es opcional: si
+ lo denegas, la aplicación seguirá funcionando sin centrar el mapa en
+ tu posición.
+ </p>
+ <p className="mt-3 text-text opacity-90 leading-relaxed">
+ Tus coordenadas <strong>no se transmiten al servidor</strong> salvo
+ cuando usas el planificador de rutas para calcular un trayecto desde
+ tu ubicación actual. En ese caso, las coordenadas se envían al
+ servidor únicamente para procesar la consulta y no se almacenan.
+ </p>
+ <p className="mt-3 text-text opacity-90 leading-relaxed">
+ La posición guardada localmente en tu dispositivo se descarta
+ automáticamente tras 30 días.
+ </p>
+ </section>
+
+ {/* 4. Almacenamiento local */}
+ <section className="mb-8">
+ <h2 className="text-2xl font-semibold mb-4 border-b border-border pb-2 text-text">
+ 4. Almacenamiento local (localStorage)
+ </h2>
+ <p className="text-text opacity-90 leading-relaxed">
+ Esta aplicación utiliza el almacenamiento local del navegador (
+ <code className="bg-surface px-1 rounded text-sm">localStorage</code>)
+ para guardar tus preferencias, favoritos e historial de uso. Este
+ almacenamiento <strong>nunca se comparte con terceros</strong> y es de
+ carácter estrictamente funcional, no de seguimiento ni publicidad.
+ </p>
+ <p className="mt-3 text-text opacity-90 leading-relaxed">
+ No se usan <em>cookies</em> de sesión ni de rastreo. La única
+ excepción es la preferencia de idioma, que puede almacenarse tanto en{" "}
+ <code className="bg-surface px-1 rounded text-sm">localStorage</code>{" "}
+ como en una cookie de primer nivel para mantener la selección entre
+ visitas.
+ </p>
+ <p className="mt-3 text-text opacity-90 leading-relaxed">
+ Puedes eliminar todos los datos personales guardados localmente en
+ cualquier momento desde{" "}
+ <strong>
+ Ajustes → Privacidad y datos → Borrar mis datos guardados
+ </strong>
+ .
+ </p>
+ </section>
+
+ {/* 5. Encargados del tratamiento */}
+ <section className="mb-8">
+ <h2 className="text-2xl font-semibold mb-4 border-b border-border pb-2 text-text">
+ 5. Encargados y destinatarios del tratamiento
+ </h2>
+ <p className="text-text opacity-90 mb-4">
+ Para prestar el servicio, algunas de tus consultas se transmiten a los
+ siguientes terceros. En ningún caso se transfiere información que
+ permita identificarte personalmente como usuario registrado, ya que la
+ aplicación no tiene sistema de cuentas.
+ </p>
+ <ul className="space-y-3 text-text opacity-90 list-disc ml-5">
+ <li>
+ <strong>Geoapify</strong> (api.geoapify.com) — buscador de
+ direcciones y geocodificación inversa. Recibe el texto de búsqueda o
+ las coordenadas de la consulta. Política de privacidad:{" "}
+ <a
+ href="https://www.geoapify.com/privacy-policy"
+ className="text-blue-600 dark:text-blue-400 hover:underline"
+ rel="nofollow noreferrer noopener"
+ target="_blank"
+ >
+ geoapify.com/privacy-policy
+ </a>
+ </li>
+ <li>
+ <strong>OpenTripPlanner</strong> — motor de planificación de rutas.
+ Recibe las coordenadas de origen y destino y la hora del viaje.
+ </li>
+ <li>
+ <strong>Vitrasa / Concello de Vigo</strong> (datos.vigo.org) — datos
+ de llegadas en tiempo real para Vigo. Solo recibe el código numérico
+ de la parada, y un resultado puede ser utilizado para varios
+ usuarios, de modo que no se asocia a un individuo concreto.
+ </li>
+ <li>
+ <strong>TUSSA</strong> (app.tussa.org) — datos de llegadas en tiempo
+ real para Santiago de Compostela. Solo recibe el código de la
+ parada, y un resultado puede ser utilizado para varios usuarios, de
+ modo que no se asocia a un individuo concreto.
+ </li>
+ <li>
+ <strong>Tranvías de A Coruña</strong> (itranvias.com) — datos de
+ llegadas en tiempo real para A Coruña. Solo recibe el código de la
+ parada, y un resultado puede ser utilizado para varios usuarios, de
+ modo que no se asocia a un individuo concreto.
+ </li>
+ <li>
+ <strong>Renfe GTFS-Realtime</strong> — feed público de posiciones de
+ trenes. No se envían datos del usuario.
+ </li>
+ </ul>
+ </section>
+
+ {/* 6. Derechos */}
+ <section className="mb-8">
+ <h2 className="text-2xl font-semibold mb-4 border-b border-border pb-2 text-text">
+ 6. Tus derechos
+ </h2>
+ <p className="text-text opacity-90 mb-3 leading-relaxed">
+ De acuerdo con el Reglamento General de Protección de Datos (RGPD) y
+ la Ley Orgánica 3/2018 de Protección de Datos Personales y garantía de
+ los derechos digitales (LOPDGDD), tienes derecho a:
+ </p>
+ <ul className="space-y-2 text-text opacity-90 list-disc ml-5">
+ <li>
+ <strong>Acceso</strong>: conocer qué datos tratamos sobre ti (los
+ datos que mencionamos previamente).
+ </li>
+ <li>
+ <strong>Rectificación</strong>: solicitar la corrección de datos
+ inexactos (puedes hacerlo a través de la aplicación).
+ </li>
+ <li>
+ <strong>Supresión</strong>: solicitar la eliminación de tus datos
+ («derecho al olvido»). Los datos guardados en el dispositivo los
+ debes eliminar manualmente, los datos en servidores se eliminan tras
+ el periodo de consevación indicado, y no se pueden eliminar
+ previamente por no ser asociados a un usuario concreto.
+ </li>
+ <li>
+ <strong>Oposición</strong>: oponerte al tratamiento basado en
+ interés legítimo. Dado que el tratamiento de datos en esta
+ aplicación se basa principalmente en el interés legítimo para
+ prestar el servicio, no es posible ejercer este derecho sin dejar de
+ usar la aplicación, o las funciones que implican este tratamiento
+ (planificador de rutas con "ubicación actual", por ejemplo).
+ </li>
+ <li>
+ <strong>Portabilidad</strong>: recibir tus datos en un formato
+ estructurado. Los datos están exclusivamente en el dispositivo,
+ basta con acceder mediante las herramientas de desarrollo del
+ navegador para copiarlos, o visualizarlos en la aplicación.
+ </li>
+ <li>
+ <strong>Limitación</strong>: solicitar que restrinjamos el
+ tratamiento de tus datos. Dado que la aplicación no almacena datos
+ personales en servidores de forma persistente, este derecho se puede
+ ejercer eliminando los datos desde la propia aplicación o dejando de
+ usar las funciones que implican el tratamiento.
+ </li>
+ </ul>
+ <p className="mt-4 text-text opacity-90 leading-relaxed">
+ Dado que la aplicación no almacena datos personales en ningún servidor
+ de forma persistente (todo lo personal reside en tu propio
+ dispositivo), la mayoría de estos derechos los puedes ejercer
+ directamente borrando los datos desde la propia aplicación (ver
+ sección 4).
+ </p>
+ <p className="mt-3 text-text opacity-90 leading-relaxed">
+ Para cualquier consulta o solicitud formal, puedes dirigirte a{" "}
+ <a
+ href="mailto:privacidad@enmarcha.app"
+ className="text-blue-600 dark:text-blue-400 hover:underline"
+ >
+ privacidad@enmarcha.app
+ </a>
+ .
+ </p>
+ </section>
+
+ {/* 7. AEPD */}
+ <section className="mb-8">
+ <h2 className="text-2xl font-semibold mb-4 border-b border-border pb-2 text-text">
+ 7. Derecho a reclamar ante la autoridad de control
+ </h2>
+ <p className="text-text opacity-90 leading-relaxed">
+ Si consideras que el tratamiento de tus datos no se ajusta a la
+ normativa vigente, puedes presentar una reclamación ante la{" "}
+ <strong>Agencia Española de Protección de Datos (AEPD)</strong>:
+ </p>
+ <ul className="mt-3 space-y-1 text-text opacity-90 list-none ml-0">
+ <li>
+ Web:{" "}
+ <a
+ href="https://www.aepd.es"
+ className="text-blue-600 dark:text-blue-400 hover:underline"
+ rel="nofollow noreferrer noopener"
+ target="_blank"
+ >
+ www.aepd.es
+ </a>
+ </li>
+ <li>
+ Sede electrónica:{" "}
+ <a
+ href="https://sedeagpd.gob.es"
+ className="text-blue-600 dark:text-blue-400 hover:underline"
+ rel="nofollow noreferrer noopener"
+ target="_blank"
+ >
+ sedeagpd.gob.es
+ </a>
+ </li>
+ </ul>
+ </section>
+
+ {/* 8. Cambios */}
+ <section className="mb-8">
+ <h2 className="text-2xl font-semibold mb-4 border-b border-border pb-2 text-text">
+ 8. Cambios en esta política
+ </h2>
+ <p className="text-text opacity-90 leading-relaxed">
+ Esta política puede actualizarse para reflejar cambios en la
+ aplicación o en la normativa aplicable. La fecha de «Última
+ actualización» indicada en la cabecera refleja la versión vigente. Te
+ recomendamos revisarla periódicamente.
+ </p>
+ </section>
+ </div>
+ );
+}
diff --git a/src/frontend/app/routes/settings.tsx b/src/frontend/app/routes/settings.tsx
index e7fdffa..0497f34 100644
--- a/src/frontend/app/routes/settings.tsx
+++ b/src/frontend/app/routes/settings.tsx
@@ -1,5 +1,7 @@
-import { Computer, Moon, Sun } from "lucide-react";
+import { Computer, Moon, Sun, Trash2 } from "lucide-react";
+import { useState } from "react";
import { useTranslation } from "react-i18next";
+import { Link } from "react-router";
import { usePageTitle } from "~/contexts/PageTitleContext";
import { useApp, type Theme } from "../AppContext";
import "../tailwind-full.css";
@@ -7,6 +9,8 @@ import "../tailwind-full.css";
export default function Settings() {
const { t, i18n } = useTranslation();
usePageTitle(t("navbar.settings", "Ajustes"));
+ const [showClearConfirm, setShowClearConfirm] = useState(false);
+ const [cleared, setCleared] = useState(false);
const {
theme,
setTheme,
@@ -192,6 +196,82 @@ export default function Settings() {
<option value="en-GB">English</option>
</select>
</section>
+
+ {/* Privacy / Clear data */}
+ <section className="mt-8 pt-8 border-t border-border">
+ <h2 className="text-xl font-semibold mb-4 text-text">
+ {t("settings.privacy_title", "Privacidad y datos")}
+ </h2>
+ {!showClearConfirm && !cleared && (
+ <button
+ onClick={() => setShowClearConfirm(true)}
+ className="flex items-center gap-2 px-4 py-3 rounded-lg border border-border
+ bg-surface text-text hover:bg-red-50 hover:border-red-300 hover:text-red-700
+ dark:hover:bg-red-950 dark:hover:border-red-700 dark:hover:text-red-400
+ transition-colors duration-200 focus:outline-none focus:ring focus:ring-red-300"
+ >
+ <Trash2 className="w-5 h-5" />
+ {t("settings.clear_data", "Borrar mis datos guardados")}
+ </button>
+ )}
+ {showClearConfirm && (
+ <div className="p-4 rounded-lg border border-red-300 bg-red-50 dark:bg-red-950 dark:border-red-700">
+ <p className="text-sm text-text mb-4">
+ {t(
+ "settings.clear_data_confirm",
+ "Se eliminarán tus paradas favoritas, nombres personalizados, paradas recientes, historial de rutas, lugares guardados y posición del mapa. Las preferencias de ajustes se conservarán."
+ )}
+ </p>
+ <div className="flex gap-3">
+ <button
+ onClick={() => {
+ const personalKeys = [
+ "mapState",
+ "specialPlace_home",
+ "specialPlace_work",
+ "recentPlaces",
+ "planner_route_history",
+ `favouriteStops`,
+ `recentStops`,
+ `customStopNames`,
+ `stops_cache`,
+ ];
+ personalKeys.forEach((key) => localStorage.removeItem(key));
+ setShowClearConfirm(false);
+ setCleared(true);
+ }}
+ className="px-4 py-2 rounded-lg bg-red-600 text-white hover:bg-red-700
+ transition-colors duration-200 focus:outline-none focus:ring focus:ring-red-300"
+ >
+ {t("settings.clear_data_confirm_btn", "Sí, borrar datos")}
+ </button>
+ <button
+ onClick={() => setShowClearConfirm(false)}
+ className="px-4 py-2 rounded-lg border border-border bg-surface text-text
+ hover:bg-surface/50 transition-colors duration-200 focus:outline-none focus:ring focus:ring-primary/50"
+ >
+ {t("settings.cancel", "Cancelar")}
+ </button>
+ </div>
+ </div>
+ )}
+ {cleared && (
+ <p className="text-sm text-green-700 dark:text-green-400">
+ {t(
+ "settings.clear_data_done",
+ "Tus datos se han borrado correctamente."
+ )}
+ </p>
+ )}
+ <p className="mt-4 text-sm text-muted">
+ <Link
+ to="/politica-privacidad"
+ className="underline hover:text-text transition-colors"
+ >
+ {t("settings.privacy_policy_link", "Política de privacidad")}
+ </Link>
+ </p>
+ </section>
</div>
);
}