@autobusal/routes-search 1.3.2 → 1.4.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 +8 -0
- package/RoutesSearch.tsx +54 -4
- package/package.json +1 -1
- package/services.ts +35 -2
- package/utilities.ts +29 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.0 (2026-08-29)
|
|
4
|
+
|
|
5
|
+
- A departure city near the visitor pre-fills From (obtapi /api/geo/origin), applied once and only while the field is untouched; opt-in through the new `suggest` prop.
|
|
6
|
+
|
|
7
|
+
## 1.3.3 (2026-08-29)
|
|
8
|
+
|
|
9
|
+
- A city with a single station lists that station instead of an identical "All Stations" entry beside it; the submitted value stays the city's slug, so URLs are unchanged.
|
|
10
|
+
|
|
3
11
|
## 1.2.0
|
|
4
12
|
|
|
5
13
|
**The date picker follows the departure city.** Choosing a "from" city now sets the picker's floor to that city's timezone, and a pre-filled form (a shared link, or the search form kept on the results page) sets it on mount rather than waiting for a keystroke.
|
package/RoutesSearch.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
2
|
import { TFunction } from 'i18next';
|
|
3
3
|
import { useForm } from 'react-hook-form';
|
|
4
4
|
import { FaArrowUp, FaHistory } from 'react-icons/fa';
|
|
@@ -9,7 +9,7 @@ import { getCities } from './utilities';
|
|
|
9
9
|
import Picker from './Picker';
|
|
10
10
|
import { Options, Columns, Column, Item, Title, Action, PickerField } from './styles';
|
|
11
11
|
import { RoutesSearchForm } from './types';
|
|
12
|
-
import { useGetSuggestions } from './services';
|
|
12
|
+
import { useGetSuggestions, useSuggestedOrigin } from './services';
|
|
13
13
|
|
|
14
14
|
interface Props {
|
|
15
15
|
type: 'departure' | 'return'
|
|
@@ -20,11 +20,21 @@ interface Props {
|
|
|
20
20
|
adultsNo: number
|
|
21
21
|
childrenNo: number
|
|
22
22
|
babiesNo: number
|
|
23
|
+
/**
|
|
24
|
+
* Edited: Claude - Date: 2026-08-29
|
|
25
|
+
*
|
|
26
|
+
* Open "From" on a departure city near the visitor instead of the
|
|
27
|
+
* brand's configured default. Only for a FRESH form (the homepage, the
|
|
28
|
+
* bare /bus-lines page) - never for the collapsed form on a results
|
|
29
|
+
* page, where the pair on screen is the one being searched and must not
|
|
30
|
+
* be quietly rewritten under the visitor.
|
|
31
|
+
*/
|
|
32
|
+
suggest?: boolean
|
|
23
33
|
t: TFunction<'common'>
|
|
24
34
|
onSearch: (data: RoutesSearchForm) => void
|
|
25
35
|
}
|
|
26
36
|
|
|
27
|
-
const RoutesSearch = ({ type, from, to, departure, _return, adultsNo, childrenNo, babiesNo, t, onSearch }: Props): JSX.Element => {
|
|
37
|
+
const RoutesSearch = ({ type, from, to, departure, _return, adultsNo, childrenNo, babiesNo, suggest = false, t, onSearch }: Props): JSX.Element => {
|
|
28
38
|
const [ selected, setSelected ] = useState<'departure' | 'return'>(type);
|
|
29
39
|
|
|
30
40
|
// Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
@@ -54,6 +64,41 @@ const RoutesSearch = ({ type, from, to, departure, _return, adultsNo, childrenNo
|
|
|
54
64
|
|
|
55
65
|
const { data: CitiesData } = useGetSuggestions();
|
|
56
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Where the visitor most likely wants to leave from.
|
|
69
|
+
*
|
|
70
|
+
* Edited: Claude - Date: 2026-08-29
|
|
71
|
+
*
|
|
72
|
+
* Applied ONCE and only while the field is untouched: it is a better
|
|
73
|
+
* opening guess than the brand default, not a correction of anybody's
|
|
74
|
+
* typing. The remount-to-reseed trick is the map picker's, above -
|
|
75
|
+
* Autocomplete is uncontrolled, so changing its defaultValue needs a new
|
|
76
|
+
* key.
|
|
77
|
+
*/
|
|
78
|
+
const { data: suggestedOrigin } = useSuggestedOrigin(suggest);
|
|
79
|
+
|
|
80
|
+
const touchedFrom = useRef<boolean>(false);
|
|
81
|
+
const appliedSuggestion = useRef<boolean>(false);
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!suggest || appliedSuggestion.current || touchedFrom.current) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!suggestedOrigin || suggestedOrigin === from) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
appliedSuggestion.current = true;
|
|
93
|
+
|
|
94
|
+
setPicked(previous => ({ ...previous, from: suggestedOrigin }));
|
|
95
|
+
|
|
96
|
+
setValue('from', suggestedOrigin);
|
|
97
|
+
|
|
98
|
+
onDeparture(suggestedOrigin);
|
|
99
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
100
|
+
}, [ suggestedOrigin ]);
|
|
101
|
+
|
|
57
102
|
/**
|
|
58
103
|
* Tell the date picker which clock this journey starts on.
|
|
59
104
|
*
|
|
@@ -158,7 +203,12 @@ const RoutesSearch = ({ type, from, to, departure, _return, adultsNo, childrenNo
|
|
|
158
203
|
validation="required"
|
|
159
204
|
t={ t }
|
|
160
205
|
refs={ register }
|
|
161
|
-
onUpdate={ (_, value) => {
|
|
206
|
+
onUpdate={ (_, value) => {
|
|
207
|
+
touchedFrom.current = true;
|
|
208
|
+
|
|
209
|
+
setValue('from', value);
|
|
210
|
+
onDeparture(String(value));
|
|
211
|
+
} }
|
|
162
212
|
/>
|
|
163
213
|
|
|
164
214
|
<Picker t={ t } onPick={ (slug) => onPick('from', slug) } />
|
package/package.json
CHANGED
package/services.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useSuspenseQuery, UseSuspenseQueryResult } from '@tanstack/react-query';
|
|
1
|
+
import { useQuery, useSuspenseQuery, UseQueryResult, UseSuspenseQueryResult } from '@tanstack/react-query';
|
|
2
2
|
import { apiClient } from '@autobusal/providers';
|
|
3
3
|
import { CityData } from '@autobusal/providers/types/locations';
|
|
4
4
|
|
|
@@ -13,4 +13,37 @@ export const useGetSuggestions = (): UseSuspenseQueryResult<CityData[]> => (
|
|
|
13
13
|
))
|
|
14
14
|
)
|
|
15
15
|
})
|
|
16
|
-
);
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The departure city to open the form on, for THIS visitor.
|
|
20
|
+
*
|
|
21
|
+
* Claude - Date: 2026-08-29
|
|
22
|
+
*
|
|
23
|
+
* obtapi answers with the origin serving the most destinations in the
|
|
24
|
+
* visitor's own country - see Libraries\Geo\Origin, and note the
|
|
25
|
+
* precision is the COUNTRY: the geo database is country-level, so this is
|
|
26
|
+
* "somewhere you can actually leave from near you", not "your city".
|
|
27
|
+
*
|
|
28
|
+
* A plain useQuery, not a suspense one: the form must render immediately
|
|
29
|
+
* with the brand's default and improve on it when the answer arrives -
|
|
30
|
+
* suspending the whole search box on a geolocation lookup would be a far
|
|
31
|
+
* worse trade than a slightly-late pre-fill. Null (no database, a private
|
|
32
|
+
* address, a country we sell nothing from) simply leaves the default.
|
|
33
|
+
*/
|
|
34
|
+
export const useSuggestedOrigin = (enabled: boolean): UseQueryResult<string | null> => (
|
|
35
|
+
useQuery({
|
|
36
|
+
queryKey: ['search-suggested-origin'],
|
|
37
|
+
enabled,
|
|
38
|
+
// the answer only changes with the brand's inventory, and the visitor's
|
|
39
|
+
// country does not change mid-session
|
|
40
|
+
staleTime: 1000 * 60 * 30,
|
|
41
|
+
retry: false,
|
|
42
|
+
queryFn: async () => (
|
|
43
|
+
await apiClient
|
|
44
|
+
.get('/api/geo/origin')
|
|
45
|
+
.then(response => response?.data?.from ?? null)
|
|
46
|
+
.catch(() => null)
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
);
|
package/utilities.ts
CHANGED
|
@@ -27,13 +27,41 @@ export const getCities = (cities: CityData[], t: TFunction<'common'>): Autocompl
|
|
|
27
27
|
city.slug
|
|
28
28
|
].filter(Boolean);
|
|
29
29
|
|
|
30
|
+
const stops = city.stops ?? [];
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* Edited: Claude - Date: 2026-08-29
|
|
34
|
+
*
|
|
35
|
+
* A CITY WITH ONE STATION OFFERS NO CHOICE. Listing both "(All
|
|
36
|
+
* Stations)" and the station itself puts two entries in the box for
|
|
37
|
+
* one place, and the visitor has to read both to discover they are the
|
|
38
|
+
* same - so the station is named and the pair collapses to a single
|
|
39
|
+
* line.
|
|
40
|
+
*
|
|
41
|
+
* The value stays the CITY's, not the station's: with one station the
|
|
42
|
+
* search is identical either way, and keeping the city slug keeps the
|
|
43
|
+
* URL (and everything keyed off it - canonicals, the prerendered pair
|
|
44
|
+
* pages, the date strip) exactly as it is. The day a second station
|
|
45
|
+
* appears, this reverts to "All Stations" plus one entry each, on its
|
|
46
|
+
* own.
|
|
47
|
+
*/
|
|
48
|
+
if (stops.length === 1) {
|
|
49
|
+
data.push({
|
|
50
|
+
name: `${ city.name }, ${ city.country?.name ?? '' } (${ stops[0].name })`,
|
|
51
|
+
value: `${ city.slug }`,
|
|
52
|
+
search: [ ...search, stops[0].name ]
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
30
58
|
data.push({
|
|
31
59
|
name: `${ city.name }, ${ city.country?.name ?? '' } (${ t('all_stations', { ns: 'common' }) })`,
|
|
32
60
|
value: `${ city.slug }`,
|
|
33
61
|
search
|
|
34
62
|
});
|
|
35
63
|
|
|
36
|
-
|
|
64
|
+
stops.forEach(stop => {
|
|
37
65
|
data.push({
|
|
38
66
|
name: `${ city.name }, ${ city.country?.name ?? '' } (${ stop.name })`,
|
|
39
67
|
value: `${ city.slug }:${ stop.id }`,
|