aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app
diff options
context:
space:
mode:
authorCopilot <198982749+Copilot@users.noreply.github.com>2025-11-07 19:44:50 +0100
committerGitHub <noreply@github.com>2025-11-07 19:44:50 +0100
commita6ec0e52ecb33915cc4c4b22df1d2512ab9b0111 (patch)
tree68dfd09c9067bb95dc4f7f5c20004dda20bef9ed /src/frontend/app
parent6941f4fb37ffa57c2e4631ff6641976d21151e54 (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.tsx8
-rw-r--r--src/frontend/app/components/StopAlert.css1
-rw-r--r--src/frontend/app/components/StopAlert.tsx8
-rw-r--r--src/frontend/app/components/StopItemSkeleton.tsx51
-rw-r--r--src/frontend/app/components/StopSheet.css4
-rw-r--r--src/frontend/app/components/StopSheet.tsx13
-rw-r--r--src/frontend/app/components/StopSheetSkeleton.tsx18
-rw-r--r--src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx27
-rw-r--r--src/frontend/app/components/TimetableSkeleton.tsx5
-rw-r--r--src/frontend/app/data/StopDataProvider.ts2
-rw-r--r--src/frontend/app/routes/estimates-$id.tsx4
-rw-r--r--src/frontend/app/routes/map.tsx2
-rw-r--r--src/frontend/app/routes/settings.tsx11
-rw-r--r--src/frontend/app/routes/stops-$id.tsx40
-rw-r--r--src/frontend/app/routes/timetable-$id.tsx35
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;