@autobusal/routes-search 1.3.3 → 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 CHANGED
@@ -1,5 +1,9 @@
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
+
3
7
  ## 1.3.3 (2026-08-29)
4
8
 
5
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.
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) => { setValue('from', value); onDeparture(String(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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/routes-search",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"
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
+ );