aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/components
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/components
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/components')
-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
18 files changed, 394 insertions, 185 deletions
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