@autobusal/routes-order 1.4.1 → 1.5.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 +48 -0
- package/Found/Booking/Booking.tsx +2 -2
- package/Found/Orders/Orders.tsx +3 -1
- package/Found/Route/Route.tsx +6 -2
- package/Step4/Passenger/Passenger.tsx +82 -31
- package/Step4/Passengers/Display.tsx +3 -1
- package/Step4/Passengers/Passengers.tsx +5 -1
- package/Step4/Step4.tsx +13 -0
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this package are documented here. This project follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/) and [Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## [1.5.0] - 2026-07-25
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **Per-route passenger details in Step4.** The passenger form now renders
|
|
11
|
+
sex, passport, phone, and the new WhatsApp/Telegram contact fields ONLY
|
|
12
|
+
when the route's `trip.passenger_fields` requires them (then required);
|
|
13
|
+
name and date of birth stay always-mandatory. A return trip uses the
|
|
14
|
+
union of both legs' configs. Routes without a config keep the pre-feature
|
|
15
|
+
form (sex + passport). Request field suffixes: `_wa`, `_tg`.
|
|
16
|
+
|
|
17
|
+
## [1.4.2] - 2026-07-19
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `Found/Route/Route.tsx`: added a neutral inline SVG data-URI `LOGO_FALLBACK`.
|
|
22
|
+
The operator logo `<img>` now falls back to it when `logo_url` is empty
|
|
23
|
+
(`data.operator.logo_url || LOGO_FALLBACK`) and via an `onError` handler
|
|
24
|
+
(which nulls `onerror` first to avoid a loop), so a missing or broken operator
|
|
25
|
+
logo no longer renders the browser's broken-image icon in route results.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- `Found/Orders/Orders.tsx`: the not-found branch now triggers on
|
|
30
|
+
`data === undefined || data.length === 0` instead of only `data?.length == 0`.
|
|
31
|
+
An `undefined` result means the search errored (e.g. same origin and
|
|
32
|
+
destination), and it now shows the not-found message instead of an empty
|
|
33
|
+
shell.
|
|
34
|
+
- `Found/Booking/Booking.tsx`: the Booking.com logo `<img>` now has an
|
|
35
|
+
`alt="Booking.com"` attribute, giving the image an accessible name.
|
|
36
|
+
- `Found/Route/Route.tsx`: the operator logo `<img>` now has an `alt` attribute
|
|
37
|
+
set to the operator company name.
|
|
38
|
+
|
|
39
|
+
### Security
|
|
40
|
+
|
|
41
|
+
- `Found/Booking/Booking.tsx`: the `LinkBooking` external link
|
|
42
|
+
(`target="_blank"`) now includes `rel="noopener noreferrer"`, closing the
|
|
43
|
+
reverse-tabnabbing / `window.opener` leak.
|
|
44
|
+
- `Found/Route/Route.tsx`: the operator `LinkCompany` and route `LinkRoute`
|
|
45
|
+
external links (`target="_blank"`) now include `rel="noopener noreferrer"` for
|
|
46
|
+
the same tabnabbing protection.
|
|
47
|
+
|
|
48
|
+
Authored by Ferjolt Ozuni. Consolidated from the magus and alvavel whitelabel patch sets into canonical @autobusal source (eliminates per-repo patch-package divergence).
|
|
@@ -39,10 +39,10 @@ const Booking = ({ data, step1, t }: Props): (JSX.Element | null) => {
|
|
|
39
39
|
<Container>
|
|
40
40
|
<About>
|
|
41
41
|
{ t('routes_order.step2.booking.title', { location: destination.city.name }) }
|
|
42
|
-
<img src="https://s-ec.bstatic.com/static/img/b26logo/booking_logo_retina/22615963add19ac6b6d715a97c8d477e8b95b7ea.png" />
|
|
42
|
+
<img src="https://s-ec.bstatic.com/static/img/b26logo/booking_logo_retina/22615963add19ac6b6d715a97c8d477e8b95b7ea.png" alt="Booking.com" />
|
|
43
43
|
</About>
|
|
44
44
|
|
|
45
|
-
<LinkBooking to={ location } target="_blank">{ t('routes_order.step2.booking.search') }</LinkBooking>
|
|
45
|
+
<LinkBooking to={ location } target="_blank" rel="noopener noreferrer">{ t('routes_order.step2.booking.search') }</LinkBooking>
|
|
46
46
|
</Container>
|
|
47
47
|
);
|
|
48
48
|
};
|
package/Found/Orders/Orders.tsx
CHANGED
|
@@ -23,7 +23,9 @@ const Orders = ({ loading, data, type, preferredStop, t, onSave }: Props): JSX.E
|
|
|
23
23
|
);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
// undefined means the search errored (e.g. same origin and destination) -
|
|
27
|
+
// show the not-found message instead of an empty shell
|
|
28
|
+
if (data === undefined || data.length === 0) {
|
|
27
29
|
return (
|
|
28
30
|
<ContainerNotFound className="box">{ t('routes_order.step2.not_found') }</ContainerNotFound>
|
|
29
31
|
);
|
package/Found/Route/Route.tsx
CHANGED
|
@@ -6,6 +6,10 @@ import { Button, RouteFeature, Policy } from '@autobusal/common';
|
|
|
6
6
|
import { Container, SpecialOffer, FavoriteStop, Trip, Company, Location, Time, Price, LinkCompany, Details, Detail, ButtonPolicy, TitleDetail, LinkRoute, Features, TitleFeatures, FeaturesItems } from './styles';
|
|
7
7
|
import { FoundData } from '@autobusal/providers/types/routes';
|
|
8
8
|
|
|
9
|
+
// neutral placeholder so a missing/broken operator logo doesn't render the
|
|
10
|
+
// browser's broken-image icon in the route results
|
|
11
|
+
const LOGO_FALLBACK = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='60' viewBox='0 0 120 60'%3E%3Crect width='120' height='60' rx='6' fill='%23e5e7eb'/%3E%3Cpath d='M42 22h36v14a2 2 0 0 1-2 2h-2a3 3 0 0 1-6 0H52a3 3 0 0 1-6 0h-2a2 2 0 0 1-2-2z' fill='%239ca3af'/%3E%3C/svg%3E";
|
|
12
|
+
|
|
9
13
|
interface Props {
|
|
10
14
|
data: FoundData
|
|
11
15
|
type: 'favorite' | 'normal'
|
|
@@ -30,7 +34,7 @@ const Route = ({ data, type, t, onSave }: Props): JSX.Element => {
|
|
|
30
34
|
|
|
31
35
|
<Trip>
|
|
32
36
|
<Company>
|
|
33
|
-
<LinkCompany to={ data.operator.link } target="_blank" title={ data.operator.company }><img src={ data.operator.logo_url } /></LinkCompany>
|
|
37
|
+
<LinkCompany to={ data.operator.link } target="_blank" rel="noopener noreferrer" title={ data.operator.company }><img src={ data.operator.logo_url || LOGO_FALLBACK } onError={ (e) => { e.currentTarget.onerror = null; e.currentTarget.src = LOGO_FALLBACK; } } alt={ data.operator.company } /></LinkCompany>
|
|
34
38
|
</Company>
|
|
35
39
|
|
|
36
40
|
<Location>
|
|
@@ -69,7 +73,7 @@ const Route = ({ data, type, t, onSave }: Props): JSX.Element => {
|
|
|
69
73
|
<Details>
|
|
70
74
|
<Detail>
|
|
71
75
|
<TitleDetail>{ t('routes_order.step2.route.name') }</TitleDetail>
|
|
72
|
-
<LinkRoute to={ data.link } target="_blank">{ data.code }</LinkRoute>
|
|
76
|
+
<LinkRoute to={ data.link } target="_blank" rel="noopener noreferrer">{ data.code }</LinkRoute>
|
|
73
77
|
</Detail>
|
|
74
78
|
|
|
75
79
|
{ customs && (
|
|
@@ -10,6 +10,7 @@ interface Props {
|
|
|
10
10
|
type: 'adult' | 'child' | 'baby'
|
|
11
11
|
number: number
|
|
12
12
|
values?: PersonData
|
|
13
|
+
fields: string[]
|
|
13
14
|
withReturn: boolean
|
|
14
15
|
busFrom: OccupiedData
|
|
15
16
|
busTo: OccupiedData
|
|
@@ -22,7 +23,13 @@ interface Props {
|
|
|
22
23
|
onUpdate: (name: string, value: (string | number)) => void
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-25
|
|
27
|
+
// The route now decides which passenger details it collects
|
|
28
|
+
// (routes_trip.passenger_fields): sex, passport, phone, whatsapp and
|
|
29
|
+
// telegram render ONLY when the route requires them - and are then
|
|
30
|
+
// required. Name and date of birth stay mandatory always. `fields` is the
|
|
31
|
+
// union of both legs on a return trip (see Step4).
|
|
32
|
+
const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, pickedFrom, pickedTo, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element => {
|
|
26
33
|
const name = `${ type }${ number }`;
|
|
27
34
|
|
|
28
35
|
const names = {
|
|
@@ -32,10 +39,14 @@ const Passenger = ({ type, number, values, withReturn, busFrom, busTo, pickedFro
|
|
|
32
39
|
sex: `${ name }_sex`,
|
|
33
40
|
passport: `${ name }_pass`,
|
|
34
41
|
phone: `${ name }_phone`,
|
|
42
|
+
whatsapp: `${ name }_wa`,
|
|
43
|
+
telegram: `${ name }_tg`,
|
|
35
44
|
seatDeparture: `${ name }_sd`,
|
|
36
45
|
seatReturn: `${ name }_sr`
|
|
37
46
|
};
|
|
38
47
|
|
|
48
|
+
const has = (field: string): boolean => fields.includes(field);
|
|
49
|
+
|
|
39
50
|
return (
|
|
40
51
|
<Container className="box">
|
|
41
52
|
<Title>
|
|
@@ -85,45 +96,85 @@ const Passenger = ({ type, number, values, withReturn, busFrom, busTo, pickedFro
|
|
|
85
96
|
{ Display(errors[names.dob]) }
|
|
86
97
|
</div>
|
|
87
98
|
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
{ has('sex') && (
|
|
100
|
+
<div className="row">
|
|
101
|
+
{ t('routes_order.step4.gender') }
|
|
90
102
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
<Gender
|
|
104
|
+
name={ names.sex }
|
|
105
|
+
defaultValue={ values !== undefined ? Number(values[names.sex]) : undefined }
|
|
106
|
+
t={ t }
|
|
107
|
+
refs={ refs }
|
|
108
|
+
/>
|
|
97
109
|
|
|
98
|
-
|
|
99
|
-
|
|
110
|
+
{ Display(errors[names.sex]) }
|
|
111
|
+
</div>
|
|
112
|
+
) }
|
|
100
113
|
</div>
|
|
101
|
-
|
|
102
|
-
<div className="column">
|
|
103
|
-
<div className="row">
|
|
104
|
-
{ t('routes_order.step4.passport') }
|
|
105
114
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
115
|
+
{ (has('passport') || has('phone')) && (
|
|
116
|
+
<div className="column">
|
|
117
|
+
{ has('passport') && (
|
|
118
|
+
<div className="row">
|
|
119
|
+
{ t('routes_order.step4.passport') }
|
|
120
|
+
|
|
121
|
+
<input
|
|
122
|
+
type="text"
|
|
123
|
+
defaultValue={ values !== undefined ? values[names.passport] : '' }
|
|
124
|
+
{ ...refs(names.passport, Validate('required|min_length:2|max_length:50', t)) }
|
|
125
|
+
/>
|
|
126
|
+
|
|
127
|
+
{ Display(errors[names.passport]) }
|
|
128
|
+
</div>
|
|
129
|
+
) }
|
|
130
|
+
|
|
131
|
+
{ has('phone') && (
|
|
132
|
+
<div className="row">
|
|
133
|
+
{ t('routes_order.step4.phone') }
|
|
134
|
+
|
|
135
|
+
<input
|
|
136
|
+
type="text"
|
|
137
|
+
defaultValue={ values !== undefined ? values[names.phone] : '' }
|
|
138
|
+
{ ...refs(names.phone, Validate('required|min_length:5|max_length:50', t)) }
|
|
139
|
+
/>
|
|
111
140
|
|
|
112
|
-
|
|
141
|
+
{ Display(errors[names.phone]) }
|
|
142
|
+
</div>
|
|
143
|
+
) }
|
|
113
144
|
</div>
|
|
145
|
+
) }
|
|
114
146
|
|
|
115
|
-
|
|
116
|
-
|
|
147
|
+
{ (has('whatsapp') || has('telegram')) && (
|
|
148
|
+
<div className="column">
|
|
149
|
+
{ has('whatsapp') && (
|
|
150
|
+
<div className="row">
|
|
151
|
+
{ t('routes_order.step4.whatsapp') }
|
|
117
152
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
153
|
+
<input
|
|
154
|
+
type="text"
|
|
155
|
+
defaultValue={ values !== undefined ? values[names.whatsapp] : '' }
|
|
156
|
+
{ ...refs(names.whatsapp, Validate('required|min_length:5|max_length:50', t)) }
|
|
157
|
+
/>
|
|
158
|
+
|
|
159
|
+
{ Display(errors[names.whatsapp]) }
|
|
160
|
+
</div>
|
|
161
|
+
) }
|
|
162
|
+
|
|
163
|
+
{ has('telegram') && (
|
|
164
|
+
<div className="row">
|
|
165
|
+
{ t('routes_order.step4.telegram') }
|
|
123
166
|
|
|
124
|
-
|
|
167
|
+
<input
|
|
168
|
+
type="text"
|
|
169
|
+
defaultValue={ values !== undefined ? values[names.telegram] : '' }
|
|
170
|
+
{ ...refs(names.telegram, Validate('required|min_length:2|max_length:100', t)) }
|
|
171
|
+
/>
|
|
172
|
+
|
|
173
|
+
{ Display(errors[names.telegram]) }
|
|
174
|
+
</div>
|
|
175
|
+
) }
|
|
125
176
|
</div>
|
|
126
|
-
|
|
177
|
+
) }
|
|
127
178
|
|
|
128
179
|
{ type !== 'baby' && (
|
|
129
180
|
<div className="column">
|
|
@@ -170,4 +221,4 @@ const Passenger = ({ type, number, values, withReturn, busFrom, busTo, pickedFro
|
|
|
170
221
|
);
|
|
171
222
|
};
|
|
172
223
|
|
|
173
|
-
export default Passenger;
|
|
224
|
+
export default Passenger;
|
|
@@ -7,6 +7,7 @@ import { OccupiedData } from '@autobusal/providers/types/buses';
|
|
|
7
7
|
interface Props {
|
|
8
8
|
type: 'adult' | 'child' | 'baby'
|
|
9
9
|
amount: number
|
|
10
|
+
fields: string[]
|
|
10
11
|
withReturn: boolean
|
|
11
12
|
values?: PersonData
|
|
12
13
|
busFrom: OccupiedData
|
|
@@ -20,7 +21,7 @@ interface Props {
|
|
|
20
21
|
onUpdate: (name: string, value: (string | number)) => void
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
const Display = ({ type, amount, withReturn, values, busFrom, busTo, pickedFrom, pickedTo, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element[] => {
|
|
24
|
+
const Display = ({ type, amount, fields, withReturn, values, busFrom, busTo, pickedFrom, pickedTo, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element[] => {
|
|
24
25
|
const passengers: JSX.Element[] = [];
|
|
25
26
|
|
|
26
27
|
for (let i = 1; i <= amount; i++) {
|
|
@@ -29,6 +30,7 @@ const Display = ({ type, amount, withReturn, values, busFrom, busTo, pickedFrom,
|
|
|
29
30
|
key={ i }
|
|
30
31
|
type={ type }
|
|
31
32
|
number={ i }
|
|
33
|
+
fields={ fields }
|
|
32
34
|
withReturn={ withReturn }
|
|
33
35
|
values={ values }
|
|
34
36
|
busFrom={ busFrom }
|
|
@@ -10,6 +10,7 @@ import { OccupiedData } from '@autobusal/providers/types/buses';
|
|
|
10
10
|
interface Props {
|
|
11
11
|
values?: PersonData
|
|
12
12
|
step1: RoutesSearchForm
|
|
13
|
+
fields: string[]
|
|
13
14
|
busFrom: OccupiedData
|
|
14
15
|
busTo: OccupiedData
|
|
15
16
|
t: TFunction<'public'>
|
|
@@ -18,7 +19,7 @@ interface Props {
|
|
|
18
19
|
onUpdate: (name: string, value: (string | number)) => void
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
const Passengers = ({ values, step1, busFrom, busTo, t, errors, refs, onUpdate }: Props): JSX.Element => {
|
|
22
|
+
const Passengers = ({ values, step1, fields, busFrom, busTo, t, errors, refs, onUpdate }: Props): JSX.Element => {
|
|
22
23
|
const [ pickedFrom, setPickedFrom ] = useState<number[]>([]);
|
|
23
24
|
const [ pickedTo, setPickedTo ] = useState<number[]>([]);
|
|
24
25
|
|
|
@@ -45,6 +46,7 @@ const Passengers = ({ values, step1, busFrom, busTo, t, errors, refs, onUpdate }
|
|
|
45
46
|
<Display
|
|
46
47
|
type="adult"
|
|
47
48
|
amount={ step1.adults }
|
|
49
|
+
fields={ fields }
|
|
48
50
|
withReturn={ step1.type === 'return' }
|
|
49
51
|
values={ values }
|
|
50
52
|
busFrom={ busFrom }
|
|
@@ -61,6 +63,7 @@ const Passengers = ({ values, step1, busFrom, busTo, t, errors, refs, onUpdate }
|
|
|
61
63
|
<Display
|
|
62
64
|
type="child"
|
|
63
65
|
amount={ step1.children }
|
|
66
|
+
fields={ fields }
|
|
64
67
|
withReturn={ step1.type === 'return' }
|
|
65
68
|
values={ values }
|
|
66
69
|
busFrom={ busFrom }
|
|
@@ -77,6 +80,7 @@ const Passengers = ({ values, step1, busFrom, busTo, t, errors, refs, onUpdate }
|
|
|
77
80
|
<Display
|
|
78
81
|
type="baby"
|
|
79
82
|
amount={ step1.babies }
|
|
83
|
+
fields={ fields }
|
|
80
84
|
withReturn={ step1.type === 'return' }
|
|
81
85
|
values={ values }
|
|
82
86
|
busFrom={ busFrom }
|
package/Step4/Step4.tsx
CHANGED
|
@@ -25,9 +25,21 @@ interface Props {
|
|
|
25
25
|
onBack: (step: number) => void
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-25
|
|
29
|
+
// Routes without a passenger_fields config keep the pre-feature form
|
|
30
|
+
const DEFAULT_FIELDS = ['sex', 'passport'];
|
|
31
|
+
|
|
28
32
|
const Step4 = ({ values, step1, step2, step3, isPartner, t, onSave, onBack }: Props): JSX.Element => {
|
|
29
33
|
const { register, handleSubmit, formState: { errors }, setValue, setError } = useForm<PersonData>();
|
|
30
34
|
|
|
35
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-25
|
|
36
|
+
// one form serves both legs of a return trip, so it collects the UNION of
|
|
37
|
+
// what either route requires (mirrored by the backend rules)
|
|
38
|
+
const fields = Array.from(new Set([
|
|
39
|
+
...(step2.trip?.passenger_fields ?? DEFAULT_FIELDS),
|
|
40
|
+
...(step3 !== undefined ? (step3.trip?.passenger_fields ?? DEFAULT_FIELDS) : [])
|
|
41
|
+
]));
|
|
42
|
+
|
|
31
43
|
const busFrom = useBusOccupied(step1.departure, step2.id);
|
|
32
44
|
const busTo = useBusOccupied(step1._return, step3?.id);
|
|
33
45
|
|
|
@@ -63,6 +75,7 @@ const Step4 = ({ values, step1, step2, step3, isPartner, t, onSave, onBack }: Pr
|
|
|
63
75
|
<Passengers
|
|
64
76
|
values={ values }
|
|
65
77
|
step1={ step1 }
|
|
78
|
+
fields={ fields }
|
|
66
79
|
busFrom={ busFrom }
|
|
67
80
|
busTo={ busTo }
|
|
68
81
|
t={ t }
|