Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/danitocsc/transporte-unrc-web-public/llms.txt

Use this file to discover all available pages before exploring further.

PublicMapPage es el componente raíz de la vista pública del mapa de paradas. Declarado con la directiva 'use client', se ejecuta exclusivamente en el navegador y orquesta toda la interacción del usuario: geolocalización, selección de paradas, filtrado por ruta y apertura del panel lateral. Recibe sus datos como props desde el Server Component padre, que los carga en el servidor mediante loadSiteData().
Este componente importa PublicStopsMap con dynamic({ ssr: false }) para evitar que Leaflet/MapLibre intente acceder a window durante la renderización en el servidor de Next.js.

Props

interface Props {
  stops: PublicStop[];
  routes: PublicRoute[];
  summary: Record<string, unknown>;
}
stops
PublicStop[]
required
Lista completa de paradas piloto cargadas desde los datos del servidor. Cada elemento incluye coordenadas, nombre, ruta asignada, turno y días de servicio. Ver PublicStop para la forma exacta del objeto.
routes
PublicRoute[]
required
Lista de rutas piloto. Cada ruta contiene un id, nombre, color hexadecimal y arreglo de puntos que forman la polilínea. Ver PublicRoute.
summary
Record<string, unknown>
required
Payload del informe de demanda (SummaryPayload) pasado como objeto genérico. Este componente no consume directamente los campos del resumen — los datos se usan en la página /informe.

Estado interno (useState)

El componente administra nueve piezas de estado local:
VariableTipoValor inicialDescripción
userLocation[number, number] | nullnullCoordenadas GPS del navegador (latitud, longitud). Se actualiza tras un fix de geolocalización exitoso.
nearestStopIdstring | nullnullid de la parada más cercana al usuario, calculado con Haversine.
selectedStopPublicStop | nullnullParada actualmente seleccionada en la lista o en el mapa.
geoErrorstring | nullnullMensaje de error de la API de geolocalización del navegador.
geoLoadingbooleanfalsetrue mientras se espera la respuesta de getCurrentPosition.
filterRoutestring'all'ID de la ruta activa para el filtro, o 'all' para mostrar todas.
sidebarOpenbooleanfalseControla la visibilidad del panel lateral.
userTurnostring''Turno seleccionado por el usuario: 'Matutino', 'Intermedio', 'Vespertino' o cadena vacía.
errorLogstring | nullnullMensaje del último error JavaScript no manejado capturado por el listener global window.addEventListener('error', ...). Se muestra en un banner rojo en la parte superior de la página.

Parámetro de URL ?sidebar=true

Al montar el componente, un useEffect que depende de searchParams lee el parámetro sidebar:
useEffect(() => {
  const sidebarParam = searchParams.get('sidebar');
  if (sidebarParam === 'true') {
    setSidebarOpen(true);
  } else if (sidebarParam === 'false') {
    setSidebarOpen(false);
  }
}, [searchParams]);
Esto permite que enlaces externos abran la vista directamente con el panel de paradas visible, por ejemplo: /mapa?sidebar=true.

Fórmula Haversine

haversine es una función auxiliar interna (no exportada) que calcula la distancia en kilómetros entre dos puntos geográficos usando la gran-elipse terrestre:
function haversine(lat1: number, lng1: number, lat2: number, lng2: number): number {
  const R = 6371; // Radio medio de la Tierra en km
  const dLat = ((lat2 - lat1) * Math.PI) / 180;
  const dLng = ((lng2 - lng1) * Math.PI) / 180;
  const a =
    Math.sin(dLat / 2) ** 2 +
    Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) *
    Math.sin(dLng / 2) ** 2;
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
Se utiliza en dos lugares:
  1. Dentro de handleGeolocate() para ordenar todas las paradas por proximidad al usuario.
  2. En el panel lateral para mostrar la distancia exacta entre userLocation y nearestStop.

handleGeolocate()

Llama a navigator.geolocation.getCurrentPosition con alta precisión y un timeout de 10 segundos. Flujo completo:
const handleGeolocate = useCallback(() => {
  if (!navigator.geolocation) {
    setGeoError('Tu navegador no soporta geolocalización.');
    return;
  }
  setGeoLoading(true);
  setGeoError(null);
  navigator.geolocation.getCurrentPosition(
    (pos) => {
      const { latitude, longitude } = pos.coords;
      setUserLocation([latitude, longitude]);

      const sorted = [...stops].sort(
        (a, b) =>
          haversine(latitude, longitude, a.lat, a.lng) -
          haversine(latitude, longitude, b.lat, b.lng)
      );
      const nearest = sorted[0];
      if (nearest) {
        setNearestStopId(nearest.id);
        setSelectedStop(nearest);
        setSidebarOpen(true);
      }
      setGeoLoading(false);
    },
    (err) => {
      setGeoError('No se pudo obtener tu ubicación. ' + err.message);
      setGeoLoading(false);
    },
    { enableHighAccuracy: true, timeout: 10000 }
  );
}, [stops]);
Si el navegador no soporta la API, se escribe el mensaje de error directamente sin intentar la llamada. El botón FAB flotante y el botón interno del panel comparten este mismo callback.

Valores memoizados (useMemo)

// Parada más cercana, encontrada por ID
const nearestStop = useMemo(
  () => stops.find((s) => s.id === nearestStopId) ?? null,
  [stops, nearestStopId]
);

// Ruta de la parada más cercana
const nearestRoute = useMemo(
  () => nearestStop ? routes.find((r) => r.id === nearestStop.route_id) : null,
  [nearestStop, routes]
);

// Paradas filtradas según filterRoute
const filteredStops = useMemo(
  () => (filterRoute === 'all' ? stops : stops.filter((s) => s.route_id === filterRoute)),
  [stops, filterRoute]
);
filteredStops es el arreglo que se pasa a PublicStopsMap — cuando el usuario selecciona una ruta específica, el mapa solo muestra los marcadores de esa ruta.

Horarios estimados por turno

El selector de turno en el panel muestra el horario de salida estimado para el servicio piloto:
TurnoHorario estimado
Matutino06:30 AM
Intermedio11:00 AM
Vespertino03:30 PM
{userTurno === 'Matutino' ? '06:30 AM' : userTurno === 'Intermedio' ? '11:00 AM' : '03:30 PM'}

Mapa de nombres de rutas

El objeto ROUTE_NAMES traduce los IDs técnicos de las rutas a nombres legibles para el usuario en los botones de filtro:
const ROUTE_NAMES: Record<string, string> = {
  'route-la-mesa-unrc-001': 'La Mesa',
  'route-centro-unrc-001': 'Centro',
};
Si una ruta no tiene entrada en ROUTE_NAMES, el componente usa r.name como fallback.

Importación dinámica de PublicStopsMap

const PublicStopsMap = dynamic(() => import('@/components/maps/PublicStopsMap'), {
  ssr: false,
  loading: () => <div className="w-full h-full bg-slate-100 animate-pulse" />,
});
La opción ssr: false es obligatoria porque MapLibre GL accede a window, document y la API Canvas al inicializarse — objetos inexistentes en el entorno Node.js de Next.js. Mientras el bundle del mapa se descarga, se muestra un placeholder gris con animación pulse.

Manejo de errores globales

Al montar, el componente registra dos listeners globales para capturar errores de JavaScript no manejados y mostrarlos en un banner rojo en la parte superior:
useEffect(() => {
  const handleError = (event: ErrorEvent) => {
    // Ignorar errores genéricos de extensiones del navegador o scripts de terceros
    if (
      event.message === "script error." ||
      event.message === "Script error." ||
      !event.filename ||
      event.lineno === 0
    ) {
      return;
    }
    setErrorLog(event.message + ' at ' + event.filename + ':' + event.lineno);
  };
  const handleRejection = (event: PromiseRejectionEvent) => {
    setErrorLog('Unhandled promise rejection: ' + event.reason);
  };
  window.addEventListener('error', handleError);
  window.addEventListener('unhandledrejection', handleRejection);
  return () => {
    window.removeEventListener('error', handleError);
    window.removeEventListener('unhandledrejection', handleRejection);
  };
}, []);
Los errores de tipo "Script error." (con o sin mayúscula inicial) o aquellos sin filename o con lineno === 0 se ignoran silenciosamente. Estos corresponden a scripts cross-origin como extensiones de traducción o herramientas del navegador que no pertenecen al código de la aplicación.

Árbol de renderizado

PublicMapPage
├── <header>                    ← Barra UNRC con navegación desktop y móvil
├── [errorLog banner]           ← Solo visible si hay un error JS capturado
└── <div.flex.flex-1>
    ├── <aside>                 ← Panel lateral (visible si sidebarOpen === true)
    │   ├── Resultado parada más cercana  (nearestStop)
    │   ├── Parada seleccionada           (selectedStop ≠ nearestStop)
    │   ├── Selector de turno + horario
    │   ├── Filtros de ruta
    │   ├── Lista de paradas scrollable
    │   └── Teaser "Próximamente: acceso con matrícula"
    ├── <div.flex-1>            ← Contenedor del mapa
    │   ├── <PublicStopsMap>    ← Mapa interactivo (SSR deshabilitado)
    │   └── FAB "Encontrar mi parada más cercana"
    └── <nav>                   ← Barra de navegación inferior (solo móvil)
El panel lateral (<aside>) usa posicionamiento absolute en móvil y relative en desktop, superponiéndose al mapa en pantallas pequeñas y desplazando el mapa lateralmente en pantallas medianas y mayores.

Build docs developers (and LLMs) love