From 093ee906eae5361bbf47ae2fdc4003f95696656a Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Thu, 6 Nov 2025 15:44:58 +0100 Subject: Rename schedules table --- .../app/components/EstimatesTableSkeleton.tsx | 114 ------------- src/frontend/app/components/SchedulesTable.css | 187 +++++++++++++++++++++ src/frontend/app/components/SchedulesTable.tsx | 167 ++++++++++++++++++ .../app/components/SchedulesTableSkeleton.tsx | 114 +++++++++++++ src/frontend/app/components/TimetableTable.css | 187 --------------------- src/frontend/app/components/TimetableTable.tsx | 167 ------------------ 6 files changed, 468 insertions(+), 468 deletions(-) delete mode 100644 src/frontend/app/components/EstimatesTableSkeleton.tsx create mode 100644 src/frontend/app/components/SchedulesTable.css create mode 100644 src/frontend/app/components/SchedulesTable.tsx create mode 100644 src/frontend/app/components/SchedulesTableSkeleton.tsx delete mode 100644 src/frontend/app/components/TimetableTable.css delete mode 100644 src/frontend/app/components/TimetableTable.tsx (limited to 'src/frontend/app/components') diff --git a/src/frontend/app/components/EstimatesTableSkeleton.tsx b/src/frontend/app/components/EstimatesTableSkeleton.tsx deleted file mode 100644 index 2ef770b..0000000 --- a/src/frontend/app/components/EstimatesTableSkeleton.tsx +++ /dev/null @@ -1,114 +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 EstimatesTableSkeleton: 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 = ({ - 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/SchedulesTable.css b/src/frontend/app/components/SchedulesTable.css new file mode 100644 index 0000000..8980fb4 --- /dev/null +++ b/src/frontend/app/components/SchedulesTable.css @@ -0,0 +1,187 @@ +.timetable-container { + margin-top: 2rem; +} + +.timetable-caption { + font-weight: bold; + margin-bottom: 1rem; + text-align: left; + font-size: 1.1rem; + color: var(--text-primary, #333); +} + +.timetable-cards { + display: flex; + flex-direction: column; + gap: 1rem; + margin-bottom: 1rem; +} + +.timetable-card { + background-color: var(--surface-future, #fff); + border: 1px solid var(--card-border, #e0e0e0); + border-radius: 10px; + padding: 1.25rem; + transition: background-color 0.2s ease, border 0.2s ease; +} + +.timetable-card.timetable-past { + background-color: var(--surface-past, #f3f3f3); + color: var(--text-secondary, #aaa); + border: 1px solid #e0e0e0; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.line-info { + flex-shrink: 0; +} + +.destination-info { + flex: 1; + text-align: left; + margin: 0 1rem; + color: var(--text-primary, #333); +} + +.destination-info strong { + font-size: 0.95rem; +} + +.timetable-card.timetable-past .destination-info { + color: var(--text-secondary, #aaa); +} + +.time-info { + display: flex; + flex-direction: column; + align-items: flex-end; + flex-shrink: 0; +} + +.departure-time { + font-weight: bold; + font-family: monospace; + font-size: 1.1rem; + color: var(--text-primary, #333); +} + +.timetable-card.timetable-past .departure-time { + color: var(--text-secondary, #aaa); +} + +.card-body { + line-height: 1.4; +} + +.route-streets { + font-size: 0.85rem; + color: var(--text-secondary, #666); + line-height: 1.8; + word-break: break-word; +} + +.service-id { + font-family: monospace; + font-size: 0.8rem; + color: var(--text-secondary, #666); + background: var(--service-background, #f0f0f0); + 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, #bbb); + background: #e8e8e8; +} + +.no-data { + text-align: center; + color: var(--text-secondary, #666); + font-style: italic; + padding: 2rem; + background: var(--card-background, #f8f9fa); + border-radius: 8px; + border: 1px solid var(--card-border, #e0e0e0); +} + +/* Responsive design */ +@media (max-width: 768px) { + .timetable-cards { + gap: 0.5rem; + } + .timetable-card { + padding: 0.75rem; + } + .card-header { + margin-bottom: 0.5rem; + } + .destination-info { + margin: 0 0.5rem; + } + .destination-info strong { + font-size: 0.9rem; + } + .departure-time { + font-size: 1rem; + } + .service-id { + font-size: 0.8rem; + padding: 0.2rem 0.4rem; + } +} + +@media (max-width: 480px) { + .card-header { + flex-direction: column; + align-items: stretch; + gap: 0.5rem; + } + + .destination-info { + text-align: left; + margin: 0; + order: 2; + } + + .time-info { + align-items: flex-start; + order: 1; + align-self: flex-end; + } + + .line-info { + order: 0; + align-self: flex-start; + } + + /* Create a flex container for line and time on mobile */ + .card-header { + position: relative; + } + + .line-info { + position: absolute; + left: 0; + top: 0; + } + + .time-info { + position: absolute; + right: 0; + top: 0; + } + + .destination-info { + margin-top: 2rem; + text-align: left; + } +} diff --git a/src/frontend/app/components/SchedulesTable.tsx b/src/frontend/app/components/SchedulesTable.tsx new file mode 100644 index 0000000..afa6f8e --- /dev/null +++ b/src/frontend/app/components/SchedulesTable.tsx @@ -0,0 +1,167 @@ +import { useTranslation } from "react-i18next"; +import LineIcon from "./LineIcon"; +import "./SchedulesTable.css"; +import { useApp } from "~/AppContext"; + +export interface ScheduledTable { + line: { + name: string; + colour: string; + }; + trip: { + id: string; + service_id: string; + headsign: string; + direction_id: number; + }; + route_id: string; + departure_time: string; + arrival_time: string; + stop_sequence: number; + shape_dist_traveled: number; + next_streets: 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 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 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.departure_time) - timeToMinutes(b.departure_time) + ); + + let currentIndex = sortedEntries.findIndex(entry => + timeToMinutes(entry.departure_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.departure_time); + const isPast = entryMinutes < nowMinutes; + return ( +
+
+
+ +
+ +
+ {entry.trip.headsign && entry.trip.headsign.trim() ? ( + {entry.trip.headsign} + ) : ( + {t("timetable.noDestination", "Línea")} {entry.line.name} + )} +
+ +
+ + {entry.departure_time.slice(0, 5)} + +
+
+
+
+ + {parseServiceId(entry.trip.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 new file mode 100644 index 0000000..50ba94d --- /dev/null +++ b/src/frontend/app/components/SchedulesTableSkeleton.tsx @@ -0,0 +1,114 @@ +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 = ({ + 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/TimetableTable.css b/src/frontend/app/components/TimetableTable.css deleted file mode 100644 index 8980fb4..0000000 --- a/src/frontend/app/components/TimetableTable.css +++ /dev/null @@ -1,187 +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, #333); -} - -.timetable-cards { - display: flex; - flex-direction: column; - gap: 1rem; - margin-bottom: 1rem; -} - -.timetable-card { - background-color: var(--surface-future, #fff); - border: 1px solid var(--card-border, #e0e0e0); - border-radius: 10px; - padding: 1.25rem; - transition: background-color 0.2s ease, border 0.2s ease; -} - -.timetable-card.timetable-past { - background-color: var(--surface-past, #f3f3f3); - color: var(--text-secondary, #aaa); - border: 1px solid #e0e0e0; -} - -.card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; -} - -.line-info { - flex-shrink: 0; -} - -.destination-info { - flex: 1; - text-align: left; - margin: 0 1rem; - color: var(--text-primary, #333); -} - -.destination-info strong { - font-size: 0.95rem; -} - -.timetable-card.timetable-past .destination-info { - color: var(--text-secondary, #aaa); -} - -.time-info { - display: flex; - flex-direction: column; - align-items: flex-end; - flex-shrink: 0; -} - -.departure-time { - font-weight: bold; - font-family: monospace; - font-size: 1.1rem; - color: var(--text-primary, #333); -} - -.timetable-card.timetable-past .departure-time { - color: var(--text-secondary, #aaa); -} - -.card-body { - line-height: 1.4; -} - -.route-streets { - font-size: 0.85rem; - color: var(--text-secondary, #666); - line-height: 1.8; - word-break: break-word; -} - -.service-id { - font-family: monospace; - font-size: 0.8rem; - color: var(--text-secondary, #666); - background: var(--service-background, #f0f0f0); - 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, #bbb); - background: #e8e8e8; -} - -.no-data { - text-align: center; - color: var(--text-secondary, #666); - font-style: italic; - padding: 2rem; - background: var(--card-background, #f8f9fa); - border-radius: 8px; - border: 1px solid var(--card-border, #e0e0e0); -} - -/* Responsive design */ -@media (max-width: 768px) { - .timetable-cards { - gap: 0.5rem; - } - .timetable-card { - padding: 0.75rem; - } - .card-header { - margin-bottom: 0.5rem; - } - .destination-info { - margin: 0 0.5rem; - } - .destination-info strong { - font-size: 0.9rem; - } - .departure-time { - font-size: 1rem; - } - .service-id { - font-size: 0.8rem; - padding: 0.2rem 0.4rem; - } -} - -@media (max-width: 480px) { - .card-header { - flex-direction: column; - align-items: stretch; - gap: 0.5rem; - } - - .destination-info { - text-align: left; - margin: 0; - order: 2; - } - - .time-info { - align-items: flex-start; - order: 1; - align-self: flex-end; - } - - .line-info { - order: 0; - align-self: flex-start; - } - - /* Create a flex container for line and time on mobile */ - .card-header { - position: relative; - } - - .line-info { - position: absolute; - left: 0; - top: 0; - } - - .time-info { - position: absolute; - right: 0; - top: 0; - } - - .destination-info { - margin-top: 2rem; - text-align: left; - } -} diff --git a/src/frontend/app/components/TimetableTable.tsx b/src/frontend/app/components/TimetableTable.tsx deleted file mode 100644 index 8215141..0000000 --- a/src/frontend/app/components/TimetableTable.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { useTranslation } from "react-i18next"; -import LineIcon from "./LineIcon"; -import "./TimetableTable.css"; -import { useApp } from "../AppContext"; - -export interface TimetableEntry { - line: { - name: string; - colour: string; - }; - trip: { - id: string; - service_id: string; - headsign: string; - direction_id: number; - }; - route_id: string; - departure_time: string; - arrival_time: string; - stop_sequence: number; - shape_dist_traveled: number; - next_streets: string[]; -} - -interface TimetableTableProps { - data: TimetableEntry[]; - 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 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 find nearby entries -const findNearbyEntries = (entries: TimetableEntry[], currentTime: string, before: number = 4, after: number = 4): TimetableEntry[] => { - if (!currentTime) return entries.slice(0, before + after); - - const currentMinutes = timeToMinutes(currentTime); - const sortedEntries = [...entries].sort((a, b) => - timeToMinutes(a.departure_time) - timeToMinutes(b.departure_time) - ); - - let currentIndex = sortedEntries.findIndex(entry => - timeToMinutes(entry.departure_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 TimetableTable: 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.departure_time); - const isPast = entryMinutes < nowMinutes; - return ( -
-
-
- -
- -
- {entry.trip.headsign && entry.trip.headsign.trim() ? ( - {entry.trip.headsign} - ) : ( - {t("timetable.noDestination", "Línea")} {entry.line.name} - )} -
- -
- - {entry.departure_time.slice(0, 5)} - -
-
-
-
- - {parseServiceId(entry.trip.service_id)} - - {entry.next_streets.length > 0 && ( - — {entry.next_streets.join(' — ')} - )} -
-
-
- ); - })} -
- {displayData.length === 0 && ( -

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

- )} -
- ); -}; -- cgit v1.3