@autobusal/routes-order 1.37.7 → 1.37.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Availability/Availability.tsx +8 -1
- package/Availability/Skeleton.tsx +119 -0
- package/Availability/styles.ts +2 -1
- package/Facts/Facts.tsx +9 -3
- package/Facts/styles.ts +12 -0
- package/Found/Dates/Dates.tsx +29 -5
- package/Found/Dates/styles.ts +20 -7
- package/Found/Filters/styles.ts +16 -3
- package/Found/Orders/Orders.tsx +36 -34
- package/Found/Orders/Skeleton.tsx +102 -0
- package/Found/Route/Route.tsx +4 -10
- package/Found/Route/styles.ts +50 -7
- package/Found/Sort/styles.ts +6 -1
- package/Found/refine.ts +0 -46
- package/RoutesOrder.tsx +24 -2
- package/Sections/Sections.tsx +25 -5
- package/Step1/Step1.tsx +32 -4
- package/Step1/prepare.ts +39 -2
- package/Step4/Passenger/Passenger.tsx +67 -14
- package/Step4/Passenger/styles.ts +31 -0
- package/Step5/Billing/Billing.tsx +12 -9
- package/Step5/Billing/styles.ts +8 -0
- package/Step5/Summary/About/About.tsx +15 -2
- package/Step5/Summary/About/styles.ts +22 -0
- package/package.json +1 -1
- package/styles.ts +43 -0
- package/Found/Group/Group.tsx +0 -68
- package/Found/Group/styles.ts +0 -44
|
@@ -17,8 +17,15 @@ interface Props {
|
|
|
17
17
|
*
|
|
18
18
|
* A week: enough that somebody asking "is there a bus this weekend" never
|
|
19
19
|
* has to expand anything, few enough that the page still ends.
|
|
20
|
+
*
|
|
21
|
+
* Edited: Claude - Date: 2026-09-16
|
|
22
|
+
*
|
|
23
|
+
* Exported now: ./Skeleton reserves height for exactly this many day-blocks,
|
|
24
|
+
* not all thirty, because days 8-30 sit inside the closed <details> below
|
|
25
|
+
* and reserve nothing themselves. One number, so the skeleton and the real
|
|
26
|
+
* table can never disagree about how many days are "above the fold".
|
|
20
27
|
*/
|
|
21
|
-
const VISIBLE = 7;
|
|
28
|
+
export const VISIBLE = 7;
|
|
22
29
|
|
|
23
30
|
/**
|
|
24
31
|
* Real departures on real dates, for the next thirty days.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { TFunction } from 'i18next';
|
|
2
|
+
import { Bone } from '../styles';
|
|
3
|
+
import { Container, Heading, Lead, Wrap, Table } from './styles';
|
|
4
|
+
import { VISIBLE } from './Availability';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
id: string
|
|
8
|
+
// Real city names where they are already known - see the call site in
|
|
9
|
+
// ../Sections/Sections, which has them from the SAME facts query this
|
|
10
|
+
// section's own heading is built from once it answers. Undefined only
|
|
11
|
+
// for the instant before that, in which case the heading is a bar rather
|
|
12
|
+
// than a guess at English words in the middle of another language.
|
|
13
|
+
from?: string
|
|
14
|
+
to?: string
|
|
15
|
+
t: TFunction<'common'>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const COLUMNS = 6;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* How many rows a day's placeholder reserves.
|
|
22
|
+
*
|
|
23
|
+
* Edited: Claude - Date: 2026-09-16
|
|
24
|
+
*
|
|
25
|
+
* Not knowable before the fetch answers - a quiet rural pair might run once
|
|
26
|
+
* a day, a trunk route ten times (see TRIPS in ./Schema, measured on
|
|
27
|
+
* Tirana-Thessaloniki). Three is not a guess pulled from nowhere: three rows
|
|
28
|
+
* across the seven VISIBLE days is twenty-one, and at this table's real
|
|
29
|
+
* padding and line-height that comes to almost exactly the ~1,290px,
|
|
30
|
+
* ~20-row block that was actually measured on production - see the
|
|
31
|
+
* height math this number produces below, which is what earns it the name
|
|
32
|
+
* "defensible" rather than "round". Under-reserving still shifts the page
|
|
33
|
+
* when the real rows land; over-reserving by a lot wastes screen before
|
|
34
|
+
* anyone has asked for anything - three is the number that keeps both
|
|
35
|
+
* errors small.
|
|
36
|
+
*/
|
|
37
|
+
const ROWS_PER_DAY = 3;
|
|
38
|
+
|
|
39
|
+
const day = (index: number): JSX.Element => (
|
|
40
|
+
<tbody key={ index }>
|
|
41
|
+
<tr>
|
|
42
|
+
<th colSpan={ COLUMNS } scope="colgroup">
|
|
43
|
+
<Bone $width={ 35 } $height={ 16 } />
|
|
44
|
+
</th>
|
|
45
|
+
</tr>
|
|
46
|
+
|
|
47
|
+
{ Array.from({ length: ROWS_PER_DAY }, (_, row) => (
|
|
48
|
+
<tr key={ row }>
|
|
49
|
+
<td><Bone $width={ 70 } /></td>
|
|
50
|
+
<td><Bone $width={ 70 } /></td>
|
|
51
|
+
<td><Bone $width={ 60 } /></td>
|
|
52
|
+
<td><Bone $width={ 80 } /></td>
|
|
53
|
+
<td><Bone $width={ 55 } /></td>
|
|
54
|
+
<td><Bone $width={ 60 } /></td>
|
|
55
|
+
</tr>
|
|
56
|
+
)) }
|
|
57
|
+
</tbody>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* What the section looks like before /api/routes/search/availability
|
|
62
|
+
* answers.
|
|
63
|
+
*
|
|
64
|
+
* Edited: Claude - Date: 2026-09-16
|
|
65
|
+
*
|
|
66
|
+
* WHY THIS EXISTS. This section used to render nothing at all while the
|
|
67
|
+
* fetch was in flight (~1.2-1.4s) and then drop a ~1,290px block in all at
|
|
68
|
+
* once - measured CLS of 0.5172-1.1646 on pair pages, against a homepage
|
|
69
|
+
* (which has no such block) of 0.0000. The fix is not to load faster, it is
|
|
70
|
+
* to occupy the space from the FIRST paint, in the shape the real content
|
|
71
|
+
* will take, so the swap moves nothing.
|
|
72
|
+
*
|
|
73
|
+
* SAME BOXES, PLACEHOLDER CONTENT. This reuses Availability's own Container,
|
|
74
|
+
* Heading, Lead, Wrap and Table rather than a hand-measured approximation of
|
|
75
|
+
* them, so the two can never quietly drift apart - a padding change in
|
|
76
|
+
* ./styles resizes both at once. Only the day count (VISIBLE, imported
|
|
77
|
+
* rather than repeated) and the row count (ROWS_PER_DAY, above) are guesses;
|
|
78
|
+
* everything else is the real box model.
|
|
79
|
+
*
|
|
80
|
+
* aria-hidden, and no live region: a placeholder table announces nothing,
|
|
81
|
+
* because it has nothing to announce - the section's actual content is
|
|
82
|
+
* either the real table that replaces this, a few seconds later, or nothing
|
|
83
|
+
* at all.
|
|
84
|
+
*/
|
|
85
|
+
const AvailabilitySkeleton = ({ id, from, to, t }: Props): JSX.Element => (
|
|
86
|
+
<Container id={ id } className="box" aria-hidden="true">
|
|
87
|
+
<Heading>
|
|
88
|
+
{ from && to
|
|
89
|
+
? t('routes_order.availability.title', { from, to })
|
|
90
|
+
: <Bone $width={ 45 } $height={ 22 } /> }
|
|
91
|
+
</Heading>
|
|
92
|
+
|
|
93
|
+
{ /* Not data-dependent - the same translation the real Lead prints,
|
|
94
|
+
so this line never has to move when the real table lands. */ }
|
|
95
|
+
<Lead>{ t('routes_order.availability.lead') }</Lead>
|
|
96
|
+
|
|
97
|
+
<Wrap>
|
|
98
|
+
<Table>
|
|
99
|
+
<thead>
|
|
100
|
+
<tr>
|
|
101
|
+
{ /* Real column headers, not placeholders - like the lead
|
|
102
|
+
above, these come from static translations rather than
|
|
103
|
+
from the fetch, so there is no reason to guess at them. */ }
|
|
104
|
+
<th>{ t('routes_order.schedule.departure') }</th>
|
|
105
|
+
<th>{ t('routes_order.schedule.arrival') }</th>
|
|
106
|
+
<th>{ t('routes_order.schedule.duration') }</th>
|
|
107
|
+
<th>{ t('routes_order.schedule.operator') }</th>
|
|
108
|
+
<th>{ t('routes_order.availability.price') }</th>
|
|
109
|
+
<th>{ t('routes_order.availability.book') }</th>
|
|
110
|
+
</tr>
|
|
111
|
+
</thead>
|
|
112
|
+
|
|
113
|
+
{ Array.from({ length: VISIBLE }, (_, index) => day(index)) }
|
|
114
|
+
</Table>
|
|
115
|
+
</Wrap>
|
|
116
|
+
</Container>
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
export default AvailabilitySkeleton;
|
package/Availability/styles.ts
CHANGED
|
@@ -185,7 +185,8 @@ export const Price = styled.strong`
|
|
|
185
185
|
* until a customer did.
|
|
186
186
|
*/
|
|
187
187
|
export const SeatsLeft = styled.span<{ $low: boolean }>`
|
|
188
|
-
|
|
188
|
+
// Claude - 2026-09-16 (audit): xs - the xxs was 10px, under the phone floor
|
|
189
|
+
font-size: ${ props => props.theme.size.xs };
|
|
189
190
|
line-height: 1.25;
|
|
190
191
|
white-space: nowrap;
|
|
191
192
|
font-weight: ${ props => (props.$low ? 700 : 400) };
|
package/Facts/Facts.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TFunction } from 'i18next';
|
|
2
|
-
import { Container, Grid, Fact, Label, Value, Note, Notice, Operators, Prose } from './styles';
|
|
2
|
+
import { Container, Heading, Grid, Fact, Label, Value, Note, Notice, Operators, Prose } from './styles';
|
|
3
3
|
import { FactsData } from './types';
|
|
4
4
|
import { DAYS, duration } from './questions';
|
|
5
5
|
import { stopped, detail } from './service';
|
|
@@ -11,6 +11,12 @@ interface Props {
|
|
|
11
11
|
// same response, and two components each running the same query would have
|
|
12
12
|
// meant the prerender crawl paying for every pair twice.
|
|
13
13
|
facts: FactsData
|
|
14
|
+
/**
|
|
15
|
+
* Claude - 2026-09-16 (audit): h1 on the pair page, where this title is
|
|
16
|
+
* the page's subject; h2 on the results steps, whose own step heading is
|
|
17
|
+
* the h1. The caller knows which page it is; this block does not.
|
|
18
|
+
*/
|
|
19
|
+
heading?: 'h1' | 'h2'
|
|
14
20
|
t: TFunction<'common'>
|
|
15
21
|
}
|
|
16
22
|
|
|
@@ -45,7 +51,7 @@ interface Props {
|
|
|
45
51
|
* is printed in the weaker wording. A pair with one weekly coach ends up
|
|
46
52
|
* with a short honest block, not a padded one.
|
|
47
53
|
*/
|
|
48
|
-
const Facts = ({ id, facts, t }: Props): JSX.Element => {
|
|
54
|
+
const Facts = ({ id, facts, heading = 'h2', t }: Props): JSX.Element => {
|
|
49
55
|
/**
|
|
50
56
|
* Edited: Claude - Date: 2026-08-20
|
|
51
57
|
*
|
|
@@ -109,7 +115,7 @@ const Facts = ({ id, facts, t }: Props): JSX.Element => {
|
|
|
109
115
|
|
|
110
116
|
return (
|
|
111
117
|
<Container id={ id } className="box">
|
|
112
|
-
<
|
|
118
|
+
<Heading as={ heading }>{ t('routes_order.facts.title', { from: facts.from, to: facts.to }) }</Heading>
|
|
113
119
|
|
|
114
120
|
{ dead && (
|
|
115
121
|
<Notice>
|
package/Facts/styles.ts
CHANGED
|
@@ -4,6 +4,18 @@ export const Container = styled.section`
|
|
|
4
4
|
margin-top: 25px;
|
|
5
5
|
`;
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* The section's title, at whichever level the page hands it.
|
|
9
|
+
*
|
|
10
|
+
* Claude - 2026-09-16 (audit): on the pair page this IS the page's subject
|
|
11
|
+
* and renders as its one h1; on the results steps it drops to h2 under the
|
|
12
|
+
* step heading. The size is pinned to the h2 scale either way, so promoting
|
|
13
|
+
* the level is an outline decision and does not resize the block.
|
|
14
|
+
*/
|
|
15
|
+
export const Heading = styled.h2`
|
|
16
|
+
font-size: ${ props => props.theme.size.xl };
|
|
17
|
+
`;
|
|
18
|
+
|
|
7
19
|
export const Grid = styled.dl`
|
|
8
20
|
display: grid;
|
|
9
21
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
package/Found/Dates/Dates.tsx
CHANGED
|
@@ -3,7 +3,10 @@ import { TFunction } from 'i18next';
|
|
|
3
3
|
import { AiOutlineCaretLeft, AiOutlineCaretRight } from 'react-icons/ai';
|
|
4
4
|
import { Button } from '@autobusal/common';
|
|
5
5
|
import { useMediaQuery } from '@autobusal/hooks';
|
|
6
|
-
import { createDate, getDayParts } from '@autobusal/utilities';
|
|
6
|
+
import { createDate, prepareDate, getDayParts } from '@autobusal/utilities';
|
|
7
|
+
import { useGetSettings } from '@autobusal/providers/services';
|
|
8
|
+
import { useGetSuggestions } from '@autobusal/routes-search/services';
|
|
9
|
+
import { todayIn } from '../../Step1/prepare';
|
|
7
10
|
import Loading from './Loading';
|
|
8
11
|
import { Container, Inner, ButtonDay, DayPrice, Cheapest, Weekday, DayNumber, Month } from './styles';
|
|
9
12
|
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
@@ -40,6 +43,29 @@ const Dates = ({ type, step1, t, onSearch }: Props): JSX.Element => {
|
|
|
40
43
|
|
|
41
44
|
const { data, isLoading } = useGetDates(step1, type, number);
|
|
42
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The first day that can still be boarded, as the departure city reckons it.
|
|
48
|
+
*
|
|
49
|
+
* Claude - 2026-09-16 (audit): the cut-off used to be `new Date() <= day`
|
|
50
|
+
* - the device's current INSTANT against the day's UTC midnight - which
|
|
51
|
+
* declared today unavailable from 00:00 UTC onwards, i.e. from 02:00 in
|
|
52
|
+
* Tirana. obtapi already starts the window on today (Search\Dates::find)
|
|
53
|
+
* and says per day whether anything runs, so the only job left here is
|
|
54
|
+
* not to offer a day that has passed where the coach leaves from: the
|
|
55
|
+
* origin's calendar day, then the server's, then the device's. The same
|
|
56
|
+
* clock the form now opens on. On the return step the strip is the
|
|
57
|
+
* reverse pair, so its origin is the destination that was searched.
|
|
58
|
+
*/
|
|
59
|
+
const { data: cities } = useGetSuggestions();
|
|
60
|
+
|
|
61
|
+
const { data: settings } = useGetSettings();
|
|
62
|
+
|
|
63
|
+
const origin = type === '_return' ? step1.to : step1.from;
|
|
64
|
+
|
|
65
|
+
const zone = cities.find(city => city.slug === origin)?.country?.timezone;
|
|
66
|
+
|
|
67
|
+
const floor = createDate(todayIn(zone) ?? settings.today ?? prepareDate(new Date()));
|
|
68
|
+
|
|
43
69
|
if (isLoading) {
|
|
44
70
|
return <Loading t={ t } />;
|
|
45
71
|
}
|
|
@@ -60,8 +86,6 @@ const Dates = ({ type, step1, t, onSearch }: Props): JSX.Element => {
|
|
|
60
86
|
setNumber(number + 1);
|
|
61
87
|
};
|
|
62
88
|
|
|
63
|
-
const today = new Date();
|
|
64
|
-
|
|
65
89
|
/**
|
|
66
90
|
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
67
91
|
*
|
|
@@ -116,8 +140,8 @@ const Dates = ({ type, step1, t, onSearch }: Props): JSX.Element => {
|
|
|
116
140
|
const items = shown.map(item => {
|
|
117
141
|
const date = createDate(item.day);
|
|
118
142
|
|
|
119
|
-
//
|
|
120
|
-
const isAvailable =
|
|
143
|
+
// today or later where the coach leaves from, and something runs that day
|
|
144
|
+
const isAvailable = date.getTime() >= floor.getTime() && item.routes;
|
|
121
145
|
|
|
122
146
|
const isCheapest = varies && lowest !== null && item.from_price === lowest;
|
|
123
147
|
|
package/Found/Dates/styles.ts
CHANGED
|
@@ -44,12 +44,18 @@ export const Inner = styled.div`
|
|
|
44
44
|
* they took 70px of a 355px row between them - a fifth of the week's worth
|
|
45
45
|
* of space spent on two chevrons, which left each day 34px and forced the
|
|
46
46
|
* prices to wrap three times.
|
|
47
|
+
*
|
|
48
|
+
* Claude - 2026-09-16 (audit): squeezed to 30px tall they were 26px wide,
|
|
49
|
+
* and a thumb misses that. 44px square each: the phone strip shows THREE
|
|
50
|
+
* days now rather than seven, so 88px of arrows still leaves each day
|
|
51
|
+
* about 89px of a 375px screen.
|
|
47
52
|
*/
|
|
48
53
|
& > button:first-child,
|
|
49
54
|
& > button:last-child {
|
|
50
55
|
flex: 0 0 auto;
|
|
51
|
-
padding: 0
|
|
52
|
-
|
|
56
|
+
padding: 0;
|
|
57
|
+
width: 44px;
|
|
58
|
+
height: 44px;
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
@media (min-width: 640px) {
|
|
@@ -134,7 +140,8 @@ export const ButtonDay = styled.button<{ $available: boolean, $selected: boolean
|
|
|
134
140
|
justify-content: center;
|
|
135
141
|
gap: 2px;
|
|
136
142
|
padding: 4px 0;
|
|
137
|
-
|
|
143
|
+
// Claude - 2026-09-16 (audit): the 44px tap minimum - the chips measured 37px
|
|
144
|
+
min-height: 44px;
|
|
138
145
|
font-weight: 700;
|
|
139
146
|
text-align: center;
|
|
140
147
|
font-size: ${ props => props.theme.size.xs };
|
|
@@ -178,11 +185,11 @@ export const DayPrice = styled.span<{ $cheapest: boolean }>`
|
|
|
178
185
|
white-space: normal;
|
|
179
186
|
overflow-wrap: anywhere;
|
|
180
187
|
line-height: 1.15;
|
|
181
|
-
|
|
188
|
+
// Claude - 2026-09-16 (audit): xs at every width - 11px is under the floor
|
|
189
|
+
font-size: ${ props => props.theme.size.xs };
|
|
182
190
|
|
|
183
191
|
@media (min-width: 640px) {
|
|
184
192
|
white-space: nowrap;
|
|
185
|
-
font-size: ${ props => props.theme.size.xs };
|
|
186
193
|
}
|
|
187
194
|
font-weight: ${ props => props.$cheapest ? 700 : 400 };
|
|
188
195
|
opacity: .85;
|
|
@@ -196,13 +203,19 @@ export const DayPrice = styled.span<{ $cheapest: boolean }>`
|
|
|
196
203
|
* unreadable at 10px. On the selected day it inherits the button's own
|
|
197
204
|
* contrast colour and leans on weight instead of hue.
|
|
198
205
|
*/
|
|
206
|
+
/*
|
|
207
|
+
* Claude - 2026-09-16 (audit): the strip is no longer dark - it sits on
|
|
208
|
+
* background.neutral - and the green that read well on it measures 2.42:1
|
|
209
|
+
* there (#79AB51 on #F2F2F2). The word is the cue now, in the ordinary ink
|
|
210
|
+
* (14.4:1 light, 11:1 dark), and at xs rather than a 10px xxs.
|
|
211
|
+
*/
|
|
199
212
|
export const Cheapest = styled.span<{ $selected: boolean }>`
|
|
200
213
|
display: block;
|
|
201
|
-
font-size: ${ props => props.theme.size.
|
|
214
|
+
font-size: ${ props => props.theme.size.xs };
|
|
202
215
|
text-transform: uppercase;
|
|
203
216
|
letter-spacing: .5px;
|
|
204
217
|
font-weight: 700;
|
|
205
|
-
color: ${ props => props.$selected ? 'inherit' : props.theme.font.
|
|
218
|
+
color: ${ props => props.$selected ? 'inherit' : props.theme.font.normal };
|
|
206
219
|
`;
|
|
207
220
|
|
|
208
221
|
export const ContainerLoading = styled.div`
|
package/Found/Filters/styles.ts
CHANGED
|
@@ -45,15 +45,28 @@ export const Toggle = styled.button<{ $open: boolean }>`
|
|
|
45
45
|
z-index: 900;
|
|
46
46
|
|
|
47
47
|
/*
|
|
48
|
-
* Clear of the home indicator on a notched phone
|
|
49
|
-
*
|
|
48
|
+
* Clear of the home indicator on a notched phone, AND clear of the
|
|
49
|
+
* cookie notice on a narrow one.
|
|
50
|
+
*
|
|
51
|
+
* Edited: Claude - Date: 2026-09-16
|
|
52
|
+
*
|
|
53
|
+
* --obt-cookie-notice-height is published by CookieNotification
|
|
54
|
+
* (packages/common) while its own bar sits fixed to this same edge under
|
|
55
|
+
* 640px - see NOTICE_HEIGHT_VAR there. Two elements anchored to the same
|
|
56
|
+
* "bottom: 0" both being fixed to the viewport does not resolve itself by
|
|
57
|
+
* z-index, which only decides which one paints on top of the other; this
|
|
58
|
+
* button was measured sitting directly under the notice at 375px. The
|
|
59
|
+
* variable defaults to 0px, so a page without the notice - or a visitor
|
|
60
|
+
* who has already dismissed it - sees no change at all.
|
|
50
61
|
*/
|
|
51
|
-
bottom: calc(20px + env(safe-area-inset-bottom, 0px));
|
|
62
|
+
bottom: calc(20px + env(safe-area-inset-bottom, 0px) + var(--obt-cookie-notice-height, 0px));
|
|
52
63
|
|
|
53
64
|
display: flex;
|
|
54
65
|
align-items: center;
|
|
55
66
|
gap: 6px;
|
|
56
67
|
padding: 9px 16px;
|
|
68
|
+
// Claude - 2026-09-16 (audit): the 44px tap minimum - this measured 35px
|
|
69
|
+
min-height: 44px;
|
|
57
70
|
font-size: ${ props => props.theme.size.xs };
|
|
58
71
|
font-weight: 700;
|
|
59
72
|
border-radius: 100px;
|
package/Found/Orders/Orders.tsx
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { TFunction } from 'i18next';
|
|
2
|
-
import { Inline } from '@autobusal/common';
|
|
3
2
|
import Route from '../Route/Route';
|
|
4
|
-
import Group from '../Group/Group';
|
|
5
3
|
import Empty from '../Empty/Empty';
|
|
4
|
+
import Skeleton from './Skeleton';
|
|
6
5
|
import { getFavorites, getNormal } from './utilities';
|
|
7
|
-
import { sortItems,
|
|
6
|
+
import { sortItems, SortKey } from '../refine';
|
|
8
7
|
import { Container, ContainerNotFound, Reset } from './styles';
|
|
9
8
|
import { FoundData } from '@autobusal/providers/types/routes';
|
|
10
9
|
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
@@ -28,12 +27,16 @@ interface Props {
|
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
const Orders = ({ loading, data, empty, refined, sort, type, passengers, preferredStop, step1, alternatives, alternativesLoading, t, onReset, onSave }: Props): JSX.Element => {
|
|
30
|
+
/*
|
|
31
|
+
* Edited: Claude - Date: 2026-09-16
|
|
32
|
+
*
|
|
33
|
+
* A skeleton the shape of the real list, not a one-line message - see
|
|
34
|
+
* ./Skeleton for why (measured on a dated results URL, which is never
|
|
35
|
+
* prerendered: 0.6868-1.1646 CLS from a page that renders almost nothing
|
|
36
|
+
* until the search answers, then drops every card in at once).
|
|
37
|
+
*/
|
|
31
38
|
if (loading) {
|
|
32
|
-
return
|
|
33
|
-
<div className="box">
|
|
34
|
-
<Inline text={ t('routes_order.step2.loading.routes') } />
|
|
35
|
-
</div>
|
|
36
|
-
);
|
|
39
|
+
return <Skeleton t={ t } />;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
// Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
@@ -67,9 +70,22 @@ const Orders = ({ loading, data, empty, refined, sort, type, passengers, preferr
|
|
|
67
70
|
* Preferred-stop pinning stays the OUTER partition and each side is
|
|
68
71
|
* sorted within itself. Sorting the whole list first would have let
|
|
69
72
|
* "cheapest" quietly un-pin a stop the user deliberately saved.
|
|
73
|
+
*/
|
|
74
|
+
/*
|
|
75
|
+
* Claude - 2026-09-16 (Ferjolt: "When we have more than one departure for
|
|
76
|
+
* the same route from the same operator, should not group them as we do
|
|
77
|
+
* now but should show them as a standalone search result in full. Is very
|
|
78
|
+
* easy to get confused visitors with grouped alternatives. So, remove the
|
|
79
|
+
* grouping and put them all as standalone.")
|
|
70
80
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
81
|
+
* EVERY DEPARTURE IS ITS OWN CARD. Interchangeable departures used to
|
|
82
|
+
* collapse into one card with a row of times under it, and the card showed
|
|
83
|
+
* only the earliest - so somebody scanning for "the 14:00" saw a 06:00 and
|
|
84
|
+
* moved on, and somebody who did find the picker booked whichever time
|
|
85
|
+
* happened to be highlighted. The grouped card also defeated the sort it
|
|
86
|
+
* sat in: ordered "latest first", a group still led with its EARLIEST
|
|
87
|
+
* time. A run of similar rows is the honest shape of an hourly service,
|
|
88
|
+
* and the sort and filters above are there to cut it down.
|
|
73
89
|
*/
|
|
74
90
|
/**
|
|
75
91
|
* Edited: Ferjolt Ozuni - Date: 2026-08-05
|
|
@@ -82,30 +98,16 @@ const Orders = ({ loading, data, empty, refined, sort, type, passengers, preferr
|
|
|
82
98
|
const date = step1[type];
|
|
83
99
|
|
|
84
100
|
const render = (list: FoundData[], kind: 'favorite' | 'normal'): JSX.Element[] => (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
onSave={ onSave }
|
|
96
|
-
/>
|
|
97
|
-
)
|
|
98
|
-
: (
|
|
99
|
-
<Route
|
|
100
|
-
key={ group[0].id }
|
|
101
|
-
type={ kind }
|
|
102
|
-
data={ group[0] }
|
|
103
|
-
date={ date }
|
|
104
|
-
passengers={ passengers }
|
|
105
|
-
t={ t }
|
|
106
|
-
onSave={ onSave }
|
|
107
|
-
/>
|
|
108
|
-
)
|
|
101
|
+
sortItems(list, sort).map(item => (
|
|
102
|
+
<Route
|
|
103
|
+
key={ item.id }
|
|
104
|
+
type={ kind }
|
|
105
|
+
data={ item }
|
|
106
|
+
date={ date }
|
|
107
|
+
passengers={ passengers }
|
|
108
|
+
t={ t }
|
|
109
|
+
onSave={ onSave }
|
|
110
|
+
/>
|
|
109
111
|
))
|
|
110
112
|
);
|
|
111
113
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { TFunction } from 'i18next';
|
|
2
|
+
import { Bone } from '../../styles';
|
|
3
|
+
import { Container as Card, Trip, Company, Location, Time, Price, Amount } from '../Route/styles';
|
|
4
|
+
import { Container } from './styles';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* How many placeholder cards to reserve room for.
|
|
8
|
+
*
|
|
9
|
+
* Edited: Claude - Date: 2026-09-16
|
|
10
|
+
*
|
|
11
|
+
* Not knowable before the search answers - see ROWS_PER_DAY in
|
|
12
|
+
* ../../Availability/Skeleton for the same problem one section over. Five
|
|
13
|
+
* is the same kind of middle estimate: enough to fill roughly one screen on
|
|
14
|
+
* a desktop, which is where a reserved-but-empty block would otherwise be
|
|
15
|
+
* most visible, and not so many that a route with genuinely few departures
|
|
16
|
+
* reserves a page and a half of blank cards. A result count either side of
|
|
17
|
+
* five is a residual shift of one card's height, not the ~20-card collapse
|
|
18
|
+
* this exists to prevent.
|
|
19
|
+
*/
|
|
20
|
+
const CARDS = 5;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One placeholder card, in the shape Route/Route.tsx renders.
|
|
24
|
+
*
|
|
25
|
+
* Edited: Claude - Date: 2026-09-16
|
|
26
|
+
*
|
|
27
|
+
* SAME STYLED COMPONENTS AS THE REAL CARD - Card, Trip, Company, Location,
|
|
28
|
+
* Time, Price, Amount all come from ../Route/styles, not a redrawn
|
|
29
|
+
* approximation of them. That is what keeps this accurate as the card
|
|
30
|
+
* itself changes: a padding or gap edited there resizes the skeleton at the
|
|
31
|
+
* same time, rather than the two silently drifting apart.
|
|
32
|
+
*
|
|
33
|
+
* The three bars inside Price carry the REAL card's own class names
|
|
34
|
+
* (price-notice, details-toggle, seats-left) so the same nth-child/class
|
|
35
|
+
* ordering rules in Price's CSS lay them out exactly as they lay out the
|
|
36
|
+
* real amount, tax note, button, details toggle and seat count - including
|
|
37
|
+
* the extra rows those last two add on a phone, which is most of what made
|
|
38
|
+
* the old three-line estimate come up short.
|
|
39
|
+
*/
|
|
40
|
+
const SkeletonCard = (): JSX.Element => (
|
|
41
|
+
<Card className="box" $type="normal" aria-hidden="true">
|
|
42
|
+
<Trip>
|
|
43
|
+
<Company>
|
|
44
|
+
<Bone $width={ 45 } $height={ 28 } />
|
|
45
|
+
<Bone $width={ 60 } $height={ 14 } />
|
|
46
|
+
</Company>
|
|
47
|
+
|
|
48
|
+
<Location $side="from">
|
|
49
|
+
<Bone $width={ 75 } $height={ 17 } />
|
|
50
|
+
<Bone $width={ 90 } $height={ 15 } />
|
|
51
|
+
</Location>
|
|
52
|
+
|
|
53
|
+
<Time>
|
|
54
|
+
<Bone $width={ 40 } $height={ 30 } />
|
|
55
|
+
</Time>
|
|
56
|
+
|
|
57
|
+
<Location $side="to">
|
|
58
|
+
<Bone $width={ 75 } $height={ 17 } />
|
|
59
|
+
<Bone $width={ 90 } $height={ 15 } />
|
|
60
|
+
</Location>
|
|
61
|
+
|
|
62
|
+
<Price>
|
|
63
|
+
<Amount><Bone $width={ 60 } $height={ 26 } /></Amount>
|
|
64
|
+
<Bone $width={ 100 } $height={ 44 } />
|
|
65
|
+
<Bone className="price-notice" $width={ 85 } $height={ 14 } />
|
|
66
|
+
<Bone className="details-toggle" $width={ 55 } $height={ 16 } />
|
|
67
|
+
<Bone className="seats-left" $width={ 65 } $height={ 14 } />
|
|
68
|
+
</Price>
|
|
69
|
+
</Trip>
|
|
70
|
+
</Card>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
interface Props {
|
|
74
|
+
t: TFunction<'common'>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* What the results list looks like before a search answers.
|
|
79
|
+
*
|
|
80
|
+
* Edited: Claude - Date: 2026-09-16
|
|
81
|
+
*
|
|
82
|
+
* WHY THIS EXISTS. A dated results URL (?departure=...) is never
|
|
83
|
+
* prerendered - it serves the SPA shell, so the very first thing a visitor
|
|
84
|
+
* sees is whatever this component renders while `loading` is true. That
|
|
85
|
+
* used to be a single line of text ("Searching for tickets..."), which
|
|
86
|
+
* collapsed the whole page to almost nothing and then dropped every result
|
|
87
|
+
* card in at once the moment the search answered - the same shape of CLS
|
|
88
|
+
* bug as ../../Availability/Skeleton, on a page that cannot be prerendered
|
|
89
|
+
* around it.
|
|
90
|
+
*
|
|
91
|
+
* `role="status"` with the SAME loading string the old text used, rather
|
|
92
|
+
* than a live region per card: a screen reader is told once that results
|
|
93
|
+
* are loading, not read five identical placeholder cards. The cards
|
|
94
|
+
* themselves are aria-hidden - there is nothing in them to announce.
|
|
95
|
+
*/
|
|
96
|
+
const Skeleton = ({ t }: Props): JSX.Element => (
|
|
97
|
+
<Container role="status" aria-label={ t('routes_order.step2.loading.routes') }>
|
|
98
|
+
{ Array.from({ length: CARDS }, (_, index) => <SkeletonCard key={ index } />) }
|
|
99
|
+
</Container>
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
export default Skeleton;
|
package/Found/Route/Route.tsx
CHANGED
|
@@ -37,13 +37,9 @@ interface Props {
|
|
|
37
37
|
passengers: number
|
|
38
38
|
t: TFunction<'common'>
|
|
39
39
|
onSave: (data: FoundData) => void
|
|
40
|
-
// Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
41
|
-
// Slot under the trip row, used by Group to hang a departure-time picker
|
|
42
|
-
// on a card that stands for several interchangeable departures.
|
|
43
|
-
children?: React.ReactNode
|
|
44
40
|
}
|
|
45
41
|
|
|
46
|
-
const Route = ({ data, type, date, passengers, t, onSave
|
|
42
|
+
const Route = ({ data, type, date, passengers, t, onSave }: Props): JSX.Element => {
|
|
47
43
|
const [ information, setInformation ] = useState<boolean>(false);
|
|
48
44
|
|
|
49
45
|
// Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
@@ -345,12 +341,10 @@ const Route = ({ data, type, date, passengers, t, onSave, children }: Props): JS
|
|
|
345
341
|
</Price>
|
|
346
342
|
</Trip>
|
|
347
343
|
|
|
348
|
-
{ children }
|
|
349
|
-
|
|
350
344
|
{ /* Claude - 2026-09-01: the wide-screen copy, back where it sat before
|
|
351
|
-
the phone rebuild - bottom-left of the card, below the itinerary
|
|
352
|
-
|
|
353
|
-
|
|
345
|
+
the phone rebuild - bottom-left of the card, below the itinerary,
|
|
346
|
+
rather than inside the fare column. Exactly one of the two is ever
|
|
347
|
+
displayed; see ButtonDetails. */ }
|
|
354
348
|
<ButtonDetails
|
|
355
349
|
$only="wide"
|
|
356
350
|
type="button"
|