@autobusal/routes-order 1.23.5 → 1.25.0
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/CHANGELOG.md +19 -0
- package/Checkout/Checkout.tsx +6 -1
- package/RoutesOrder.tsx +83 -11
- package/Sections/Sections.tsx +21 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.25.0
|
|
4
|
+
|
|
5
|
+
**Reviews on the route-pair page.** A new section between the timetable and the internal links, with its own nav anchor.
|
|
6
|
+
|
|
7
|
+
Renders nothing below the display threshold, so a pair without enough reviews does not gain an empty box.
|
|
8
|
+
|
|
9
|
+
## 1.24.0
|
|
10
|
+
|
|
11
|
+
**The booking wizard has real URLs.** The step now lives in the URL and `current` is derived from it, rather than the reverse.
|
|
12
|
+
|
|
13
|
+
Before: the wizard held the step in component state and never touched the address bar, so the URL still said "search" while the buyer was on checkout. Nothing in the flow was shareable, a refresh dropped you back to the search, and Back did not step back through the wizard — it left it entirely, because no step had ever created a history entry.
|
|
14
|
+
|
|
15
|
+
- A **search param** (`?step=checkout` / `?step=return`), not a path segment. `/bus-lines/:from/:to` is a crawlable page in the sitemap, and child routes would put near-identical URLs in front of a crawler for what is UI state. The search spec is already in the path on the 9-segment route, so between the two the whole position is in the URL.
|
|
16
|
+
- **Guards, not a lookup.** A step is honoured only when the state it needs exists: a cold `?step=checkout` has no chosen route, so it falls back to results and clears the stale param rather than rendering half a screen.
|
|
17
|
+
|
|
18
|
+
**Bug fixed on the way.** `Checkout` chose its back target with `step1._return ? 3 : 2`. On the 9-segment URL a one-way trip carries the literal segment `none` as its return date — a truthy string — so **every one-way booking's Back button aimed at the return-leg step**, which that booking does not have. Now keyed on `step1.type`, and guarded again in `onBack` so a step nobody can honour never reaches the URL.
|
|
19
|
+
|
|
20
|
+
Verified: results → checkout writes the param; browser Back and Forward move between them; the in-page Back returns to results; a cold checkout URL falls back; and eight back/forward transitions produced no blank frame and no console error.
|
|
21
|
+
|
|
3
22
|
## 1.23.5
|
|
4
23
|
|
|
5
24
|
**The operator cell centres its contents at every width.** LinkCompany already carried an auto horizontal margin, so the logo was centred in the cell whatever the screen - but the rating and the route code sat at flex-start, under the left edge of a box whose logo was in the middle of it. On a 768px card that was a ~300px gap between the logo and the code below it.
|
package/Checkout/Checkout.tsx
CHANGED
|
@@ -290,7 +290,12 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
|
|
|
290
290
|
// than coming back to an empty one
|
|
291
291
|
onStore(passenger.getValues());
|
|
292
292
|
|
|
293
|
-
|
|
293
|
+
// Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
294
|
+
// `type`, not `_return`. On the 9-segment search URL a one-way
|
|
295
|
+
// trip carries the literal segment "none" as its return date,
|
|
296
|
+
// which is a truthy string - so this always chose the
|
|
297
|
+
// return-leg step, even for a booking that has no return leg.
|
|
298
|
+
onBack(step1.type === 'return' ? 3 : 2);
|
|
294
299
|
} }
|
|
295
300
|
/>
|
|
296
301
|
</Actions>
|
package/RoutesOrder.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import { useParams } from 'react-router-dom';
|
|
2
|
+
import { useParams, useSearchParams } from 'react-router-dom';
|
|
3
3
|
import { TFunction } from 'i18next';
|
|
4
4
|
import { AiOutlineSearch, AiOutlineCaretDown, AiOutlineCaretUp } from 'react-icons/ai';
|
|
5
5
|
import { Meta } from '@autobusal/common';
|
|
@@ -54,12 +54,74 @@ const RoutesOrder = ({ t }: Props): JSX.Element => {
|
|
|
54
54
|
|
|
55
55
|
const [ searchOpen, setSearchOpen ] = useState<boolean>(false);
|
|
56
56
|
const [ redirected, setRedirected ] = useState<boolean>(false);
|
|
57
|
-
const [ current, setCurrent ] = useState<number>(1);
|
|
58
57
|
const [ step1, setStep1 ] = useState<RoutesSearchForm | undefined>(undefined);
|
|
59
58
|
const [ step2, setStep2 ] = useState<FoundData | undefined>(undefined);
|
|
60
59
|
const [ step3, setStep3 ] = useState<FoundData | undefined>(undefined);
|
|
61
60
|
const [ step4, setStep4 ] = useState<PersonData | undefined>(undefined);
|
|
62
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
64
|
+
*
|
|
65
|
+
* THE STEP LIVES IN THE URL, and `current` is derived from it - not the
|
|
66
|
+
* other way round. The wizard held the step in component state and never
|
|
67
|
+
* touched the address bar, so the URL still said "search" while the buyer
|
|
68
|
+
* was on the checkout screen. Nothing in the flow was shareable, a refresh
|
|
69
|
+
* dropped you back to the search, and Back did not step back through the
|
|
70
|
+
* wizard - it left it entirely, because no step had ever created a history
|
|
71
|
+
* entry.
|
|
72
|
+
*
|
|
73
|
+
* A SEARCH PARAM rather than a path segment, deliberately. The pair page
|
|
74
|
+
* /bus-lines/:from/:to is a crawlable page in the sitemap, and hanging
|
|
75
|
+
* child routes off it would put near-identical URLs in front of a crawler
|
|
76
|
+
* for what is UI state rather than content. The search spec itself is
|
|
77
|
+
* already in the path on the 9-segment route, so between the two the whole
|
|
78
|
+
* position is in the URL.
|
|
79
|
+
*
|
|
80
|
+
* THE GUARDS ARE THE POINT. A step is only honoured when the state it
|
|
81
|
+
* needs actually exists - arriving at ?step=checkout in a fresh tab has no
|
|
82
|
+
* chosen route to check out, so it falls back to the results rather than
|
|
83
|
+
* rendering half a screen. That is why this reads as a chain of conditions
|
|
84
|
+
* and not a lookup.
|
|
85
|
+
*/
|
|
86
|
+
const [ searchParams, setSearchParams ] = useSearchParams();
|
|
87
|
+
|
|
88
|
+
const step = searchParams.get('step');
|
|
89
|
+
|
|
90
|
+
const current = (() => {
|
|
91
|
+
if (step === 'checkout' && step1 !== undefined && step2 !== undefined) {
|
|
92
|
+
return 4;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (step === 'return' && step1 !== undefined && step2 !== undefined) {
|
|
96
|
+
return 3;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// A search has run, so the results are the honest screen to be on -
|
|
100
|
+
// whether the step param says so, names something we cannot honour, or
|
|
101
|
+
// is absent because the URL carried a complete search spec of its own.
|
|
102
|
+
return step1 !== undefined ? 2 : 1;
|
|
103
|
+
})();
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Move to a step, leaving a history entry so Back returns here.
|
|
107
|
+
*
|
|
108
|
+
* `replace` for the screen a search lands on: a search is not somewhere a
|
|
109
|
+
* buyer chose to go back to mid-flow, it is where they already are.
|
|
110
|
+
*/
|
|
111
|
+
const goto = (to: (string | null), replace = false): void => {
|
|
112
|
+
setSearchParams(previous => {
|
|
113
|
+
const next = new URLSearchParams(previous);
|
|
114
|
+
|
|
115
|
+
if (to === null) {
|
|
116
|
+
next.delete('step');
|
|
117
|
+
} else {
|
|
118
|
+
next.set('step', to);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return next;
|
|
122
|
+
}, { replace });
|
|
123
|
+
};
|
|
124
|
+
|
|
63
125
|
const onSearchDate = (type: ('departure' | '_return'), day: string): void => {
|
|
64
126
|
if (step1 !== undefined) {
|
|
65
127
|
setStep1({
|
|
@@ -71,7 +133,7 @@ const RoutesOrder = ({ t }: Props): JSX.Element => {
|
|
|
71
133
|
|
|
72
134
|
const onStep1 = (data: RoutesSearchForm): void => {
|
|
73
135
|
setStep1(data);
|
|
74
|
-
|
|
136
|
+
goto(null, true);
|
|
75
137
|
|
|
76
138
|
// we mark it as redirected
|
|
77
139
|
setRedirected(true);
|
|
@@ -80,20 +142,30 @@ const RoutesOrder = ({ t }: Props): JSX.Element => {
|
|
|
80
142
|
const onStep2 = (data: FoundData): void => {
|
|
81
143
|
setStep2(data);
|
|
82
144
|
|
|
83
|
-
|
|
84
|
-
setCurrent(3);
|
|
85
|
-
} else {
|
|
86
|
-
setCurrent(4);
|
|
87
|
-
}
|
|
145
|
+
goto(step1?.type === 'return' ? 'return' : 'checkout');
|
|
88
146
|
};
|
|
89
147
|
|
|
90
148
|
const onStep3 = (data: FoundData): void => {
|
|
91
149
|
setStep3(data);
|
|
92
|
-
|
|
150
|
+
|
|
151
|
+
goto('checkout');
|
|
93
152
|
};
|
|
94
153
|
|
|
95
|
-
|
|
96
|
-
|
|
154
|
+
/**
|
|
155
|
+
* The in-page Back buttons, moving through the URL like the browser's own
|
|
156
|
+
* - so the two can never disagree about where the buyer is.
|
|
157
|
+
*
|
|
158
|
+
* The return step is only reachable on a booking that HAS a return leg.
|
|
159
|
+
* Guarded here as well as at the call site, on the same principle as
|
|
160
|
+
* `current` above: a step nobody can honour should never reach the URL,
|
|
161
|
+
* whoever asked for it.
|
|
162
|
+
*/
|
|
163
|
+
const onBack = (to: number): void => {
|
|
164
|
+
if (to === 3 && step1?.type === 'return') {
|
|
165
|
+
return goto('return');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
goto(to === 4 ? 'checkout' : null);
|
|
97
169
|
};
|
|
98
170
|
|
|
99
171
|
const showStep1 = current === 1;
|
package/Sections/Sections.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { TFunction } from 'i18next';
|
|
|
2
2
|
import Facts from '../Facts/Facts';
|
|
3
3
|
import Schedule from '../Schedule/Schedule';
|
|
4
4
|
import Links from '../Links/Links';
|
|
5
|
+
import { Reviews } from '@autobusal/reviews';
|
|
5
6
|
import Schema from './Schema';
|
|
6
7
|
import { useGetFacts } from '../Facts/services';
|
|
7
8
|
import { Nav, Anchor, Anchored } from './styles';
|
|
@@ -16,6 +17,7 @@ const TRIPS = 'trips';
|
|
|
16
17
|
const FACTS = 'facts';
|
|
17
18
|
const SCHEDULE = 'schedule';
|
|
18
19
|
const LINKS = 'links';
|
|
20
|
+
const REVIEWS = 'reviews';
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Everything on a route-pair page that is not the booking wizard.
|
|
@@ -63,6 +65,8 @@ const Sections = ({ from, to, t }: Props): (JSX.Element | null) => {
|
|
|
63
65
|
{ schedule.length > 0 && (
|
|
64
66
|
<Anchor href={ `#${ SCHEDULE }` }>{ t('routes_order.sections.schedule') }</Anchor>
|
|
65
67
|
) }
|
|
68
|
+
|
|
69
|
+
<Anchor href={ `#${ REVIEWS }` }>{ t('routes_order.sections.reviews') }</Anchor>
|
|
66
70
|
</Nav>
|
|
67
71
|
|
|
68
72
|
{ /* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
@@ -78,6 +82,23 @@ const Sections = ({ from, to, t }: Props): (JSX.Element | null) => {
|
|
|
78
82
|
<Schedule id={ SCHEDULE } from={ facts.from } to={ facts.to } rows={ schedule } t={ t } />
|
|
79
83
|
</Anchored>
|
|
80
84
|
|
|
85
|
+
{ /* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
86
|
+
Reviews for the PAIR, not for one coach on it. A review belongs to
|
|
87
|
+
a route, but a single route rarely has enough published reviews to
|
|
88
|
+
clear the display threshold - the pair it runs on often does, so
|
|
89
|
+
aggregating across the routes that sell this leg is what makes the
|
|
90
|
+
block appear at all rather than a nicety.
|
|
91
|
+
|
|
92
|
+
Renders nothing below the threshold, so a pair without enough
|
|
93
|
+
reviews does not gain an empty box. */ }
|
|
94
|
+
<Anchored>
|
|
95
|
+
{ /* the anchor lives on a wrapper: Reviews takes the SUBJECT's id,
|
|
96
|
+
which a pair does not have - it is named by its two ends */ }
|
|
97
|
+
<div id={ REVIEWS }>
|
|
98
|
+
<Reviews type="pair" id={ 0 } pair={ { from: from ?? '', to: to ?? '' } } t={ t } />
|
|
99
|
+
</div>
|
|
100
|
+
</Anchored>
|
|
101
|
+
|
|
81
102
|
<Links
|
|
82
103
|
id={ LINKS }
|
|
83
104
|
from={ links.from }
|