@autobusal/routes-order 1.23.5 → 1.24.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 +13 -0
- package/Checkout/Checkout.tsx +6 -1
- package/RoutesOrder.tsx +83 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.24.0
|
|
4
|
+
|
|
5
|
+
**The booking wizard has real URLs.** The step now lives in the URL and `current` is derived from it, rather than the reverse.
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
- 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.
|
|
10
|
+
- **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.
|
|
11
|
+
|
|
12
|
+
**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.
|
|
13
|
+
|
|
14
|
+
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.
|
|
15
|
+
|
|
3
16
|
## 1.23.5
|
|
4
17
|
|
|
5
18
|
**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;
|