From a68ba30716062b265f85c4be078a736c7135d7bc Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Sun, 30 Nov 2025 20:49:48 +0100 Subject: Refactor StopMap and Settings components; replace region config usage with REGION_DATA, update StopDataProvider calls, and improve UI elements. Remove unused timetable files and add Tailwind CSS support. --- src/frontend/app/components/GroupedTable.tsx | 90 --------- src/frontend/app/components/LineIcon.css | 12 ++ src/frontend/app/components/LineIcon.tsx | 20 +- src/frontend/app/components/PullToRefresh.css | 2 +- src/frontend/app/components/PullToRefresh.tsx | 13 +- src/frontend/app/components/RegularTable.tsx | 94 --------- src/frontend/app/components/SchedulesTable.css | 208 -------------------- src/frontend/app/components/SchedulesTable.tsx | 213 --------------------- .../app/components/SchedulesTableSkeleton.tsx | 136 ------------- src/frontend/app/components/StopGalleryItem.tsx | 12 +- src/frontend/app/components/StopItem.tsx | 9 +- src/frontend/app/components/StopMapModal.tsx | 32 ++-- src/frontend/app/components/StopMapSheet.tsx | 38 ++-- src/frontend/app/components/StopSheet.tsx | 86 ++++----- .../Stops/ConsolidatedCirculationCard.css | 10 +- .../Stops/ConsolidatedCirculationCard.tsx | 37 ++-- .../Stops/ConsolidatedCirculationList.tsx | 4 - src/frontend/app/components/layout/Header.tsx | 1 - src/frontend/app/components/ui/Button.css | 39 ---- src/frontend/app/components/ui/Button.tsx | 25 --- src/frontend/app/components/ui/PageContainer.css | 20 -- src/frontend/app/components/ui/PageContainer.tsx | 14 -- 22 files changed, 122 insertions(+), 993 deletions(-) delete mode 100644 src/frontend/app/components/GroupedTable.tsx delete mode 100644 src/frontend/app/components/RegularTable.tsx delete mode 100644 src/frontend/app/components/SchedulesTable.css delete mode 100644 src/frontend/app/components/SchedulesTable.tsx delete mode 100644 src/frontend/app/components/SchedulesTableSkeleton.tsx delete mode 100644 src/frontend/app/components/ui/Button.css delete mode 100644 src/frontend/app/components/ui/Button.tsx delete mode 100644 src/frontend/app/components/ui/PageContainer.css delete mode 100644 src/frontend/app/components/ui/PageContainer.tsx (limited to 'src/frontend/app/components') diff --git a/src/frontend/app/components/GroupedTable.tsx b/src/frontend/app/components/GroupedTable.tsx deleted file mode 100644 index 175899a..0000000 --- a/src/frontend/app/components/GroupedTable.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { type RegionConfig } from "../config/RegionConfig"; -import { type Estimate } from "../routes/estimates-$id"; -import LineIcon from "./LineIcon"; - -interface GroupedTable { - data: Estimate[]; - dataDate: Date | null; - regionConfig: RegionConfig; -} - -export const GroupedTable: React.FC = ({ - data, - dataDate, - regionConfig, -}) => { - const formatDistance = (meters: number) => { - if (meters > 1024) { - return `${(meters / 1000).toFixed(1)} km`; - } else { - return `${meters} m`; - } - }; - - const groupedEstimates = data.reduce( - (acc, estimate) => { - if (!acc[estimate.line]) { - acc[estimate.line] = []; - } - acc[estimate.line].push(estimate); - return acc; - }, - {} as Record - ); - - const sortedLines = Object.keys(groupedEstimates).sort((a, b) => { - const firstArrivalA = groupedEstimates[a][0].minutes; - const firstArrivalB = groupedEstimates[b][0].minutes; - return firstArrivalA - firstArrivalB; - }); - - return ( - - - - - - - - - {regionConfig.showMeters && } - - - - - {sortedLines.map((line) => - groupedEstimates[line].map((estimate, idx) => ( - - {idx === 0 && ( - - )} - - - {regionConfig.showMeters && ( - - )} - - )) - )} - - - {data?.length === 0 && ( - - - - - - )} -
- Estimaciones de llegadas a las {dataDate?.toLocaleTimeString()} -
LíneaRutaLlegadaDistancia
- - {estimate.route}{`${estimate.minutes} min`} - {estimate.meters > -1 - ? formatDistance(estimate.meters) - : "No disponible"} -
- No hay estimaciones disponibles -
- ); -}; diff --git a/src/frontend/app/components/LineIcon.css b/src/frontend/app/components/LineIcon.css index 01ff2bd..89f8bdb 100644 --- a/src/frontend/app/components/LineIcon.css +++ b/src/frontend/app/components/LineIcon.css @@ -71,6 +71,18 @@ --line-u2-text: hsl(0, 0%, 100%); --line-vts: hsl(300, 33%, 30%); --line-vts-text: hsl(0, 0%, 100%); + + /* Special christmas line - Touristic bus */ + --line-nad: hsl(0, 100%, 40%); + --line-nad-text: hsl(0, 0%, 100%); + + --line-mar: hsl(208, 68%, 66%); + --line-mar-text: hsl(0, 0%, 100%); + --line-rio: hsl(208, 68%, 66%); + --line-rio-text: hsl(0, 0%, 100%); + --line-gol: hsl(208, 68%, 66%); + --line-gol-text: hsl(0, 0%, 100%); + } .line-icon-default { diff --git a/src/frontend/app/components/LineIcon.tsx b/src/frontend/app/components/LineIcon.tsx index 5ccf80a..fc40824 100644 --- a/src/frontend/app/components/LineIcon.tsx +++ b/src/frontend/app/components/LineIcon.tsx @@ -1,25 +1,23 @@ import React, { useMemo } from "react"; -import { type RegionId } from "../config/RegionConfig"; import "./LineIcon.css"; interface LineIconProps { line: string; - - /** - * @deprecated Unused since region is only Vigo - */ - region?: RegionId; - - mode?: "rounded"|"pill"|"default"; + mode?: "rounded" | "pill" | "default"; } const LineIcon: React.FC = ({ line, mode = "default", }) => { + const actualLine = useMemo(() => { + return line.trim().replace('510', 'NAD'); + }, [line]) + const formattedLine = useMemo(() => { - return /^[a-zA-Z]/.test(line) ? line : `L${line}`; - }, [line]); + return /^[a-zA-Z]/.test(actualLine) ? actualLine : `L${actualLine}`; + }, [actualLine]); + const cssVarName = `--line-${formattedLine.toLowerCase()}`; const cssTextVarName = `--line-${formattedLine.toLowerCase()}-text`; @@ -33,7 +31,7 @@ const LineIcon: React.FC = ({ } as React.CSSProperties } > - {line} + {actualLine} ); }; diff --git a/src/frontend/app/components/PullToRefresh.css b/src/frontend/app/components/PullToRefresh.css index d4946d2..3e8f802 100644 --- a/src/frontend/app/components/PullToRefresh.css +++ b/src/frontend/app/components/PullToRefresh.css @@ -6,7 +6,7 @@ .pull-to-refresh-indicator { position: fixed; - top: 20px; + top: 80px; left: 50%; transform: translateX(-50%); z-index: 1000; diff --git a/src/frontend/app/components/PullToRefresh.tsx b/src/frontend/app/components/PullToRefresh.tsx index b3abe86..2de1a4f 100644 --- a/src/frontend/app/components/PullToRefresh.tsx +++ b/src/frontend/app/components/PullToRefresh.tsx @@ -1,6 +1,6 @@ -import React, { useRef, useState, useEffect, useCallback } from "react"; import { motion, useMotionValue, useTransform } from "framer-motion"; import { RefreshCw } from "lucide-react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import "./PullToRefresh.css"; interface PullToRefreshProps { @@ -26,15 +26,6 @@ export const PullToRefresh: React.FC = ({ const scale = useTransform(y, [0, threshold], [0.5, 1]); const rotate = useTransform(y, [0, threshold], [0, 180]); - const isAtPageTop = useCallback(() => { - const scrollTop = - window.pageYOffset || - document.documentElement.scrollTop || - document.body.scrollTop || - 0; - return scrollTop <= 10; // Increased tolerance to 10px - }, []); - const handleTouchStart = useCallback( (e: TouchEvent) => { // Very strict check - must be at absolute top @@ -160,7 +151,6 @@ export const PullToRefresh: React.FC = ({ return (
- {/* Simple indicator */} {isPulling && ( = ({ )} - {/* Normal content - no transform interference */}
{children}
); diff --git a/src/frontend/app/components/RegularTable.tsx b/src/frontend/app/components/RegularTable.tsx deleted file mode 100644 index a738d03..0000000 --- a/src/frontend/app/components/RegularTable.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { type RegionConfig } from "../config/RegionConfig"; -import { type Estimate } from "../routes/estimates-$id"; -import LineIcon from "./LineIcon"; - -interface RegularTableProps { - data: Estimate[]; - dataDate: Date | null; - regionConfig: RegionConfig; -} - -export const RegularTable: React.FC = ({ - data, - dataDate, - regionConfig, -}) => { - const { t } = useTranslation(); - - const absoluteArrivalTime = (minutes: number) => { - const now = new Date(); - const arrival = new Date(now.getTime() + minutes * 60000); - return Intl.DateTimeFormat( - typeof navigator !== "undefined" ? navigator.language : "en", - { - hour: "2-digit", - minute: "2-digit", - } - ).format(arrival); - }; - - const formatDistance = (meters: number) => { - if (meters > 1024) { - return `${(meters / 1000).toFixed(1)} km`; - } else { - return `${meters} ${t("estimates.meters", "m")}`; - } - }; - - return ( - - - - - - - - - {regionConfig.showMeters && ( - - )} - - - - - {data - .sort((a, b) => a.minutes - b.minutes) - .map((estimate, idx) => ( - - - - - {regionConfig.showMeters && ( - - )} - - ))} - - - {data?.length === 0 && ( - - - - - - )} -
- {t("estimates.caption", "Estimaciones de llegadas a las {{time}}", { - time: dataDate?.toLocaleTimeString(), - })} -
{t("estimates.line", "Línea")}{t("estimates.route", "Ruta")}{t("estimates.arrival", "Llegada")}{t("estimates.distance", "Distancia")}
- - {estimate.route} - {estimate.minutes > 15 - ? absoluteArrivalTime(estimate.minutes) - : `${estimate.minutes} ${t("estimates.minutes", "min")}`} - - {estimate.meters > -1 - ? formatDistance(estimate.meters) - : t("estimates.not_available", "No disponible")} -
- {t("estimates.none", "No hay estimaciones disponibles")} -
- ); -}; diff --git a/src/frontend/app/components/SchedulesTable.css b/src/frontend/app/components/SchedulesTable.css deleted file mode 100644 index c0c5cb7..0000000 --- a/src/frontend/app/components/SchedulesTable.css +++ /dev/null @@ -1,208 +0,0 @@ -.timetable-container { - margin-top: 2rem; -} - -.timetable-caption { - font-weight: bold; - margin-bottom: 1rem; - text-align: left; - font-size: 1.1rem; - color: var(--text-primary); -} - -.timetable-cards { - display: flex; - flex-direction: column; - gap: 1rem; - margin-bottom: 1rem; -} - -.timetable-card { - background-color: var(--surface-future); - border: 1px solid var(--card-border); - border-radius: 10px; - padding: 1.25rem; - transition: - background-color 0.2s ease, - border 0.2s ease; -} - -/* Next upcoming service: slight emphasis */ -.timetable-card.timetable-next { - background-color: var(--surface-next); - border-color: var(--card-border); - position: relative; -} - -.timetable-card.timetable-next::before { - content: ""; - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 4px; - border-top-left-radius: 10px; - border-bottom-left-radius: 10px; - background: var(--accent-next); -} - -.timetable-card.timetable-past { - background-color: var(--surface-past); - color: var(--text-secondary); - border: 1px solid var(--card-border); -} - -.timetable-card .card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; -} - -.timetable-card .line-info { - flex-shrink: 0; -} - -.timetable-card .destination-info { - flex: 1; - text-align: left; - margin: 0 1rem; - color: var(--text-primary); -} - -.timetable-card .destination-info strong { - font-size: 0.95rem; -} - -.timetable-card.timetable-past .destination-info { - color: var(--text-secondary); -} - -.timetable-card .time-info { - display: flex; - flex-direction: column; - align-items: flex-end; - flex-shrink: 0; -} - -.timetable-card .timetable-card .departure-time { - font-weight: bold; - font-family: monospace; - font-size: 1.1rem; - color: var(--text-primary); -} - -.timetable-card.timetable-past .departure-time { - color: var(--text-secondary); -} - -.timetable-card .card-body { - line-height: 1.4; -} - -.timetable-card .route-streets { - font-size: 0.85rem; - color: var(--text-secondary); - line-height: 1.8; - word-break: break-word; -} - -.timetable-card .timetable-card .service-id { - font-family: monospace; - font-size: 0.8rem; - color: var(--text-secondary); - background: var(--service-background); - padding: 0.15rem 0.4rem; - border-radius: 3px; - font-weight: 500; - display: inline; - margin-right: 0.2em; -} - -.timetable-card.timetable-past .service-id { - color: var(--text-secondary); - background: var(--service-background-past); -} - -.timetable-container .no-data { - text-align: center; - color: var(--text-secondary); - font-style: italic; - padding: 2rem; - background: var(--card-background); - border-radius: 8px; - border: 1px solid var(--card-border); -} - -/* Responsive design */ -@media (max-width: 768px) { - .timetable-cards { - gap: 0.5rem; - } - .timetable-card { - padding: 0.75rem; - } - .timetable-card .card-header { - margin-bottom: 0.5rem; - } - .timetable-card .destination-info { - margin: 0 0.5rem; - } - .timetable-card .destination-info strong { - font-size: 0.9rem; - } - .timetable-card .departure-time { - font-size: 1rem; - } - .timetable-card .service-id { - font-size: 0.8rem; - padding: 0.2rem 0.4rem; - } -} - -@media (max-width: 480px) { - .timetable-card .card-header { - flex-direction: column; - align-items: stretch; - gap: 0.5rem; - } - - .timetable-card .destination-info { - text-align: left; - margin: 0; - order: 2; - } - - .timetable-card .time-info { - align-items: flex-start; - order: 1; - align-self: flex-end; - } - - .timetable-card .line-info { - order: 0; - align-self: flex-start; - } - - /* Create a flex container for line and time on mobile */ - .timetable-card .card-header { - position: relative; - } - - .timetable-card .line-info { - position: absolute; - left: 0; - top: 0; - } - - .timetable-card .time-info { - position: absolute; - right: 0; - top: 0; - } - - .timetable-card .destination-info { - margin-top: 2rem; - text-align: left; - } -} diff --git a/src/frontend/app/components/SchedulesTable.tsx b/src/frontend/app/components/SchedulesTable.tsx deleted file mode 100644 index faf7b01..0000000 --- a/src/frontend/app/components/SchedulesTable.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { useApp } from "~/AppContext"; -import LineIcon from "./LineIcon"; -import "./SchedulesTable.css"; - -export type ScheduledTable = { - trip_id: string; - service_id: string; - - line: string; - route: string; - - stop_sequence: number; - shape_dist_traveled: number; - next_streets: string[]; - - starting_code: string; - starting_name: string; - starting_time: string; - - calling_time: string; - calling_ssm: number; - - terminus_code: string; - terminus_name: string; - terminus_time: string; -}; - -interface TimetableTableProps { - data: ScheduledTable[]; - showAll?: boolean; - currentTime?: string; // HH:MM:SS format -} - -// Utility function to parse service ID and get the turn number -const parseServiceId = (serviceId: string): string => { - const parts = serviceId.split("_"); - if (parts.length === 0) return ""; - - const lastPart = parts[parts.length - 1]; - if (lastPart.length < 6) return ""; - - const last6 = lastPart.slice(-6); - const lineCode = last6.slice(0, 3); - const turnCode = last6.slice(-3); - - // Remove leading zeros from turn - const turnNumber = parseInt(turnCode, 10).toString(); - - // Parse line number with special cases - const lineNumber = parseInt(lineCode, 10); - let displayLine: string; - - switch (lineNumber) { - case 1: - displayLine = "C1"; - break; - case 3: - displayLine = "C3"; - break; - case 30: - displayLine = "N1"; - break; - case 33: - displayLine = "N4"; - break; - case 8: - displayLine = "A"; - break; - case 101: - displayLine = "H"; - break; - case 150: - displayLine = "REF"; - break; - case 201: - displayLine = "U1"; - break; - case 202: - displayLine = "U2"; - break; - case 500: - displayLine = "TUR"; - break; - default: - displayLine = `L${lineNumber}`; - } - - return `${displayLine}-${turnNumber}`; -}; - -// Utility function to compare times -const timeToMinutes = (time: string): number => { - const [hours, minutes] = time.split(":").map(Number); - return hours * 60 + minutes; -}; - -// Utility function to format GTFS time for display (handle hours >= 24) -const formatTimeForDisplay = (time: string): string => { - const [hours, minutes] = time.split(":").map(Number); - const normalizedHours = hours % 24; - return `${normalizedHours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; -}; - -// Utility function to find nearby entries -const findNearbyEntries = ( - entries: ScheduledTable[], - currentTime: string, - before: number = 4, - after: number = 4 -): ScheduledTable[] => { - if (!currentTime) return entries.slice(0, before + after); - - const currentMinutes = timeToMinutes(currentTime); - const sortedEntries = [...entries].sort( - (a, b) => timeToMinutes(a.calling_time) - timeToMinutes(b.calling_time) - ); - - let currentIndex = sortedEntries.findIndex( - (entry) => timeToMinutes(entry.calling_time) >= currentMinutes - ); - - if (currentIndex === -1) { - // All entries are before current time, show last ones - return sortedEntries.slice(-before - after); - } - - const startIndex = Math.max(0, currentIndex - before); - const endIndex = Math.min(sortedEntries.length, currentIndex + after); - - return sortedEntries.slice(startIndex, endIndex); -}; - -export const SchedulesTable: React.FC = ({ - data, - showAll = false, - currentTime, -}) => { - const { t } = useTranslation(); - const { region } = useApp(); - - const displayData = showAll - ? data - : findNearbyEntries(data, currentTime || ""); - const nowMinutes = currentTime - ? timeToMinutes(currentTime) - : timeToMinutes(new Date().toTimeString().slice(0, 8)); - - return ( -
-
- {showAll - ? t("timetable.fullCaption", "Horarios teóricos de la parada") - : t("timetable.nearbyCaption", "Próximos horarios teóricos")} -
- -
- {displayData.map((entry, index) => { - const entryMinutes = timeToMinutes(entry.calling_time); - const isPast = entryMinutes < nowMinutes; - return ( -
-
-
- -
- -
- {entry.route && entry.route.trim() ? ( - {entry.route} - ) : ( - - {t("timetable.noDestination", "Línea")} {entry.line} - - )} -
- -
- - {formatTimeForDisplay(entry.calling_time)} - -
-
-
-
- - {parseServiceId(entry.service_id)} - - {entry.next_streets.length > 0 && ( - — {entry.next_streets.join(" — ")} - )} -
-
-
- ); - })} -
- {displayData.length === 0 && ( -

- {t("timetable.noData", "No hay datos de horarios disponibles")} -

- )} -
- ); -}; diff --git a/src/frontend/app/components/SchedulesTableSkeleton.tsx b/src/frontend/app/components/SchedulesTableSkeleton.tsx deleted file mode 100644 index 3ae9729..0000000 --- a/src/frontend/app/components/SchedulesTableSkeleton.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import React from "react"; -import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; -import "react-loading-skeleton/dist/skeleton.css"; -import { useTranslation } from "react-i18next"; - -interface EstimatesTableSkeletonProps { - rows?: number; -} - -export const SchedulesTableSkeleton: React.FC = ({ - rows = 3, -}) => { - const { t } = useTranslation(); - - return ( - - - - - - - - - - - - - - - {Array.from({ length: rows }, (_, index) => ( - - - - - - - ))} - -
- -
{t("estimates.line", "Línea")}{t("estimates.route", "Ruta")}{t("estimates.arrival", "Llegada")}{t("estimates.distance", "Distancia")}
- - - - -
- - -
-
- -
-
- ); -}; - -interface EstimatesGroupedSkeletonProps { - groups?: number; - rowsPerGroup?: number; -} - -export const EstimatesGroupedSkeleton: React.FC< - EstimatesGroupedSkeletonProps -> = ({ groups = 3, rowsPerGroup = 2 }) => { - const { t } = useTranslation(); - - return ( - - - - - - - - - - - - - - - {Array.from({ length: groups }, (_, groupIndex) => ( - - {Array.from({ length: rowsPerGroup }, (_, rowIndex) => ( - - - - - - - ))} - - ))} - -
- -
{t("estimates.line", "Línea")}{t("estimates.route", "Ruta")}{t("estimates.arrival", "Llegada")}{t("estimates.distance", "Distancia")}
- {rowIndex === 0 && ( - - )} - - - -
- - -
-
- -
-
- ); -}; diff --git a/src/frontend/app/components/StopGalleryItem.tsx b/src/frontend/app/components/StopGalleryItem.tsx index b32661a..72a13e5 100644 --- a/src/frontend/app/components/StopGalleryItem.tsx +++ b/src/frontend/app/components/StopGalleryItem.tsx @@ -1,30 +1,26 @@ import React from "react"; import { Link } from "react-router"; -import { type Stop } from "../data/StopDataProvider"; +import StopDataProvider, { type Stop } from "../data/StopDataProvider"; import LineIcon from "./LineIcon"; -import { useApp } from "../AppContext"; -import StopDataProvider from "../data/StopDataProvider"; interface StopGalleryItemProps { stop: Stop; } const StopGalleryItem: React.FC = ({ stop }) => { - const { region } = useApp(); - return (
- +
{stop.favourite && } ({stop.stopId})
- {StopDataProvider.getDisplayName(region, stop)} + {StopDataProvider.getDisplayName(stop)}
{stop.lines?.slice(0, 5).map((line) => ( - + ))} {stop.lines && stop.lines.length > 5 && ( +{stop.lines.length - 5} diff --git a/src/frontend/app/components/StopItem.tsx b/src/frontend/app/components/StopItem.tsx index de51576..7875b59 100644 --- a/src/frontend/app/components/StopItem.tsx +++ b/src/frontend/app/components/StopItem.tsx @@ -1,6 +1,5 @@ import React from "react"; import { Link } from "react-router"; -import { useApp } from "../AppContext"; import StopDataProvider, { type Stop } from "../data/StopDataProvider"; import LineIcon from "./LineIcon"; @@ -9,15 +8,13 @@ interface StopItemProps { } const StopItem: React.FC = ({ stop }) => { - const { region } = useApp(); - return (
  • - +
    {stop.favourite && } - {StopDataProvider.getDisplayName(region, stop)} + {StopDataProvider.getDisplayName(stop)} ({stop.stopId}) @@ -25,7 +22,7 @@ const StopItem: React.FC = ({ stop }) => {
    {stop.lines?.map((line) => ( - + ))}
    diff --git a/src/frontend/app/components/StopMapModal.tsx b/src/frontend/app/components/StopMapModal.tsx index 74f20d9..55ad848 100644 --- a/src/frontend/app/components/StopMapModal.tsx +++ b/src/frontend/app/components/StopMapModal.tsx @@ -1,15 +1,15 @@ import maplibregl from "maplibre-gl"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import Map, { - Layer, - Marker, - Source, - type MapRef + Layer, + Marker, + Source, + type MapRef } from "react-map-gl/maplibre"; import { Sheet } from "react-modal-sheet"; import { useApp } from "~/AppContext"; -import { getRegionConfig, type RegionId } from "~/config/RegionConfig"; -import { getLineColor } from "~/data/LineColors"; +import { REGION_DATA } from "~/config/RegionConfig"; +import { getLineColour } from "~/data/LineColors"; import type { Stop } from "~/data/StopDataProvider"; import { loadStyle } from "~/maps/styleloader"; import "./StopMapModal.css"; @@ -37,7 +37,6 @@ export interface ConsolidatedCirculationForMap { interface StopMapModalProps { stop: Stop; circulations: ConsolidatedCirculationForMap[]; - region: RegionId; isOpen: boolean; onClose: () => void; selectedCirculationId?: string; @@ -46,7 +45,6 @@ interface StopMapModalProps { export const StopMapModal: React.FC = ({ stop, circulations, - region, isOpen, onClose, selectedCirculationId, @@ -59,8 +57,6 @@ export const StopMapModal: React.FC = ({ const [shapeData, setShapeData] = useState(null); const [previousShapeData, setPreviousShapeData] = useState(null); - const regionConfig = getRegionConfig(region); - // Filter circulations that have GPS coordinates const busesWithPosition = useMemo( () => circulations.filter((c) => !!c.currentPosition), @@ -165,7 +161,7 @@ export const StopMapModal: React.FC = ({ maxZoom: 17, } as any); } - } catch {} + } catch { } }, [stop, selectedBus, shapeData, previousShapeData]); // Load style without traffic layers for the stop map @@ -246,7 +242,7 @@ export const StopMapModal: React.FC = ({ !selectedBus || !selectedBus.schedule?.shapeId || selectedBus.currentPosition?.shapeIndex === undefined || - !regionConfig.shapeEndpoint + !REGION_DATA.shapeEndpoint ) { setShapeData(null); setPreviousShapeData(null); @@ -266,7 +262,7 @@ export const StopMapModal: React.FC = ({ sLat?: number, sLon?: number ) => { - let url = `${regionConfig.shapeEndpoint}?shapeId=${sId}`; + let url = `${REGION_DATA.shapeEndpoint}?shapeId=${sId}`; if (bIndex !== undefined) url += `&busShapeIndex=${bIndex}`; if (sIndex !== undefined) url += `&stopShapeIndex=${sIndex}`; else if (sLat && sLon) url += `&stopLat=${sLat}&stopLon=${sLon}`; @@ -334,7 +330,7 @@ export const StopMapModal: React.FC = ({ }; loadShapes().catch((err) => console.error("Failed to load shape", err)); - }, [isOpen, selectedBus, regionConfig.shapeEndpoint]); + }, [isOpen, selectedBus]); if (busesWithPosition.length === 0) { return null; // Don't render if no buses with GPS coordinates @@ -362,7 +358,7 @@ export const StopMapModal: React.FC = ({ }} style={{ width: "100%", height: "50vh" }} mapStyle={styleSpec} - attributionControl={{compact: false, customAttribution: "Concello de Vigo & Viguesa de Transportes SL"}} + attributionControl={{ compact: false, customAttribution: "Concello de Vigo & Viguesa de Transportes SL" }} ref={mapRef} interactive={true} onMove={(e) => { @@ -422,7 +418,7 @@ export const StopMapModal: React.FC = ({ id="prev-route-shape-inner" type="line" paint={{ - "line-color": getLineColor(region, selectedBus.line) + "line-color": getLineColour(selectedBus.line) .background, "line-width": 4, "line-dasharray": [2, 2], @@ -455,7 +451,7 @@ export const StopMapModal: React.FC = ({ id="route-shape-inner" type="line" paint={{ - "line-color": getLineColor(region, selectedBus.line) + "line-color": getLineColour(selectedBus.line) .background, "line-width": 3, "line-opacity": 0.7, @@ -534,7 +530,7 @@ export const StopMapModal: React.FC = ({ > = ({ stop, - circulations, - region, + circulations }) => { const { theme } = useApp(); const [styleSpec, setStyleSpec] = useState(null); @@ -70,18 +68,16 @@ export const StopMap: React.FC = ({ const [showAttribution, setShowAttribution] = useState(false); const [shapes, setShapes] = useState>({}); - const regionConfig = getRegionConfig(region); - useEffect(() => { circulations.forEach((c) => { if ( c.schedule?.shapeId && c.currentPosition?.shapeIndex !== undefined && - regionConfig.shapeEndpoint + REGION_DATA.shapeEndpoint ) { const key = `${c.schedule.shapeId}_${c.currentPosition.shapeIndex}`; if (!shapes[key]) { - let url = `${regionConfig.shapeEndpoint}?shapeId=${c.schedule.shapeId}&busShapeIndex=${c.currentPosition.shapeIndex}`; + let url = `${REGION_DATA.shapeEndpoint}?shapeId=${c.schedule.shapeId}&busShapeIndex=${c.currentPosition.shapeIndex}`; if (c.stopShapeIndex !== undefined) { url += `&stopShapeIndex=${c.stopShapeIndex}`; } else { @@ -102,7 +98,7 @@ export const StopMap: React.FC = ({ } } }); - }, [circulations, regionConfig.shapeEndpoint, shapes]); + }, [circulations, shapes]); type Pt = { lat: number; lon: number }; const haversineKm = (a: Pt, b: Pt) => { @@ -195,7 +191,7 @@ export const StopMap: React.FC = ({ accuracy: pos.coords.accuracy, }); }, - () => {}, + () => { }, { enableHighAccuracy: true, maximumAge: 15000, timeout: 5000 } ); geoWatchId.current = navigator.geolocation.watchPosition( @@ -206,15 +202,15 @@ export const StopMap: React.FC = ({ accuracy: pos.coords.accuracy, }); }, - () => {}, + () => { }, { enableHighAccuracy: true, maximumAge: 30000, timeout: 10000 } ); - } catch {} + } catch { } return () => { if (geoWatchId.current != null && "geolocation" in navigator) { try { navigator.geolocation.clearWatch(geoWatchId.current); - } catch {} + } catch { } } }; }, []); @@ -278,7 +274,7 @@ export const StopMap: React.FC = ({ maxZoom: 17, } as any); } - } catch {} + } catch { } }; const handleCenter = () => { @@ -318,7 +314,7 @@ export const StopMap: React.FC = ({ maxZoom: 17, } as any); } - } catch {} + } catch { } }; return ( @@ -347,7 +343,7 @@ export const StopMap: React.FC = ({ const key = `${c.schedule.shapeId}_${c.currentPosition.shapeIndex}`; const shapeData = shapes[key]; if (!shapeData) return null; - const lineColor = getLineColor(region, c.line); + const lineColor = getLineColour(c.line); return ( = ({ const pts = busPositions.map((c) => c.currentPosition ? map.project([ - c.currentPosition.longitude, - c.currentPosition.latitude, - ]) + c.currentPosition.longitude, + c.currentPosition.latitude, + ]) : null ); for (let i = 0; i < pts.length; i++) { @@ -462,7 +458,7 @@ export const StopMap: React.FC = ({ return busPositions.map((c, idx) => { const p = c.currentPosition!; - const lineColor = getLineColor(region, c.line); + const lineColor = getLineColour(c.line); const showLabel = zoom >= 13; const labelGap = gaps[idx] ?? baseGap; return ( diff --git a/src/frontend/app/components/StopSheet.tsx b/src/frontend/app/components/StopSheet.tsx index 6d2abf0..77bb5f1 100644 --- a/src/frontend/app/components/StopSheet.tsx +++ b/src/frontend/app/components/StopSheet.tsx @@ -3,9 +3,8 @@ import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Sheet } from "react-modal-sheet"; import { Link } from "react-router"; +import { REGION_DATA } from "~/config/RegionConfig"; import type { Stop } from "~/data/StopDataProvider"; -import { useApp } from "../AppContext"; -import { type RegionId, getRegionConfig } from "../config/RegionConfig"; import { type ConsolidatedCirculation } from "../routes/stops-$id"; import { ErrorDisplay } from "./ErrorDisplay"; import LineIcon from "./LineIcon"; @@ -27,12 +26,10 @@ interface ErrorInfo { } const loadConsolidatedData = async ( - region: RegionId, stopId: number ): Promise => { - const regionConfig = getRegionConfig(region); const resp = await fetch( - `${regionConfig.consolidatedCirculationsEndpoint}?stopId=${stopId}`, + `${REGION_DATA.consolidatedCirculationsEndpoint}?stopId=${stopId}`, { headers: { Accept: "application/json", @@ -53,8 +50,6 @@ export const StopSheet: React.FC = ({ stop, }) => { const { t } = useTranslation(); - const { region } = useApp(); - const regionConfig = getRegionConfig(region); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -87,7 +82,7 @@ export const StopSheet: React.FC = ({ setError(null); setData(null); - const stopData = await loadConsolidatedData(region, stop.stopId); + const stopData = await loadConsolidatedData(stop.stopId); setData(stopData); setLastUpdated(new Date()); } catch (err) { @@ -102,15 +97,15 @@ export const StopSheet: React.FC = ({ if (isOpen && stop.stopId) { loadData(); } - }, [isOpen, stop.stopId, region]); + }, [isOpen, stop.stopId]); // Show only the next 4 arrivals const sortedData = data ? [...data].sort( - (a, b) => - (a.realTime?.minutes ?? a.schedule?.minutes ?? 999) - - (b.realTime?.minutes ?? b.schedule?.minutes ?? 999) - ) + (a, b) => + (a.realTime?.minutes ?? a.schedule?.minutes ?? 999) - + (b.realTime?.minutes ?? b.schedule?.minutes ?? 999) + ) : []; const limitedEstimates = sortedData.slice(0, 4); @@ -130,7 +125,7 @@ export const StopSheet: React.FC = ({ > {stop.lines.map((line) => (
    - +
    ))}
  • @@ -166,7 +161,6 @@ export const StopSheet: React.FC = ({ ))} @@ -179,39 +173,39 @@ export const StopSheet: React.FC = ({
    - {lastUpdated && ( -
    - {t("estimates.last_updated", "Actualizado a las")}{" "} - {lastUpdated.toLocaleTimeString(undefined, { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - })} -
    - )} - -
    - - - - {t("map.view_all_estimates", "Ver todas las estimaciones")} - + {lastUpdated && ( +
    + {t("estimates.last_updated", "Actualizado a las")}{" "} + {lastUpdated.toLocaleTimeString(undefined, { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + })}
    + )} + +
    + + + + {t("map.view_all_estimates", "Ver todas las estimaciones")} +
    +
    diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.css b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.css index 3dc33ea..e61ac25 100644 --- a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.css +++ b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.css @@ -1,4 +1,4 @@ -@import "tailwindcss"; +@import '../../tailwind.css'; .consolidated-circulation-card { all: unset; @@ -133,19 +133,19 @@ } .meta-chip.delay-ok { - @apply bg-green-400/30 dark:bg-green-600/30 border-green-500 dark:border-green-700 text-green-800 dark:text-green-200; + @apply bg-green-600/80 dark:bg-green-600/30 border-green-500 dark:border-green-700 text-white dark:text-green-200; } .meta-chip.delay-warn { - @apply bg-yellow-400/30 dark:bg-yellow-600/30 border-yellow-500 dark:border-yellow-700 text-yellow-800 dark:text-yellow-200; + @apply bg-amber-600/80 dark:bg-yellow-600/30 border-yellow-500 dark:border-yellow-700 text-white dark:text-yellow-200; } .meta-chip.delay-critical { - @apply bg-red-400/30 dark:bg-red-600/30 border-red-500 dark:border-red-700 text-white; + @apply bg-red-400/80 dark:bg-red-600/30 border-red-500 dark:border-red-700 text-white; } .meta-chip.delay-early { - @apply bg-blue-400/30 dark:bg-blue-600/30 border-blue-500 dark:border-blue-700 text-blue-800 dark:text-blue-200; + @apply bg-blue-400/80 dark:bg-blue-600/30 border-blue-500 dark:border-blue-700 text-white dark:text-blue-200; } /* GPS Indicator */ diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx index 0b97c11..6f92644 100644 --- a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx +++ b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx @@ -1,6 +1,5 @@ import { useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { type RegionConfig } from "~/config/RegionConfig"; import LineIcon from "~components/LineIcon"; import { type ConsolidatedCirculation } from "~routes/stops-$id"; @@ -8,7 +7,6 @@ import "./ConsolidatedCirculationCard.css"; interface ConsolidatedCirculationCardProps { estimate: ConsolidatedCirculation; - regionConfig: RegionConfig; onMapClick?: () => void; readonly?: boolean; } @@ -72,7 +70,7 @@ const parseServiceId = (serviceId: string): string => { export const ConsolidatedCirculationCard: React.FC< ConsolidatedCirculationCardProps -> = ({ estimate, regionConfig, onMapClick, readonly }) => { +> = ({ estimate, onMapClick, readonly }) => { const { t } = useTranslation(); const formatDistance = (meters: number) => { @@ -171,30 +169,28 @@ export const ConsolidatedCirculationCard: React.FC< const interactiveProps = readonly ? {} : { - onClick: onMapClick, - type: "button" as const, - disabled: !hasGpsPosition, - }; + onClick: onMapClick, + type: "button" as const, + disabled: !hasGpsPosition, + }; return (
    - +
    {estimate.route} @@ -202,9 +198,8 @@ export const ConsolidatedCirculationCard: React.FC< {hasGpsPosition && (
    )} diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx index 4c2916a..547fdf7 100644 --- a/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx +++ b/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx @@ -1,5 +1,4 @@ import { useTranslation } from "react-i18next"; -import { type RegionConfig } from "~/config/RegionConfig"; import { type ConsolidatedCirculation } from "~routes/stops-$id"; import { ConsolidatedCirculationCard } from "./ConsolidatedCirculationCard"; @@ -8,14 +7,12 @@ import "./ConsolidatedCirculationList.css"; interface RegularTableProps { data: ConsolidatedCirculation[]; dataDate: Date | null; - regionConfig: RegionConfig; onCirculationClick?: (estimate: ConsolidatedCirculation, index: number) => void; } export const ConsolidatedCirculationList: React.FC = ({ data, dataDate, - regionConfig, onCirculationClick, }) => { const { t } = useTranslation(); @@ -44,7 +41,6 @@ export const ConsolidatedCirculationList: React.FC = ({ onCirculationClick?.(estimate, idx)} /> ))} diff --git a/src/frontend/app/components/layout/Header.tsx b/src/frontend/app/components/layout/Header.tsx index 2bdd764..b8184f1 100644 --- a/src/frontend/app/components/layout/Header.tsx +++ b/src/frontend/app/components/layout/Header.tsx @@ -1,5 +1,4 @@ import { Menu } from "lucide-react"; -import React from "react"; import "./Header.css"; interface HeaderProps { diff --git a/src/frontend/app/components/ui/Button.css b/src/frontend/app/components/ui/Button.css deleted file mode 100644 index bf02a7c..0000000 --- a/src/frontend/app/components/ui/Button.css +++ /dev/null @@ -1,39 +0,0 @@ -.ui-button { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1.5rem; - border-radius: 0.5rem; - font-size: 0.9rem; - font-weight: 500; - cursor: pointer; - transition: background-color 0.2s ease, transform 0.1s ease; - border: none; -} - -.ui-button:active { - transform: translateY(1px); -} - -.ui-button--primary { - background: var(--button-background-color); - color: white; -} - -.ui-button--primary:hover { - background: var(--button-hover-background-color); -} - -.ui-button--secondary { - background: var(--border-color); - color: var(--text-color); -} - -.ui-button--secondary:hover { - background: #e0e0e0; -} - -.ui-button__icon { - display: flex; - align-items: center; -} diff --git a/src/frontend/app/components/ui/Button.tsx b/src/frontend/app/components/ui/Button.tsx deleted file mode 100644 index 18a15b2..0000000 --- a/src/frontend/app/components/ui/Button.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; -import "./Button.css"; - -interface ButtonProps extends React.ButtonHTMLAttributes { - variant?: "primary" | "secondary" | "outline"; - icon?: React.ReactNode; -} - -export const Button: React.FC = ({ - children, - variant = "primary", - icon, - className = "", - ...props -}) => { - return ( - - ); -}; diff --git a/src/frontend/app/components/ui/PageContainer.css b/src/frontend/app/components/ui/PageContainer.css deleted file mode 100644 index 8a86035..0000000 --- a/src/frontend/app/components/ui/PageContainer.css +++ /dev/null @@ -1,20 +0,0 @@ -.page-container { - max-width: 100%; - padding: 0 16px; - background-color: var(--background-color); - color: var(--text-color); -} - -@media (min-width: 768px) { - .page-container { - width: 90%; - max-width: 768px; - margin: 0 auto; - } -} - -@media (min-width: 1024px) { - .page-container { - max-width: 1024px; - } -} diff --git a/src/frontend/app/components/ui/PageContainer.tsx b/src/frontend/app/components/ui/PageContainer.tsx deleted file mode 100644 index 4c9684a..0000000 --- a/src/frontend/app/components/ui/PageContainer.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from "react"; -import "./PageContainer.css"; - -interface PageContainerProps { - children: React.ReactNode; - className?: string; -} - -export const PageContainer: React.FC = ({ - children, - className = "", -}) => { - return
    {children}
    ; -}; -- cgit v1.3