diff options
| author | copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> | 2025-11-06 22:52:02 +0000 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-11-07 10:47:20 +0100 |
| commit | ee77f38cdb324cbcf12518490df77fc9e6b89282 (patch) | |
| tree | 407f64a434291e1e375e6a1ccb55f59fa886a1ef /src/frontend/app/components | |
| parent | e51cdd89afc08274ca622e18b8127feca29e90a3 (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')
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 |
