@autobusal/routes-order 1.9.3 → 1.11.3

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.
@@ -1,6 +1,6 @@
1
1
  import { TFunction } from 'i18next';
2
2
  import { FieldErrors, FieldValues, UseFormRegister } from 'react-hook-form';
3
- import { Calendar, Gender, ChooseSeat } from '@autobusal/common';
3
+ import { Calendar, Gender, ChooseSeat, Required } from '@autobusal/common';
4
4
  import { Validate, Display } from '@autobusal/utilities';
5
5
  import { Container, Title } from './styles';
6
6
  import { PersonData } from '@autobusal/providers/types/persons';
@@ -55,7 +55,7 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
55
55
 
56
56
  <div className="column">
57
57
  <div className="row">
58
- { t('routes_order.step4.first_name') }
58
+ <Required>{ t('routes_order.step4.first_name') }</Required>
59
59
 
60
60
  <input
61
61
  type="text"
@@ -67,7 +67,7 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
67
67
  </div>
68
68
 
69
69
  <div className="row">
70
- { t('routes_order.step4.last_name') }
70
+ <Required>{ t('routes_order.step4.last_name') }</Required>
71
71
 
72
72
  <input
73
73
  type="text"
@@ -81,7 +81,7 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
81
81
 
82
82
  <div className="column">
83
83
  <div className="row">
84
- { t('routes_order.step4.dob') }
84
+ <Required>{ t('routes_order.step4.dob') }</Required>
85
85
 
86
86
  <Calendar
87
87
  type="dob"
@@ -116,7 +116,7 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
116
116
  <div className="column">
117
117
  { has('passport') && (
118
118
  <div className="row">
119
- { t('routes_order.step4.passport') }
119
+ <Required>{ t('routes_order.step4.passport') }</Required>
120
120
 
121
121
  <input
122
122
  type="text"
@@ -130,7 +130,7 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
130
130
 
131
131
  { has('phone') && (
132
132
  <div className="row">
133
- { t('routes_order.step4.phone') }
133
+ <Required>{ t('routes_order.step4.phone') }</Required>
134
134
 
135
135
  <input
136
136
  type="text"
@@ -148,7 +148,7 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
148
148
  <div className="column">
149
149
  { has('whatsapp') && (
150
150
  <div className="row">
151
- { t('routes_order.step4.whatsapp') }
151
+ <Required>{ t('routes_order.step4.whatsapp') }</Required>
152
152
 
153
153
  <input
154
154
  type="text"
@@ -162,7 +162,7 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
162
162
 
163
163
  { has('telegram') && (
164
164
  <div className="row">
165
- { t('routes_order.step4.telegram') }
165
+ <Required>{ t('routes_order.step4.telegram') }</Required>
166
166
 
167
167
  <input
168
168
  type="text"
@@ -179,7 +179,7 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
179
179
  { type !== 'baby' && (
180
180
  <div className="column">
181
181
  <div className="row">
182
- { t('seats.departure') }
182
+ <Required>{ t('seats.departure') }</Required>
183
183
 
184
184
  <ChooseSeat
185
185
  name={ names.seatDeparture }
@@ -198,7 +198,7 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
198
198
 
199
199
  { withReturn && (
200
200
  <div className="row">
201
- { t('seats.return') }
201
+ <Required>{ t('seats.return') }</Required>
202
202
 
203
203
  <ChooseSeat
204
204
  name={ names.seatReturn }
@@ -1,5 +1,6 @@
1
1
  import { useState } from 'react';
2
2
  import { TFunction } from 'i18next';
3
+ import { Required } from '@autobusal/common';
3
4
  import { UseFormRegister, FieldErrors } from 'react-hook-form';
4
5
  import { RiPencilFill } from 'react-icons/ri';
5
6
  import { AiOutlineInfoCircle } from 'react-icons/ai';
@@ -42,7 +43,7 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
42
43
 
43
44
  <div className="column">
44
45
  <div className="row">
45
- { t('routes_order.step5.billing.first_name') }
46
+ <Required>{ t('routes_order.step5.billing.first_name') }</Required>
46
47
 
47
48
  <input type="text" { ...refs('bill_fn', Validate('required|min_length:2|max_length:100', t)) } />
48
49
 
@@ -50,7 +51,7 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
50
51
  </div>
51
52
 
52
53
  <div className="row">
53
- { t('routes_order.step5.billing.last_name') }
54
+ <Required>{ t('routes_order.step5.billing.last_name') }</Required>
54
55
 
55
56
  <input type="text" { ...refs('bill_ln', Validate('required|min_length:2|max_length:100', t)) } />
56
57
 
@@ -60,7 +61,7 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
60
61
 
61
62
  <div className="column">
62
63
  <div className="row">
63
- { t('routes_order.step5.billing.email') }
64
+ <Required>{ t('routes_order.step5.billing.email') }</Required>
64
65
 
65
66
  <input type="email" { ...refs('email', Validate('required|email|max_length:255', t)) } />
66
67
 
@@ -72,7 +73,7 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
72
73
  </div>
73
74
 
74
75
  <div className="row">
75
- { t('routes_order.step5.billing.phone') }
76
+ <Required>{ t('routes_order.step5.billing.phone') }</Required>
76
77
 
77
78
  <input type="text" { ...refs('bill_phone', Validate('required|min_length:10|max_length:20', t)) } />
78
79
 
@@ -81,7 +82,7 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
81
82
  </div>
82
83
 
83
84
  <div className="row">
84
- { t('routes_order.step5.billing.address') }
85
+ <Required>{ t('routes_order.step5.billing.address') }</Required>
85
86
 
86
87
  <textarea { ...refs('bill_address', Validate('required|min_length:2', t)) }></textarea>
87
88
 
@@ -90,7 +91,7 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
90
91
 
91
92
  <div className="column">
92
93
  <div className="row">
93
- { t('routes_order.step5.billing.city') }
94
+ <Required>{ t('routes_order.step5.billing.city') }</Required>
94
95
 
95
96
  <input type="text" { ...refs('bill_city', Validate('required|min_length:2|max_length:100', t)) } />
96
97
 
@@ -98,7 +99,7 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
98
99
  </div>
99
100
 
100
101
  <div className="row">
101
- { t('routes_order.step5.billing.country') }
102
+ <Required>{ t('routes_order.step5.billing.country') }</Required>
102
103
 
103
104
  <select { ...refs('bill_country', Validate('required|min:1', t)) }>
104
105
  <option value={ 0 }>{ t('routes_order.step5.billing.country_choose') }</option>
@@ -1,10 +1,8 @@
1
1
  import { TFunction } from 'i18next';
2
2
  import { useForm } from 'react-hook-form';
3
3
  import { RiCoupon3Line } from 'react-icons/ri';
4
- import { Button } from '@autobusal/common';
5
4
  import { Validate, Display } from '@autobusal/utilities';
6
- import { SubTitle } from '../../styles';
7
- import { Container, ContainerForm, ContainerInput } from './styles';
5
+ import { Container, ContainerForm, ContainerInput, Apply } from './styles';
8
6
  import { CouponData } from '@autobusal/providers/types/orders';
9
7
  import { FoundData } from '@autobusal/providers/types/routes';
10
8
  import { TotalData, CouponForm } from '../../types';
@@ -46,27 +44,35 @@ const Coupons = ({ current, step2, t, total, onApplied }: Props): (JSX.Element |
46
44
  return null;
47
45
  }
48
46
 
47
+ /**
48
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
49
+ *
50
+ * One field with the apply control tucked inside it. In the checkout
51
+ * sidebar this used to be a card of its own, with a heading and a
52
+ * full-width button, taking more room than the totals it exists to
53
+ * change - for something most buyers do not have.
54
+ *
55
+ * The button keeps its accessible name via aria-label and title, so
56
+ * dropping the visible text costs a screen reader nothing.
57
+ */
49
58
  return (
50
- <Container className="box">
51
- <SubTitle>
52
- <RiCoupon3Line />
53
- { t('routes_order.step5.coupon.title') }
54
- </SubTitle>
55
-
59
+ <Container>
56
60
  <form onSubmit={ handleSubmit(onSubmit) }>
57
61
  <ContainerForm>
58
62
  <ContainerInput>
59
63
  <input type="text" placeholder={ t('routes_order.step5.coupon.code') } { ...register('code', Validate('required|min_length:4', t)) } />
60
- { Display(errors.code) }
64
+
65
+ <Apply
66
+ type="submit"
67
+ disabled={ isPending }
68
+ aria-label={ t('routes_order.step5.coupon.apply') }
69
+ title={ t('routes_order.step5.coupon.apply') }
70
+ >
71
+ <RiCoupon3Line />
72
+ </Apply>
61
73
  </ContainerInput>
62
74
 
63
- <Button
64
- type="submit"
65
- loading={ isPending }
66
- text={ t('routes_order.step5.coupon.apply') }
67
- noMargin
68
- maxWidth={ 175 }
69
- />
75
+ { Display(errors.code) }
70
76
  </ContainerForm>
71
77
  </form>
72
78
  </Container>
@@ -1,20 +1,53 @@
1
1
  import styled from 'styled-components';
2
2
 
3
3
  export const Container = styled.div`
4
- margin-top: 25px;
4
+ margin-top: 0;
5
5
  `;
6
6
 
7
7
  export const ContainerForm = styled.div`
8
8
  display: flex;
9
9
  flex-direction: column;
10
- gap: 15px;
11
- max-width: 450px;
10
+ gap: 4px;
11
+ `;
12
+
13
+ /**
14
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
15
+ *
16
+ * The apply control sits INSIDE the field rather than beside it. Beside it
17
+ * in a narrow sidebar, the two either fought for width or stacked into two
18
+ * full-width rows for something most buyers never use.
19
+ */
20
+ export const ContainerInput = styled.div`
21
+ position: relative;
12
22
 
13
- @media (min-width: 480px) {
14
- flex-direction: row;
23
+ & > input {
24
+ width: 100%;
25
+ padding-right: 42px;
15
26
  }
16
27
  `;
17
28
 
18
- export const ContainerInput = styled.div`
19
- flex: 1;
20
- `;
29
+ export const Apply = styled.button`
30
+ position: absolute;
31
+ top: 50%;
32
+ right: 6px;
33
+ transform: translateY(-50%);
34
+ display: flex;
35
+ align-items: center;
36
+ justify-content: center;
37
+ width: 30px;
38
+ height: 30px;
39
+ font-size: ${ props => props.theme.size.l };
40
+ color: ${ props => props.theme.primary.contrast };
41
+ background: ${ props => props.theme.primary.normal };
42
+ border-radius: ${ props => props.theme.borderRadius };
43
+ transition: all 0.2s ease;
44
+
45
+ &:hover {
46
+ opacity: .85;
47
+ }
48
+
49
+ &:disabled {
50
+ opacity: .5;
51
+ cursor: default;
52
+ }
53
+ `;
@@ -31,7 +31,7 @@ const Payments = ({ selected, isPartner, t, onChange }: Props): JSX.Element => {
31
31
 
32
32
  if (isLoading) {
33
33
  return (
34
- <Container className="box">
34
+ <Container>
35
35
  <Inline text={ t('routes_order.step5.actions.loading') } />
36
36
  </Container>
37
37
  );
@@ -39,7 +39,7 @@ const Payments = ({ selected, isPartner, t, onChange }: Props): JSX.Element => {
39
39
 
40
40
  return (
41
41
  <>
42
- <Container className="box">
42
+ <Container>
43
43
  <SubTitle>
44
44
  <RiSecurePaymentLine />
45
45
  { t('routes_order.step5.actions.title') }
@@ -1,14 +1,20 @@
1
1
  import styled, { css } from 'styled-components';
2
2
 
3
3
  export const Container = styled.div`
4
- margin-top: 25px;
4
+ margin-top: 0;
5
5
  `;
6
6
 
7
+ /**
8
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
9
+ *
10
+ * All options on one row inside the checkout sidebar. They are a choice of
11
+ * three short labels, not three products, so stacking them into tall cards
12
+ * gave a minor decision more visual weight than the total above it.
13
+ */
7
14
  export const ContainerPayments = styled.div`
8
15
  display: flex;
9
16
  flex-direction: row;
10
- flex-wrap: wrap;
11
- gap: 15px;
17
+ gap: 6px;
12
18
  justify-content: center;
13
19
  `;
14
20
 
@@ -17,9 +23,12 @@ export const ButtonPayment = styled.button<{ $selected: boolean }>`
17
23
  flex-direction: column;
18
24
  align-items: center;
19
25
  justify-content: center;
20
- gap: 10px;
21
- flex-basis: 150px;
22
- padding: 10px;
26
+ gap: 4px;
27
+ // equal shares of one row, never wrapping - min-width:0 lets them shrink
28
+ // below their label width instead of pushing each other onto new lines
29
+ flex: 1 1 0;
30
+ min-width: 0;
31
+ padding: 8px 4px;
23
32
  text-align: center;
24
33
  font-weight: 700;
25
34
  border: 1px solid ${ props => props.theme.background.neutral };
@@ -27,7 +36,7 @@ export const ButtonPayment = styled.button<{ $selected: boolean }>`
27
36
  transition: all 0.3s ease;
28
37
 
29
38
  & > svg {
30
- font-size: 35px;
39
+ font-size: 22px;
31
40
  }
32
41
 
33
42
  &:hover {
@@ -1,6 +1,7 @@
1
1
  import { TFunction } from 'i18next';
2
2
  import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';
3
- import { Container, ContainerIcon, Icon, Information, Title, SubTitle, Operator } from './styles';
3
+ import { FaCircleDot, FaLocationDot } from 'react-icons/fa6';
4
+ import { Container, ContainerIcon, Icon, Information, Title, Operator, Carrier, Logo, Leg, Point, PointIcon, PointCity, Country, PointMeta } from './styles';
4
5
  import { RoutesSearchForm } from '@autobusal/routes-search/types';
5
6
  import { FoundData } from '@autobusal/providers/types/routes';
6
7
 
@@ -11,24 +12,28 @@ interface Props {
11
12
  t: TFunction<'public'>
12
13
  }
13
14
 
15
+ /**
16
+ * One leg of the trip, both ends of it.
17
+ *
18
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
19
+ *
20
+ * This used to render a single line - the departure point on the outbound
21
+ * leg, the arrival point on the return one - so a one-way buyer reached the
22
+ * payment screen having never been shown where the bus actually drops them.
23
+ * Both ends are now stated, origin above destination.
24
+ *
25
+ * The country is named in full rather than coded: this is the last screen
26
+ * before someone pays, and "Thessaloniki, Greece" settles a doubt that
27
+ * "(GRC)" only half answers.
28
+ */
14
29
  const About = ({ type, step1, data, t }: Props): (JSX.Element | null) => {
15
- const icon = type === 'departure' ? <FaArrowRight /> : <FaArrowLeft />;
16
-
17
30
  if (!data) {
18
31
  return null;
19
32
  }
20
33
 
21
- const details = type === 'departure' ? {
22
- date: step1.departure,
23
- hour: data.locations.from.departure,
24
- stop: data.locations.from.stop?.name,
25
- city: data.locations.from.city.name
26
- } : {
27
- date: step1._return,
28
- hour: data.locations.to.arrival,
29
- stop: data.locations.to.stop?.name,
30
- city: data.locations.to.city.name
31
- };
34
+ const icon = type === 'departure' ? <FaArrowRight /> : <FaArrowLeft />;
35
+
36
+ const date = type === 'departure' ? step1.departure : step1._return;
32
37
 
33
38
  return (
34
39
  <Container>
@@ -38,11 +43,62 @@ const About = ({ type, step1, data, t }: Props): (JSX.Element | null) => {
38
43
 
39
44
  <Information>
40
45
  <Title>{ data.name } ({ data.code })</Title>
41
- <SubTitle>{ t(`routes_order.step5.summary.${ type }`, details) }</SubTitle>
42
- <Operator>{ data.operator.company }</Operator>
46
+
47
+ { /* Edited: Ferjolt Ozuni - Date: 2026-08-01
48
+ Mark and name together, ranged right. The name used to sit
49
+ alone at the foot of the block, so the logo read as decoration
50
+ rather than as the operator being identified. */ }
51
+ <Carrier>
52
+ { data.operator.logo_url && (
53
+ <Logo src={ data.operator.logo_url } alt={ data.operator.company } />
54
+ ) }
55
+
56
+ <Operator>{ data.operator.company }</Operator>
57
+ </Carrier>
58
+
59
+ <Leg>
60
+ <Point>
61
+ <PointIcon><FaCircleDot /></PointIcon>
62
+
63
+ <PointCity>
64
+ { data.locations.from.city.name }
65
+ { data.locations.from.city.country?.name && (
66
+ <Country>, { data.locations.from.city.country.name }</Country>
67
+ ) }
68
+ </PointCity>
69
+
70
+ { /* the date and time are what a buyer double-checks before
71
+ paying, so they carry the weight here */ }
72
+ <PointMeta $strong>
73
+ { t('routes_order.step5.summary.departs', {
74
+ date,
75
+ hour: data.locations.from.departure,
76
+ stop: data.locations.from.stop?.name
77
+ }) }
78
+ </PointMeta>
79
+ </Point>
80
+
81
+ <Point>
82
+ <PointIcon><FaLocationDot /></PointIcon>
83
+
84
+ <PointCity>
85
+ { data.locations.to.city.name }
86
+ { data.locations.to.city.country?.name && (
87
+ <Country>, { data.locations.to.city.country.name }</Country>
88
+ ) }
89
+ </PointCity>
90
+
91
+ <PointMeta>
92
+ { t('routes_order.step5.summary.arrives', {
93
+ hour: data.locations.to.arrival,
94
+ stop: data.locations.to.stop?.name
95
+ }) }
96
+ </PointMeta>
97
+ </Point>
98
+ </Leg>
43
99
  </Information>
44
100
  </Container>
45
101
  );
46
102
  };
47
103
 
48
- export default About;
104
+ export default About;
@@ -42,8 +42,96 @@ export const SubTitle = styled.div`
42
42
  border-radius: ${ props => props.theme.borderRadius };
43
43
  `;
44
44
 
45
+ /**
46
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
47
+ *
48
+ * Origin above destination, each with a city line and a detail line.
49
+ * Replaces the single pill that carried only one end of the leg.
50
+ */
51
+ /**
52
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
53
+ *
54
+ * The line runs down the Leg itself, not down each Point, so it reads as
55
+ * one continuous journey rather than two separate bars. Each point's icon
56
+ * then sits ON that line - centred over it with a background matching the
57
+ * card, so the line appears to pass behind the icon.
58
+ */
59
+ export const Leg = styled.div`
60
+ position: relative;
61
+ display: flex;
62
+ flex-direction: column;
63
+ gap: 12px;
64
+ margin: 10px 0;
65
+ padding-left: 26px;
66
+
67
+ &::before {
68
+ content: '';
69
+ position: absolute;
70
+ left: 8px;
71
+ top: 6px;
72
+ bottom: 6px;
73
+ width: 2px;
74
+ background: ${ props => props.theme.background.neutral };
75
+ }
76
+ `;
77
+
78
+ export const Point = styled.div`
79
+ position: relative;
80
+ display: flex;
81
+ flex-direction: column;
82
+ `;
83
+
84
+ export const PointIcon = styled.span`
85
+ position: absolute;
86
+ left: -26px;
87
+ top: 0;
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ width: 18px;
92
+ height: 18px;
93
+ font-size: 11px;
94
+ color: ${ props => props.theme.primary.contrast };
95
+ background: ${ props => props.theme.primary.normal };
96
+ border-radius: 100px;
97
+ `;
98
+
99
+ export const PointCity = styled.div`
100
+ font-weight: 700;
101
+ `;
102
+
103
+ export const Country = styled.span`
104
+ font-weight: 400;
105
+ color: ${ props => props.theme.font.faded };
106
+ `;
107
+
108
+ export const PointMeta = styled.div<{ $strong?: boolean }>`
109
+ font-size: calc(${ props => props.theme.size.s } - 1px);
110
+ font-weight: ${ props => props.$strong ? 700 : 400 };
111
+ color: ${ props => props.$strong ? props.theme.font.normal : props.theme.font.faded };
112
+ `;
113
+
114
+ // Edited: Ferjolt Ozuni - Date: 2026-08-01
115
+ // The operator's mark under the route name - who the buyer is actually
116
+ // travelling with, which a company name alone conveys less quickly.
117
+ export const Carrier = styled.div`
118
+ display: flex;
119
+ align-items: center;
120
+ justify-content: flex-end;
121
+ gap: 8px;
122
+ margin: 6px 0;
123
+ `;
124
+
125
+ export const Logo = styled.img`
126
+ display: block;
127
+ max-width: 90px;
128
+ max-height: 30px;
129
+ object-fit: contain;
130
+ `;
131
+
45
132
  export const Operator = styled.div`
46
- padding-left: 10px;
47
133
  font-size: calc(${ props => props.theme.size.s } - 1px);
134
+ font-weight: 700;
135
+ text-align: right;
48
136
  color: ${ props => props.theme.font.faded };
49
137
  `;
package/Steps/Steps.tsx CHANGED
@@ -8,7 +8,9 @@ interface Props {
8
8
  const Steps = ({ value }: Props): JSX.Element => {
9
9
  const items: JSX.Element[] = [];
10
10
 
11
- for (let i = 1; i <= 5; i++) {
11
+ // Edited: Ferjolt Ozuni - Date: 2026-08-01
12
+ // Four, not five: passenger details and checkout are one screen now.
13
+ for (let i = 1; i <= 4; i++) {
12
14
  items.push(
13
15
  <Item key={ i }>
14
16
  <Content $selected={ value > i }>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/routes-order",
3
- "version": "1.9.3",
3
+ "version": "1.11.3",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"
package/services.ts CHANGED
@@ -4,7 +4,7 @@ import { RoutesSearchForm } from '@autobusal/routes-search/types';
4
4
  import { FoundData } from '@autobusal/providers/types/routes';
5
5
  import { CouponData } from '@autobusal/providers/types/orders';
6
6
  import { BillingData, PersonData } from '@autobusal/providers/types/persons';
7
- import { FoundDay, ActionData, SaveData, OrderedData, CouponForm, AddonsSelection } from './types';
7
+ import { FoundDay, ActionData, SaveData, OrderedData, CouponForm, AddonsSelection, AlternativesData } from './types';
8
8
 
9
9
  export const useGetDepartures = (data: RoutesSearchForm): UseQueryResult<FoundData[]> => (
10
10
  useQuery({
@@ -21,6 +21,32 @@ export const useGetDepartures = (data: RoutesSearchForm): UseQueryResult<FoundDa
21
21
  })
22
22
  );
23
23
 
24
+ /**
25
+ * What to offer when a search finds nothing.
26
+ *
27
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
28
+ *
29
+ * `enabled` is the whole point: this only runs once the departure search
30
+ * has come back empty, so a search that succeeded never pays for it. On
31
+ * obtapi's side it costs a handful of real searches, which is why it is
32
+ * not simply folded into /departure.
33
+ */
34
+ export const useGetAlternatives = (data: RoutesSearchForm, enabled: boolean): UseQueryResult<AlternativesData> => (
35
+ useQuery({
36
+ queryKey: ['search-alternatives', { data }],
37
+ enabled,
38
+ queryFn: async () => (
39
+ await apiClient
40
+ .get('/api/routes/search/alternatives', {
41
+ params: data
42
+ })
43
+ .then(response => (
44
+ response?.data ?? { from: [], to: [], dates: [] }
45
+ ))
46
+ )
47
+ })
48
+ );
49
+
24
50
  export const useGetReturns = (data: RoutesSearchForm, departure: FoundData): UseQueryResult<FoundData[]> => (
25
51
  useQuery({
26
52
  queryKey: ['search-routes-step3', { data }],
@@ -74,18 +100,34 @@ export const useGetDates = (step1: RoutesSearchForm, type: ('departure' | '_retu
74
100
  })
75
101
  };
76
102
 
103
+ /**
104
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
105
+ *
106
+ * The passenger details moved from a hook argument into the mutation
107
+ * variables. On the merged checkout screen they are only known at the
108
+ * moment the buyer presses pay - validated right then, in the same handler
109
+ * - so baking them into the hook's closure at render time would have sent
110
+ * whatever was in state one render earlier.
111
+ */
112
+ export interface OrderSubmission {
113
+ billing: BillingData
114
+ person: PersonData
115
+ }
116
+
77
117
  export const usePostOrder = (
78
118
  step1: RoutesSearchForm,
79
119
  step2: FoundData,
80
120
  step3: (FoundData | undefined),
81
- step4: PersonData,
82
121
  coupon: (CouponData | undefined),
83
122
  action: (string | undefined),
84
123
  addons: AddonsSelection
85
- ): UseMutationResult<OrderedData, Error, BillingData, unknown> => (
124
+ ): UseMutationResult<OrderedData, Error, OrderSubmission, unknown> => (
86
125
  useMutation({
87
126
  mutationKey: ['save-order'],
88
- mutationFn: async (data: BillingData) => {
127
+ mutationFn: async ({ billing, person }: OrderSubmission) => {
128
+ const data = billing;
129
+ const step4 = person;
130
+
89
131
  const type = step1.type ?? 'departure';
90
132
 
91
133
  const order: SaveData = {};