@autobusal/routes-order 1.9.0 → 1.11.1
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 +176 -0
- package/Checkout/Checkout.tsx +280 -0
- package/Checkout/Sidebar/Sidebar.tsx +110 -0
- package/Checkout/Sidebar/styles.ts +44 -0
- package/Found/Empty/Empty.tsx +109 -0
- package/Found/Empty/styles.ts +99 -0
- package/Found/Filters/Filters.tsx +10 -7
- package/Found/Filters/styles.ts +79 -8
- package/Found/Found.tsx +47 -26
- package/Found/Header/styles.ts +20 -4
- package/Found/Orders/Orders.tsx +13 -4
- package/Found/Route/Route.tsx +46 -18
- package/Found/Route/styles.ts +109 -31
- package/Found/Summary/Summary.tsx +1 -1
- package/Found/Summary/styles.ts +12 -0
- package/Found/refine.ts +69 -0
- package/RoutesOrder.tsx +45 -23
- package/Step2.tsx +34 -1
- package/Step4/Passenger/Passenger.tsx +10 -10
- package/Step5/Billing/Billing.tsx +8 -7
- package/Step5/Coupons/styles.ts +16 -9
- package/Step5/Payments/styles.ts +7 -4
- package/Step5/Summary/About/About.tsx +66 -16
- package/Step5/Summary/About/styles.ts +81 -0
- package/Steps/Steps.tsx +3 -1
- package/package.json +1 -1
- package/services.ts +46 -4
- package/styles.ts +49 -0
- package/types.ts +24 -0
- package/Step4/Step4.tsx +0 -114
- package/Step5/Step5.tsx +0 -185
package/Found/Route/Route.tsx
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import { TFunction } from 'i18next';
|
|
3
3
|
import { HiOutlineArrowNarrowRight, HiOutlineDocumentText } from 'react-icons/hi';
|
|
4
|
+
import { BiHourglass } from 'react-icons/bi';
|
|
4
5
|
import { RiBus2Line } from 'react-icons/ri';
|
|
6
|
+
import { formatDuration, splitPrice } from '../refine';
|
|
5
7
|
import { Button, RouteFeature, Policy } from '@autobusal/common';
|
|
6
|
-
import { Container, SpecialOffer, FavoriteStop, Trip, Company, Location, Time, Price, PriceNotice, LinkCompany, Details, Detail, ButtonPolicy, TitleDetail, LinkRoute,
|
|
8
|
+
import { Container, SpecialOffer, FavoriteStop, Trip, Company, Location, Stop, Iso, Time, Price, Amount, Decimals, Currency, PriceNotice, LinkCompany, Details, Detail, ButtonPolicy, TitleDetail, LinkRoute, FeaturesItems } from './styles';
|
|
7
9
|
import { FoundData } from '@autobusal/providers/types/routes';
|
|
8
10
|
|
|
9
11
|
// neutral placeholder so a missing/broken operator logo doesn't render the
|
|
@@ -28,8 +30,10 @@ const Route = ({ data, type, passengers, t, onSave, children }: Props): JSX.Elem
|
|
|
28
30
|
const customs = Object.values(data.customs).join(', ');
|
|
29
31
|
const transiting = Object.values(data.transiting).join(', ');
|
|
30
32
|
|
|
33
|
+
const price = splitPrice(data.price.display);
|
|
34
|
+
|
|
31
35
|
const features = data.trip.features_data.map((item, index) => (
|
|
32
|
-
<RouteFeature key={ index } item={ item } />
|
|
36
|
+
<RouteFeature key={ index } item={ item } compact />
|
|
33
37
|
));
|
|
34
38
|
|
|
35
39
|
// Edited: Ferjolt Ozuni - Date: 2026-07-27
|
|
@@ -54,22 +58,41 @@ const Route = ({ data, type, passengers, t, onSave, children }: Props): JSX.Elem
|
|
|
54
58
|
</Company>
|
|
55
59
|
|
|
56
60
|
<Location>
|
|
57
|
-
<h2>
|
|
58
|
-
|
|
61
|
+
<h2>
|
|
62
|
+
{ data.locations.from.city.name }
|
|
63
|
+
<Iso>({ data.locations.from.city.country?.iso_code })</Iso>
|
|
64
|
+
{ data.locations.from.departure }
|
|
65
|
+
</h2>
|
|
66
|
+
|
|
67
|
+
<Stop>{ data.locations.from.stop?.name }</Stop>
|
|
59
68
|
</Location>
|
|
60
69
|
|
|
61
70
|
<Time>
|
|
62
71
|
<HiOutlineArrowNarrowRight />
|
|
63
|
-
|
|
72
|
+
|
|
73
|
+
{ /* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
74
|
+
The clock is the point: this number sits between two real
|
|
75
|
+
departure times, and "08:30" gave nothing to distinguish a
|
|
76
|
+
trip length from a time of day. */ }
|
|
77
|
+
<span><BiHourglass />{ formatDuration(data.duration, t) }</span>
|
|
64
78
|
</Time>
|
|
65
79
|
|
|
66
80
|
<Location>
|
|
67
|
-
<h2>
|
|
68
|
-
|
|
81
|
+
<h2>
|
|
82
|
+
{ data.locations.to.city.name }
|
|
83
|
+
<Iso>({ data.locations.to.city.country?.iso_code })</Iso>
|
|
84
|
+
{ data.locations.to.arrival }
|
|
85
|
+
</h2>
|
|
86
|
+
|
|
87
|
+
<Stop>{ data.locations.to.stop?.name }</Stop>
|
|
69
88
|
</Location>
|
|
70
89
|
|
|
71
90
|
<Price>
|
|
72
|
-
<
|
|
91
|
+
<Amount>
|
|
92
|
+
{ price.whole }
|
|
93
|
+
{ price.decimals !== null && <Decimals>{ price.decimals }</Decimals> }
|
|
94
|
+
{ price.currency !== null && <Currency>{ price.currency }</Currency> }
|
|
95
|
+
</Amount>
|
|
73
96
|
|
|
74
97
|
{ /* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
75
98
|
obtapi's price.value is the total for the WHOLE party
|
|
@@ -104,6 +127,21 @@ const Route = ({ data, type, passengers, t, onSave, children }: Props): JSX.Elem
|
|
|
104
127
|
{ children }
|
|
105
128
|
|
|
106
129
|
<Details>
|
|
130
|
+
{ /* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
131
|
+
Amenities were a separate full-width block under the card.
|
|
132
|
+
They are the same kind of thing as route, transit and policies
|
|
133
|
+
- supporting detail - so they join that row as the first
|
|
134
|
+
column, which is one fewer band of vertical space per result. */ }
|
|
135
|
+
{ data.trip.features_data.length > 0 && (
|
|
136
|
+
<Detail>
|
|
137
|
+
<TitleDetail>{ t('routes_order.step2.route.features') }</TitleDetail>
|
|
138
|
+
|
|
139
|
+
<FeaturesItems>
|
|
140
|
+
{ features }
|
|
141
|
+
</FeaturesItems>
|
|
142
|
+
</Detail>
|
|
143
|
+
) }
|
|
144
|
+
|
|
107
145
|
<Detail>
|
|
108
146
|
<TitleDetail>{ t('routes_order.step2.route.name') }</TitleDetail>
|
|
109
147
|
<LinkRoute to={ bookOnUrl ?? data.link } target="_blank" rel="noopener noreferrer">{ data.code }</LinkRoute>
|
|
@@ -135,16 +173,6 @@ const Route = ({ data, type, passengers, t, onSave, children }: Props): JSX.Elem
|
|
|
135
173
|
) }
|
|
136
174
|
</Details>
|
|
137
175
|
|
|
138
|
-
{ data.trip.features_data.length > 0 && (
|
|
139
|
-
<Features>
|
|
140
|
-
<TitleFeatures>{ t('routes_order.step2.route.features') }</TitleFeatures>
|
|
141
|
-
|
|
142
|
-
<FeaturesItems>
|
|
143
|
-
{ features }
|
|
144
|
-
</FeaturesItems>
|
|
145
|
-
</Features>
|
|
146
|
-
) }
|
|
147
|
-
|
|
148
176
|
{ policy && typeof data.id === 'number' && <Policy id={ data.id } t={ t } onClose={ () => setPolicy(false) } /> }
|
|
149
177
|
</Container>
|
|
150
178
|
);
|
package/Found/Route/styles.ts
CHANGED
|
@@ -64,26 +64,67 @@ export const Company = styled.div`
|
|
|
64
64
|
flex-basis: 100%;
|
|
65
65
|
|
|
66
66
|
@media (min-width: 1024px) {
|
|
67
|
-
flex
|
|
67
|
+
flex: 0 0 185px;
|
|
68
68
|
}
|
|
69
69
|
`;
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
73
|
+
*
|
|
74
|
+
* min-width: 0 is what makes `flex: 1` mean "an equal share". Without it
|
|
75
|
+
* the default min-width:auto stops a column shrinking below its content,
|
|
76
|
+
* so the arrival column (long city plus a long stop name) grew to 215px
|
|
77
|
+
* while the departure column got 155px - the two halves of the same row
|
|
78
|
+
* were different widths, and no fixed header could ever line up with
|
|
79
|
+
* both. It also stops one long stop name stealing width from the other
|
|
80
|
+
* side of the card.
|
|
81
|
+
*/
|
|
71
82
|
export const Location = styled.div`
|
|
72
83
|
flex: 1;
|
|
84
|
+
min-width: 0;
|
|
73
85
|
display: flex;
|
|
74
86
|
flex-direction: column;
|
|
75
87
|
align-items: center;
|
|
76
|
-
gap:
|
|
88
|
+
gap: 3px;
|
|
77
89
|
text-align: center;
|
|
78
90
|
|
|
79
91
|
& > h2 {
|
|
80
92
|
margin-bottom: 0;
|
|
81
93
|
}
|
|
94
|
+
`;
|
|
82
95
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
98
|
+
*
|
|
99
|
+
* The stop name is supporting detail, not a heading. At the same 16px as
|
|
100
|
+
* body copy it competed with the city above it; a step down lets the eye
|
|
101
|
+
* land on the city first and read the stop only when it needs to.
|
|
102
|
+
*/
|
|
103
|
+
export const Stop = styled.span`
|
|
104
|
+
font-size: ${ props => props.theme.size.xs };
|
|
105
|
+
font-weight: 700;
|
|
106
|
+
line-height: 1.3;
|
|
107
|
+
color: ${ props => props.theme.font.faded };
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
112
|
+
*
|
|
113
|
+
* The country code comes out of the database lowercase ("alb", "grc").
|
|
114
|
+
* An ISO country code is by definition upper case, so it is cased here at
|
|
115
|
+
* render time rather than left to the data - every consumer would otherwise
|
|
116
|
+
* have to remember to do it. Set smaller and muted so it qualifies the city
|
|
117
|
+
* without competing with it.
|
|
118
|
+
*/
|
|
119
|
+
export const Iso = styled.span`
|
|
120
|
+
// JSX drops the literal spaces around this element, so the gap on either
|
|
121
|
+
// side of "(ALB)" is this margin's job, not whitespace's
|
|
122
|
+
margin: 0 5px;
|
|
123
|
+
font-size: ${ props => props.theme.size.xs };
|
|
124
|
+
font-weight: 700;
|
|
125
|
+
text-transform: uppercase;
|
|
126
|
+
letter-spacing: .5px;
|
|
127
|
+
color: ${ props => props.theme.font.faded };
|
|
87
128
|
`;
|
|
88
129
|
|
|
89
130
|
export const Time = styled.div`
|
|
@@ -98,15 +139,26 @@ export const Time = styled.div`
|
|
|
98
139
|
}
|
|
99
140
|
|
|
100
141
|
& > span {
|
|
101
|
-
display:
|
|
102
|
-
|
|
103
|
-
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
gap: 5px;
|
|
145
|
+
padding: 3px 12px;
|
|
146
|
+
white-space: nowrap;
|
|
147
|
+
font-size: ${ props => props.theme.size.m };
|
|
104
148
|
background: ${ props => props.theme.background.neutral };
|
|
105
149
|
border-radius: 100px;
|
|
106
150
|
}
|
|
107
151
|
|
|
152
|
+
// the hourglass is a hint, not a heading - the big arrow above already
|
|
153
|
+
// owns the visual weight in this column
|
|
154
|
+
& > span > svg {
|
|
155
|
+
flex-shrink: 0;
|
|
156
|
+
font-size: ${ props => props.theme.size.m };
|
|
157
|
+
opacity: .7;
|
|
158
|
+
}
|
|
159
|
+
|
|
108
160
|
@media (min-width: 640px) {
|
|
109
|
-
flex
|
|
161
|
+
flex: 0 0 120px;
|
|
110
162
|
}
|
|
111
163
|
`;
|
|
112
164
|
|
|
@@ -114,21 +166,52 @@ export const Price = styled.div`
|
|
|
114
166
|
flex-basis: 100%;
|
|
115
167
|
display: flex;
|
|
116
168
|
flex-direction: column;
|
|
117
|
-
gap:
|
|
169
|
+
gap: 8px;
|
|
118
170
|
align-items: center;
|
|
119
171
|
|
|
120
|
-
& > strong {
|
|
121
|
-
font-size: ${ props => props.theme.size.xxl };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
172
|
@media (min-width: 768px) {
|
|
125
|
-
flex
|
|
173
|
+
flex: 0 0 165px;
|
|
126
174
|
}
|
|
127
175
|
`;
|
|
128
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
179
|
+
*
|
|
180
|
+
* The decimals ride as superscript rather than at full size. On a fare
|
|
181
|
+
* board the cents are almost never what anyone is comparing, and at the
|
|
182
|
+
* headline size they were taking a third of the width of the widest thing
|
|
183
|
+
* on the card. The line-height is pinned to 1 so the shrunk decimals do
|
|
184
|
+
* not leave a gap under the row.
|
|
185
|
+
*/
|
|
186
|
+
export const Amount = styled.strong`
|
|
187
|
+
display: flex;
|
|
188
|
+
align-items: flex-start;
|
|
189
|
+
line-height: 1;
|
|
190
|
+
font-size: ${ props => props.theme.size.xxl };
|
|
191
|
+
`;
|
|
192
|
+
|
|
193
|
+
export const Decimals = styled.span`
|
|
194
|
+
margin-left: 1px;
|
|
195
|
+
font-size: ${ props => props.theme.size.s };
|
|
196
|
+
line-height: 1;
|
|
197
|
+
`;
|
|
198
|
+
|
|
199
|
+
export const Currency = styled.span`
|
|
200
|
+
margin-left: 4px;
|
|
201
|
+
align-self: center;
|
|
202
|
+
font-size: ${ props => props.theme.size.m };
|
|
203
|
+
`;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
207
|
+
*
|
|
208
|
+
* Tucked up against the price it qualifies, which reads as one unit and
|
|
209
|
+
* frees the vertical space the Select button needs.
|
|
210
|
+
*/
|
|
129
211
|
export const PriceNotice = styled.span`
|
|
130
|
-
margin-top: -
|
|
131
|
-
font-size: ${ props => props.theme.size.
|
|
212
|
+
margin-top: -4px;
|
|
213
|
+
font-size: ${ props => props.theme.size.xxs };
|
|
214
|
+
line-height: 1.25;
|
|
132
215
|
color: ${ props => props.theme.font.faded };
|
|
133
216
|
text-align: center;
|
|
134
217
|
`;
|
|
@@ -195,20 +278,15 @@ export const ButtonPolicy = styled.button`
|
|
|
195
278
|
}
|
|
196
279
|
`;
|
|
197
280
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
font-size: ${ props => props.theme.size.s };
|
|
206
|
-
margin-bottom: 8px;
|
|
207
|
-
`;
|
|
208
|
-
|
|
281
|
+
/**
|
|
282
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
283
|
+
*
|
|
284
|
+
* Left-aligned now that this sits inside a Detail column alongside route,
|
|
285
|
+
* transit and policies, rather than centred in a full-width band of its
|
|
286
|
+
* own under the card.
|
|
287
|
+
*/
|
|
209
288
|
export const FeaturesItems = styled.div`
|
|
210
289
|
display: flex;
|
|
211
290
|
flex-wrap: wrap;
|
|
212
|
-
gap:
|
|
213
|
-
justify-content: center;
|
|
291
|
+
gap: 6px;
|
|
214
292
|
`;
|
|
@@ -20,7 +20,7 @@ interface Props {
|
|
|
20
20
|
* results, which is otherwise easy to forget after scrolling.
|
|
21
21
|
*/
|
|
22
22
|
const Summary = ({ shown, total, range, t, children }: Props): JSX.Element => (
|
|
23
|
-
<Container>
|
|
23
|
+
<Container className="box">
|
|
24
24
|
<Result>
|
|
25
25
|
{ /* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
26
26
|
`total`, not `count` - i18next treats a `count` variable as a
|
package/Found/Summary/styles.ts
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import styled from 'styled-components';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
5
|
+
*
|
|
6
|
+
* Carries the `box` surface (padding is overridden to something tighter -
|
|
7
|
+
* this is a bar, not a panel) so the count and the sort control sit on the
|
|
8
|
+
* same card background as everything else on the page rather than floating
|
|
9
|
+
* loose on the page background.
|
|
10
|
+
*/
|
|
3
11
|
export const Container = styled.div`
|
|
4
12
|
display: flex;
|
|
5
13
|
flex-direction: column;
|
|
6
14
|
gap: 12px;
|
|
7
15
|
margin-bottom: 15px;
|
|
8
16
|
|
|
17
|
+
&.box {
|
|
18
|
+
padding: 12px 15px;
|
|
19
|
+
}
|
|
20
|
+
|
|
9
21
|
@media (min-width: 1024px) {
|
|
10
22
|
flex-direction: row;
|
|
11
23
|
align-items: center;
|
package/Found/refine.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TFunction } from 'i18next';
|
|
1
2
|
import { FoundData } from '@autobusal/providers/types/routes';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -97,6 +98,74 @@ export const durationOf = (item: FoundData): number | null => {
|
|
|
97
98
|
return minutesOf(item.duration);
|
|
98
99
|
};
|
|
99
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Turn obtapi's "HH:MM" trip length into something that reads as a
|
|
103
|
+
* duration rather than a clock time.
|
|
104
|
+
*
|
|
105
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
106
|
+
*
|
|
107
|
+
* "08:30" sitting between two departure times is genuinely ambiguous -
|
|
108
|
+
* nothing about it says "eight and a half hours" rather than "half past
|
|
109
|
+
* eight", and on this card it sits directly between 09:00 and 16:30. The
|
|
110
|
+
* unit labels come from the translation files because "h"/"m" are not
|
|
111
|
+
* universal; Greek and the Cyrillic locales carry their own.
|
|
112
|
+
*
|
|
113
|
+
* The "-" sentinel (obtapi could not compute a duration) passes straight
|
|
114
|
+
* through - it must not become "0h 0m", which would read as a real answer.
|
|
115
|
+
*/
|
|
116
|
+
export const formatDuration = (duration: string, t: TFunction<'common'>): string => {
|
|
117
|
+
if (!duration || duration === '-') {
|
|
118
|
+
return duration;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const total = minutesOf(duration);
|
|
122
|
+
|
|
123
|
+
const hours = Math.floor(total / 60);
|
|
124
|
+
const minutes = total % 60;
|
|
125
|
+
|
|
126
|
+
const pad = (value: number): string => (value <= 9 ? `0${ value }` : String(value));
|
|
127
|
+
|
|
128
|
+
const parts: string[] = [];
|
|
129
|
+
|
|
130
|
+
if (hours > 0) {
|
|
131
|
+
parts.push(t('routes_order.step2.route.duration.hours', { value: pad(hours) }));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// a trip under an hour still needs a number, and so does an exact
|
|
135
|
+
// number of hours - but "08h 00m" is noise, so zero minutes are dropped
|
|
136
|
+
if (minutes > 0 || hours === 0) {
|
|
137
|
+
parts.push(t('routes_order.step2.route.duration.minutes', { value: pad(minutes) }));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return parts.join(' ');
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Split obtapi's formatted price into its parts so the decimals can be set
|
|
145
|
+
* as superscript.
|
|
146
|
+
*
|
|
147
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
148
|
+
*
|
|
149
|
+
* obtapi sends a rendered string ("1,048.00 EUR" - number_format, so the
|
|
150
|
+
* thousands separator is a comma and the decimal point a dot). Splitting it
|
|
151
|
+
* here rather than reformatting from `price.value` keeps the label's own
|
|
152
|
+
* currency and formatting as the single source of truth; we only decide how
|
|
153
|
+
* big each piece is drawn.
|
|
154
|
+
*
|
|
155
|
+
* Anything that does not match the expected shape is returned whole, so an
|
|
156
|
+
* unusual currency format degrades to today's rendering instead of being
|
|
157
|
+
* mangled.
|
|
158
|
+
*/
|
|
159
|
+
export const splitPrice = (display: string): { whole: string, decimals: string | null, currency: string | null } => {
|
|
160
|
+
const match = /^(.+)\.(\d{2})\s+(\S+)$/.exec(display ?? '');
|
|
161
|
+
|
|
162
|
+
if (match === null) {
|
|
163
|
+
return { whole: display ?? '', decimals: null, currency: null };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { whole: match[1], decimals: match[2], currency: match[3] };
|
|
167
|
+
};
|
|
168
|
+
|
|
100
169
|
export const bucketOf = (item: FoundData): BucketKey => {
|
|
101
170
|
const hour = Math.floor(departureOf(item) / 60);
|
|
102
171
|
|
package/RoutesOrder.tsx
CHANGED
|
@@ -2,12 +2,13 @@ import { useState } from 'react';
|
|
|
2
2
|
import { useParams } from 'react-router-dom';
|
|
3
3
|
import { TFunction } from 'i18next';
|
|
4
4
|
import { Meta } from '@autobusal/common';
|
|
5
|
+
import RoutesSearch from '@autobusal/routes-search';
|
|
5
6
|
import { useGetSuggestions } from '@autobusal/routes-search/services';
|
|
7
|
+
import { SearchAgain } from './styles';
|
|
6
8
|
import Step1 from './Step1/Step1';
|
|
7
9
|
import Step2 from './Step2';
|
|
8
10
|
import Step3 from './Step3';
|
|
9
|
-
import
|
|
10
|
-
import Step5 from './Step5/Step5';
|
|
11
|
+
import Checkout from './Checkout/Checkout';
|
|
11
12
|
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
12
13
|
import { FoundData } from '@autobusal/providers/types/routes';
|
|
13
14
|
import { PersonData } from '@autobusal/providers/types/persons';
|
|
@@ -80,11 +81,6 @@ const RoutesOrder = ({ t }: Props): JSX.Element => {
|
|
|
80
81
|
setCurrent(4);
|
|
81
82
|
};
|
|
82
83
|
|
|
83
|
-
const onStep4 = (data: PersonData): void => {
|
|
84
|
-
setStep4(data);
|
|
85
|
-
setCurrent(5);
|
|
86
|
-
};
|
|
87
|
-
|
|
88
84
|
const onBack = (step: number): void => {
|
|
89
85
|
setCurrent(step);
|
|
90
86
|
};
|
|
@@ -92,11 +88,48 @@ const RoutesOrder = ({ t }: Props): JSX.Element => {
|
|
|
92
88
|
const showStep1 = current === 1;
|
|
93
89
|
const showStep2 = current === 2 && step1 !== undefined;
|
|
94
90
|
const showStep3 = current === 3 && step1 !== undefined && step2 !== undefined;
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
|
|
92
|
+
// Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
93
|
+
// Step 4 is now the whole of the rest: passenger details and payment on
|
|
94
|
+
// one screen, so there is no step 5 and no intermediate handoff of
|
|
95
|
+
// passenger data between them.
|
|
96
|
+
const showCheckout = current === 4 && step1 !== undefined && step2 !== undefined;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
100
|
+
*
|
|
101
|
+
* The search form stays on screen while the user is choosing a route.
|
|
102
|
+
* Adjusting a date or a city used to mean "Back to Search", which threw
|
|
103
|
+
* away the results and any filters with them.
|
|
104
|
+
*
|
|
105
|
+
* Only on the route-selection screens: past that point a passenger has
|
|
106
|
+
* committed to a specific trip, and quietly re-running the search under
|
|
107
|
+
* them would discard passenger details they had already typed.
|
|
108
|
+
*
|
|
109
|
+
* onStep1 is the same handler Step1 uses, so submitting here re-runs the
|
|
110
|
+
* search and lands back on step 2 exactly as a fresh search would.
|
|
111
|
+
*/
|
|
112
|
+
const showSearchAgain = (current === 2 || current === 3) && step1 !== undefined;
|
|
97
113
|
|
|
98
114
|
return (
|
|
99
115
|
<Meta title={ title } description={ description }>
|
|
116
|
+
{ showSearchAgain && step1 !== undefined && (
|
|
117
|
+
<SearchAgain className="box">
|
|
118
|
+
<RoutesSearch
|
|
119
|
+
type={ step1.type ?? 'departure' }
|
|
120
|
+
from={ `${ step1.from }${ step1.stop_from ? `:${ step1.stop_from }` : '' }` }
|
|
121
|
+
to={ `${ step1.to }${ step1.stop_to ? `:${ step1.stop_to }` : '' }` }
|
|
122
|
+
departure={ step1.departure }
|
|
123
|
+
_return={ step1._return ?? '' }
|
|
124
|
+
adultsNo={ step1.adults }
|
|
125
|
+
childrenNo={ step1.children }
|
|
126
|
+
babiesNo={ step1.babies }
|
|
127
|
+
t={ t }
|
|
128
|
+
onSearch={ onStep1 }
|
|
129
|
+
/>
|
|
130
|
+
</SearchAgain>
|
|
131
|
+
) }
|
|
132
|
+
|
|
100
133
|
{ showStep1 && (
|
|
101
134
|
<Step1
|
|
102
135
|
value={ step1 }
|
|
@@ -127,25 +160,14 @@ const RoutesOrder = ({ t }: Props): JSX.Element => {
|
|
|
127
160
|
/>
|
|
128
161
|
) }
|
|
129
162
|
|
|
130
|
-
{
|
|
131
|
-
<
|
|
163
|
+
{ showCheckout && (
|
|
164
|
+
<Checkout
|
|
132
165
|
values={ step4 }
|
|
133
166
|
step1={ step1 }
|
|
134
167
|
step2={ step2 }
|
|
135
168
|
step3={ step3 }
|
|
136
169
|
t={ t }
|
|
137
|
-
|
|
138
|
-
onBack={ onBack }
|
|
139
|
-
/>
|
|
140
|
-
) }
|
|
141
|
-
|
|
142
|
-
{ showStep5 && (
|
|
143
|
-
<Step5
|
|
144
|
-
step1={ step1 }
|
|
145
|
-
step2={ step2 }
|
|
146
|
-
step3={ step3 }
|
|
147
|
-
step4={ step4 }
|
|
148
|
-
t={ t }
|
|
170
|
+
onStore={ setStep4 }
|
|
149
171
|
onBack={ onBack }
|
|
150
172
|
/>
|
|
151
173
|
) }
|
package/Step2.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import { Title, Actions } from './styles';
|
|
|
8
8
|
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
9
9
|
import { FoundData } from '@autobusal/providers/types/routes';
|
|
10
10
|
import { useGetDepartures } from './services';
|
|
11
|
+
import { useGetSuggestions } from '@autobusal/routes-search/services';
|
|
11
12
|
|
|
12
13
|
interface Props {
|
|
13
14
|
step1: RoutesSearchForm
|
|
@@ -20,11 +21,43 @@ interface Props {
|
|
|
20
21
|
const Step2 = ({ step1, t, onSearch, onSave, onBack }: Props): JSX.Element => {
|
|
21
22
|
const { data, isFetching } = useGetDepartures(step1);
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
26
|
+
*
|
|
27
|
+
* "Select a Departure Schedule" said nothing about the trip and nothing
|
|
28
|
+
* a search engine could use. The heading now names the actual journey.
|
|
29
|
+
*
|
|
30
|
+
* Countries are dropped when both ends share one: "from Tirana Albania
|
|
31
|
+
* to Durres Albania" is noise, while on a cross-border trip the country
|
|
32
|
+
* is the part that matters.
|
|
33
|
+
*
|
|
34
|
+
* Read from the city list rather than from the results, so the heading
|
|
35
|
+
* is right even when the search finds nothing - which is exactly when
|
|
36
|
+
* being told what was searched for matters most. That list is the same
|
|
37
|
+
* cached query the search form's autocomplete already uses, so it costs
|
|
38
|
+
* no extra request.
|
|
39
|
+
*/
|
|
40
|
+
const { data: cities } = useGetSuggestions();
|
|
41
|
+
|
|
42
|
+
const from = cities.find(city => city.slug === step1.from);
|
|
43
|
+
const to = cities.find(city => city.slug === step1.to);
|
|
44
|
+
|
|
45
|
+
const sameCountry = from?.country?.id !== undefined && from.country.id === to?.country?.id;
|
|
46
|
+
|
|
47
|
+
const title = from && to
|
|
48
|
+
? t('routes_order.step2.title_pair', {
|
|
49
|
+
from: from.name,
|
|
50
|
+
to: to.name,
|
|
51
|
+
from_country: sameCountry ? '' : ` ${ from.country?.name ?? '' }`,
|
|
52
|
+
to_country: sameCountry ? '' : ` ${ to.country?.name ?? '' }`
|
|
53
|
+
})
|
|
54
|
+
: t('routes_order.step2.title');
|
|
55
|
+
|
|
23
56
|
return (
|
|
24
57
|
<>
|
|
25
58
|
<Title>
|
|
26
59
|
<RiBus2Line />
|
|
27
|
-
{
|
|
60
|
+
{ title }
|
|
28
61
|
</Title>
|
|
29
62
|
|
|
30
63
|
<Steps value={ 2 } />
|
|
@@ -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 }
|