Skip to content
13 changes: 13 additions & 0 deletions src/pages/components/StickyInputs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { styled } from '@mui/material/styles'

// Pinned filter section. Uses the MUI theme's default page background (the same
// color ScopedCssBaseline paints) so scrolling results don't bleed through it
// in either light or dark mode. `background: inherit` was transparent here
// because PageContainer has no explicit background.
export const StickyInputs = styled('div')(({ theme }) => ({
position: 'sticky',
top: 0,
zIndex: 100,
backgroundColor: theme.palette.background.default,
paddingBottom: 8,
}))
89 changes: 46 additions & 43 deletions src/pages/gaps/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import OperatorSelector from '../components/OperatorSelector'
import { PageContainer } from '../components/PageContainer'
import RouteSelector from '../components/RouteSelector'
import { Row } from '../components/Row'
import { StickyInputs } from '../components/StickyInputs'
import GapsTable from './GapsTable'

const GapsPage = () => {
Expand Down Expand Up @@ -110,50 +111,52 @@ const GapsPage = () => {
<Alert severity="info" variant="outlined" icon={false}>
{t('gaps_page_description')}
</Alert>
<Grid container spacing={2} sx={{ maxWidth: INPUT_SIZE }}>
{/* choose date */}
<Grid size={{ xs: 4 }}>
<Label text={t('choose_date')} />
<StickyInputs>
<Grid container spacing={2} sx={{ maxWidth: INPUT_SIZE }}>
{/* choose date */}
<Grid size={{ xs: 4 }}>
<Label text={t('choose_date')} />
</Grid>
<Grid size={{ xs: 8 }}>
<DateSelector time={dayjs(timestamp)} onChange={handleTimestampChange} />
</Grid>
{/* choose operator */}
<Grid size={{ xs: 4 }}>
<Label text={t('choose_operator')} />
</Grid>
<Grid size={{ xs: 8 }}>
<OperatorSelector operatorId={operatorId} setOperatorId={handleOperatorChange} />
</Grid>
{/* choose line */}
<Grid size={{ xs: 4 }}>
<Label text={t('choose_line')} />
</Grid>
<Grid size={{ xs: 8 }}>
<LineNumberSelector lineNumber={lineNumber} setLineNumber={handleLineNumberChange} />
</Grid>
{/* choose routes */}
<Grid size={{ xs: 12 }}>
{routes?.length === 0 ? (
<NotFound>{t('line_not_found')}</NotFound>
) : (
<RouteSelector
routes={routes || []}
disabled={!routes}
routeKey={routeKey}
setRouteKey={handleRouteKeyChange}
/>
)}
</Grid>
<Grid size={{ xs: 12 }}>
{gapsIsLoading && (
<Row>
<Label text={t('loading_gaps')} />
<CircularProgress />
</Row>
)}
</Grid>
</Grid>
<Grid size={{ xs: 8 }}>
<DateSelector time={dayjs(timestamp)} onChange={handleTimestampChange} />
</Grid>
{/* choose operator */}
<Grid size={{ xs: 4 }}>
<Label text={t('choose_operator')} />
</Grid>
<Grid size={{ xs: 8 }}>
<OperatorSelector operatorId={operatorId} setOperatorId={handleOperatorChange} />
</Grid>
{/* choose line */}
<Grid size={{ xs: 4 }}>
<Label text={t('choose_line')} />
</Grid>
<Grid size={{ xs: 8 }}>
<LineNumberSelector lineNumber={lineNumber} setLineNumber={handleLineNumberChange} />
</Grid>
{/* choose routes */}
<Grid size={{ xs: 12 }}>
{routes?.length === 0 ? (
<NotFound>{t('line_not_found')}</NotFound>
) : (
<RouteSelector
routes={routes || []}
disabled={!routes}
routeKey={routeKey}
setRouteKey={handleRouteKeyChange}
/>
)}
</Grid>
<Grid size={{ xs: 12 }}>
{gapsIsLoading && (
<Row>
<Label text={t('loading_gaps')} />
<CircularProgress />
</Row>
)}
</Grid>
</Grid>
</StickyInputs>
{routeKey && routeKey !== '' && (
<GapsTable
loading={gapsIsLoading}
Expand Down
205 changes: 104 additions & 101 deletions src/pages/historicTimeline/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import LineNumberSelector from 'src/pages/components/LineSelector'
import OperatorSelector from 'src/pages/components/OperatorSelector'
import RouteSelector from 'src/pages/components/RouteSelector'
import { Row } from 'src/pages/components/Row'
import { StickyInputs } from 'src/pages/components/StickyInputs'

Copilot AI Apr 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import style for StickyInputs is inconsistent across pages: this file uses an absolute src/... import while src/pages/gaps/index.tsx uses a relative ../components/StickyInputs. Standardizing on one convention improves consistency and avoids path-alias-only builds causing import churn (and can prevent accidental duplicate module instances in some setups).

Suggested change
import { StickyInputs } from 'src/pages/components/StickyInputs'
import { StickyInputs } from '../components/StickyInputs'

Copilot uses AI. Check for mistakes.
import StopSelector from 'src/pages/components/StopSelector'
import { TimelineBoard } from 'src/pages/components/timeline/TimelineBoard'
import Widget from 'src/shared/Widget'
Expand Down Expand Up @@ -101,110 +102,112 @@ const TimelinePage = () => {
{t('no_data_from_ETL')}
</Alert>
)}
<Grid container spacing={2}>
{/* choose date */}
<Grid size={{ lg: 4, md: 6, xs: 12 }}>
<DateSelector
time={dayjs(timestamp)}
onChange={(ts) => {
if (!ts) return
const currentTime = dayjs(timestamp)
const newTimestamp = ts
.hour(currentTime.hour())
.minute(currentTime.minute())
.startOf('minute')
.valueOf()
setSearch((current) => ({ ...current, timestamp: newTimestamp }))
}}
/>
</Grid>
{/* choose time */}
<Grid size={{ lg: 4, md: 6, xs: 12 }}>
<TimeSelector
time={dayjs(timestamp)}
onChange={(ts) => {
if (!ts) return
const currentDate = dayjs(timestamp)
const newTimestamp = currentDate
.hour(ts.hour())
.minute(ts.minute())
.startOf('minute')
.valueOf()
setSearch((current) => ({ ...current, timestamp: newTimestamp }))
}}
/>
</Grid>
{/* choose operator */}
<Grid size={{ lg: 4, md: 6, xs: 12 }}>
<OperatorSelector
operatorId={operatorId}
setOperatorId={(id) => setSearch((current) => ({ ...current, operatorId: id }))}
/>
</Grid>
{/* choose line */}
<Grid size={{ lg: 4, md: 6, xs: 12 }}>
<LineNumberSelector
lineNumber={lineNumber}
setLineNumber={(number) => setSearch((current) => ({ ...current, lineNumber: number }))}
/>
</Grid>
{/* routes */}
<Grid container size={{ lg: 4, md: 6, xs: 12 }}>
<Row style={{ width: '100%' }}>
<div style={{ width: '100%' }}>
{routesQuery.data?.length === 0 ? (
<NotFound>{t('line_not_found')}</NotFound>
) : (
<RouteSelector
disabled={!routesQuery.data}
routes={routesQuery.data || []}
routeKey={routeKey}
setRouteKey={(key) => setSearch((current) => ({ ...current, routeKey: key }))}
<StickyInputs>
<Grid container spacing={2}>
{/* choose date */}
<Grid size={{ lg: 4, md: 6, xs: 12 }}>
<DateSelector
time={dayjs(timestamp)}
onChange={(ts) => {
if (!ts) return
const currentTime = dayjs(timestamp)
const newTimestamp = ts
.hour(currentTime.hour())
.minute(currentTime.minute())
.startOf('minute')
.valueOf()
setSearch((current) => ({ ...current, timestamp: newTimestamp }))
}}
/>
</Grid>
{/* choose time */}
<Grid size={{ lg: 4, md: 6, xs: 12 }}>
<TimeSelector
time={dayjs(timestamp)}
onChange={(ts) => {
if (!ts) return
const currentDate = dayjs(timestamp)
const newTimestamp = currentDate
.hour(ts.hour())
.minute(ts.minute())
.startOf('minute')
.valueOf()
setSearch((current) => ({ ...current, timestamp: newTimestamp }))
}}
/>
</Grid>
{/* choose operator */}
<Grid size={{ lg: 4, md: 6, xs: 12 }}>
<OperatorSelector
operatorId={operatorId}
setOperatorId={(id) => setSearch((current) => ({ ...current, operatorId: id }))}
/>
</Grid>
{/* choose line */}
<Grid size={{ lg: 4, md: 6, xs: 12 }}>
<LineNumberSelector
lineNumber={lineNumber}
setLineNumber={(number) =>
setSearch((current) => ({ ...current, lineNumber: number }))
}
/>
</Grid>
{/* routes */}
<Grid container size={{ lg: 4, md: 6, xs: 12 }}>
<Row style={{ width: '100%' }}>
<div style={{ width: '100%' }}>
{routesQuery.data?.length === 0 ? (
<NotFound>{t('line_not_found')}</NotFound>
) : (
<RouteSelector
disabled={!routesQuery.data}
routes={routesQuery.data || []}
routeKey={routeKey}
setRouteKey={(key) => setSearch((current) => ({ ...current, routeKey: key }))}
/>
)}
</div>
{routesQuery.isLoading && <CircularProgress />}
</Row>
</Grid>
{/* stops */}
<Grid container size={{ lg: 4, md: 6, xs: 12 }}>
<Row style={{ width: '100%' }}>
<div style={{ width: '100%' }}>
<StopSelector
disabled={!stopsQuery.data}
stops={stopsQuery.data || []}
stopKey={stopKey}
setStopKey={(key) => setStopKey(key)}
/>
)}
</div>
{routesQuery.isLoading && <CircularProgress />}
</Row>
</div>
{stopsQuery.isLoading && <CircularProgress />}
</Row>
</Grid>
</Grid>
{/* stops */}
<Grid container size={{ lg: 4, md: 6, xs: 12 }}>
<Row style={{ width: '100%' }}>
<div style={{ width: '100%' }}>
<StopSelector
disabled={!stopsQuery.data}
stops={stopsQuery.data || []}
stopKey={stopKey}
setStopKey={(key) => setStopKey(key)}
</StickyInputs>
{/* hits timeline */}
{selectedRoute && selectedStop && (
<Widget marginBottom>
{hitsQuery.isLoading && (
<Row>
<Label text={t('loading_hits')} />
<CircularProgress />
</Row>
)}
{!hitsQuery.isLoading &&
((hitsQuery.data?.gtfsTime && hitsQuery.data.gtfsTime.length > 0) ||
(hitsQuery.data?.siriTime && hitsQuery.data.siriTime.length > 0) ? (
<TimelineBoard
target={dayjs(timestamp)}
gtfsTimes={hitsQuery.data.gtfsTime}
siriTimes={hitsQuery.data.siriTime}
/>
</div>
{stopsQuery.isLoading && <CircularProgress />}
</Row>
</Grid>
{/* hits timeline */}
{selectedRoute && selectedStop && (
<Grid size={{ xs: 12 }}>
<Widget marginBottom>
{hitsQuery.isLoading && (
<Row>
<Label text={t('loading_hits')} />
<CircularProgress />
</Row>
)}
{!hitsQuery.isLoading &&
((hitsQuery.data?.gtfsTime && hitsQuery.data.gtfsTime.length > 0) ||
(hitsQuery.data?.siriTime && hitsQuery.data.siriTime.length > 0) ? (
<TimelineBoard
target={dayjs(timestamp)}
gtfsTimes={hitsQuery.data.gtfsTime}
siriTimes={hitsQuery.data.siriTime}
/>
) : (
<NotFound>{t('hits_not_found')}</NotFound>
))}
</Widget>
</Grid>
)}
</Grid>
) : (
<NotFound>{t('hits_not_found')}</NotFound>
))}
</Widget>
)}
</PageContainer>
)
}
Expand Down
Loading