@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 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
  };
@@ -23,7 +23,9 @@ const Orders = ({ loading, data, type, preferredStop, t, onSave }: Props): JSX.E
23
23
  );
24
24
  }
25
25
 
26
- if (data?.length == 0) {
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
  );
@@ -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
- const Passenger = ({ type, number, values, withReturn, busFrom, busTo, pickedFrom, pickedTo, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element => {
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
- <div className="row">
89
- { t('routes_order.step4.gender') }
99
+ { has('sex') && (
100
+ <div className="row">
101
+ { t('routes_order.step4.gender') }
90
102
 
91
- <Gender
92
- name={ names.sex }
93
- defaultValue={ values !== undefined ? Number(values[names.sex]) : undefined }
94
- t={ t }
95
- refs={ refs }
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
- { Display(errors[names.sex]) }
99
- </div>
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
- <input
107
- type="text"
108
- defaultValue={ values !== undefined ? values[names.passport] : '' }
109
- { ...refs(names.passport, Validate('required|min_length:2|max_length:50', t)) }
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
- { Display(errors[names.passport]) }
141
+ { Display(errors[names.phone]) }
142
+ </div>
143
+ ) }
113
144
  </div>
145
+ ) }
114
146
 
115
- <div className="row">
116
- { t('routes_order.step4.phone') }
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
- <input
119
- type="text"
120
- defaultValue={ values !== undefined ? values[names.phone] : '' }
121
- { ...refs(names.phone) }
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
- { Display(errors[names.phone]) }
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
- </div>
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 }
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@autobusal/routes-order",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
+ "author": "Ferjolt Ozuni",
4
5
  "type": "module",
5
6
  "main": "index.ts"
6
7
  }