aboutsummaryrefslogtreecommitdiff
path: root/src/frontend
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-12-08 01:37:10 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2025-12-08 01:37:10 +0100
commit107295575e3a7c37911ae192baf426b0003975a4 (patch)
treeceb528a428716d5313517a0fa72fcac3ea1360fb /src/frontend
parent3b3fd2f6880eaa9170b480d41d43311925483bea (diff)
Refactor code structure for improved readability and maintainability; removed redundant code blocks and optimized functions.
Diffstat (limited to 'src/frontend')
-rw-r--r--src/frontend/app/AppContext.tsx13
-rw-r--r--src/frontend/app/components/LineIcon.css10
-rw-r--r--src/frontend/app/components/Stops/ConsolidatedCirculationCard.css1
-rw-r--r--src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx96
-rw-r--r--src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx3
-rw-r--r--src/frontend/app/config/RegionConfig.ts25
-rw-r--r--src/frontend/app/routes/home.tsx10
-rw-r--r--src/frontend/app/routes/stops-$id.tsx5
8 files changed, 127 insertions, 36 deletions
diff --git a/src/frontend/app/AppContext.tsx b/src/frontend/app/AppContext.tsx
index 59f2724..12a54da 100644
--- a/src/frontend/app/AppContext.tsx
+++ b/src/frontend/app/AppContext.tsx
@@ -3,11 +3,11 @@ import { type ReactNode } from "react";
import { type RegionId } from "./config/RegionConfig";
import { MapProvider, useMap } from "./contexts/MapContext";
import {
- SettingsProvider,
- useSettings,
- type MapPositionMode,
- type TableStyle,
- type Theme,
+ SettingsProvider,
+ useSettings,
+ type MapPositionMode,
+ type TableStyle,
+ type Theme,
} from "./contexts/SettingsContext";
// Re-export types for compatibility
@@ -21,6 +21,9 @@ export const useApp = () => {
return {
...settings,
...map,
+ // Mock region support for now since we only have one region
+ region: "vigo" as RegionId,
+ setRegion: (region: RegionId) => { console.log("Set region", region); },
};
};
diff --git a/src/frontend/app/components/LineIcon.css b/src/frontend/app/components/LineIcon.css
index 6363c85..6492d39 100644
--- a/src/frontend/app/components/LineIcon.css
+++ b/src/frontend/app/components/LineIcon.css
@@ -87,6 +87,16 @@
--line-gol: hsl(208, 68%, 66%);
--line-gol-text: hsl(0, 0%, 100%);
+ --line-md: hsl(316, 99%, 27%);
+ --line-md-text: hsl(0, 0%, 100%);
+ --line-ave: hsl(316, 99%, 27%);
+ --line-ave-text: hsl(0, 0%, 100%);
+ --line-alvia: hsl(316, 99%, 27%);
+ --line-alvia-text: hsl(0, 0%, 100%);
+ --line-trencelta: hsl(135, 58%, 25%);
+ --line-trencelta-text: hsl(0, 0%, 100%);
+ --line-regional: hsl(316, 99%, 27%);
+ --line-regional-text: hsl(0, 0%, 100%);
}
.line-icon-default {
diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.css b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.css
index 57d30c8..9922b03 100644
--- a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.css
+++ b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.css
@@ -61,7 +61,6 @@
}
.consolidated-circulation-card .route-info strong {
- font-size: 1rem;
color: var(--text-color);
overflow: hidden;
text-overflow: ellipsis;
diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx
index 635c0ce..70a9355 100644
--- a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx
+++ b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx
@@ -1,4 +1,4 @@
-import { useMemo } from "react";
+import { useEffect, useMemo, useRef, useState } from "react";
import Marquee from 'react-fast-marquee';
import { useTranslation } from "react-i18next";
import LineIcon from "~components/LineIcon";
@@ -12,6 +12,7 @@ interface ConsolidatedCirculationCardProps {
onMapClick?: () => void;
readonly?: boolean;
reduced?: boolean;
+ driver?: string;
}
// Utility function to parse service ID and get the turn number
@@ -71,9 +72,52 @@ const parseServiceId = (serviceId: string): string => {
return `${displayLine}-${turnNumber}`;
};
+const AutoMarquee = ({ text }: { text: string }) => {
+ const containerRef = useRef<HTMLDivElement>(null);
+ const [shouldScroll, setShouldScroll] = useState(false);
+
+ useEffect(() => {
+ const el = containerRef.current;
+ if (!el) return;
+
+ const checkScroll = () => {
+ // 9px per char for text-sm font-mono is a safe upper bound estimate
+ // (14px * 0.6 = 8.4px)
+ const charWidth = 9;
+ const availableWidth = el.offsetWidth;
+ const textWidth = text.length * charWidth;
+
+ setShouldScroll(textWidth > availableWidth);
+ };
+
+ checkScroll();
+
+ const observer = new ResizeObserver(checkScroll);
+ observer.observe(el);
+
+ return () => observer.disconnect();
+ }, [text]);
+
+ if (shouldScroll) {
+ return (
+ <div ref={containerRef} className="w-full overflow-hidden">
+ <Marquee speed={60} gradient={false}>
+ <div className="mr-64 text-sm font-mono">{text}</div>
+ </Marquee>
+ </div>
+ );
+ }
+
+ return (
+ <div ref={containerRef} className="w-full overflow-hidden text-sm font-mono truncate">
+ {text}
+ </div>
+ );
+};
+
export const ConsolidatedCirculationCard: React.FC<
ConsolidatedCirculationCardProps
-> = ({ estimate, onMapClick, readonly, reduced }) => {
+> = ({ estimate, onMapClick, readonly, reduced, driver }) => {
const { t } = useTranslation();
const formatDistance = (meters: number) => {
@@ -157,7 +201,7 @@ export const ConsolidatedCirculationCard: React.FC<
chips.push(delayChip);
}
- if (estimate.schedule) {
+ if (estimate.schedule && driver !== 'renfe') {
chips.push({
label: `${parseServiceId(estimate.schedule.serviceId)} · ${getTripIdDisplay(
estimate.schedule.tripId
@@ -199,14 +243,17 @@ export const ConsolidatedCirculationCard: React.FC<
// Check if bus has GPS position (live tracking)
const hasGpsPosition = !!estimate.currentPosition;
+ const isRenfe = driver === 'renfe';
+ const isClickable = hasGpsPosition;
+ const looksDisabled = !isClickable && !isRenfe;
const Tag = readonly ? "div" : "button";
const interactiveProps = readonly
? {}
: {
- onClick: onMapClick,
+ onClick: isClickable ? onMapClick : undefined,
type: "button" as const,
- disabled: !hasGpsPosition,
+ disabled: !isClickable,
};
if (reduced) {
@@ -217,12 +264,14 @@ export const ConsolidatedCirculationCard: React.FC<
bg-(--message-background-color) border border-(--border-color)
rounded-xl px-3 py-2.5 transition-all
${readonly
- ? !hasGpsPosition
+ ? looksDisabled
? "opacity-70 cursor-not-allowed"
: ""
- : hasGpsPosition
+ : isClickable
? "cursor-pointer hover:shadow-[0_4px_14px_rgba(0,0,0,0.08)] hover:border-(--button-background-color) hover:bg-[color-mix(in_oklab,var(--button-background-color)_5%,var(--message-background-color))] active:scale-[0.98]"
- : "opacity-70 cursor-not-allowed"
+ : looksDisabled
+ ? "opacity-70 cursor-not-allowed"
+ : ""
}
`.trim()}
{...interactiveProps}
@@ -232,6 +281,9 @@ export const ConsolidatedCirculationCard: React.FC<
</div>
<div className="flex-1 min-w-0 flex flex-col gap-1">
<strong className="text-base text-(--text-color) overflow-hidden text-ellipsis line-clamp-2 leading-tight">
+ {driver === 'renfe' && estimate.schedule?.tripId && (
+ <span className="font-mono text-slate-500 mr-1.5 text-sm">{estimate.schedule.tripId}</span>
+ )}
{estimate.route}
</strong>
{metaChips.length > 0 && (
@@ -295,12 +347,14 @@ export const ConsolidatedCirculationCard: React.FC<
return (
<Tag
className={`consolidated-circulation-card ${readonly
- ? !hasGpsPosition
+ ? looksDisabled
? "no-gps"
: ""
- : hasGpsPosition
+ : isClickable
? "has-gps"
- : "no-gps"
+ : looksDisabled
+ ? "no-gps"
+ : ""
}`}
{...interactiveProps}
>
@@ -310,17 +364,15 @@ export const ConsolidatedCirculationCard: React.FC<
<LineIcon line={estimate.line} mode="pill" />
</div>
<div className="route-info">
- <strong>{estimate.route}</strong>
- {estimate.nextStreets && estimate.nextStreets.length > 0 && (() => {
- const text = estimate.nextStreets.join(" — ");
- return (
- <Marquee speed={85} play={text.length > 30}>
- <div className="mr-32 font-mono">
- {text}
- </div>
- </Marquee>
- );
- })()}
+ <strong className="uppercase">
+ {driver === 'renfe' && estimate.schedule?.tripId && (
+ <span className="font-mono text-slate-500 mr-2 text-[0.9em]">{estimate.schedule.tripId}</span>
+ )}
+ {estimate.route}
+ </strong>
+ {estimate.nextStreets && estimate.nextStreets.length > 0 && (
+ <AutoMarquee text={estimate.nextStreets.join(" — ")} />
+ )}
</div>
<div className={`eta-badge ${timeClass}`}>
<div className="eta-text">
diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx
index 088f978..ec79f1c 100644
--- a/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx
+++ b/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx
@@ -9,12 +9,14 @@ interface ConsolidatedCirculationListProps {
data: ConsolidatedCirculation[];
onCirculationClick?: (estimate: ConsolidatedCirculation, index: number) => void;
reduced?: boolean;
+ driver?: string;
}
export const ConsolidatedCirculationList: React.FC<ConsolidatedCirculationListProps> = ({
data,
onCirculationClick,
reduced,
+ driver,
}) => {
const { t } = useTranslation();
@@ -43,6 +45,7 @@ export const ConsolidatedCirculationList: React.FC<ConsolidatedCirculationListPr
{sortedData.map((estimate, idx) => (
<ConsolidatedCirculationCard
reduced={reduced}
+ driver={driver}
key={generateKey(estimate)}
estimate={estimate}
onMapClick={() => onCirculationClick?.(estimate, idx)}
diff --git a/src/frontend/app/config/RegionConfig.ts b/src/frontend/app/config/RegionConfig.ts
index 4677509..43fe70a 100644
--- a/src/frontend/app/config/RegionConfig.ts
+++ b/src/frontend/app/config/RegionConfig.ts
@@ -1,6 +1,26 @@
import type { LngLatLike } from "maplibre-gl";
-export const REGION_DATA = {
+export type RegionId = "vigo";
+
+export interface RegionData {
+ id: RegionId;
+ name: string;
+ stopsEndpoint: string;
+ estimatesEndpoint: string;
+ consolidatedCirculationsEndpoint: string;
+ timetableEndpoint: string;
+ shapeEndpoint: string;
+ defaultCenter: LngLatLike;
+ bounds: {
+ sw: LngLatLike;
+ ne: LngLatLike;
+ };
+ textColour: string;
+ defaultZoom: number;
+ showMeters: boolean;
+}
+
+export const REGION_DATA: RegionData = {
id: "vigo",
name: "Vigo",
stopsEndpoint: "/stops/vigo.json",
@@ -20,3 +40,6 @@ export const REGION_DATA = {
defaultZoom: 14,
showMeters: true,
};
+
+export const getAvailableRegions = (): RegionData[] => [REGION_DATA];
+
diff --git a/src/frontend/app/routes/home.tsx b/src/frontend/app/routes/home.tsx
index 5d56b48..f97fdf7 100644
--- a/src/frontend/app/routes/home.tsx
+++ b/src/frontend/app/routes/home.tsx
@@ -104,7 +104,7 @@ export default function StopList() {
}
if (!userLocation) {
- return [...data].sort((a, b) => a.stopId - b.stopId);
+ return [...data].sort((a, b) => a.stopId.localeCompare(b.stopId));
}
const toRadians = (value: number) => (value * Math.PI) / 180;
@@ -147,7 +147,7 @@ export default function StopList() {
})
.sort((a, b) => {
if (a.distance === b.distance) {
- return a.stop.stopId - b.stop.stopId;
+ return a.stop.stopId.localeCompare(b.stop.stopId);
}
return a.distance - b.distance;
})
@@ -216,8 +216,8 @@ export default function StopList() {
let items: Stop[];
if (isNumericSearch) {
// Direct match for stop codes
- const stopId = parseInt(searchQuery.trim(), 10);
- const exactMatch = data.filter((stop) => stop.stopId === stopId);
+ const stopId = searchQuery.trim();
+ const exactMatch = data.filter((stop) => stop.stopId === stopId || stop.stopId.endsWith(`:${stopId}`));
if (exactMatch.length > 0) {
items = exactMatch;
} else {
@@ -281,7 +281,7 @@ export default function StopList() {
{/* Favourites Gallery */}
{!loading && (
<StopGallery
- stops={favouriteStops.sort((a, b) => a.stopId - b.stopId)}
+ stops={favouriteStops.sort((a, b) => a.stopId.localeCompare(b.stopId))}
title={t("stoplist.favourites")}
emptyMessage={t("stoplist.no_favourites")}
/>
diff --git a/src/frontend/app/routes/stops-$id.tsx b/src/frontend/app/routes/stops-$id.tsx
index 5260c32..553b8e7 100644
--- a/src/frontend/app/routes/stops-$id.tsx
+++ b/src/frontend/app/routes/stops-$id.tsx
@@ -247,8 +247,8 @@ export default function Estimates() {
<div className="flex items-center gap-8">
<Star
className={`cursor-pointer transition-colors ${favourited
- ? "fill-[var(--star-color)] text-[var(--star-color)]"
- : "text-slate-500"
+ ? "fill-[var(--star-color)] text-[var(--star-color)]"
+ : "text-slate-500"
}`}
onClick={toggleFavourite}
/>
@@ -273,6 +273,7 @@ export default function Estimates() {
<ConsolidatedCirculationList
data={data}
reduced={isReducedView}
+ driver={stopData?.stopId.split(':')[0]}
onCirculationClick={(estimate, idx) => {
setSelectedCirculationId(getCirculationId(estimate));
setIsMapModalOpen(true);