diff options
| author | Copilot <198982749+Copilot@users.noreply.github.com> | 2025-11-07 19:44:50 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-07 19:44:50 +0100 |
| commit | a6ec0e52ecb33915cc4c4b22df1d2512ab9b0111 (patch) | |
| tree | 68dfd09c9067bb95dc4f7f5c20004dda20bef9ed /src/frontend/app | |
| parent | 6941f4fb37ffa57c2e4631ff6641976d21151e54 (diff) | |
PWA: use standalone display mode and disable scroll on modal sheets (#82)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com>
Diffstat (limited to 'src/frontend/app')
| -rw-r--r-- | src/frontend/app/components/NavBar.tsx | 8 | ||||
| -rw-r--r-- | src/frontend/app/components/StopAlert.css | 1 | ||||
| -rw-r--r-- | src/frontend/app/components/StopAlert.tsx | 8 | ||||
| -rw-r--r-- | src/frontend/app/components/StopItemSkeleton.tsx | 51 | ||||
| -rw-r--r-- | src/frontend/app/components/StopSheet.css | 4 | ||||
| -rw-r--r-- | src/frontend/app/components/StopSheet.tsx | 13 | ||||
| -rw-r--r-- | src/frontend/app/components/StopSheetSkeleton.tsx | 18 | ||||
| -rw-r--r-- | src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx | 27 | ||||
| -rw-r--r-- | src/frontend/app/components/TimetableSkeleton.tsx | 5 | ||||
| -rw-r--r-- | src/frontend/app/data/StopDataProvider.ts | 2 | ||||
| -rw-r--r-- | src/frontend/app/routes/estimates-$id.tsx | 4 | ||||
| -rw-r--r-- | src/frontend/app/routes/map.tsx | 2 | ||||
| -rw-r--r-- | src/frontend/app/routes/settings.tsx | 11 | ||||
| -rw-r--r-- | src/frontend/app/routes/stops-$id.tsx | 40 | ||||
| -rw-r--r-- | src/frontend/app/routes/timetable-$id.tsx | 35 |
15 files changed, 139 insertions, 90 deletions
diff --git a/src/frontend/app/components/NavBar.tsx b/src/frontend/app/components/NavBar.tsx index bb73d58..f9f1a03 100644 --- a/src/frontend/app/components/NavBar.tsx +++ b/src/frontend/app/components/NavBar.tsx @@ -28,7 +28,7 @@ export default function NavBar() { name: t("navbar.stops", "Paradas"), icon: MapPin, path: "/", - exact: true + exact: true, }, { name: t("navbar.map", "Mapa"), @@ -66,9 +66,9 @@ export default function NavBar() { <nav className="navigation-bar"> {navItems.map((item) => { const Icon = item.icon; - const isActive = item.exact ? - location.pathname === item.path : - location.pathname.startsWith(item.path); + const isActive = item.exact + ? location.pathname === item.path + : location.pathname.startsWith(item.path); return ( <Link diff --git a/src/frontend/app/components/StopAlert.css b/src/frontend/app/components/StopAlert.css index c1d9a0a..8bcc4b0 100644 --- a/src/frontend/app/components/StopAlert.css +++ b/src/frontend/app/components/StopAlert.css @@ -83,4 +83,3 @@ .stop-alert-compact .stop-alert-message { font-size: 0.85rem; } - diff --git a/src/frontend/app/components/StopAlert.tsx b/src/frontend/app/components/StopAlert.tsx index 1cbbd75..6b8da12 100644 --- a/src/frontend/app/components/StopAlert.tsx +++ b/src/frontend/app/components/StopAlert.tsx @@ -31,11 +31,15 @@ export const StopAlert: React.FC<StopAlertProps> = ({ }, [stop.alert]); return ( - <div className={`stop-alert ${alertType} ${compact ? 'stop-alert-compact' : ''}`}> + <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>} + {stop.message && ( + <div className="stop-alert-message">{stop.message}</div> + )} </div> </div> ); diff --git a/src/frontend/app/components/StopItemSkeleton.tsx b/src/frontend/app/components/StopItemSkeleton.tsx index 2791bdc..68172fd 100644 --- a/src/frontend/app/components/StopItemSkeleton.tsx +++ b/src/frontend/app/components/StopItemSkeleton.tsx @@ -11,32 +11,31 @@ const StopItemSkeleton: React.FC<StopItemSkeletonProps> = ({ showId = false, stopId, }) => { - return ( - <SkeletonTheme baseColor="var(--skeleton-base)" highlightColor="var(--skeleton-highlight)"> - <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> - ); + return ( + <SkeletonTheme + baseColor="var(--skeleton-base)" + highlightColor="var(--skeleton-highlight)" + > + <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> + ); }; export default StopItemSkeleton; diff --git a/src/frontend/app/components/StopSheet.css b/src/frontend/app/components/StopSheet.css index 75b12f0..41dfbe0 100644 --- a/src/frontend/app/components/StopSheet.css +++ b/src/frontend/app/components/StopSheet.css @@ -1,6 +1,7 @@ /* Stop Sheet Styles */ .react-modal-sheet-container { background-color: var(--background-color) !important; + touch-action: none; } .stop-sheet-content { @@ -8,6 +9,7 @@ display: flex; flex-direction: column; overflow: hidden; + touch-action: pan-y; } .stop-sheet-header { @@ -275,6 +277,7 @@ [data-rsbs-header] { background-color: var(--background-color); border-bottom: 1px solid var(--border-color); + touch-action: none; } [data-rsbs-header]:before { @@ -292,4 +295,5 @@ border-top-right-radius: 16px; max-height: 95vh; overflow: hidden; + touch-action: none; } diff --git a/src/frontend/app/components/StopSheet.tsx b/src/frontend/app/components/StopSheet.tsx index 2f37519..4bc6f63 100644 --- a/src/frontend/app/components/StopSheet.tsx +++ b/src/frontend/app/components/StopSheet.tsx @@ -129,7 +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 drag="y"> <Sheet.Header /> <Sheet.Content> @@ -193,11 +193,12 @@ export const StopSheet: React.FC<StopSheetProps> = ({ <Clock /> {formatTime(estimate.minutes)} </div> - {REGIONS[region].showMeters && estimate.meters >= 0 && ( - <div className="stop-sheet-estimate-distance"> - {formatDistance(estimate.meters)} - </div> - )} + {REGIONS[region].showMeters && + estimate.meters >= 0 && ( + <div className="stop-sheet-estimate-distance"> + {formatDistance(estimate.meters)} + </div> + )} </div> </div> ))} diff --git a/src/frontend/app/components/StopSheetSkeleton.tsx b/src/frontend/app/components/StopSheetSkeleton.tsx index 0ae83b8..3874038 100644 --- a/src/frontend/app/components/StopSheetSkeleton.tsx +++ b/src/frontend/app/components/StopSheetSkeleton.tsx @@ -13,7 +13,10 @@ export const StopSheetSkeleton: React.FC<StopSheetSkeletonProps> = ({ const { t } = useTranslation(); return ( - <SkeletonTheme baseColor="var(--skeleton-base)" highlightColor="var(--skeleton-highlight)"> + <SkeletonTheme + baseColor="var(--skeleton-base)" + highlightColor="var(--skeleton-highlight)" + > <div className="stop-sheet-estimates"> <h3 className="stop-sheet-subtitle"> {t("estimates.next_arrivals", "Next arrivals")} @@ -59,11 +62,14 @@ export const StopSheetSkeleton: React.FC<StopSheetSkeletonProps> = ({ <Skeleton width="70px" height="0.85rem" /> </div> - <div className="stop-sheet-view-all" style={{ - background: "var(--service-background)", - cursor: "not-allowed", - pointerEvents: "none" - }}> + <div + className="stop-sheet-view-all" + style={{ + background: "var(--service-background)", + cursor: "not-allowed", + pointerEvents: "none", + }} + > <Skeleton width="180px" height="0.85rem" /> </div> </div> diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx index a1b50f2..1ba460b 100644 --- a/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx +++ b/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx @@ -4,7 +4,7 @@ import { type ConsolidatedCirculation } from "~routes/stops-$id"; import LineIcon from "~components/LineIcon"; import { type RegionConfig } from "~data/RegionConfig"; -import './ConsolidatedCirculationList.css'; +import "./ConsolidatedCirculationList.css"; interface RegularTableProps { data: ConsolidatedCirculation[]; @@ -106,9 +106,13 @@ export const ConsolidatedCirculationList: React.FC<RegularTableProps> = ({ if (delay >= -1 && delay <= 2) { return t("estimates.on_time", "on time"); } else if (delay > 2) { - return t("estimates.minutes_late", "{{minutes}} minutes late", { minutes: delay }); + return t("estimates.minutes_late", "{{minutes}} minutes late", { + minutes: delay, + }); } else { - return t("estimates.minutes_early", "{{minutes}} minutes early", { minutes: Math.abs(delay) }); + return t("estimates.minutes_early", "{{minutes}} minutes early", { + minutes: Math.abs(delay), + }); } }; @@ -124,9 +128,7 @@ export const ConsolidatedCirculationList: React.FC<RegularTableProps> = ({ if (estimate.realTime && !estimate.schedule) { return "time-running"; - } - - else if (estimate.realTime && !estimate.schedule?.running) { + } else if (estimate.realTime && !estimate.schedule?.running) { return "time-delayed"; } @@ -134,8 +136,9 @@ export const ConsolidatedCirculationList: React.FC<RegularTableProps> = ({ }; const sortedData = [...data].sort( - (a, b) => (a.realTime?.minutes ?? a.schedule?.minutes ?? 999) - - (b.realTime?.minutes ?? b.schedule?.minutes ?? 999) + (a, b) => + (a.realTime?.minutes ?? a.schedule?.minutes ?? 999) - + (b.realTime?.minutes ?? b.schedule?.minutes ?? 999), ); return ( @@ -153,7 +156,8 @@ export const ConsolidatedCirculationList: React.FC<RegularTableProps> = ({ ) : ( <div className="consolidated-circulation-list"> {sortedData.map((estimate, idx) => { - const displayMinutes = estimate.realTime?.minutes ?? estimate.schedule?.minutes ?? 0; + const displayMinutes = + estimate.realTime?.minutes ?? estimate.schedule?.minutes ?? 0; const timeClass = getTimeClass(estimate); const delayText = getDelayText(estimate); @@ -202,7 +206,10 @@ export const ConsolidatedCirculationList: React.FC<RegularTableProps> = ({ </> ) : ( <> - {t("estimates.unknown_service", "Unknown service. It may be a reinforcement or the service has a different name than planned.")} + {t( + "estimates.unknown_service", + "Unknown service. It may be a reinforcement or the service has a different name than planned.", + )} </> )} </span> diff --git a/src/frontend/app/components/TimetableSkeleton.tsx b/src/frontend/app/components/TimetableSkeleton.tsx index cd5bc81..2d4fc29 100644 --- a/src/frontend/app/components/TimetableSkeleton.tsx +++ b/src/frontend/app/components/TimetableSkeleton.tsx @@ -13,7 +13,10 @@ export const TimetableSkeleton: React.FC<TimetableSkeletonProps> = ({ const { t } = useTranslation(); return ( - <SkeletonTheme baseColor="var(--skeleton-base)" highlightColor="var(--skeleton-highlight)"> + <SkeletonTheme + baseColor="var(--skeleton-base)" + highlightColor="var(--skeleton-highlight)" + > <div className="timetable-container"> <div className="timetable-caption"> <Skeleton width="250px" height="1.1rem" /> diff --git a/src/frontend/app/data/StopDataProvider.ts b/src/frontend/app/data/StopDataProvider.ts index 7725e15..e3936b4 100644 --- a/src/frontend/app/data/StopDataProvider.ts +++ b/src/frontend/app/data/StopDataProvider.ts @@ -21,7 +21,7 @@ export interface Stop { title?: string; message?: string; - alert?: "info"|"warning"|"error"; + alert?: "info" | "warning" | "error"; cancelled?: boolean; } diff --git a/src/frontend/app/routes/estimates-$id.tsx b/src/frontend/app/routes/estimates-$id.tsx index 81c83ea..84d25c1 100644 --- a/src/frontend/app/routes/estimates-$id.tsx +++ b/src/frontend/app/routes/estimates-$id.tsx @@ -299,9 +299,7 @@ export default function Estimates() { </div> {stopData && stopData.lines && stopData.lines.length > 0 && ( - <div - className={`estimates-lines-container`} - > + <div className={`estimates-lines-container`}> {stopData.lines.map((line) => ( <div key={line} className="estimates-line-icon"> <LineIcon line={line} region={region} rounded /> diff --git a/src/frontend/app/routes/map.tsx b/src/frontend/app/routes/map.tsx index e8b3cf3..40be174 100644 --- a/src/frontend/app/routes/map.tsx +++ b/src/frontend/app/routes/map.tsx @@ -144,7 +144,7 @@ export default function StopMap() { longitude: getLongitude(mapState.center), zoom: mapState.zoom, }} - attributionControl={{compact: false}} + attributionControl={{ compact: false }} maxBounds={ REGIONS[region].bounds ? [REGIONS[region].bounds!.sw, REGIONS[region].bounds!.ne] diff --git a/src/frontend/app/routes/settings.tsx b/src/frontend/app/routes/settings.tsx index c916877..85ec5a1 100644 --- a/src/frontend/app/routes/settings.tsx +++ b/src/frontend/app/routes/settings.tsx @@ -123,12 +123,19 @@ export default function Settings() { className="form-select-inline" value={tableStyle} onChange={(e) => - setTableStyle(e.target.value as "regular" | "grouped" | "experimental_consolidated") + setTableStyle( + e.target.value as + | "regular" + | "grouped" + | "experimental_consolidated", + ) } > <option value="regular">{t("about.table_style_regular")}</option> <option value="grouped">{t("about.table_style_grouped")}</option> - <option value="experimental_consolidated">{t("about.table_style_experimental_consolidated")}</option> + <option value="experimental_consolidated"> + {t("about.table_style_experimental_consolidated")} + </option> </select> </div> <details className="form-details"> diff --git a/src/frontend/app/routes/stops-$id.tsx b/src/frontend/app/routes/stops-$id.tsx index ea60da7..7d533a5 100644 --- a/src/frontend/app/routes/stops-$id.tsx +++ b/src/frontend/app/routes/stops-$id.tsx @@ -27,7 +27,6 @@ export interface ConsolidatedCirculation { }; } - interface ErrorInfo { type: "network" | "server" | "unknown"; status?: number; @@ -39,11 +38,14 @@ const loadConsolidatedData = async ( stopId: string, ): Promise<ConsolidatedCirculation[]> => { const regionConfig = getRegionConfig(region); - const resp = await fetch(`${regionConfig.consolidatedCirculationsEndpoint}?stopId=${stopId}`, { - headers: { - Accept: "application/json", + const resp = await fetch( + `${regionConfig.consolidatedCirculationsEndpoint}?stopId=${stopId}`, + { + headers: { + Accept: "application/json", + }, }, - }); + ); if (!resp.ok) { throw new Error(`HTTP ${resp.status}: ${resp.statusText}`); @@ -232,9 +234,7 @@ export default function Estimates() { </div> {stopData && stopData.lines && stopData.lines.length > 0 && ( - <div - className={`estimates-lines-container`} - > + <div className={`estimates-lines-container`}> {stopData.lines.map((line) => ( <div key={line} className="estimates-line-icon"> <LineIcon line={line} region={region} rounded /> @@ -244,18 +244,30 @@ export default function Estimates() { )} <div className="experimental-notice"> - <strong>{t("estimates.experimental_feature", "Experimental feature")}</strong> - <p>{t("estimates.experimental_description", "This view uses consolidated data from multiple real-time sources. This feature is experimental and may not be completely accurate.")}</p> + <strong> + {t("estimates.experimental_feature", "Experimental feature")} + </strong> + <p> + {t( + "estimates.experimental_description", + "This view uses consolidated data from multiple real-time sources. This feature is experimental and may not be completely accurate.", + )} + </p> </div> {stopData && <StopAlert stop={stopData} />} <div className="table-responsive"> - {data ? (<> - <ConsolidatedCirculationList data={data} dataDate={dataDate} regionConfig={regionConfig} /> - </>) : null} + {data ? ( + <> + <ConsolidatedCirculationList + data={data} + dataDate={dataDate} + regionConfig={regionConfig} + /> + </> + ) : null} </div> - </div> </PullToRefresh> ); diff --git a/src/frontend/app/routes/timetable-$id.tsx b/src/frontend/app/routes/timetable-$id.tsx index df77372..da7a2e7 100644 --- a/src/frontend/app/routes/timetable-$id.tsx +++ b/src/frontend/app/routes/timetable-$id.tsx @@ -450,7 +450,7 @@ const ScrollFabManager: React.FC<{ const style = getComputedStyle(el); const hasScroll = el.scrollHeight > el.clientHeight + 8; const overflowY = style.overflowY; - if (hasScroll && (overflowY === 'auto' || overflowY === 'scroll')) { + if (hasScroll && (overflowY === "auto" || overflowY === "scroll")) { return el; } el = el.parentElement; @@ -465,12 +465,14 @@ const ScrollFabManager: React.FC<{ const handleScroll = () => { const scrollTop = useWindowScroll - ? (window.scrollY || document.documentElement.scrollTop || 0) + ? window.scrollY || document.documentElement.scrollTop || 0 : scrollEl!.scrollTop; const scrollHeight = useWindowScroll ? document.documentElement.scrollHeight : scrollEl!.scrollHeight; - const clientHeight = useWindowScroll ? window.innerHeight : scrollEl!.clientHeight; + const clientHeight = useWindowScroll + ? window.innerHeight + : scrollEl!.clientHeight; const scrollBottom = scrollHeight - scrollTop - clientHeight; const threshold = 80; // slightly smaller threshold for responsiveness @@ -479,39 +481,46 @@ const ScrollFabManager: React.FC<{ if (nextEntryRef.current) { const rect = nextEntryRef.current.getBoundingClientRect(); - const isNextVisible = rect.top >= 0 && rect.bottom <= window.innerHeight; + const isNextVisible = + rect.top >= 0 && rect.bottom <= window.innerHeight; setShowGoToNow(!isNextVisible); } }; const target: any = useWindowScroll ? window : scrollEl!; - target.addEventListener('scroll', handleScroll, { passive: true }); - window.addEventListener('resize', handleScroll); + target.addEventListener("scroll", handleScroll, { passive: true }); + window.addEventListener("resize", handleScroll); handleScroll(); return () => { - target.removeEventListener('scroll', handleScroll); - window.removeEventListener('resize', handleScroll); + target.removeEventListener("scroll", handleScroll); + window.removeEventListener("resize", handleScroll); }; }, [containerRef, nextEntryRef, disabled, data, currentTime]); const scrollToTop = () => { const scrollEl = getScrollContainer(); if (!scrollEl) { - window.scrollTo({ top: 0, behavior: 'smooth' }); + window.scrollTo({ top: 0, behavior: "smooth" }); } else { - scrollEl.scrollTo({ top: 0, behavior: 'smooth' }); + scrollEl.scrollTo({ top: 0, behavior: "smooth" }); } }; const scrollToBottom = () => { const scrollEl = getScrollContainer(); if (!scrollEl) { - window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'smooth' }); + window.scrollTo({ + top: document.documentElement.scrollHeight, + behavior: "smooth", + }); } else { - scrollEl.scrollTo({ top: scrollEl.scrollHeight, behavior: 'smooth' }); + scrollEl.scrollTo({ top: scrollEl.scrollHeight, behavior: "smooth" }); } }; const scrollToNow = () => { - nextEntryRef.current?.scrollIntoView({ behavior: "smooth", block: "center" }); + nextEntryRef.current?.scrollIntoView({ + behavior: "smooth", + block: "center", + }); }; if (disabled) return null; |
