aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app
diff options
context:
space:
mode:
authorcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>2025-11-06 22:52:02 +0000
committerAriel Costas Guerrero <ariel@costas.dev>2025-11-07 10:47:20 +0100
commitee77f38cdb324cbcf12518490df77fc9e6b89282 (patch)
tree407f64a434291e1e375e6a1ccb55f59fa886a1ef /src/frontend/app
parente51cdd89afc08274ca622e18b8127feca29e90a3 (diff)
Improve gallery scroll indicators and format code
Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com>
Diffstat (limited to 'src/frontend/app')
-rw-r--r--src/frontend/app/AppContext.tsx14
-rw-r--r--src/frontend/app/components/ErrorDisplay.tsx45
-rw-r--r--src/frontend/app/components/GroupedTable.tsx6
-rw-r--r--src/frontend/app/components/LineIcon.css4
-rw-r--r--src/frontend/app/components/LineIcon.tsx15
-rw-r--r--src/frontend/app/components/PullToRefresh.tsx161
-rw-r--r--src/frontend/app/components/RegularTable.tsx2
-rw-r--r--src/frontend/app/components/SchedulesTable.css4
-rw-r--r--src/frontend/app/components/SchedulesTable.tsx85
-rw-r--r--src/frontend/app/components/SchedulesTableSkeleton.tsx42
-rw-r--r--src/frontend/app/components/ServiceAlerts.tsx4
-rw-r--r--src/frontend/app/components/StopAlert.tsx24
-rw-r--r--src/frontend/app/components/StopGallery.css7
-rw-r--r--src/frontend/app/components/StopGallery.tsx51
-rw-r--r--src/frontend/app/components/StopItem.tsx4
-rw-r--r--src/frontend/app/components/StopItemSkeleton.tsx33
-rw-r--r--src/frontend/app/components/StopSheet.tsx52
-rw-r--r--src/frontend/app/components/StopSheetSkeleton.tsx30
-rw-r--r--src/frontend/app/components/TimetableSkeleton.tsx10
-rw-r--r--src/frontend/app/data/RegionConfig.ts4
-rw-r--r--src/frontend/app/data/StopDataProvider.ts37
-rw-r--r--src/frontend/app/maps/styleloader.ts2
-rw-r--r--src/frontend/app/root.tsx10
-rw-r--r--src/frontend/app/routes/estimates-$id.css8
-rw-r--r--src/frontend/app/routes/estimates-$id.tsx89
-rw-r--r--src/frontend/app/routes/map.tsx13
-rw-r--r--src/frontend/app/routes/settings.css8
-rw-r--r--src/frontend/app/routes/settings.tsx45
-rw-r--r--src/frontend/app/routes/stoplist.tsx57
29 files changed, 575 insertions, 291 deletions
diff --git a/src/frontend/app/AppContext.tsx b/src/frontend/app/AppContext.tsx
index 8f47a49..b986880 100644
--- a/src/frontend/app/AppContext.tsx
+++ b/src/frontend/app/AppContext.tsx
@@ -7,7 +7,13 @@ import {
type ReactNode,
} from "react";
import { type LngLatLike } from "maplibre-gl";
-import { type RegionId, DEFAULT_REGION, getRegionConfig, isValidRegion, REGIONS } from "./data/RegionConfig";
+import {
+ type RegionId,
+ DEFAULT_REGION,
+ getRegionConfig,
+ isValidRegion,
+ REGIONS,
+} from "./data/RegionConfig";
export type Theme = "light" | "dark" | "system";
type TableStyle = "regular" | "grouped";
@@ -62,7 +68,11 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
const [theme, setTheme] = useState<Theme>(() => {
const savedTheme = localStorage.getItem("theme");
- if (savedTheme === "light" || savedTheme === "dark" || savedTheme === "system") {
+ if (
+ savedTheme === "light" ||
+ savedTheme === "dark" ||
+ savedTheme === "system"
+ ) {
return savedTheme;
}
return "system";
diff --git a/src/frontend/app/components/ErrorDisplay.tsx b/src/frontend/app/components/ErrorDisplay.tsx
index 3c91db6..f63c995 100644
--- a/src/frontend/app/components/ErrorDisplay.tsx
+++ b/src/frontend/app/components/ErrorDisplay.tsx
@@ -5,7 +5,7 @@ import "./ErrorDisplay.css";
interface ErrorDisplayProps {
error: {
- type: 'network' | 'server' | 'unknown';
+ type: "network" | "server" | "unknown";
status?: number;
message?: string;
};
@@ -18,15 +18,15 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
error,
onRetry,
title,
- className = ""
+ className = "",
}) => {
const { t } = useTranslation();
const getErrorIcon = () => {
switch (error.type) {
- case 'network':
+ case "network":
return <WifiOff className="error-icon" />;
- case 'server':
+ case "server":
return <AlertTriangle className="error-icon" />;
default:
return <AlertTriangle className="error-icon" />;
@@ -35,21 +35,38 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
const getErrorMessage = () => {
switch (error.type) {
- case 'network':
- return t("errors.network", "No hay conexión a internet. Comprueba tu conexión y vuelve a intentarlo.");
- case 'server':
+ case "network":
+ return t(
+ "errors.network",
+ "No hay conexión a internet. Comprueba tu conexión y vuelve a intentarlo.",
+ );
+ case "server":
if (error.status === 404) {
- return t("errors.not_found", "No se encontraron datos para esta parada.");
+ return t(
+ "errors.not_found",
+ "No se encontraron datos para esta parada.",
+ );
}
if (error.status === 500) {
- return t("errors.server_error", "Error del servidor. Inténtalo de nuevo más tarde.");
+ return t(
+ "errors.server_error",
+ "Error del servidor. Inténtalo de nuevo más tarde.",
+ );
}
if (error.status && error.status >= 400) {
- return t("errors.client_error", "Error en la solicitud. Verifica que la parada existe.");
+ return t(
+ "errors.client_error",
+ "Error en la solicitud. Verifica que la parada existe.",
+ );
}
- return t("errors.server_generic", "Error del servidor ({{status}})", { status: error.status || 'desconocido' });
+ return t("errors.server_generic", "Error del servidor ({{status}})", {
+ status: error.status || "desconocido",
+ });
default:
- return error.message || t("errors.unknown", "Ha ocurrido un error inesperado.");
+ return (
+ error.message ||
+ t("errors.unknown", "Ha ocurrido un error inesperado.")
+ );
}
};
@@ -57,9 +74,9 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
if (title) return title;
switch (error.type) {
- case 'network':
+ case "network":
return t("errors.network_title", "Sin conexión");
- case 'server':
+ case "server":
return t("errors.server_title", "Error del servidor");
default:
return t("errors.unknown_title", "Error");
diff --git a/src/frontend/app/components/GroupedTable.tsx b/src/frontend/app/components/GroupedTable.tsx
index 9bdd5a0..f116537 100644
--- a/src/frontend/app/components/GroupedTable.tsx
+++ b/src/frontend/app/components/GroupedTable.tsx
@@ -8,7 +8,11 @@ interface GroupedTable {
regionConfig: RegionConfig;
}
-export const GroupedTable: React.FC<GroupedTable> = ({ data, dataDate, regionConfig }) => {
+export const GroupedTable: React.FC<GroupedTable> = ({
+ data,
+ dataDate,
+ regionConfig,
+}) => {
const formatDistance = (meters: number) => {
if (meters > 1024) {
return `${(meters / 1000).toFixed(1)} km`;
diff --git a/src/frontend/app/components/LineIcon.css b/src/frontend/app/components/LineIcon.css
index 94d5848..fdfdd06 100644
--- a/src/frontend/app/components/LineIcon.css
+++ b/src/frontend/app/components/LineIcon.css
@@ -87,7 +87,6 @@
--line-santiago-p6: #999999;
--line-santiago-p7: #d2438c;
--line-santiago-p8: #e28c3a;
-
}
.line-icon {
@@ -118,6 +117,5 @@
border-radius: 50%;
font: 600 14px / 1 monospace;
- letter-spacing: .05em;
+ letter-spacing: 0.05em;
}
-
diff --git a/src/frontend/app/components/LineIcon.tsx b/src/frontend/app/components/LineIcon.tsx
index ef05987..8e9a4bd 100644
--- a/src/frontend/app/components/LineIcon.tsx
+++ b/src/frontend/app/components/LineIcon.tsx
@@ -8,7 +8,11 @@ interface LineIconProps {
rounded?: boolean;
}
-const LineIcon: React.FC<LineIconProps> = ({ line, region = "vigo", rounded = false }) => {
+const LineIcon: React.FC<LineIconProps> = ({
+ line,
+ region = "vigo",
+ rounded = false,
+}) => {
const formattedLine = useMemo(() => {
return /^[a-zA-Z]/.test(line) ? line : `L${line}`;
}, [line]);
@@ -17,8 +21,13 @@ const LineIcon: React.FC<LineIconProps> = ({ line, region = "vigo", rounded = fa
return (
<span
- className={rounded ? 'line-icon-rounded' : 'line-icon'}
- style={{ '--line-colour': `var(${cssVarName})`, '--line-text-colour': `var(${cssTextVarName}, unset)` } as React.CSSProperties}
+ className={rounded ? "line-icon-rounded" : "line-icon"}
+ style={
+ {
+ "--line-colour": `var(${cssVarName})`,
+ "--line-text-colour": `var(${cssTextVarName}, unset)`,
+ } as React.CSSProperties
+ }
>
{line}
</span>
diff --git a/src/frontend/app/components/PullToRefresh.tsx b/src/frontend/app/components/PullToRefresh.tsx
index 9def8f5..d5ea51b 100644
--- a/src/frontend/app/components/PullToRefresh.tsx
+++ b/src/frontend/app/components/PullToRefresh.tsx
@@ -27,75 +27,97 @@ export const PullToRefresh: React.FC<PullToRefreshProps> = ({
const rotate = useTransform(y, [0, threshold], [0, 180]);
const isAtPageTop = useCallback(() => {
- const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
+ 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
- const windowScroll = window.pageYOffset || window.scrollY || 0;
- const htmlScroll = document.documentElement.scrollTop;
- const bodyScroll = document.body.scrollTop;
- const containerScroll = containerRef.current?.scrollTop || 0;
- const parentScroll = containerRef.current?.parentElement?.scrollTop || 0;
- const maxScroll = Math.max(windowScroll, htmlScroll, bodyScroll, containerScroll, parentScroll);
+ const handleTouchStart = useCallback(
+ (e: TouchEvent) => {
+ // Very strict check - must be at absolute top
+ const windowScroll = window.pageYOffset || window.scrollY || 0;
+ const htmlScroll = document.documentElement.scrollTop;
+ const bodyScroll = document.body.scrollTop;
+ const containerScroll = containerRef.current?.scrollTop || 0;
+ const parentScroll = containerRef.current?.parentElement?.scrollTop || 0;
+ const maxScroll = Math.max(
+ windowScroll,
+ htmlScroll,
+ bodyScroll,
+ containerScroll,
+ parentScroll,
+ );
- if (maxScroll > 0 || isRefreshing) {
- setIsPulling(false);
- setIsActive(false);
- return;
- }
+ if (maxScroll > 0 || isRefreshing) {
+ setIsPulling(false);
+ setIsActive(false);
+ return;
+ }
- startY.current = e.touches[0].clientY;
- setIsPulling(true);
- }, [isRefreshing]);
+ startY.current = e.touches[0].clientY;
+ setIsPulling(true);
+ },
+ [isRefreshing],
+ );
- const handleTouchMove = useCallback((e: TouchEvent) => {
- if (!isPulling) return;
+ const handleTouchMove = useCallback(
+ (e: TouchEvent) => {
+ if (!isPulling) return;
- // Continuously check if we're still at the top during the gesture
- const windowScroll = window.pageYOffset || window.scrollY || 0;
- const htmlScroll = document.documentElement.scrollTop;
- const bodyScroll = document.body.scrollTop;
- const containerScroll = containerRef.current?.scrollTop || 0;
- const parentScroll = containerRef.current?.parentElement?.scrollTop || 0;
- const maxScroll = Math.max(windowScroll, htmlScroll, bodyScroll, containerScroll, parentScroll);
+ // Continuously check if we're still at the top during the gesture
+ const windowScroll = window.pageYOffset || window.scrollY || 0;
+ const htmlScroll = document.documentElement.scrollTop;
+ const bodyScroll = document.body.scrollTop;
+ const containerScroll = containerRef.current?.scrollTop || 0;
+ const parentScroll = containerRef.current?.parentElement?.scrollTop || 0;
+ const maxScroll = Math.max(
+ windowScroll,
+ htmlScroll,
+ bodyScroll,
+ containerScroll,
+ parentScroll,
+ );
- if (maxScroll > 10) {
- // Cancel pull-to-refresh if we've scrolled away from top
- setIsPulling(false);
- setIsActive(false);
- y.set(0);
- return;
- }
+ if (maxScroll > 10) {
+ // Cancel pull-to-refresh if we've scrolled away from top
+ setIsPulling(false);
+ setIsActive(false);
+ y.set(0);
+ return;
+ }
- const currentY = e.touches[0].clientY;
- const pullDistance = currentY - startY.current;
+ const currentY = e.touches[0].clientY;
+ const pullDistance = currentY - startY.current;
- if (pullDistance > 0) {
- // Only prevent default when the event is cancelable
- if (e.cancelable) {
- e.preventDefault();
- }
+ if (pullDistance > 0) {
+ // Only prevent default when the event is cancelable
+ if (e.cancelable) {
+ e.preventDefault();
+ }
- const dampedDistance = Math.min(pullDistance * 0.5, threshold * 1.2);
- y.set(dampedDistance);
+ const dampedDistance = Math.min(pullDistance * 0.5, threshold * 1.2);
+ y.set(dampedDistance);
- if (dampedDistance >= threshold && !isActive) {
- setIsActive(true);
- // Only vibrate if user activation is available and vibrate is supported
- if (navigator.vibrate && navigator.userActivation?.hasBeenActive) {
- navigator.vibrate(50);
+ if (dampedDistance >= threshold && !isActive) {
+ setIsActive(true);
+ // Only vibrate if user activation is available and vibrate is supported
+ if (navigator.vibrate && navigator.userActivation?.hasBeenActive) {
+ navigator.vibrate(50);
+ }
+ } else if (dampedDistance < threshold && isActive) {
+ setIsActive(false);
}
- } else if (dampedDistance < threshold && isActive) {
+ } else {
+ // Reset if pulling up
+ y.set(0);
setIsActive(false);
}
- } else {
- // Reset if pulling up
- y.set(0);
- setIsActive(false);
- }
- }, [isPulling, threshold, isActive, y]);
+ },
+ [isPulling, threshold, isActive, y],
+ );
const handleTouchEnd = useCallback(async () => {
if (!isPulling) return;
@@ -106,7 +128,7 @@ export const PullToRefresh: React.FC<PullToRefreshProps> = ({
try {
await onRefresh();
} catch (error) {
- console.error('Refresh failed:', error);
+ console.error("Refresh failed:", error);
}
}
@@ -121,14 +143,18 @@ export const PullToRefresh: React.FC<PullToRefreshProps> = ({
if (!container) return;
// Use passive: false for touchmove to allow preventDefault
- container.addEventListener('touchstart', handleTouchStart, { passive: true });
- container.addEventListener('touchmove', handleTouchMove, { passive: false });
- container.addEventListener('touchend', handleTouchEnd, { passive: true });
+ container.addEventListener("touchstart", handleTouchStart, {
+ passive: true,
+ });
+ container.addEventListener("touchmove", handleTouchMove, {
+ passive: false,
+ });
+ container.addEventListener("touchend", handleTouchEnd, { passive: true });
return () => {
- container.removeEventListener('touchstart', handleTouchStart);
- container.removeEventListener('touchmove', handleTouchMove);
- container.removeEventListener('touchend', handleTouchEnd);
+ container.removeEventListener("touchstart", handleTouchStart);
+ container.removeEventListener("touchmove", handleTouchMove);
+ container.removeEventListener("touchend", handleTouchEnd);
};
}, [handleTouchStart, handleTouchMove, handleTouchEnd]);
@@ -136,25 +162,20 @@ export const PullToRefresh: React.FC<PullToRefreshProps> = ({
<div className="pull-to-refresh-container" ref={containerRef}>
{/* Simple indicator */}
{isPulling && (
- <motion.div
- className="pull-to-refresh-indicator"
- style={{ opacity }}
- >
+ <motion.div className="pull-to-refresh-indicator" style={{ opacity }}>
<motion.div
- className={`refresh-icon-container ${isActive ? 'active' : ''}`}
+ className={`refresh-icon-container ${isActive ? "active" : ""}`}
style={{ scale, rotate: isRefreshing ? 0 : rotate }}
>
<RefreshCw
- className={`refresh-icon ${isRefreshing ? 'spinning' : ''}`}
+ className={`refresh-icon ${isRefreshing ? "spinning" : ""}`}
/>
</motion.div>
</motion.div>
)}
{/* Normal content - no transform interference */}
- <div className="pull-to-refresh-content">
- {children}
- </div>
+ <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
index 868332f..baa3804 100644
--- a/src/frontend/app/components/RegularTable.tsx
+++ b/src/frontend/app/components/RegularTable.tsx
@@ -24,7 +24,7 @@ export const RegularTable: React.FC<RegularTableProps> = ({
{
hour: "2-digit",
minute: "2-digit",
- }
+ },
).format(arrival);
};
diff --git a/src/frontend/app/components/SchedulesTable.css b/src/frontend/app/components/SchedulesTable.css
index 6d2f201..74d7569 100644
--- a/src/frontend/app/components/SchedulesTable.css
+++ b/src/frontend/app/components/SchedulesTable.css
@@ -22,7 +22,9 @@
border: 1px solid var(--card-border);
border-radius: 10px;
padding: 1.25rem;
- transition: background-color 0.2s ease, border 0.2s ease;
+ transition:
+ background-color 0.2s ease,
+ border 0.2s ease;
}
/* Next upcoming service: slight emphasis */
diff --git a/src/frontend/app/components/SchedulesTable.tsx b/src/frontend/app/components/SchedulesTable.tsx
index a3bbd9f..9f3f062 100644
--- a/src/frontend/app/components/SchedulesTable.tsx
+++ b/src/frontend/app/components/SchedulesTable.tsx
@@ -24,7 +24,7 @@ export type ScheduledTable = {
terminus_code: string;
terminus_name: string;
terminus_time: string;
-}
+};
interface TimetableTableProps {
data: ScheduledTable[];
@@ -34,11 +34,11 @@ interface TimetableTableProps {
// 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 parts = serviceId.split("_");
+ if (parts.length === 0) return "";
const lastPart = parts[parts.length - 1];
- if (lastPart.length < 6) return '';
+ if (lastPart.length < 6) return "";
const last6 = lastPart.slice(-6);
const lineCode = last6.slice(0, 3);
@@ -52,15 +52,32 @@ const parseServiceId = (serviceId: string): string => {
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}`;
+ 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}`;
@@ -68,21 +85,26 @@ const parseServiceId = (serviceId: string): string => {
// Utility function to compare times
const timeToMinutes = (time: string): number => {
- const [hours, minutes] = time.split(':').map(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[] => {
+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)
+ const sortedEntries = [...entries].sort(
+ (a, b) => timeToMinutes(a.calling_time) - timeToMinutes(b.calling_time),
);
- let currentIndex = sortedEntries.findIndex(entry =>
- timeToMinutes(entry.calling_time) >= currentMinutes
+ let currentIndex = sortedEntries.findIndex(
+ (entry) => timeToMinutes(entry.calling_time) >= currentMinutes,
);
if (currentIndex === -1) {
@@ -99,21 +121,24 @@ const findNearbyEntries = (entries: ScheduledTable[], currentTime: string, befor
export const SchedulesTable: React.FC<TimetableTableProps> = ({
data,
showAll = false,
- currentTime
+ 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));
+ 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")
- }
+ : t("timetable.nearbyCaption", "Próximos horarios teóricos")}
</div>
<div className="timetable-cards">
@@ -127,7 +152,7 @@ export const SchedulesTable: React.FC<TimetableTableProps> = ({
style={{
background: isPast
? "var(--surface-past, #f3f3f3)"
- : "var(--surface-future, #fff)"
+ : "var(--surface-future, #fff)",
}}
>
<div className="card-header">
@@ -139,7 +164,9 @@ export const SchedulesTable: React.FC<TimetableTableProps> = ({
{entry.route && entry.route.trim() ? (
<strong>{entry.route}</strong>
) : (
- <strong>{t("timetable.noDestination", "Línea")} {entry.line}</strong>
+ <strong>
+ {t("timetable.noDestination", "Línea")} {entry.line}
+ </strong>
)}
</div>
@@ -155,7 +182,7 @@ export const SchedulesTable: React.FC<TimetableTableProps> = ({
{parseServiceId(entry.service_id)}
</span>
{entry.next_streets.length > 0 && (
- <span> — {entry.next_streets.join(' — ')}</span>
+ <span> — {entry.next_streets.join(" — ")}</span>
)}
</div>
</div>
@@ -164,7 +191,9 @@ export const SchedulesTable: React.FC<TimetableTableProps> = ({
})}
</div>
{displayData.length === 0 && (
- <p className="no-data">{t("timetable.noData", "No hay datos de horarios disponibles")}</p>
+ <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
index 50ba94d..3ae9729 100644
--- a/src/frontend/app/components/SchedulesTableSkeleton.tsx
+++ b/src/frontend/app/components/SchedulesTableSkeleton.tsx
@@ -8,7 +8,7 @@ interface EstimatesTableSkeletonProps {
}
export const SchedulesTableSkeleton: React.FC<EstimatesTableSkeletonProps> = ({
- rows = 3
+ rows = 3,
}) => {
const { t } = useTranslation();
@@ -32,13 +32,23 @@ export const SchedulesTableSkeleton: React.FC<EstimatesTableSkeletonProps> = ({
{Array.from({ length: rows }, (_, index) => (
<tr key={`skeleton-${index}`}>
<td>
- <Skeleton width="40px" height="24px" style={{ borderRadius: "4px" }} />
+ <Skeleton
+ width="40px"
+ height="24px"
+ style={{ borderRadius: "4px" }}
+ />
</td>
<td>
<Skeleton width="120px" />
</td>
<td>
- <div style={{ display: "flex", flexDirection: "column", gap: "2px" }}>
+ <div
+ style={{
+ display: "flex",
+ flexDirection: "column",
+ gap: "2px",
+ }}
+ >
<Skeleton width="60px" />
<Skeleton width="40px" />
</div>
@@ -59,10 +69,9 @@ interface EstimatesGroupedSkeletonProps {
rowsPerGroup?: number;
}
-export const EstimatesGroupedSkeleton: React.FC<EstimatesGroupedSkeletonProps> = ({
- groups = 3,
- rowsPerGroup = 2
-}) => {
+export const EstimatesGroupedSkeleton: React.FC<
+ EstimatesGroupedSkeletonProps
+> = ({ groups = 3, rowsPerGroup = 2 }) => {
const { t } = useTranslation();
return (
@@ -85,17 +94,30 @@ export const EstimatesGroupedSkeleton: React.FC<EstimatesGroupedSkeletonProps> =
{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" : ""}>
+ <tr
+ key={`skeleton-${groupIndex}-${rowIndex}`}
+ className={rowIndex === 0 ? "group-start" : ""}
+ >
<td>
{rowIndex === 0 && (
- <Skeleton width="40px" height="24px" style={{ borderRadius: "4px" }} />
+ <Skeleton
+ width="40px"
+ height="24px"
+ style={{ borderRadius: "4px" }}
+ />
)}
</td>
<td>
<Skeleton width="120px" />
</td>
<td>
- <div style={{ display: "flex", flexDirection: "column", gap: "2px" }}>
+ <div
+ style={{
+ display: "flex",
+ flexDirection: "column",
+ gap: "2px",
+ }}
+ >
<Skeleton width="60px" />
<Skeleton width="40px" />
</div>
diff --git a/src/frontend/app/components/ServiceAlerts.tsx b/src/frontend/app/components/ServiceAlerts.tsx
index 7966f2a..eba8a92 100644
--- a/src/frontend/app/components/ServiceAlerts.tsx
+++ b/src/frontend/app/components/ServiceAlerts.tsx
@@ -12,7 +12,9 @@ const ServiceAlerts: React.FC = () => {
<div className="alert-icon">ℹ️</div>
<div className="alert-content">
<div className="alert-title">{t("stoplist.alerts_coming_soon")}</div>
- <div className="alert-message">{t("stoplist.alerts_description")}</div>
+ <div className="alert-message">
+ {t("stoplist.alerts_description")}
+ </div>
</div>
</div>
</div>
diff --git a/src/frontend/app/components/StopAlert.tsx b/src/frontend/app/components/StopAlert.tsx
index d969108..f1356e6 100644
--- a/src/frontend/app/components/StopAlert.tsx
+++ b/src/frontend/app/components/StopAlert.tsx
@@ -8,7 +8,10 @@ interface StopAlertProps {
compact?: boolean;
}
-export const StopAlert: React.FC<StopAlertProps> = ({ stop, compact = false }) => {
+export const StopAlert: React.FC<StopAlertProps> = ({
+ stop,
+ compact = false,
+}) => {
// Don't render anything if there's no alert content
const hasContent = stop.title || stop.message;
if (!hasContent) {
@@ -28,11 +31,30 @@ export const StopAlert: React.FC<StopAlertProps> = ({ stop, compact = false }) =
}, [stop.alert]);
return (
+<<<<<<< HEAD
<div className={`stop-alert ${alertType} ${compact ? 'stop-alert-compact' : ''}`}>
{alertIcon}
<div className="stop-alert-content">
{stop.title && <div className="stop-alert-title">{stop.title}</div>}
{stop.message && <div className="stop-alert-message">{stop.message}</div>}
+=======
+ <div
+ className={`stop-alert ${isError ? "stop-alert-error" : "stop-alert-info"} ${compact ? "stop-alert-compact" : ""}`}
+ >
+ <div className="stop-alert-icon">
+ {isError ? <AlertCircle /> : <Info />}
+ </div>
+ <div className="stop-alert-content">
+ {stop.title && <div className="stop-alert-title">{stop.title}</div>}
+ {stop.message && (
+ <div className="stop-alert-message">{stop.message}</div>
+ )}
+ {stop.alternateCodes && stop.alternateCodes.length > 0 && (
+ <div className="stop-alert-alternate-codes">
+ Alternative stops: {stop.alternateCodes.join(", ")}
+ </div>
+ )}
+>>>>>>> 88e0621 (Improve gallery scroll indicators and format code)
</div>
</div>
);
diff --git a/src/frontend/app/components/StopGallery.css b/src/frontend/app/components/StopGallery.css
index f53f2a5..adba001 100644
--- a/src/frontend/app/components/StopGallery.css
+++ b/src/frontend/app/components/StopGallery.css
@@ -57,7 +57,10 @@
.gallery-item-link {
display: block;
padding: 1rem;
- background-color: var(--card-background-color, var(--message-background-color));
+ background-color: var(
+ --card-background-color,
+ var(--message-background-color)
+ );
border: 1px solid var(--border-color);
border-radius: 12px;
text-decoration: none;
@@ -146,7 +149,7 @@
.gallery-item {
flex: 0 0 320px;
}
-
+
.gallery-scroll-container {
margin: 0;
padding: 0;
diff --git a/src/frontend/app/components/StopGallery.tsx b/src/frontend/app/components/StopGallery.tsx
index 7dda637..3646fdd 100644
--- a/src/frontend/app/components/StopGallery.tsx
+++ b/src/frontend/app/components/StopGallery.tsx
@@ -1,5 +1,4 @@
-import React, { useRef } from "react";
-import { motion, useMotionValue } from "framer-motion";
+import React, { useRef, useState, useEffect } from "react";
import { type Stop } from "../data/StopDataProvider";
import StopGalleryItem from "./StopGalleryItem";
import "./StopGallery.css";
@@ -10,9 +9,28 @@ interface StopGalleryProps {
emptyMessage?: string;
}
-const StopGallery: React.FC<StopGalleryProps> = ({ stops, title, emptyMessage }) => {
+const StopGallery: React.FC<StopGalleryProps> = ({
+ stops,
+ title,
+ emptyMessage,
+}) => {
const scrollRef = useRef<HTMLDivElement>(null);
- const x = useMotionValue(0);
+ const [activeIndex, setActiveIndex] = useState(0);
+
+ useEffect(() => {
+ const element = scrollRef.current;
+ if (!element) return;
+
+ const handleScroll = () => {
+ const scrollLeft = element.scrollLeft;
+ const itemWidth = element.scrollWidth / stops.length;
+ const index = Math.round(scrollLeft / itemWidth);
+ setActiveIndex(index);
+ };
+
+ element.addEventListener("scroll", handleScroll);
+ return () => element.removeEventListener("scroll", handleScroll);
+ }, [stops.length]);
if (stops.length === 0 && emptyMessage) {
return (
@@ -33,24 +51,25 @@ const StopGallery: React.FC<StopGalleryProps> = ({ stops, title, emptyMessage })
<h2 className="page-subtitle">{title}</h2>
<span className="gallery-counter">{stops.length}</span>
</div>
-
- <motion.div
- className="gallery-scroll-container"
- ref={scrollRef}
- style={{ x }}
- >
+
+ <div className="gallery-scroll-container" ref={scrollRef}>
<div className="gallery-track">
{stops.map((stop) => (
<StopGalleryItem key={stop.stopId} stop={stop} />
))}
</div>
- </motion.div>
-
- <div className="gallery-indicators">
- {stops.map((_, index) => (
- <div key={index} className="gallery-indicator" />
- ))}
</div>
+
+ {stops.length > 1 && (
+ <div className="gallery-indicators">
+ {stops.map((_, index) => (
+ <div
+ key={index}
+ className={`gallery-indicator ${index === activeIndex ? "active" : ""}`}
+ />
+ ))}
+ </div>
+ )}
</div>
);
};
diff --git a/src/frontend/app/components/StopItem.tsx b/src/frontend/app/components/StopItem.tsx
index 7d89d7d..ae51df8 100644
--- a/src/frontend/app/components/StopItem.tsx
+++ b/src/frontend/app/components/StopItem.tsx
@@ -17,7 +17,9 @@ const StopItem: React.FC<StopItemProps> = ({ stop }) => {
{stop.favourite && <span className="favourite-icon">★</span>} (
{stop.stopId}) {StopDataProvider.getDisplayName(region, stop)}
<div className="line-icons">
- {stop.lines?.map((line) => <LineIcon key={line} line={line} region={region} />)}
+ {stop.lines?.map((line) => (
+ <LineIcon key={line} line={line} region={region} />
+ ))}
</div>
</Link>
</li>
diff --git a/src/frontend/app/components/StopItemSkeleton.tsx b/src/frontend/app/components/StopItemSkeleton.tsx
index 9133393..76559de 100644
--- a/src/frontend/app/components/StopItemSkeleton.tsx
+++ b/src/frontend/app/components/StopItemSkeleton.tsx
@@ -3,14 +3,15 @@ import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
import "react-loading-skeleton/dist/skeleton.css";
interface StopItemSkeletonProps {
- showId?: boolean;
- stopId?: number;
+ showId?: boolean;
+ stopId?: number;
}
const StopItemSkeleton: React.FC<StopItemSkeletonProps> = ({
- showId = false,
- stopId
+ showId = false,
+ stopId,
}) => {
+<<<<<<< HEAD
return (
<SkeletonTheme baseColor="var(--skeleton-base)" highlightColor="var(--skeleton-highlight)">
<li className="list-item">
@@ -37,6 +38,30 @@ const StopItemSkeleton: React.FC<StopItemSkeletonProps> = ({
</li>
</SkeletonTheme>
);
+=======
+ return (
+ <SkeletonTheme baseColor="#f0f0f0" highlightColor="#e0e0e0">
+ <li className="list-item">
+ <div className="list-item-link">
+ <span>{showId && stopId && <>({stopId}) </>}</span>
+ <Skeleton
+ width={showId ? "60%" : "80%"}
+ style={{ display: "inline-block" }}
+ />
+ <div className="line-icons" style={{ marginTop: "4px" }}>
+ <Skeleton
+ count={3}
+ width="30px"
+ height="20px"
+ inline={true}
+ style={{ marginRight: "0.5rem" }}
+ />
+ </div>
+ </div>
+ </li>
+ </SkeletonTheme>
+ );
+>>>>>>> 88e0621 (Improve gallery scroll indicators and format code)
};
export default StopItemSkeleton;
diff --git a/src/frontend/app/components/StopSheet.tsx b/src/frontend/app/components/StopSheet.tsx
index 695b18e..422558b 100644
--- a/src/frontend/app/components/StopSheet.tsx
+++ b/src/frontend/app/components/StopSheet.tsx
@@ -20,12 +20,15 @@ interface StopSheetProps {
}
interface ErrorInfo {
- type: 'network' | 'server' | 'unknown';
+ type: "network" | "server" | "unknown";
status?: number;
message?: string;
}
-const loadStopData = async (region: RegionId, stopId: number): Promise<Estimate[]> => {
+const loadStopData = async (
+ region: RegionId,
+ stopId: number,
+): Promise<Estimate[]> => {
const regionConfig = getRegionConfig(region);
const resp = await fetch(`${regionConfig.estimatesEndpoint}?id=${stopId}`, {
headers: {
@@ -43,7 +46,7 @@ const loadStopData = async (region: RegionId, stopId: number): Promise<Estimate[
export const StopSheet: React.FC<StopSheetProps> = ({
isOpen,
onClose,
- stop
+ stop,
}) => {
const { t } = useTranslation();
const { region } = useApp();
@@ -55,20 +58,23 @@ export const StopSheet: React.FC<StopSheetProps> = ({
const parseError = (error: any): ErrorInfo => {
if (!navigator.onLine) {
- return { type: 'network', message: 'No internet connection' };
+ return { type: "network", message: "No internet connection" };
}
- if (error.message?.includes('Failed to fetch') || error.message?.includes('NetworkError')) {
- return { type: 'network' };
+ if (
+ error.message?.includes("Failed to fetch") ||
+ error.message?.includes("NetworkError")
+ ) {
+ return { type: "network" };
}
- if (error.message?.includes('HTTP')) {
+ if (error.message?.includes("HTTP")) {
const statusMatch = error.message.match(/HTTP (\d+):/);
const status = statusMatch ? parseInt(statusMatch[1]) : undefined;
- return { type: 'server', status };
+ return { type: "server", status };
}
- return { type: 'unknown', message: error.message };
+ return { type: "unknown", message: error.message };
};
const loadData = async () => {
@@ -103,7 +109,7 @@ export const StopSheet: React.FC<StopSheetProps> = ({
{
hour: "2-digit",
minute: "2-digit",
- }
+ },
).format(arrival);
} else {
return `${minutes} ${t("estimates.minutes", "min")}`;
@@ -123,11 +129,7 @@ export const StopSheet: React.FC<StopSheetProps> = ({
data?.sort((a, b) => a.minutes - b.minutes).slice(0, 4) || [];
return (
- <Sheet
- isOpen={isOpen}
- onClose={onClose}
- detent={"content-height" as any}
- >
+ <Sheet isOpen={isOpen} onClose={onClose} detent={"content-height" as any}>
<Sheet.Container>
<Sheet.Header />
<Sheet.Content>
@@ -153,7 +155,10 @@ export const StopSheet: React.FC<StopSheetProps> = ({
<ErrorDisplay
error={error}
onRetry={loadData}
- title={t("errors.estimates_title", "Error al cargar estimaciones")}
+ title={t(
+ "errors.estimates_title",
+ "Error al cargar estimaciones",
+ )}
className="compact"
/>
) : data ? (
@@ -180,7 +185,9 @@ export const StopSheet: React.FC<StopSheetProps> = ({
</div>
</div>
<div className="stop-sheet-estimate-arrival">
- <div className={`stop-sheet-estimate-time ${estimate.minutes <= 15 ? 'is-minutes' : ''}`}>
+ <div
+ className={`stop-sheet-estimate-time ${estimate.minutes <= 15 ? "is-minutes" : ""}`}
+ >
<Clock />
{formatTime(estimate.minutes)}
</div>
@@ -203,7 +210,7 @@ export const StopSheet: React.FC<StopSheetProps> = ({
{lastUpdated.toLocaleTimeString(undefined, {
hour: "2-digit",
minute: "2-digit",
- second: "2-digit"
+ second: "2-digit",
})}
</div>
)}
@@ -215,7 +222,9 @@ export const StopSheet: React.FC<StopSheetProps> = ({
disabled={loading}
title={t("estimates.reload", "Recargar estimaciones")}
>
- <RefreshCw className={`reload-icon ${loading ? 'spinning' : ''}`} />
+ <RefreshCw
+ className={`reload-icon ${loading ? "spinning" : ""}`}
+ />
{t("estimates.reload", "Recargar")}
</button>
@@ -224,7 +233,10 @@ export const StopSheet: React.FC<StopSheetProps> = ({
className="stop-sheet-view-all"
onClick={onClose}
>
- {t("map.view_all_estimates", "Ver todas las estimaciones")}
+ {t(
+ "map.view_all_estimates",
+ "Ver todas las estimaciones",
+ )}
</Link>
</div>
</div>
diff --git a/src/frontend/app/components/StopSheetSkeleton.tsx b/src/frontend/app/components/StopSheetSkeleton.tsx
index 6870af2..36ce546 100644
--- a/src/frontend/app/components/StopSheetSkeleton.tsx
+++ b/src/frontend/app/components/StopSheetSkeleton.tsx
@@ -8,7 +8,7 @@ interface StopSheetSkeletonProps {
}
export const StopSheetSkeleton: React.FC<StopSheetSkeletonProps> = ({
- rows = 4
+ rows = 4,
}) => {
const { t } = useTranslation();
@@ -23,7 +23,11 @@ export const StopSheetSkeleton: React.FC<StopSheetSkeletonProps> = ({
{Array.from({ length: rows }, (_, index) => (
<div key={`skeleton-${index}`} className="stop-sheet-estimate-item">
<div className="stop-sheet-estimate-line">
- <Skeleton width="40px" height="24px" style={{ borderRadius: "4px" }} />
+ <Skeleton
+ width="40px"
+ height="24px"
+ style={{ borderRadius: "4px" }}
+ />
</div>
<div className="stop-sheet-estimate-details">
@@ -45,18 +49,32 @@ export const StopSheetSkeleton: React.FC<StopSheetSkeletonProps> = ({
</div>
<div className="stop-sheet-actions">
- <div className="stop-sheet-reload" style={{
- opacity: 0.6,
- pointerEvents: "none"
- }}>
+ <div
+ className="stop-sheet-reload"
+ style={{
+ opacity: 0.6,
+ pointerEvents: "none",
+ }}
+ >
<Skeleton width="70px" height="0.85rem" />
</div>
+<<<<<<< HEAD
<div className="stop-sheet-view-all" style={{
background: "var(--service-background)",
cursor: "not-allowed",
pointerEvents: "none"
}}>
+=======
+ <div
+ className="stop-sheet-view-all"
+ style={{
+ background: "#f0f0f0",
+ cursor: "not-allowed",
+ pointerEvents: "none",
+ }}
+ >
+>>>>>>> 88e0621 (Improve gallery scroll indicators and format code)
<Skeleton width="180px" height="0.85rem" />
</div>
</div>
diff --git a/src/frontend/app/components/TimetableSkeleton.tsx b/src/frontend/app/components/TimetableSkeleton.tsx
index 79c7725..cd5bc81 100644
--- a/src/frontend/app/components/TimetableSkeleton.tsx
+++ b/src/frontend/app/components/TimetableSkeleton.tsx
@@ -8,7 +8,7 @@ interface TimetableSkeletonProps {
}
export const TimetableSkeleton: React.FC<TimetableSkeletonProps> = ({
- rows = 4
+ rows = 4,
}) => {
const { t } = useTranslation();
@@ -24,7 +24,11 @@ export const TimetableSkeleton: React.FC<TimetableSkeletonProps> = ({
<div key={`timetable-skeleton-${index}`} className="timetable-card">
<div className="card-header">
<div className="line-info">
- <Skeleton width="40px" height="24px" style={{ borderRadius: "4px" }} />
+ <Skeleton
+ width="40px"
+ height="24px"
+ style={{ borderRadius: "4px" }}
+ />
</div>
<div className="destination-info">
@@ -48,7 +52,7 @@ export const TimetableSkeleton: React.FC<TimetableSkeletonProps> = ({
style={{
display: "inline-block",
borderRadius: "3px",
- marginRight: "0.5rem"
+ marginRight: "0.5rem",
}}
/>
<Skeleton
diff --git a/src/frontend/app/data/RegionConfig.ts b/src/frontend/app/data/RegionConfig.ts
index c3786ba..08d915f 100644
--- a/src/frontend/app/data/RegionConfig.ts
+++ b/src/frontend/app/data/RegionConfig.ts
@@ -26,7 +26,7 @@ export const REGIONS: Record<RegionId, RegionConfig> = {
defaultCenter: [42.229188855975046, -8.72246955783102],
bounds: {
sw: [-8.951059, 42.098923],
- ne: [-8.447748, 42.3496]
+ ne: [-8.447748, 42.3496],
},
textColour: "#e72b37",
defaultZoom: 14,
@@ -41,7 +41,7 @@ export const REGIONS: Record<RegionId, RegionConfig> = {
defaultCenter: [42.8782, -8.5448],
bounds: {
sw: [-8.884454, 42.719102],
- ne: [-8.243814, 43.02205]
+ ne: [-8.243814, 43.02205],
},
textColour: "#6bb238",
defaultZoom: 14,
diff --git a/src/frontend/app/data/StopDataProvider.ts b/src/frontend/app/data/StopDataProvider.ts
index 799dcb5..7725e15 100644
--- a/src/frontend/app/data/StopDataProvider.ts
+++ b/src/frontend/app/data/StopDataProvider.ts
@@ -47,7 +47,10 @@ async function initStops(region: RegionId) {
// load custom names
const rawCustom = localStorage.getItem(`customStopNames_${region}`);
if (rawCustom) {
- customNamesByRegion[region] = JSON.parse(rawCustom) as Record<number, string>;
+ customNamesByRegion[region] = JSON.parse(rawCustom) as Record<
+ number,
+ string
+ >;
} else {
customNamesByRegion[region] = {};
}
@@ -66,7 +69,10 @@ async function getStops(region: RegionId): Promise<Stop[]> {
}
// New: get single stop by id
-async function getStopById(region: RegionId, stopId: number): Promise<Stop | undefined> {
+async function getStopById(
+ region: RegionId,
+ stopId: number,
+): Promise<Stop | undefined> {
await initStops(region);
const stop = stopsMapByRegion[region]?.[stopId];
if (stop) {
@@ -91,13 +97,19 @@ function setCustomName(region: RegionId, stopId: number, label: string) {
customNamesByRegion[region] = {};
}
customNamesByRegion[region][stopId] = label;
- localStorage.setItem(`customStopNames_${region}`, JSON.stringify(customNamesByRegion[region]));
+ localStorage.setItem(
+ `customStopNames_${region}`,
+ JSON.stringify(customNamesByRegion[region]),
+ );
}
function removeCustomName(region: RegionId, stopId: number) {
if (customNamesByRegion[region]) {
delete customNamesByRegion[region][stopId];
- localStorage.setItem(`customStopNames_${region}`, JSON.stringify(customNamesByRegion[region]));
+ localStorage.setItem(
+ `customStopNames_${region}`,
+ JSON.stringify(customNamesByRegion[region]),
+ );
}
}
@@ -115,7 +127,10 @@ function addFavourite(region: RegionId, stopId: number) {
if (!favouriteStops.includes(stopId)) {
favouriteStops.push(stopId);
- localStorage.setItem(`favouriteStops_${region}`, JSON.stringify(favouriteStops));
+ localStorage.setItem(
+ `favouriteStops_${region}`,
+ JSON.stringify(favouriteStops),
+ );
}
}
@@ -127,7 +142,10 @@ function removeFavourite(region: RegionId, stopId: number) {
}
const newFavouriteStops = favouriteStops.filter((id) => id !== stopId);
- localStorage.setItem(`favouriteStops_${region}`, JSON.stringify(newFavouriteStops));
+ localStorage.setItem(
+ `favouriteStops_${region}`,
+ JSON.stringify(newFavouriteStops),
+ );
}
function isFavourite(region: RegionId, stopId: number): boolean {
@@ -155,7 +173,10 @@ function pushRecent(region: RegionId, stopId: number) {
recentStops.delete(val);
}
- localStorage.setItem(`recentStops_${region}`, JSON.stringify(Array.from(recentStops)));
+ localStorage.setItem(
+ `recentStops_${region}`,
+ JSON.stringify(Array.from(recentStops)),
+ );
}
function getRecent(region: RegionId): number[] {
@@ -179,7 +200,7 @@ async function loadStopsFromNetwork(region: RegionId): Promise<Stop[]> {
const regionConfig = getRegionConfig(region);
const response = await fetch(regionConfig.stopsEndpoint);
const stops = (await response.json()) as Stop[];
- return stops.map((stop) => ({ ...stop, favourite: false } as Stop));
+ return stops.map((stop) => ({ ...stop, favourite: false }) as Stop);
}
export default {
diff --git a/src/frontend/app/maps/styleloader.ts b/src/frontend/app/maps/styleloader.ts
index 08086f1..93f6693 100644
--- a/src/frontend/app/maps/styleloader.ts
+++ b/src/frontend/app/maps/styleloader.ts
@@ -15,7 +15,7 @@ export async function loadStyle(
const style = await resp.json();
return style as StyleSpecification;
- }
+ }
const stylePath = `/maps/styles/${styleName}-${colorScheme}.json`;
const resp = await fetch(stylePath);
diff --git a/src/frontend/app/root.tsx b/src/frontend/app/root.tsx
index 9bc69e4..6bb3da6 100644
--- a/src/frontend/app/root.tsx
+++ b/src/frontend/app/root.tsx
@@ -102,12 +102,10 @@ export function Layout({ children }: { children: React.ReactNode }) {
import NavBar from "./components/NavBar";
export default function App() {
- if ('serviceWorker' in navigator) {
- navigator.serviceWorker
- .register('/pwa-worker.js')
- .catch((error) => {
- console.error('Error registering SW:', error);
- });
+ if ("serviceWorker" in navigator) {
+ navigator.serviceWorker.register("/pwa-worker.js").catch((error) => {
+ console.error("Error registering SW:", error);
+ });
}
return (
diff --git a/src/frontend/app/routes/estimates-$id.css b/src/frontend/app/routes/estimates-$id.css
index 66e7fb6..fde4099 100644
--- a/src/frontend/app/routes/estimates-$id.css
+++ b/src/frontend/app/routes/estimates-$id.css
@@ -74,8 +74,12 @@
}
@keyframes spin {
- from { transform: rotate(0deg); }
- to { transform: rotate(360deg); }
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
}
@media (max-width: 640px) {
diff --git a/src/frontend/app/routes/estimates-$id.tsx b/src/frontend/app/routes/estimates-$id.tsx
index 21186fb..c8c52b5 100644
--- a/src/frontend/app/routes/estimates-$id.tsx
+++ b/src/frontend/app/routes/estimates-$id.tsx
@@ -7,8 +7,14 @@ import { RegularTable } from "../components/RegularTable";
import { useApp } from "../AppContext";
import { GroupedTable } from "../components/GroupedTable";
import { useTranslation } from "react-i18next";
-import { SchedulesTable, type ScheduledTable } from "~/components/SchedulesTable";
-import { SchedulesTableSkeleton, EstimatesGroupedSkeleton } from "~/components/SchedulesTableSkeleton";
+import {
+ SchedulesTable,
+ type ScheduledTable,
+} from "~/components/SchedulesTable";
+import {
+ SchedulesTableSkeleton,
+ EstimatesGroupedSkeleton,
+} from "~/components/SchedulesTableSkeleton";
import { TimetableSkeleton } from "~/components/TimetableSkeleton";
import { ErrorDisplay } from "~/components/ErrorDisplay";
import { PullToRefresh } from "~/components/PullToRefresh";
@@ -24,12 +30,15 @@ export interface Estimate {
}
interface ErrorInfo {
- type: 'network' | 'server' | 'unknown';
+ type: "network" | "server" | "unknown";
status?: number;
message?: string;
}
-const loadData = async (region: RegionId, stopId: string): Promise<Estimate[]> => {
+const loadData = async (
+ region: RegionId,
+ stopId: string,
+): Promise<Estimate[]> => {
const regionConfig = getRegionConfig(region);
const resp = await fetch(`${regionConfig.estimatesEndpoint}?id=${stopId}`, {
headers: {
@@ -44,7 +53,10 @@ const loadData = async (region: RegionId, stopId: string): Promise<Estimate[]> =
return await resp.json();
};
-const loadTimetableData = async (region: RegionId, stopId: string): Promise<ScheduledTable[]> => {
+const loadTimetableData = async (
+ region: RegionId,
+ stopId: string,
+): Promise<ScheduledTable[]> => {
const regionConfig = getRegionConfig(region);
// Check if timetable is available for this region
@@ -52,12 +64,15 @@ const loadTimetableData = async (region: RegionId, stopId: string): Promise<Sche
throw new Error("Timetable not available for this region");
}
- const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
- const resp = await fetch(`${regionConfig.timetableEndpoint}?date=${today}&stopId=${stopId}`, {
- headers: {
- Accept: "application/json",
+ const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format
+ const resp = await fetch(
+ `${regionConfig.timetableEndpoint}?date=${today}&stopId=${stopId}`,
+ {
+ headers: {
+ Accept: "application/json",
+ },
},
- });
+ );
if (!resp.ok) {
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
@@ -91,20 +106,23 @@ export default function Estimates() {
const parseError = (error: any): ErrorInfo => {
if (!navigator.onLine) {
- return { type: 'network', message: 'No internet connection' };
+ return { type: "network", message: "No internet connection" };
}
- if (error.message?.includes('Failed to fetch') || error.message?.includes('NetworkError')) {
- return { type: 'network' };
+ if (
+ error.message?.includes("Failed to fetch") ||
+ error.message?.includes("NetworkError")
+ ) {
+ return { type: "network" };
}
- if (error.message?.includes('HTTP')) {
+ if (error.message?.includes("HTTP")) {
const statusMatch = error.message.match(/HTTP (\d+):/);
const status = statusMatch ? parseInt(statusMatch[1]) : undefined;
- return { type: 'server', status };
+ return { type: "server", status };
}
- return { type: 'unknown', message: error.message };
+ return { type: "unknown", message: error.message };
};
const loadEstimatesData = useCallback(async () => {
@@ -121,7 +139,7 @@ export default function Estimates() {
setStopData(stop);
setCustomName(StopDataProvider.getCustomName(region, stopIdNum));
} catch (error) {
- console.error('Error loading estimates data:', error);
+ console.error("Error loading estimates data:", error);
setEstimatesError(parseError(error));
setData(null);
setDataDate(null);
@@ -144,7 +162,7 @@ export default function Estimates() {
const timetableBody = await loadTimetableData(region, params.id!);
setTimetableData(timetableBody);
} catch (error) {
- console.error('Error loading timetable data:', error);
+ console.error("Error loading timetable data:", error);
setTimetableError(parseError(error));
setTimetableData([]);
} finally {
@@ -153,10 +171,7 @@ export default function Estimates() {
}, [params.id, region, regionConfig.timetableEndpoint]);
const refreshData = useCallback(async () => {
- await Promise.all([
- loadEstimatesData(),
- loadTimetableDataAsync()
- ]);
+ await Promise.all([loadEstimatesData(), loadTimetableDataAsync()]);
}, [loadEstimatesData, loadTimetableDataAsync]);
// Manual refresh function for pull-to-refresh and button
@@ -183,7 +198,9 @@ export default function Estimates() {
loadTimetableDataAsync();
StopDataProvider.pushRecent(region, parseInt(params.id ?? ""));
- setFavourited(StopDataProvider.isFavourite(region, parseInt(params.id ?? "")));
+ setFavourited(
+ StopDataProvider.isFavourite(region, parseInt(params.id ?? "")),
+ );
}, [params.id, region, loadEstimatesData, loadTimetableDataAsync]);
const toggleFavourite = () => {
@@ -273,7 +290,9 @@ export default function Estimates() {
disabled={isManualRefreshing || estimatesLoading}
title={t("estimates.reload", "Recargar estimaciones")}
>
- <RefreshCw className={`refresh-icon ${isManualRefreshing ? 'spinning' : ''}`} />
+ <RefreshCw
+ className={`refresh-icon ${isManualRefreshing ? "spinning" : ""}`}
+ />
</button>
</div>
@@ -290,13 +309,24 @@ export default function Estimates() {
<ErrorDisplay
error={estimatesError}
onRetry={loadEstimatesData}
- title={t("errors.estimates_title", "Error al cargar estimaciones")}
+ title={t(
+ "errors.estimates_title",
+ "Error al cargar estimaciones",
+ )}
/>
) : data ? (
tableStyle === "grouped" ? (
- <GroupedTable data={data} dataDate={dataDate} regionConfig={regionConfig} />
+ <GroupedTable
+ data={data}
+ dataDate={dataDate}
+ regionConfig={regionConfig}
+ />
) : (
- <RegularTable data={data} dataDate={dataDate} regionConfig={regionConfig} />
+ <RegularTable
+ data={data}
+ dataDate={dataDate}
+ regionConfig={regionConfig}
+ />
)
) : null}
</div>
@@ -318,10 +348,7 @@ export default function Estimates() {
currentTime={new Date().toTimeString().slice(0, 8)} // HH:MM:SS
/>
<div className="timetable-actions">
- <Link
- to={`/timetable/${params.id}`}
- className="view-all-link"
- >
+ <Link to={`/timetable/${params.id}`} className="view-all-link">
<ExternalLink className="external-icon" />
{t("timetable.viewAll", "Ver todos los horarios")}
</Link>
diff --git a/src/frontend/app/routes/map.tsx b/src/frontend/app/routes/map.tsx
index f705617..d3288e9 100644
--- a/src/frontend/app/routes/map.tsx
+++ b/src/frontend/app/routes/map.tsx
@@ -145,10 +145,11 @@ export default function StopMap() {
zoom: mapState.zoom,
}}
attributionControl={false}
- maxBounds={REGIONS[region].bounds ? [
- REGIONS[region].bounds!.sw,
- REGIONS[region].bounds!.ne,
- ] : undefined}
+ maxBounds={
+ REGIONS[region].bounds
+ ? [REGIONS[region].bounds!.sw, REGIONS[region].bounds!.ne]
+ : undefined
+ }
>
<NavigationControl position="top-right" />
<GeolocateControl position="top-right" trackUserLocation={true} />
@@ -188,12 +189,12 @@ export default function StopMap() {
"text-offset": [0, 3],
"text-anchor": "center",
"text-justify": "center",
- "text-size": ["interpolate", ["linear"], ["zoom"], 11, 8, 22, 16]
+ "text-size": ["interpolate", ["linear"], ["zoom"], 11, 8, 22, 16],
}}
paint={{
"text-color": `${REGIONS[region].textColour || "#000"}`,
"text-halo-color": "#FFF",
- "text-halo-width": 1
+ "text-halo-width": 1,
}}
/>
diff --git a/src/frontend/app/routes/settings.css b/src/frontend/app/routes/settings.css
index c08ed68..d82cf8b 100644
--- a/src/frontend/app/routes/settings.css
+++ b/src/frontend/app/routes/settings.css
@@ -179,8 +179,12 @@
}
@keyframes spin {
- from { transform: rotate(0deg); }
- to { transform: rotate(360deg); }
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
}
/* Modal styles */
diff --git a/src/frontend/app/routes/settings.tsx b/src/frontend/app/routes/settings.tsx
index cb64f4e..d9efa2e 100644
--- a/src/frontend/app/routes/settings.tsx
+++ b/src/frontend/app/routes/settings.tsx
@@ -159,30 +159,31 @@ export default function Settings() {
</a>
</p>
{region === "vigo" && (
- <p>
- {t("about.data_source_prefix")}{" "}
- <a
- href="https://datos.vigo.org"
- className="about-link"
- rel="nofollow noreferrer noopener"
- >
- datos.vigo.org
- </a>{" "}
- {t("about.data_source_middle")}{" "}
- <a
- href="https://opendefinition.org/licenses/odc-by/"
- className="about-link"
- rel="nofollow noreferrer noopener"
- >
- Open Data Commons Attribution License
- </a>.
- </p>
+ <p>
+ {t("about.data_source_prefix")}{" "}
+ <a
+ href="https://datos.vigo.org"
+ className="about-link"
+ rel="nofollow noreferrer noopener"
+ >
+ datos.vigo.org
+ </a>{" "}
+ {t("about.data_source_middle")}{" "}
+ <a
+ href="https://opendefinition.org/licenses/odc-by/"
+ className="about-link"
+ rel="nofollow noreferrer noopener"
+ >
+ Open Data Commons Attribution License
+ </a>
+ .
+ </p>
)}
{region === "santiago" && (
<p>
- Datos obtenidos de app MaisBus (Concello de Santiago/TUSSA),
- gracias a la documentación de [TP Galicia](https://tpgalicia.github.io/urban/santiago/)
- en GitHub.
+ Datos obtenidos de app MaisBus (Concello de Santiago/TUSSA), gracias a
+ la documentación de [TP
+ Galicia](https://tpgalicia.github.io/urban/santiago/) en GitHub.
</p>
)}
@@ -193,7 +194,7 @@ export default function Settings() {
<p>
{t(
"about.region_change_message",
- "¿Estás seguro de que quieres cambiar la región? Serás redirigido a la lista de paradas."
+ "¿Estás seguro de que quieres cambiar la región? Serás redirigido a la lista de paradas.",
)}
</p>
<div className="modal-buttons">
diff --git a/src/frontend/app/routes/stoplist.tsx b/src/frontend/app/routes/stoplist.tsx
index 80267ea..fe6d2f1 100644
--- a/src/frontend/app/routes/stoplist.tsx
+++ b/src/frontend/app/routes/stoplist.tsx
@@ -32,10 +32,11 @@ export default function StopList() {
);
const fuse = useMemo(
- () => new Fuse(data || [], {
- threshold: 0.3,
- keys: ["name.original", "name.intersect", "stopId"]
- }),
+ () =>
+ new Fuse(data || [], {
+ threshold: 0.3,
+ keys: ["name.original", "name.intersect", "stopId"],
+ }),
[data],
);
@@ -77,7 +78,9 @@ export default function StopList() {
const checkPermission = async () => {
try {
if (navigator.permissions?.query) {
- permissionStatus = await navigator.permissions.query({ name: "geolocation" });
+ permissionStatus = await navigator.permissions.query({
+ name: "geolocation",
+ });
if (permissionStatus.state === "granted") {
requestUserLocation();
}
@@ -109,7 +112,12 @@ export default function StopList() {
}
const toRadians = (value: number) => (value * Math.PI) / 180;
- const getDistance = (lat1: number, lon1: number, lat2: number, lon2: number) => {
+ const getDistance = (
+ lat1: number,
+ lon1: number,
+ lat2: number,
+ lon2: number,
+ ) => {
const R = 6371000; // meters
const dLat = toRadians(lat2 - lat1);
const dLon = toRadians(lon2 - lon1);
@@ -125,7 +133,10 @@ export default function StopList() {
return data
.map((stop) => {
- if (typeof stop.latitude !== "number" || typeof stop.longitude !== "number") {
+ if (
+ typeof stop.latitude !== "number" ||
+ typeof stop.longitude !== "number"
+ ) {
return { stop, distance: Number.POSITIVE_INFINITY };
}
@@ -162,25 +173,24 @@ export default function StopList() {
// Add favourite flags to stops
const favouriteStopsIds = StopDataProvider.getFavouriteIds(region);
- const stopsWithFavourites = stops.map(stop => ({
+ const stopsWithFavourites = stops.map((stop) => ({
...stop,
- favourite: favouriteStopsIds.includes(stop.stopId)
+ favourite: favouriteStopsIds.includes(stop.stopId),
}));
setData(stopsWithFavourites);
// Update favourite and recent stops with full data
- const favStops = stopsWithFavourites.filter(stop =>
- favouriteStopsIds.includes(stop.stopId)
+ const favStops = stopsWithFavourites.filter((stop) =>
+ favouriteStopsIds.includes(stop.stopId),
);
setFavouriteStops(favStops);
const recIds = StopDataProvider.getRecent(region);
const recStops = recIds
- .map(id => stopsWithFavourites.find(stop => stop.stopId === id))
+ .map((id) => stopsWithFavourites.find((stop) => stop.stopId === id))
.filter(Boolean) as Stop[];
setRecentStops(recStops.reverse());
-
} catch (error) {
console.error("Failed to load stops:", error);
} finally {
@@ -212,12 +222,12 @@ export default function StopList() {
// Check if search query is a number (stop code search)
const isNumericSearch = /^\d+$/.test(searchQuery.trim());
-
+
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 exactMatch = data.filter((stop) => stop.stopId === stopId);
if (exactMatch.length > 0) {
items = exactMatch;
} else {
@@ -230,14 +240,14 @@ export default function StopList() {
const results = fuse.search(searchQuery);
items = results.map((result) => result.item);
}
-
+
setSearchResults(items);
}, 300);
};
return (
<div className="page-container stoplist-page">
- <h1 className="page-title">BusUrbano - {REGIONS[region].name}</h1>
+ <h1 className="page-title">BusUrbano - {REGIONS[region].name}</h1>
<form className="search-form">
<div className="form-group">
@@ -286,10 +296,9 @@ export default function StopList() {
<div className="list-container">
<h2 className="page-subtitle">
- {userLocation
- ? t("stoplist.nearby_stops", "Nearby stops")
- : t("stoplist.all_stops", "Paradas")
- }
+ {userLocation
+ ? t("stoplist.nearby_stops", "Nearby stops")
+ : t("stoplist.all_stops", "Paradas")}
</h2>
<ul className="list">
@@ -301,9 +310,9 @@ export default function StopList() {
</>
)}
{!loading && data
- ? (userLocation ? sortedAllStops.slice(0, 6) : sortedAllStops).map((stop) => (
- <StopItem key={stop.stopId} stop={stop} />
- ))
+ ? (userLocation ? sortedAllStops.slice(0, 6) : sortedAllStops).map(
+ (stop) => <StopItem key={stop.stopId} stop={stop} />,
+ )
: null}
</ul>
</div>