@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.
- package/CHANGELOG.md +122 -0
- package/Checkout/Checkout.tsx +280 -0
- package/Checkout/Sidebar/Sidebar.tsx +110 -0
- package/Checkout/Sidebar/styles.ts +52 -0
- package/Found/Empty/Empty.tsx +109 -0
- package/Found/Empty/styles.ts +99 -0
- package/Found/Found.tsx +19 -2
- package/Found/Header/styles.ts +20 -4
- package/Found/Orders/Orders.tsx +13 -4
- package/Found/Route/Route.tsx +25 -14
- package/Found/Route/styles.ts +61 -23
- package/Found/refine.ts +26 -0
- package/RoutesOrder.tsx +69 -23
- package/Step2.tsx +34 -1
- package/Step4/Passenger/Passenger.tsx +10 -10
- package/Step5/Billing/Billing.tsx +8 -7
- package/Step5/Coupons/Coupons.tsx +23 -17
- package/Step5/Coupons/styles.ts +41 -8
- package/Step5/Payments/Payments.tsx +2 -2
- package/Step5/Payments/styles.ts +16 -7
- package/Step5/Summary/About/About.tsx +73 -17
- package/Step5/Summary/About/styles.ts +89 -1
- package/Steps/Steps.tsx +3 -1
- package/package.json +1 -1
- package/services.ts +46 -4
- package/styles.ts +77 -0
- package/types.ts +24 -0
- package/Step4/Step4.tsx +0 -114
- package/Step5/Step5.tsx +0 -185
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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>
|
package/Step5/Coupons/styles.ts
CHANGED
|
@@ -1,20 +1,53 @@
|
|
|
1
1
|
import styled from 'styled-components';
|
|
2
2
|
|
|
3
3
|
export const Container = styled.div`
|
|
4
|
-
margin-top:
|
|
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:
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
23
|
+
& > input {
|
|
24
|
+
width: 100%;
|
|
25
|
+
padding-right: 42px;
|
|
15
26
|
}
|
|
16
27
|
`;
|
|
17
28
|
|
|
18
|
-
export const
|
|
19
|
-
|
|
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
|
|
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
|
|
42
|
+
<Container>
|
|
43
43
|
<SubTitle>
|
|
44
44
|
<RiSecurePaymentLine />
|
|
45
45
|
{ t('routes_order.step5.actions.title') }
|
package/Step5/Payments/styles.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import styled, { css } from 'styled-components';
|
|
2
2
|
|
|
3
3
|
export const Container = styled.div`
|
|
4
|
-
margin-top:
|
|
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
|
-
|
|
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:
|
|
21
|
-
|
|
22
|
-
|
|
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:
|
|
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 {
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
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,
|
|
124
|
+
): UseMutationResult<OrderedData, Error, OrderSubmission, unknown> => (
|
|
86
125
|
useMutation({
|
|
87
126
|
mutationKey: ['save-order'],
|
|
88
|
-
mutationFn: async (
|
|
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 = {};
|