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,
}))
99 changes: 51 additions & 48 deletions src/pages/gaps/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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 @@ -119,56 +120,58 @@ 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')} />
</Grid>
<Grid size={{ xs: 8 }}>
<DateSelector time={dayjs.tz(date, ISRAEL_TIMEZONE)} onChange={handleDateChange} />
</Grid>
{/* choose operator */}
<Grid size={{ xs: 4 }}>
<Label text={t('choose_operator')} />
</Grid>
<Grid size={{ xs: 8 }}>
<OperatorSelector
operatorId={operatorId ?? undefined}
setOperatorId={handleOperatorChange}
/>
</Grid>
{/* choose line */}
<Grid size={{ xs: 4 }}>
<Label text={t('choose_line')} />
</Grid>
<Grid size={{ xs: 8 }}>
<LineNumberSelector
lineNumber={lineNumber ?? undefined}
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 ?? undefined}
setRouteKey={handleRouteKeyChange}
<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.tz(date, ISRAEL_TIMEZONE)} onChange={handleDateChange} />
</Grid>
{/* choose operator */}
<Grid size={{ xs: 4 }}>
<Label text={t('choose_operator')} />
</Grid>
<Grid size={{ xs: 8 }}>
<OperatorSelector
operatorId={operatorId ?? undefined}
setOperatorId={handleOperatorChange}
/>
)}
</Grid>
<Grid size={{ xs: 12 }}>
{gapsIsLoading && (
<Row>
<Label text={t('loading_gaps')} />
<CircularProgress />
</Row>
)}
</Grid>
{/* choose line */}
<Grid size={{ xs: 4 }}>
<Label text={t('choose_line')} />
</Grid>
<Grid size={{ xs: 8 }}>
<LineNumberSelector
lineNumber={lineNumber ?? undefined}
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 ?? undefined}
setRouteKey={handleRouteKeyChange}
/>
)}
</Grid>
<Grid size={{ xs: 12 }}>
{gapsIsLoading && (
<Row>
<Label text={t('loading_gaps')} />
<CircularProgress />
</Row>
)}
</Grid>
</Grid>
</Grid>
</StickyInputs>
{routeKey && routeKey !== '' && (
<GapsTable
loading={gapsIsLoading}
Expand Down
191 changes: 98 additions & 93 deletions src/pages/historicTimeline/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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 @@ -108,103 +109,107 @@ 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.tz(date, ISRAEL_TIMEZONE)}
onChange={(ts) => {
if (!ts) return
setSearch((current) => ({
...current,
date: ts.format('YYYY-MM-DD'),
}))
}}
/>
</Grid>
{/* choose time */}
<Grid size={{ lg: 4, md: 6, xs: 12 }}>
<TimeSelector
time={timeOfDay}
onChange={(ts) => {
if (!ts) return
setTimeOfDay(ts.startOf('minute'))
}}
/>
</Grid>
{/* choose operator */}
<Grid size={{ lg: 4, md: 6, xs: 12 }}>
<OperatorSelector
operatorId={operatorId ?? undefined}
setOperatorId={(id) => setSearch((current) => ({ ...current, operatorId: id }))}
/>
</Grid>
{/* choose line */}
<Grid size={{ lg: 4, md: 6, xs: 12 }}>
<LineNumberSelector
lineNumber={lineNumber ?? undefined}
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 ?? undefined}
setRouteKey={(key) =>
setSearch((current) => ({ ...current, routeKey: key ?? null }))
<StickyInputs>
<Grid container spacing={2}>
{/* choose date */}
<Grid size={{ lg: 4, md: 6, xs: 12 }}>
<DateSelector
time={dayjs.tz(date, ISRAEL_TIMEZONE)}
onChange={(ts) => {
if (!ts) return
setSearch((current) => ({
...current,
date: ts.format('YYYY-MM-DD'),
}))
}}
/>
</Grid>
{/* choose time */}
<Grid size={{ lg: 4, md: 6, xs: 12 }}>
<TimeSelector
time={timeOfDay}
onChange={(ts) => {
if (!ts) return
setTimeOfDay(ts.startOf('minute'))
}}
/>
</Grid>
{/* choose operator */}
<Grid size={{ lg: 4, md: 6, xs: 12 }}>
<OperatorSelector
operatorId={operatorId ?? undefined}
setOperatorId={(id) => setSearch((current) => ({ ...current, operatorId: id }))}
/>
</Grid>
{/* choose line */}
<Grid size={{ lg: 4, md: 6, xs: 12 }}>
<LineNumberSelector
lineNumber={lineNumber ?? undefined}
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 ?? undefined}
setRouteKey={(key) =>
setSearch((current) => ({ ...current, routeKey: key ?? null }))
}
/>
)}
</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={search.stopKey ?? undefined}
setStopKey={(key) =>
setSearch((current) => ({ ...current, stopKey: key ?? null }))
}
/>
)}
</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={search.stopKey ?? undefined}
setStopKey={(key) => setSearch((current) => ({ ...current, stopKey: key ?? null }))}
</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={time}
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={time}
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