@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 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.
@@ -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
- onBack(step1._return ? 3 : 2);
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
- setCurrent(2);
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
- if (step1?.type === 'return') {
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
- setCurrent(4);
150
+
151
+ goto('checkout');
93
152
  };
94
153
 
95
- const onBack = (step: number): void => {
96
- setCurrent(step);
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;
@@ -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 }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/routes-order",
3
- "version": "1.23.5",
3
+ "version": "1.25.0",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"