@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.
@@ -0,0 +1,109 @@
1
+ import { TFunction } from 'i18next';
2
+ import { Container, Headline, Title, Lead, Group, GroupTitle, Options, Option, OptionName, OptionMeta, OptionPrice } from './styles';
3
+ import { RoutesSearchForm } from '@autobusal/routes-search/types';
4
+ import { AlternativesData, NearbyCity, FoundDay } from '../../types';
5
+
6
+ interface Props {
7
+ step1: RoutesSearchForm
8
+ data?: AlternativesData
9
+ loading: boolean
10
+ t: TFunction<'common'>
11
+ }
12
+
13
+ /**
14
+ * A search that found nothing, answered rather than apologised for.
15
+ *
16
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
17
+ *
18
+ * "We couldn't find any tickets" is where people leave. Every suggestion
19
+ * here is a real search that obtapi has already confirmed returns seats on
20
+ * the requested date, with the fare it would cost - so a click is never a
21
+ * second dead end.
22
+ *
23
+ * Nearby cities are ordered by actual distance from the city that was
24
+ * asked for, which is why a 29km alternative is offered before a 73km one
25
+ * even when the further city is cheaper: the passenger's problem here is
26
+ * getting to the bus, not saving two euros.
27
+ */
28
+ const Empty = ({ step1, data, loading, t }: Props): JSX.Element => {
29
+ // obtapi wants DD/MM/YYYY, the URL carries DD-MM-YYYY
30
+ const asPath = (date: string): string => date.replace(/\//g, '-');
31
+
32
+ const link = (from: string, to: string, departure: string): string => ([
33
+ '/bus-lines',
34
+ from,
35
+ to,
36
+ asPath(departure),
37
+ step1._return ? asPath(step1._return) : 'none',
38
+ step1.type ?? 'departure',
39
+ String(step1.adults),
40
+ String(step1.children),
41
+ String(step1.babies)
42
+ ].join('/'));
43
+
44
+ const city = (item: NearbyCity, side: 'from' | 'to'): JSX.Element => (
45
+ <Option
46
+ key={ item.city.id }
47
+ reloadDocument
48
+ to={ side === 'from'
49
+ ? link(item.city.slug, step1.to, step1.departure)
50
+ : link(step1.from, item.city.slug, step1.departure) }
51
+ >
52
+ <OptionName title={ item.city.name }>{ item.city.name }</OptionName>
53
+ <OptionMeta>{ t('routes_order.step2.empty.distance', { km: item.distance }) }</OptionMeta>
54
+ <OptionPrice>{ item.from_price_display }</OptionPrice>
55
+ </Option>
56
+ );
57
+
58
+ const day = (item: FoundDay): JSX.Element => (
59
+ <Option key={ item.id } reloadDocument to={ link(step1.from, step1.to, item.day) }>
60
+ <OptionName>{ item.day }</OptionName>
61
+ <OptionMeta>{ t('routes_order.step2.empty.available') }</OptionMeta>
62
+ { item.from_price_display && <OptionPrice>{ item.from_price_display }</OptionPrice> }
63
+ </Option>
64
+ );
65
+
66
+ const hasFrom = (data?.from.length ?? 0) > 0;
67
+ const hasTo = (data?.to.length ?? 0) > 0;
68
+ const hasDates = (data?.dates.length ?? 0) > 0;
69
+
70
+ return (
71
+ <Container className="box">
72
+ <Headline>
73
+ <Title>{ t('routes_order.step2.empty.title') }</Title>
74
+
75
+ { /* while the alternatives are still being fetched, say nothing
76
+ about them - promising suggestions that then turn out not to
77
+ exist is worse than the plain message */ }
78
+ <Lead>
79
+ { loading || hasFrom || hasTo || hasDates
80
+ ? t('routes_order.step2.empty.lead')
81
+ : t('routes_order.step2.empty.lead_none') }
82
+ </Lead>
83
+ </Headline>
84
+
85
+ { hasDates && (
86
+ <Group>
87
+ <GroupTitle>{ t('routes_order.step2.empty.dates') }</GroupTitle>
88
+ <Options>{ data?.dates.map(day) }</Options>
89
+ </Group>
90
+ ) }
91
+
92
+ { hasFrom && (
93
+ <Group>
94
+ <GroupTitle>{ t('routes_order.step2.empty.from') }</GroupTitle>
95
+ <Options>{ data?.from.map(item => city(item, 'from')) }</Options>
96
+ </Group>
97
+ ) }
98
+
99
+ { hasTo && (
100
+ <Group>
101
+ <GroupTitle>{ t('routes_order.step2.empty.to') }</GroupTitle>
102
+ <Options>{ data?.to.map(item => city(item, 'to')) }</Options>
103
+ </Group>
104
+ ) }
105
+ </Container>
106
+ );
107
+ };
108
+
109
+ export default Empty;
@@ -0,0 +1,99 @@
1
+ import styled from 'styled-components';
2
+ import { Link } from 'react-router-dom';
3
+
4
+ export const Container = styled.div`
5
+ display: flex;
6
+ flex-direction: column;
7
+ gap: 20px;
8
+ `;
9
+
10
+ export const Headline = styled.div`
11
+ text-align: center;
12
+ `;
13
+
14
+ export const Title = styled.h3`
15
+ margin-bottom: 6px;
16
+ `;
17
+
18
+ export const Lead = styled.p`
19
+ margin: 0;
20
+ color: ${ props => props.theme.font.faded };
21
+ `;
22
+
23
+ export const Group = styled.div`
24
+ display: flex;
25
+ flex-direction: column;
26
+ gap: 10px;
27
+ `;
28
+
29
+ export const GroupTitle = styled.h6`
30
+ margin-bottom: 0;
31
+ font-size: ${ props => props.theme.size.xs };
32
+ text-transform: uppercase;
33
+ color: ${ props => props.theme.font.faded };
34
+ `;
35
+
36
+ export const Options = styled.div`
37
+ display: grid;
38
+ gap: 10px;
39
+ grid-template-columns: minmax(0, 1fr);
40
+
41
+ @media (min-width: 640px) {
42
+ grid-template-columns: repeat(3, minmax(0, 1fr));
43
+ }
44
+ `;
45
+
46
+ /**
47
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
48
+ *
49
+ * These links carry `reloadDocument` at the call site. RoutesOrder holds
50
+ * the search in component state and Step1 only kicks off a search when it
51
+ * has not already redirected, so a client-side navigation changes the URL
52
+ * and nothing else - the old, empty result set just stays on screen.
53
+ *
54
+ * A full load re-enters the flow from the URL exactly as arriving from a
55
+ * search engine does. Teaching RoutesOrder to resync its state from
56
+ * changing params would be the tidier fix, but that is the main booking
57
+ * flow and this is a dead-end page: not worth the risk for the gain.
58
+ */
59
+ export const Option = styled(Link)`
60
+ display: flex;
61
+ flex-direction: column;
62
+ gap: 3px;
63
+ padding: 12px 15px;
64
+ border-radius: ${ props => props.theme.borderRadius };
65
+ border: 1px solid ${ props => props.theme.inputs.border };
66
+ background: ${ props => props.theme.background.normal };
67
+ transition: all 0.2s ease;
68
+
69
+ // Edited: Ferjolt Ozuni - Date: 2026-08-01
70
+ // These are cards, not sentences, so they lift and take the brand colour
71
+ // on hover rather than underlining. The global anchor rule underlines on
72
+ // hover, hence the explicit none.
73
+ &:hover {
74
+ text-decoration: none;
75
+ transform: translateY(-2px);
76
+ border-color: ${ props => props.theme.primary.normal };
77
+ box-shadow: ${ props => props.theme.boxShadow };
78
+ }
79
+
80
+ &:hover strong {
81
+ color: ${ props => props.theme.primary.normal };
82
+ }
83
+ `;
84
+
85
+ export const OptionName = styled.strong`
86
+ overflow: hidden;
87
+ text-overflow: ellipsis;
88
+ white-space: nowrap;
89
+ `;
90
+
91
+ export const OptionMeta = styled.span`
92
+ font-size: ${ props => props.theme.size.xs };
93
+ color: ${ props => props.theme.font.faded };
94
+ `;
95
+
96
+ export const OptionPrice = styled.span`
97
+ font-weight: 700;
98
+ font-size: ${ props => props.theme.size.s };
99
+ `;
@@ -1,7 +1,7 @@
1
1
  import { TFunction } from 'i18next';
2
2
  import { FaFilter } from 'react-icons/fa';
3
3
  import { AiOutlineCaretDown, AiOutlineCaretUp } from 'react-icons/ai';
4
- import { Container, Toggle, Count, Panel, Group, GroupTitle, Choice, ChoiceName, ChoiceMeta, Slider, Reset } from './styles';
4
+ import { Container, Toggle, Count, Panel, Group, GroupTitle, Choice, ChoiceName, ChoiceMeta, ChoiceCount, ChoicePrice, Slider, Reset } from './styles';
5
5
  import { Refinement, Facets, FacetValue, BucketKey, EMPTY, isRefined } from '../refine';
6
6
 
7
7
  interface Props {
@@ -83,7 +83,8 @@ const Filters = ({ open, refinement, facets, range, currency, t, onToggle, onCha
83
83
  <ChoiceName title={ item.label }>{ item.label }</ChoiceName>
84
84
 
85
85
  <ChoiceMeta>
86
- { item.count }{ item.fromDisplay ? ` · ${ item.fromDisplay }` : '' }
86
+ <ChoiceCount>{ item.count }</ChoiceCount>
87
+ { item.fromDisplay && <ChoicePrice>{ item.fromDisplay }</ChoicePrice> }
87
88
  </ChoiceMeta>
88
89
  </Choice>
89
90
  )) }
@@ -100,8 +101,10 @@ const Filters = ({ open, refinement, facets, range, currency, t, onToggle, onCha
100
101
  { open ? <AiOutlineCaretUp /> : <AiOutlineCaretDown /> }
101
102
  </Toggle>
102
103
 
103
- { open && (
104
- <Panel className="box">
104
+ { /* Edited: Ferjolt Ozuni - Date: 2026-08-01
105
+ Rendered unconditionally and hidden with CSS rather than gated on
106
+ `open`, because at sidebar widths there is no toggle to open it. */ }
107
+ <Panel className="box" $open={ open }>
105
108
  { facets.buckets.length > 1 && (
106
109
  <Group>
107
110
  <GroupTitle>{ t('routes_order.step2.filters.times') }</GroupTitle>
@@ -118,7 +121,8 @@ const Filters = ({ open, refinement, facets, range, currency, t, onToggle, onCha
118
121
  <ChoiceName>{ bucket.label }</ChoiceName>
119
122
 
120
123
  <ChoiceMeta>
121
- { bucket.count }{ bucket.fromDisplay ? ` · ${ bucket.fromDisplay }` : '' }
124
+ <ChoiceCount>{ bucket.count }</ChoiceCount>
125
+ { bucket.fromDisplay && <ChoicePrice>{ bucket.fromDisplay }</ChoicePrice> }
122
126
  </ChoiceMeta>
123
127
  </Choice>
124
128
  )) }
@@ -182,8 +186,7 @@ const Filters = ({ open, refinement, facets, range, currency, t, onToggle, onCha
182
186
  </Reset>
183
187
  </Group>
184
188
  ) }
185
- </Panel>
186
- ) }
189
+ </Panel>
187
190
  </Container>
188
191
  );
189
192
  };
@@ -1,7 +1,21 @@
1
1
  import styled, { css } from 'styled-components';
2
2
 
3
+ /**
4
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
5
+ *
6
+ * Below 1280px the filters stay a collapsible panel above the list; at and
7
+ * above it they become a permanent sidebar. 1280 rather than 1024 on
8
+ * purpose: the result CARDS switch to their wide row layout at 1024px based
9
+ * on the viewport, not on their container, so carving a 280px column out of
10
+ * a 1024px window would leave the cards using a layout their remaining
11
+ * ~700px cannot carry.
12
+ */
3
13
  export const Container = styled.div`
4
14
  margin-bottom: 15px;
15
+
16
+ @media (min-width: 1280px) {
17
+ margin-bottom: 0;
18
+ }
5
19
  `;
6
20
 
7
21
  export const Toggle = styled.button<{ $open: boolean }>`
@@ -24,6 +38,11 @@ export const Toggle = styled.button<{ $open: boolean }>`
24
38
  ${ props => props.$open && css`
25
39
  border-color: ${ props => props.theme.primary.normal };
26
40
  ` }
41
+
42
+ // the sidebar is always open, so there is nothing left to toggle
43
+ @media (min-width: 1280px) {
44
+ display: none;
45
+ }
27
46
  `;
28
47
 
29
48
  export const Count = styled.span`
@@ -35,18 +54,35 @@ export const Count = styled.span`
35
54
  border-radius: 100px;
36
55
  `;
37
56
 
38
- export const Panel = styled.div`
39
- display: grid;
57
+ export const Panel = styled.div<{ $open: boolean }>`
58
+ display: ${ props => props.$open ? 'grid' : 'none' };
40
59
  gap: 20px;
41
60
  margin-top: 12px;
42
61
  padding: 20px;
62
+ // minmax(0, 1fr) rather than 1fr: a grid track's implicit min-width is
63
+ // auto, so a long stop name like "Terminali Lindor Interurban dhe
64
+ // Nderkombetar i Autobusave (TEG)" pushed the whole panel wider than its
65
+ // column and pushed the counts and prices out of sight
66
+ grid-template-columns: minmax(0, 1fr);
43
67
 
44
68
  @media (min-width: 640px) {
45
- grid-template-columns: repeat(2, 1fr);
69
+ grid-template-columns: repeat(2, minmax(0, 1fr));
46
70
  }
47
71
 
48
72
  @media (min-width: 1024px) {
49
- grid-template-columns: repeat(3, 1fr);
73
+ grid-template-columns: repeat(3, minmax(0, 1fr));
74
+ }
75
+
76
+ // as a sidebar: always rendered, one column, and it follows the page down
77
+ // so the filters stay reachable while scrolling a long result list
78
+ @media (min-width: 1280px) {
79
+ display: grid;
80
+ grid-template-columns: minmax(0, 1fr);
81
+ margin-top: 0;
82
+ position: sticky;
83
+ top: 15px;
84
+ max-height: calc(100vh - 30px);
85
+ overflow-y: auto;
50
86
  }
51
87
  `;
52
88
 
@@ -54,6 +90,7 @@ export const Group = styled.div`
54
90
  display: flex;
55
91
  flex-direction: column;
56
92
  gap: 8px;
93
+ min-width: 0;
57
94
  `;
58
95
 
59
96
  export const GroupTitle = styled.h6`
@@ -65,8 +102,9 @@ export const GroupTitle = styled.h6`
65
102
 
66
103
  export const Choice = styled.label<{ $disabled: boolean }>`
67
104
  display: flex;
68
- align-items: center;
105
+ align-items: flex-start;
69
106
  gap: 8px;
107
+ line-height: 1.35;
70
108
  font-size: ${ props => props.theme.size.s };
71
109
  cursor: pointer;
72
110
 
@@ -85,19 +123,52 @@ export const Choice = styled.label<{ $disabled: boolean }>`
85
123
  ` }
86
124
  `;
87
125
 
126
+ /**
127
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
128
+ *
129
+ * Wraps rather than truncating. In the sidebar column an ellipsis turned
130
+ * "Terminali Lindor Interurban dhe Nderkombetar i Autobusave (TEG)" into
131
+ * "Terminali Lindor Inte..." - two stops at the same city are then
132
+ * indistinguishable, which is the one thing this filter exists to tell
133
+ * apart. A second line is cheaper than a useless label.
134
+ */
88
135
  export const ChoiceName = styled.span`
89
136
  flex: 1;
90
- overflow: hidden;
91
- text-overflow: ellipsis;
92
- white-space: nowrap;
137
+ min-width: 0;
138
+ overflow-wrap: anywhere;
93
139
  `;
94
140
 
95
141
  export const ChoiceMeta = styled.span`
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 6px;
96
145
  font-size: ${ props => props.theme.size.xs };
97
146
  color: ${ props => props.theme.font.faded };
98
147
  white-space: nowrap;
99
148
  `;
100
149
 
150
+ /**
151
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
152
+ *
153
+ * How many trips a facet value leaves, as a filled pill. It used to sit
154
+ * next to the price as "5 · 38.00 EUR" in one muted string, where the two
155
+ * numbers read as one and you had to work out which was which. A count and
156
+ * a fare are different kinds of thing and now look like it.
157
+ */
158
+ export const ChoiceCount = styled.span`
159
+ min-width: 22px;
160
+ padding: 1px 6px;
161
+ text-align: center;
162
+ font-weight: 700;
163
+ color: ${ props => props.theme.font.normal };
164
+ background: ${ props => props.theme.background.neutral };
165
+ border-radius: 100px;
166
+ `;
167
+
168
+ export const ChoicePrice = styled.span`
169
+ color: ${ props => props.theme.font.faded };
170
+ `;
171
+
101
172
  export const Slider = styled.input`
102
173
  width: 100%;
103
174
  accent-color: ${ props => props.theme.primary.normal };
package/Found/Found.tsx CHANGED
@@ -9,6 +9,8 @@ import Sort from './Sort/Sort';
9
9
  import Picks from './Picks/Picks';
10
10
  import Filters from './Filters/Filters';
11
11
  import Summary from './Summary/Summary';
12
+ import { Results, Sidebar, Listing } from '../styles';
13
+ import { useGetAlternatives } from '../services';
12
14
  import { RoutesSearchForm } from '@autobusal/routes-search/types';
13
15
  import { FoundData } from '@autobusal/providers/types/routes';
14
16
  import { Refinement, BucketKey, fromParams, toParams, filterItems, buildFacets, priceRange, currencyOf, isRefined } from './refine';
@@ -72,6 +74,14 @@ const Found = ({ type, loading, data, step1, preferredStop, t, onSearch, onSave
72
74
 
73
75
  const passengers = Number(step1.adults ?? 0) + Number(step1.children ?? 0) + Number(step1.babies ?? 0);
74
76
 
77
+ // Edited: Ferjolt Ozuni - Date: 2026-08-01
78
+ // Only fetched once the search itself has finished and come back empty,
79
+ // so a search that found something never pays for the alternatives.
80
+ const { data: alternatives, isFetching: alternativesLoading } = useGetAlternatives(
81
+ step1,
82
+ !loading && items.length === 0
83
+ );
84
+
75
85
  // controls are pointless while there is nothing (or only one thing) to
76
86
  // refine, and they'd only add noise above an empty state
77
87
  const showControls = !loading && items.length > 1;
@@ -88,17 +98,6 @@ const Found = ({ type, loading, data, step1, preferredStop, t, onSearch, onSave
88
98
  <Sort value={ refinement.sort } t={ t } onChange={ sort => onRefine({ ...refinement, sort }) } />
89
99
  </Summary>
90
100
 
91
- <Filters
92
- open={ open }
93
- refinement={ refinement }
94
- facets={ facets }
95
- range={ bounds }
96
- currency={ currency }
97
- t={ t }
98
- onToggle={ () => setOpen(!open) }
99
- onChange={ onRefine }
100
- />
101
-
102
101
  <Picks
103
102
  items={ refined }
104
103
  sort={ refinement.sort }
@@ -108,21 +107,43 @@ const Found = ({ type, loading, data, step1, preferredStop, t, onSearch, onSave
108
107
  </>
109
108
  ) }
110
109
 
111
- <Header t={ t } />
112
-
113
- <Orders
114
- loading={ loading }
115
- data={ refined }
116
- empty={ items.length === 0 }
117
- refined={ isRefined(refinement) }
118
- sort={ refinement.sort }
119
- type={ type }
120
- passengers={ passengers }
121
- preferredStop={ Number(preferredStop) }
122
- t={ t }
123
- onReset={ () => onRefine({ ...refinement, ...{ operators: [], buckets: [], direct: false, fromStops: [], toStops: [], features: [], maxPrice: null } }) }
124
- onSave={ onSave }
125
- />
110
+ <Results>
111
+ { showControls && (
112
+ <Sidebar>
113
+ <Filters
114
+ open={ open }
115
+ refinement={ refinement }
116
+ facets={ facets }
117
+ range={ bounds }
118
+ currency={ currency }
119
+ t={ t }
120
+ onToggle={ () => setOpen(!open) }
121
+ onChange={ onRefine }
122
+ />
123
+ </Sidebar>
124
+ ) }
125
+
126
+ <Listing>
127
+ <Header t={ t } />
128
+
129
+ <Orders
130
+ loading={ loading }
131
+ data={ refined }
132
+ empty={ items.length === 0 }
133
+ step1={ step1 }
134
+ alternatives={ alternatives }
135
+ alternativesLoading={ alternativesLoading }
136
+ refined={ isRefined(refinement) }
137
+ sort={ refinement.sort }
138
+ type={ type }
139
+ passengers={ passengers }
140
+ preferredStop={ Number(preferredStop) }
141
+ t={ t }
142
+ onReset={ () => onRefine({ ...refinement, ...{ operators: [], buckets: [], direct: false, fromStops: [], toStops: [], features: [], maxPrice: null } }) }
143
+ onSave={ onSave }
144
+ />
145
+ </Listing>
146
+ </Results>
126
147
  </>
127
148
  );
128
149
  };
@@ -1,8 +1,21 @@
1
1
  import styled from 'styled-components';
2
2
 
3
+ /**
4
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
5
+ *
6
+ * This row only means anything if its labels sit over the columns they
7
+ * name, so its geometry has to match the result card's exactly: the same
8
+ * 15px horizontal padding and the same 15px gap between columns. It had
9
+ * 4px padding and no gap at all, which pushed every label right of the
10
+ * data it labelled - Duration was out by 34px.
11
+ *
12
+ * If Found/Route/styles.ts ever changes the card's padding or the Trip
13
+ * gap, this has to move with it.
14
+ */
3
15
  export const Container = styled.div`
4
16
  display: none;
5
- padding: 8px 4px;
17
+ gap: 15px;
18
+ padding: 8px 15px;
6
19
  margin-bottom: 15px;
7
20
 
8
21
  @media (min-width: 1024px) {
@@ -23,18 +36,21 @@ const Item = styled.div`
23
36
  }
24
37
  `;
25
38
 
39
+ // these five must stay identical to their counterparts in
40
+ // Found/Route/styles.ts, or the labels stop sitting over their columns
26
41
  export const Company = styled(Item)`
27
- flex-basis: 185px;
42
+ flex: 0 0 185px;
28
43
  `;
29
44
 
30
45
  export const Location = styled(Item)`
31
46
  flex: 1;
47
+ min-width: 0;
32
48
  `;
33
49
 
34
50
  export const Time = styled(Item)`
35
- flex-basis: 120px;
51
+ flex: 0 0 120px;
36
52
  `;
37
53
 
38
54
  export const Price = styled(Item)`
39
- flex-basis: 165px;
55
+ flex: 0 0 165px;
40
56
  `;
@@ -2,10 +2,13 @@ import { TFunction } from 'i18next';
2
2
  import { Inline } from '@autobusal/common';
3
3
  import Route from '../Route/Route';
4
4
  import Group from '../Group/Group';
5
+ import Empty from '../Empty/Empty';
5
6
  import { getFavorites, getNormal } from './utilities';
6
7
  import { sortItems, groupItems, SortKey } from '../refine';
7
8
  import { Container, ContainerNotFound, Reset } from './styles';
8
9
  import { FoundData } from '@autobusal/providers/types/routes';
10
+ import { RoutesSearchForm } from '@autobusal/routes-search/types';
11
+ import { AlternativesData } from '../../types';
9
12
 
10
13
  interface Props {
11
14
  loading: boolean
@@ -16,12 +19,15 @@ interface Props {
16
19
  type: 'departure' | '_return'
17
20
  passengers: number
18
21
  preferredStop?: number
22
+ step1: RoutesSearchForm
23
+ alternatives?: AlternativesData
24
+ alternativesLoading: boolean
19
25
  t: TFunction<'common'>
20
26
  onReset: () => void
21
27
  onSave: (data: FoundData) => void
22
28
  }
23
29
 
24
- const Orders = ({ loading, data, empty, refined, sort, type, passengers, preferredStop, t, onReset, onSave }: Props): JSX.Element => {
30
+ const Orders = ({ loading, data, empty, refined, sort, type, passengers, preferredStop, step1, alternatives, alternativesLoading, t, onReset, onSave }: Props): JSX.Element => {
25
31
  if (loading) {
26
32
  return (
27
33
  <div className="box">
@@ -30,11 +36,14 @@ const Orders = ({ loading, data, empty, refined, sort, type, passengers, preferr
30
36
  );
31
37
  }
32
38
 
33
- // no results at all - the search itself found nothing (or errored, e.g.
34
- // same origin and destination) - show the not-found message
39
+ // Edited: Ferjolt Ozuni - Date: 2026-08-01
40
+ // No results at all - the search itself found nothing (or errored, e.g.
41
+ // same origin and destination). Rather than a bare apology, offer the
42
+ // nearby origins, nearby destinations and other dates that obtapi has
43
+ // confirmed actually have seats.
35
44
  if (empty) {
36
45
  return (
37
- <ContainerNotFound className="box">{ t('routes_order.step2.not_found') }</ContainerNotFound>
46
+ <Empty step1={ step1 } data={ alternatives } loading={ alternativesLoading } t={ t } />
38
47
  );
39
48
  }
40
49