aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/components
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-11-30 20:49:48 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2025-11-30 20:49:48 +0100
commita68ba30716062b265f85c4be078a736c7135d7bc (patch)
treedd079a2d3860349402ad5b614659fedcb90c2b99 /src/frontend/app/components
parentcee521142a4e0673b155d97c3e4825b7fec1987f (diff)
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.
Diffstat (limited to 'src/frontend/app/components')
-rw-r--r--src/frontend/app/components/GroupedTable.tsx90
-rw-r--r--src/frontend/app/components/LineIcon.css12
-rw-r--r--src/frontend/app/components/LineIcon.tsx20
-rw-r--r--src/frontend/app/components/PullToRefresh.css2
-rw-r--r--src/frontend/app/components/PullToRefresh.tsx13
-rw-r--r--src/frontend/app/components/RegularTable.tsx94
-rw-r--r--src/frontend/app/components/SchedulesTable.css208
-rw-r--r--src/frontend/app/components/SchedulesTable.tsx213
-rw-r--r--src/frontend/app/components/SchedulesTableSkeleton.tsx136
-rw-r--r--src/frontend/app/components/StopGalleryItem.tsx12
-rw-r--r--src/frontend/app/components/StopItem.tsx9
-rw-r--r--src/frontend/app/components/StopMapModal.tsx32
-rw-r--r--src/frontend/app/components/StopMapSheet.tsx38
-rw-r--r--src/frontend/app/components/StopSheet.tsx84
-rw-r--r--src/frontend/app/components/Stops/ConsolidatedCirculationCard.css10
-rw-r--r--src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx37
-rw-r--r--src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx4
-rw-r--r--src/frontend/app/components/layout/Header.tsx1
-rw-r--r--src/frontend/app/components/ui/Button.css39
-rw-r--r--src/frontend/app/components/ui/Button.tsx25
-rw-r--r--src/frontend/app/components/ui/PageContainer.css20
-rw-r--r--src/frontend/app/components/ui/PageContainer.tsx14
22 files changed, 121 insertions, 992 deletions
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<GroupedTable> = ({
- 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<string, typeof data>
- );
-
- const sortedLines = Object.keys(groupedEstimates).sort((a, b) => {
- const firstArrivalA = groupedEstimates[a][0].minutes;
- const firstArrivalB = groupedEstimates[b][0].minutes;
- return firstArrivalA - firstArrivalB;
- });
-
- return (
- <table className="table">
- <caption>
- Estimaciones de llegadas a las {dataDate?.toLocaleTimeString()}
- </caption>
-
- <thead>
- <tr>
- <th>Línea</th>
- <th>Ruta</th>
- <th>Llegada</th>
- {regionConfig.showMeters && <th>Distancia</th>}
- </tr>
- </thead>
-
- <tbody>
- {sortedLines.map((line) =>
- groupedEstimates[line].map((estimate, idx) => (
- <tr key={`${line}-${idx}`}>
- {idx === 0 && (
- <td rowSpan={groupedEstimates[line].length}>
- <LineIcon line={line} region={regionConfig.id} />
- </td>
- )}
- <td>{estimate.route}</td>
- <td>{`${estimate.minutes} min`}</td>
- {regionConfig.showMeters && (
- <td>
- {estimate.meters > -1
- ? formatDistance(estimate.meters)
- : "No disponible"}
- </td>
- )}
- </tr>
- ))
- )}
- </tbody>
-
- {data?.length === 0 && (
- <tfoot>
- <tr>
- <td colSpan={regionConfig.showMeters ? 4 : 3}>
- No hay estimaciones disponibles
- </td>
- </tr>
- </tfoot>
- )}
- </table>
- );
-};
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<LineIconProps> = ({
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<LineIconProps> = ({
} as React.CSSProperties
}
>
- {line}
+ {actualLine}
</span>
);
};
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<PullToRefreshProps> = ({
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<PullToRefreshProps> = ({
return (
<div className="pull-to-refresh-container" ref={containerRef}>
- {/* Simple indicator */}
{isPulling && (
<motion.div className="pull-to-refresh-indicator" style={{ opacity }}>
<motion.div
@@ -174,7 +164,6 @@ export const PullToRefresh: React.FC<PullToRefreshProps> = ({
</motion.div>
)}
- {/* Normal content - no transform interference */}
<div className="pull-to-refresh-content">{children}</div>
</div>
);
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<RegularTableProps> = ({
- 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 (
- <table className="table">
- <caption>
- {t("estimates.caption", "Estimaciones de llegadas a las {{time}}", {
- time: dataDate?.toLocaleTimeString(),
- })}
- </caption>
-
- <thead>
- <tr>
- <th>{t("estimates.line", "Línea")}</th>
- <th>{t("estimates.route", "Ruta")}</th>
- <th>{t("estimates.arrival", "Llegada")}</th>
- {regionConfig.showMeters && (
- <th>{t("estimates.distance", "Distancia")}</th>
- )}
- </tr>
- </thead>
-
- <tbody>
- {data
- .sort((a, b) => a.minutes - b.minutes)
- .map((estimate, idx) => (
- <tr key={idx}>
- <td>
- <LineIcon line={estimate.line} region={regionConfig.id} />
- </td>
- <td>{estimate.route}</td>
- <td>
- {estimate.minutes > 15
- ? absoluteArrivalTime(estimate.minutes)
- : `${estimate.minutes} ${t("estimates.minutes", "min")}`}
- </td>
- {regionConfig.showMeters && (
- <td>
- {estimate.meters > -1
- ? formatDistance(estimate.meters)
- : t("estimates.not_available", "No disponible")}
- </td>
- )}
- </tr>
- ))}
- </tbody>
-
- {data?.length === 0 && (
- <tfoot>
- <tr>
- <td colSpan={regionConfig.showMeters ? 4 : 3}>
- {t("estimates.none", "No hay estimaciones disponibles")}
- </td>
- </tr>
- </tfoot>
- )}
- </table>
- );
-};
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<TimetableTableProps> = ({
- 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 (
- <div className="timetable-container">
- <div className="timetable-caption">
- {showAll
- ? t("timetable.fullCaption", "Horarios teóricos de la parada")
- : t("timetable.nearbyCaption", "Próximos horarios teóricos")}
- </div>
-
- <div className="timetable-cards">
- {displayData.map((entry, index) => {
- const entryMinutes = timeToMinutes(entry.calling_time);
- const isPast = entryMinutes < nowMinutes;
- return (
- <div
- key={`${entry.trip_id}-${index}`}
- className={`timetable-card${isPast ? " timetable-past" : ""}`}
- style={{
- background: isPast
- ? "var(--surface-past, #f3f3f3)"
- : "var(--surface-future, #fff)",
- }}
- >
- <div className="card-header">
- <div className="line-info">
- <LineIcon line={entry.line} region={region} />
- </div>
-
- <div className="destination-info">
- {entry.route && entry.route.trim() ? (
- <strong>{entry.route}</strong>
- ) : (
- <strong>
- {t("timetable.noDestination", "Línea")} {entry.line}
- </strong>
- )}
- </div>
-
- <div className="time-info">
- <span className="departure-time">
- {formatTimeForDisplay(entry.calling_time)}
- </span>
- </div>
- </div>
- <div className="card-body">
- <div className="route-streets">
- <span className="service-id">
- {parseServiceId(entry.service_id)}
- </span>
- {entry.next_streets.length > 0 && (
- <span> — {entry.next_streets.join(" — ")}</span>
- )}
- </div>
- </div>
- </div>
- );
- })}
- </div>
- {displayData.length === 0 && (
- <p className="no-data">
- {t("timetable.noData", "No hay datos de horarios disponibles")}
- </p>
- )}
- </div>
- );
-};
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<EstimatesTableSkeletonProps> = ({
- rows = 3,
-}) => {
- const { t } = useTranslation();
-
- return (
- <SkeletonTheme baseColor="#f0f0f0" highlightColor="#e0e0e0">
- <table className="table">
- <caption>
- <Skeleton width="250px" />
- </caption>
-
- <thead>
- <tr>
- <th>{t("estimates.line", "Línea")}</th>
- <th>{t("estimates.route", "Ruta")}</th>
- <th>{t("estimates.arrival", "Llegada")}</th>
- <th>{t("estimates.distance", "Distancia")}</th>
- </tr>
- </thead>
-
- <tbody>
- {Array.from({ length: rows }, (_, index) => (
- <tr key={`skeleton-${index}`}>
- <td>
- <Skeleton
- width="40px"
- height="24px"
- style={{ borderRadius: "4px" }}
- />
- </td>
- <td>
- <Skeleton width="120px" />
- </td>
- <td>
- <div
- style={{
- display: "flex",
- flexDirection: "column",
- gap: "2px",
- }}
- >
- <Skeleton width="60px" />
- <Skeleton width="40px" />
- </div>
- </td>
- <td>
- <Skeleton width="50px" />
- </td>
- </tr>
- ))}
- </tbody>
- </table>
- </SkeletonTheme>
- );
-};
-
-interface EstimatesGroupedSkeletonProps {
- groups?: number;
- rowsPerGroup?: number;
-}
-
-export const EstimatesGroupedSkeleton: React.FC<
- EstimatesGroupedSkeletonProps
-> = ({ groups = 3, rowsPerGroup = 2 }) => {
- const { t } = useTranslation();
-
- return (
- <SkeletonTheme baseColor="#f0f0f0" highlightColor="#e0e0e0">
- <table className="table grouped-table">
- <caption>
- <Skeleton width="250px" />
- </caption>
-
- <thead>
- <tr>
- <th>{t("estimates.line", "Línea")}</th>
- <th>{t("estimates.route", "Ruta")}</th>
- <th>{t("estimates.arrival", "Llegada")}</th>
- <th>{t("estimates.distance", "Distancia")}</th>
- </tr>
- </thead>
-
- <tbody>
- {Array.from({ length: groups }, (_, groupIndex) => (
- <React.Fragment key={`group-${groupIndex}`}>
- {Array.from({ length: rowsPerGroup }, (_, rowIndex) => (
- <tr
- key={`skeleton-${groupIndex}-${rowIndex}`}
- className={rowIndex === 0 ? "group-start" : ""}
- >
- <td>
- {rowIndex === 0 && (
- <Skeleton
- width="40px"
- height="24px"
- style={{ borderRadius: "4px" }}
- />
- )}
- </td>
- <td>
- <Skeleton width="120px" />
- </td>
- <td>
- <div
- style={{
- display: "flex",
- flexDirection: "column",
- gap: "2px",
- }}
- >
- <Skeleton width="60px" />
- <Skeleton width="40px" />
- </div>
- </td>
- <td>
- <Skeleton width="50px" />
- </td>
- </tr>
- ))}
- </React.Fragment>
- ))}
- </tbody>
- </table>
- </SkeletonTheme>
- );
-};
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<StopGalleryItemProps> = ({ stop }) => {
- const { region } = useApp();
-
return (
<div className="gallery-item">
- <Link className="gallery-item-link" to={`/estimates/${stop.stopId}`}>
+ <Link className="gallery-item-link" to={`/stops/${stop.stopId}`}>
<div className="gallery-item-header">
{stop.favourite && <span className="favourite-icon">★</span>}
<span className="gallery-item-code">({stop.stopId})</span>
</div>
<div className="gallery-item-name">
- {StopDataProvider.getDisplayName(region, stop)}
+ {StopDataProvider.getDisplayName(stop)}
</div>
<div className="gallery-item-lines">
{stop.lines?.slice(0, 5).map((line) => (
- <LineIcon key={line} line={line} region={region} />
+ <LineIcon key={line} line={line} />
))}
{stop.lines && stop.lines.length > 5 && (
<span className="more-lines">+{stop.lines.length - 5}</span>
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<StopItemProps> = ({ stop }) => {
- const { region } = useApp();
-
return (
<li className="list-item">
- <Link className="list-item-link" to={`/estimates/${stop.stopId}`}>
+ <Link className="list-item-link" to={`/stops/${stop.stopId}`}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
<span style={{ fontWeight: 600 }}>
{stop.favourite && <span className="favourite-icon">★</span>}
- {StopDataProvider.getDisplayName(region, stop)}
+ {StopDataProvider.getDisplayName(stop)}
</span>
<span style={{ fontSize: "0.85em", color: "var(--subtitle-color)", marginLeft: "0.5rem" }}>
({stop.stopId})
@@ -25,7 +22,7 @@ const StopItem: React.FC<StopItemProps> = ({ stop }) => {
</div>
<div className="line-icons" style={{ marginTop: "0.25rem" }}>
{stop.lines?.map((line) => (
- <LineIcon key={line} line={line} region={region} />
+ <LineIcon key={line} line={line} />
))}
</div>
</Link>
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<StopMapModalProps> = ({
stop,
circulations,
- region,
isOpen,
onClose,
selectedCirculationId,
@@ -59,8 +57,6 @@ export const StopMapModal: React.FC<StopMapModalProps> = ({
const [shapeData, setShapeData] = useState<any | null>(null);
const [previousShapeData, setPreviousShapeData] = useState<any | null>(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<StopMapModalProps> = ({
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<StopMapModalProps> = ({
!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<StopMapModalProps> = ({
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<StopMapModalProps> = ({
};
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<StopMapModalProps> = ({
}}
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<StopMapModalProps> = ({
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<StopMapModalProps> = ({
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<StopMapModalProps> = ({
>
<path
d="M12 2 L22 22 L12 17 L2 22 Z"
- fill={getLineColor(region, selectedBus.line).background}
+ fill={getLineColour(selectedBus.line).background}
stroke="#000"
strokeWidth="2"
strokeLinejoin="round"
diff --git a/src/frontend/app/components/StopMapSheet.tsx b/src/frontend/app/components/StopMapSheet.tsx
index afad1c3..b00ca1c 100644
--- a/src/frontend/app/components/StopMapSheet.tsx
+++ b/src/frontend/app/components/StopMapSheet.tsx
@@ -2,8 +2,8 @@ import maplibregl from "maplibre-gl";
import React, { useEffect, useMemo, useRef, useState } from "react";
import Map, { Layer, Marker, Source, type MapRef } from "react-map-gl/maplibre";
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 "./StopMapSheet.css";
@@ -28,13 +28,11 @@ export interface ConsolidatedCirculationForMap {
interface StopMapProps {
stop: Stop;
circulations: ConsolidatedCirculationForMap[];
- region: RegionId;
}
export const StopMap: React.FC<StopMapProps> = ({
stop,
- circulations,
- region,
+ circulations
}) => {
const { theme } = useApp();
const [styleSpec, setStyleSpec] = useState<any | null>(null);
@@ -70,18 +68,16 @@ export const StopMap: React.FC<StopMapProps> = ({
const [showAttribution, setShowAttribution] = useState(false);
const [shapes, setShapes] = useState<Record<string, any>>({});
- 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<StopMapProps> = ({
}
}
});
- }, [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<StopMapProps> = ({
accuracy: pos.coords.accuracy,
});
},
- () => {},
+ () => { },
{ enableHighAccuracy: true, maximumAge: 15000, timeout: 5000 }
);
geoWatchId.current = navigator.geolocation.watchPosition(
@@ -206,15 +202,15 @@ export const StopMap: React.FC<StopMapProps> = ({
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<StopMapProps> = ({
maxZoom: 17,
} as any);
}
- } catch {}
+ } catch { }
};
const handleCenter = () => {
@@ -318,7 +314,7 @@ export const StopMap: React.FC<StopMapProps> = ({
maxZoom: 17,
} as any);
}
- } catch {}
+ } catch { }
};
return (
@@ -347,7 +343,7 @@ export const StopMap: React.FC<StopMapProps> = ({
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 (
<Source
@@ -439,9 +435,9 @@ export const StopMap: React.FC<StopMapProps> = ({
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<StopMapProps> = ({
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<ConsolidatedCirculation[]> => {
- 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<StopSheetProps> = ({
stop,
}) => {
const { t } = useTranslation();
- const { region } = useApp();
- const regionConfig = getRegionConfig(region);
const [data, setData] = useState<ConsolidatedCirculation[] | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorInfo | null>(null);
@@ -87,7 +82,7 @@ export const StopSheet: React.FC<StopSheetProps> = ({
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<StopSheetProps> = ({
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<StopSheetProps> = ({
>
{stop.lines.map((line) => (
<div key={line} className="stop-sheet-line-icon">
- <LineIcon line={line} region={region} mode="rounded" />
+ <LineIcon line={line} mode="rounded" />
</div>
))}
</div>
@@ -166,7 +161,6 @@ export const StopSheet: React.FC<StopSheetProps> = ({
<ConsolidatedCirculationCard
key={idx}
estimate={estimate}
- regionConfig={regionConfig}
readonly
/>
))}
@@ -179,39 +173,39 @@ export const StopSheet: React.FC<StopSheetProps> = ({
</Sheet.Content>
<div className="stop-sheet-footer">
- {lastUpdated && (
- <div className="stop-sheet-timestamp">
- {t("estimates.last_updated", "Actualizado a las")}{" "}
- {lastUpdated.toLocaleTimeString(undefined, {
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit",
- })}
- </div>
- )}
+ {lastUpdated && (
+ <div className="stop-sheet-timestamp">
+ {t("estimates.last_updated", "Actualizado a las")}{" "}
+ {lastUpdated.toLocaleTimeString(undefined, {
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ })}
+ </div>
+ )}
- <div className="stop-sheet-actions">
- <button
- className="stop-sheet-reload"
- onClick={loadData}
- disabled={loading}
- title={t("estimates.reload", "Recargar estimaciones")}
- >
- <RefreshCw
- className={`reload-icon ${loading ? "spinning" : ""}`}
- />
- {t("estimates.reload", "Recargar")}
- </button>
+ <div className="stop-sheet-actions">
+ <button
+ className="stop-sheet-reload"
+ onClick={loadData}
+ disabled={loading}
+ title={t("estimates.reload", "Recargar estimaciones")}
+ >
+ <RefreshCw
+ className={`reload-icon ${loading ? "spinning" : ""}`}
+ />
+ {t("estimates.reload", "Recargar")}
+ </button>
- <Link
- to={`/stops/${stop.stopId}`}
- className="stop-sheet-view-all"
- onClick={onClose}
- >
- {t("map.view_all_estimates", "Ver todas las estimaciones")}
- </Link>
- </div>
+ <Link
+ to={`/stops/${stop.stopId}`}
+ className="stop-sheet-view-all"
+ onClick={onClose}
+ >
+ {t("map.view_all_estimates", "Ver todas las estimaciones")}
+ </Link>
</div>
+ </div>
</Sheet.Container>
<Sheet.Backdrop onTap={onClose} />
</Sheet>
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 (
<Tag
- className={`consolidated-circulation-card ${
- readonly
- ? !hasGpsPosition
- ? "no-gps"
- : ""
- : hasGpsPosition
+ className={`consolidated-circulation-card ${readonly
+ ? !hasGpsPosition
+ ? "no-gps"
+ : ""
+ : hasGpsPosition
? "has-gps"
: "no-gps"
- }`}
+ }`}
{...interactiveProps}
- aria-label={`${hasGpsPosition ? "View" : "No GPS data for"} ${
- estimate.line
- } to ${estimate.route}${hasGpsPosition ? " on map" : ""}`}
+ aria-label={`${hasGpsPosition ? "View" : "No GPS data for"} ${estimate.line
+ } to ${estimate.route}${hasGpsPosition ? " on map" : ""}`}
>
<div className="card-row main">
<div className="line-info">
- <LineIcon line={estimate.line} region={regionConfig.id} mode="pill" />
+ <LineIcon line={estimate.line} mode="pill" />
</div>
<div className="route-info">
<strong>{estimate.route}</strong>
@@ -202,9 +198,8 @@ export const ConsolidatedCirculationCard: React.FC<
{hasGpsPosition && (
<div className="gps-indicator" title="Live GPS tracking">
<span
- className={`gps-pulse ${
- estimate.isPreviousTrip ? "previous-trip" : ""
- }`}
+ className={`gps-pulse ${estimate.isPreviousTrip ? "previous-trip" : ""
+ }`}
/>
</div>
)}
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<RegularTableProps> = ({
data,
dataDate,
- regionConfig,
onCirculationClick,
}) => {
const { t } = useTranslation();
@@ -44,7 +41,6 @@ export const ConsolidatedCirculationList: React.FC<RegularTableProps> = ({
<ConsolidatedCirculationCard
key={idx}
estimate={estimate}
- regionConfig={regionConfig}
onMapClick={() => 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<HTMLButtonElement> {
- variant?: "primary" | "secondary" | "outline";
- icon?: React.ReactNode;
-}
-
-export const Button: React.FC<ButtonProps> = ({
- children,
- variant = "primary",
- icon,
- className = "",
- ...props
-}) => {
- return (
- <button
- className={`ui-button ui-button--${variant} ${className}`}
- {...props}
- >
- {icon && <span className="ui-button__icon">{icon}</span>}
- {children}
- </button>
- );
-};
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<PageContainerProps> = ({
- children,
- className = "",
-}) => {
- return <div className={`page-container ${className}`}>{children}</div>;
-};