@autobusal/routes-order 1.9.0 → 1.9.3
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 +54 -0
- package/Found/Filters/Filters.tsx +10 -7
- package/Found/Filters/styles.ts +79 -8
- package/Found/Found.tsx +35 -26
- package/Found/Route/Route.tsx +23 -6
- package/Found/Route/styles.ts +48 -8
- package/Found/Summary/Summary.tsx +1 -1
- package/Found/Summary/styles.ts +12 -0
- package/Found/refine.ts +43 -0
- package/package.json +1 -1
- package/styles.ts +36 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,60 @@ All notable changes to this package are documented here. This project follows
|
|
|
6
6
|
> Note: 1.8.0 was published without a changelog entry. The gap is left as-is
|
|
7
7
|
> rather than reconstructed after the fact.
|
|
8
8
|
|
|
9
|
+
## [1.9.3] - 2026-08-01
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **Trip length reads as a duration, not a clock time.** "08:30" sat
|
|
14
|
+
directly between two real departure times with nothing to say it meant
|
|
15
|
+
eight and a half hours rather than half past eight. It now renders as
|
|
16
|
+
`07h 30m` behind an hourglass, and an exact number of hours drops the
|
|
17
|
+
minutes (`08h`, not `08h 00m`). The unit labels come from the translation
|
|
18
|
+
files - "h"/"m" are a Latin-script convention, so Greek reads `07ω 30λ`,
|
|
19
|
+
Bulgarian and Macedonian `07ч 30м`, Albanian `07o 30m`.
|
|
20
|
+
obtapi's `-` sentinel (no computable duration) still passes straight
|
|
21
|
+
through rather than becoming a confident "0h 0m".
|
|
22
|
+
- **Stop name down to 12px** on the result cards.
|
|
23
|
+
|
|
24
|
+
## [1.9.2] - 2026-08-01
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **Stop name stepped down to 14px** on the result cards. At the same 16px
|
|
29
|
+
as body copy it competed with the city name above it; the city should be
|
|
30
|
+
read first and the stop only when it is needed.
|
|
31
|
+
- **Country code renders upper case and smaller.** It arrives from the
|
|
32
|
+
database lower case ("alb", "grc") - an ISO country code is upper case by
|
|
33
|
+
definition, so it is cased at render time rather than left to the data,
|
|
34
|
+
where every consumer would have to remember.
|
|
35
|
+
|
|
36
|
+
## [1.9.1] - 2026-08-01
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- **Filters became a sidebar** at 1280px and up - always visible, sticky
|
|
41
|
+
while the list scrolls, no click needed to discover them. Below 1280px
|
|
42
|
+
they stay the collapsible panel above the list. The breakpoint is 1280
|
|
43
|
+
rather than 1024 because the result cards switch to their wide row layout
|
|
44
|
+
at 1024px on VIEWPORT width, not container width, so taking a column out
|
|
45
|
+
of a 1024px window would leave the cards using a layout their remaining
|
|
46
|
+
space cannot carry.
|
|
47
|
+
- **Facet counts are a filled pill**, separate from the from-price. They
|
|
48
|
+
used to read as one muted string ("5 · 38.00 EUR") where the two numbers
|
|
49
|
+
ran together and it was not obvious which was which.
|
|
50
|
+
- **Facet labels wrap instead of truncating.** In a sidebar column an
|
|
51
|
+
ellipsis reduced two different stops at the same city to the same
|
|
52
|
+
"Terminali Lindor Inte..." - the one distinction the filter exists to
|
|
53
|
+
make.
|
|
54
|
+
- **The result-count / sort bar sits on the card surface** rather than
|
|
55
|
+
floating on the page background.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- Filter panel grid tracks used `1fr`, whose implicit min-width is `auto`,
|
|
60
|
+
so a long stop name pushed the whole panel wider than its column and
|
|
61
|
+
pushed the counts and prices out of view entirely. Now `minmax(0, 1fr)`.
|
|
62
|
+
|
|
9
63
|
## [1.9.0] - 2026-08-01
|
|
10
64
|
|
|
11
65
|
### Added
|
|
@@ -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 }
|
|
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
|
-
{
|
|
104
|
-
|
|
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 }
|
|
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
|
-
|
|
186
|
-
) }
|
|
189
|
+
</Panel>
|
|
187
190
|
</Container>
|
|
188
191
|
);
|
|
189
192
|
};
|
package/Found/Filters/styles.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
91
|
-
|
|
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,7 @@ 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';
|
|
12
13
|
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
13
14
|
import { FoundData } from '@autobusal/providers/types/routes';
|
|
14
15
|
import { Refinement, BucketKey, fromParams, toParams, filterItems, buildFacets, priceRange, currencyOf, isRefined } from './refine';
|
|
@@ -88,17 +89,6 @@ const Found = ({ type, loading, data, step1, preferredStop, t, onSearch, onSave
|
|
|
88
89
|
<Sort value={ refinement.sort } t={ t } onChange={ sort => onRefine({ ...refinement, sort }) } />
|
|
89
90
|
</Summary>
|
|
90
91
|
|
|
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
92
|
<Picks
|
|
103
93
|
items={ refined }
|
|
104
94
|
sort={ refinement.sort }
|
|
@@ -108,21 +98,40 @@ const Found = ({ type, loading, data, step1, preferredStop, t, onSearch, onSave
|
|
|
108
98
|
</>
|
|
109
99
|
) }
|
|
110
100
|
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
101
|
+
<Results>
|
|
102
|
+
{ showControls && (
|
|
103
|
+
<Sidebar>
|
|
104
|
+
<Filters
|
|
105
|
+
open={ open }
|
|
106
|
+
refinement={ refinement }
|
|
107
|
+
facets={ facets }
|
|
108
|
+
range={ bounds }
|
|
109
|
+
currency={ currency }
|
|
110
|
+
t={ t }
|
|
111
|
+
onToggle={ () => setOpen(!open) }
|
|
112
|
+
onChange={ onRefine }
|
|
113
|
+
/>
|
|
114
|
+
</Sidebar>
|
|
115
|
+
) }
|
|
116
|
+
|
|
117
|
+
<Listing>
|
|
118
|
+
<Header t={ t } />
|
|
119
|
+
|
|
120
|
+
<Orders
|
|
121
|
+
loading={ loading }
|
|
122
|
+
data={ refined }
|
|
123
|
+
empty={ items.length === 0 }
|
|
124
|
+
refined={ isRefined(refinement) }
|
|
125
|
+
sort={ refinement.sort }
|
|
126
|
+
type={ type }
|
|
127
|
+
passengers={ passengers }
|
|
128
|
+
preferredStop={ Number(preferredStop) }
|
|
129
|
+
t={ t }
|
|
130
|
+
onReset={ () => onRefine({ ...refinement, ...{ operators: [], buckets: [], direct: false, fromStops: [], toStops: [], features: [], maxPrice: null } }) }
|
|
131
|
+
onSave={ onSave }
|
|
132
|
+
/>
|
|
133
|
+
</Listing>
|
|
134
|
+
</Results>
|
|
126
135
|
</>
|
|
127
136
|
);
|
|
128
137
|
};
|
package/Found/Route/Route.tsx
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import { TFunction } from 'i18next';
|
|
3
3
|
import { HiOutlineArrowNarrowRight, HiOutlineDocumentText } from 'react-icons/hi';
|
|
4
|
+
import { BiHourglass } from 'react-icons/bi';
|
|
4
5
|
import { RiBus2Line } from 'react-icons/ri';
|
|
6
|
+
import { formatDuration } from '../refine';
|
|
5
7
|
import { Button, RouteFeature, Policy } from '@autobusal/common';
|
|
6
|
-
import { Container, SpecialOffer, FavoriteStop, Trip, Company, Location, Time, Price, PriceNotice, LinkCompany, Details, Detail, ButtonPolicy, TitleDetail, LinkRoute, Features, TitleFeatures, FeaturesItems } from './styles';
|
|
8
|
+
import { Container, SpecialOffer, FavoriteStop, Trip, Company, Location, Stop, Iso, Time, Price, PriceNotice, LinkCompany, Details, Detail, ButtonPolicy, TitleDetail, LinkRoute, Features, TitleFeatures, FeaturesItems } from './styles';
|
|
7
9
|
import { FoundData } from '@autobusal/providers/types/routes';
|
|
8
10
|
|
|
9
11
|
// neutral placeholder so a missing/broken operator logo doesn't render the
|
|
@@ -54,18 +56,33 @@ const Route = ({ data, type, passengers, t, onSave, children }: Props): JSX.Elem
|
|
|
54
56
|
</Company>
|
|
55
57
|
|
|
56
58
|
<Location>
|
|
57
|
-
<h2>
|
|
58
|
-
|
|
59
|
+
<h2>
|
|
60
|
+
{ data.locations.from.city.name }
|
|
61
|
+
<Iso>({ data.locations.from.city.country?.iso_code })</Iso>
|
|
62
|
+
{ data.locations.from.departure }
|
|
63
|
+
</h2>
|
|
64
|
+
|
|
65
|
+
<Stop>{ data.locations.from.stop?.name }</Stop>
|
|
59
66
|
</Location>
|
|
60
67
|
|
|
61
68
|
<Time>
|
|
62
69
|
<HiOutlineArrowNarrowRight />
|
|
63
|
-
|
|
70
|
+
|
|
71
|
+
{ /* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
72
|
+
The clock is the point: this number sits between two real
|
|
73
|
+
departure times, and "08:30" gave nothing to distinguish a
|
|
74
|
+
trip length from a time of day. */ }
|
|
75
|
+
<span><BiHourglass />{ formatDuration(data.duration, t) }</span>
|
|
64
76
|
</Time>
|
|
65
77
|
|
|
66
78
|
<Location>
|
|
67
|
-
<h2>
|
|
68
|
-
|
|
79
|
+
<h2>
|
|
80
|
+
{ data.locations.to.city.name }
|
|
81
|
+
<Iso>({ data.locations.to.city.country?.iso_code })</Iso>
|
|
82
|
+
{ data.locations.to.arrival }
|
|
83
|
+
</h2>
|
|
84
|
+
|
|
85
|
+
<Stop>{ data.locations.to.stop?.name }</Stop>
|
|
69
86
|
</Location>
|
|
70
87
|
|
|
71
88
|
<Price>
|
package/Found/Route/styles.ts
CHANGED
|
@@ -73,17 +73,46 @@ export const Location = styled.div`
|
|
|
73
73
|
display: flex;
|
|
74
74
|
flex-direction: column;
|
|
75
75
|
align-items: center;
|
|
76
|
-
gap:
|
|
76
|
+
gap: 3px;
|
|
77
77
|
text-align: center;
|
|
78
78
|
|
|
79
79
|
& > h2 {
|
|
80
80
|
margin-bottom: 0;
|
|
81
81
|
}
|
|
82
|
+
`;
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
86
|
+
*
|
|
87
|
+
* The stop name is supporting detail, not a heading. At the same 16px as
|
|
88
|
+
* body copy it competed with the city above it; a step down lets the eye
|
|
89
|
+
* land on the city first and read the stop only when it needs to.
|
|
90
|
+
*/
|
|
91
|
+
export const Stop = styled.span`
|
|
92
|
+
font-size: ${ props => props.theme.size.xs };
|
|
93
|
+
font-weight: 700;
|
|
94
|
+
line-height: 1.3;
|
|
95
|
+
color: ${ props => props.theme.font.faded };
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
100
|
+
*
|
|
101
|
+
* The country code comes out of the database lowercase ("alb", "grc").
|
|
102
|
+
* An ISO country code is by definition upper case, so it is cased here at
|
|
103
|
+
* render time rather than left to the data - every consumer would otherwise
|
|
104
|
+
* have to remember to do it. Set smaller and muted so it qualifies the city
|
|
105
|
+
* without competing with it.
|
|
106
|
+
*/
|
|
107
|
+
export const Iso = styled.span`
|
|
108
|
+
// JSX drops the literal spaces around this element, so the gap on either
|
|
109
|
+
// side of "(ALB)" is this margin's job, not whitespace's
|
|
110
|
+
margin: 0 5px;
|
|
111
|
+
font-size: ${ props => props.theme.size.xs };
|
|
112
|
+
font-weight: 700;
|
|
113
|
+
text-transform: uppercase;
|
|
114
|
+
letter-spacing: .5px;
|
|
115
|
+
color: ${ props => props.theme.font.faded };
|
|
87
116
|
`;
|
|
88
117
|
|
|
89
118
|
export const Time = styled.div`
|
|
@@ -98,13 +127,24 @@ export const Time = styled.div`
|
|
|
98
127
|
}
|
|
99
128
|
|
|
100
129
|
& > span {
|
|
101
|
-
display:
|
|
102
|
-
|
|
103
|
-
|
|
130
|
+
display: flex;
|
|
131
|
+
align-items: center;
|
|
132
|
+
gap: 5px;
|
|
133
|
+
padding: 3px 12px;
|
|
134
|
+
white-space: nowrap;
|
|
135
|
+
font-size: ${ props => props.theme.size.m };
|
|
104
136
|
background: ${ props => props.theme.background.neutral };
|
|
105
137
|
border-radius: 100px;
|
|
106
138
|
}
|
|
107
139
|
|
|
140
|
+
// the hourglass is a hint, not a heading - the big arrow above already
|
|
141
|
+
// owns the visual weight in this column
|
|
142
|
+
& > span > svg {
|
|
143
|
+
flex-shrink: 0;
|
|
144
|
+
font-size: ${ props => props.theme.size.m };
|
|
145
|
+
opacity: .7;
|
|
146
|
+
}
|
|
147
|
+
|
|
108
148
|
@media (min-width: 640px) {
|
|
109
149
|
flex-basis: 120px;
|
|
110
150
|
}
|
|
@@ -20,7 +20,7 @@ interface Props {
|
|
|
20
20
|
* results, which is otherwise easy to forget after scrolling.
|
|
21
21
|
*/
|
|
22
22
|
const Summary = ({ shown, total, range, t, children }: Props): JSX.Element => (
|
|
23
|
-
<Container>
|
|
23
|
+
<Container className="box">
|
|
24
24
|
<Result>
|
|
25
25
|
{ /* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
26
26
|
`total`, not `count` - i18next treats a `count` variable as a
|
package/Found/Summary/styles.ts
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import styled from 'styled-components';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
5
|
+
*
|
|
6
|
+
* Carries the `box` surface (padding is overridden to something tighter -
|
|
7
|
+
* this is a bar, not a panel) so the count and the sort control sit on the
|
|
8
|
+
* same card background as everything else on the page rather than floating
|
|
9
|
+
* loose on the page background.
|
|
10
|
+
*/
|
|
3
11
|
export const Container = styled.div`
|
|
4
12
|
display: flex;
|
|
5
13
|
flex-direction: column;
|
|
6
14
|
gap: 12px;
|
|
7
15
|
margin-bottom: 15px;
|
|
8
16
|
|
|
17
|
+
&.box {
|
|
18
|
+
padding: 12px 15px;
|
|
19
|
+
}
|
|
20
|
+
|
|
9
21
|
@media (min-width: 1024px) {
|
|
10
22
|
flex-direction: row;
|
|
11
23
|
align-items: center;
|
package/Found/refine.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TFunction } from 'i18next';
|
|
1
2
|
import { FoundData } from '@autobusal/providers/types/routes';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -97,6 +98,48 @@ export const durationOf = (item: FoundData): number | null => {
|
|
|
97
98
|
return minutesOf(item.duration);
|
|
98
99
|
};
|
|
99
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Turn obtapi's "HH:MM" trip length into something that reads as a
|
|
103
|
+
* duration rather than a clock time.
|
|
104
|
+
*
|
|
105
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
106
|
+
*
|
|
107
|
+
* "08:30" sitting between two departure times is genuinely ambiguous -
|
|
108
|
+
* nothing about it says "eight and a half hours" rather than "half past
|
|
109
|
+
* eight", and on this card it sits directly between 09:00 and 16:30. The
|
|
110
|
+
* unit labels come from the translation files because "h"/"m" are not
|
|
111
|
+
* universal; Greek and the Cyrillic locales carry their own.
|
|
112
|
+
*
|
|
113
|
+
* The "-" sentinel (obtapi could not compute a duration) passes straight
|
|
114
|
+
* through - it must not become "0h 0m", which would read as a real answer.
|
|
115
|
+
*/
|
|
116
|
+
export const formatDuration = (duration: string, t: TFunction<'common'>): string => {
|
|
117
|
+
if (!duration || duration === '-') {
|
|
118
|
+
return duration;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const total = minutesOf(duration);
|
|
122
|
+
|
|
123
|
+
const hours = Math.floor(total / 60);
|
|
124
|
+
const minutes = total % 60;
|
|
125
|
+
|
|
126
|
+
const pad = (value: number): string => (value <= 9 ? `0${ value }` : String(value));
|
|
127
|
+
|
|
128
|
+
const parts: string[] = [];
|
|
129
|
+
|
|
130
|
+
if (hours > 0) {
|
|
131
|
+
parts.push(t('routes_order.step2.route.duration.hours', { value: pad(hours) }));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// a trip under an hour still needs a number, and so does an exact
|
|
135
|
+
// number of hours - but "08h 00m" is noise, so zero minutes are dropped
|
|
136
|
+
if (minutes > 0 || hours === 0) {
|
|
137
|
+
parts.push(t('routes_order.step2.route.duration.minutes', { value: pad(minutes) }));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return parts.join(' ');
|
|
141
|
+
};
|
|
142
|
+
|
|
100
143
|
export const bucketOf = (item: FoundData): BucketKey => {
|
|
101
144
|
const hour = Math.floor(departureOf(item) / 60);
|
|
102
145
|
|
package/package.json
CHANGED
package/styles.ts
CHANGED
|
@@ -21,4 +21,40 @@ export const Actions = styled.div`
|
|
|
21
21
|
justify-content: center;
|
|
22
22
|
gap: 20px;
|
|
23
23
|
margin-top: 25px;
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Results layout: filters beside the list, not above it.
|
|
28
|
+
*
|
|
29
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
30
|
+
*
|
|
31
|
+
* Below 1280px this is a plain column and the filters stay a collapsible
|
|
32
|
+
* panel. At and above it the filters take a fixed column and the results
|
|
33
|
+
* take the rest. See Found/Filters/styles.ts for why the breakpoint is
|
|
34
|
+
* 1280 rather than 1024.
|
|
35
|
+
*/
|
|
36
|
+
export const Results = styled.div`
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
|
|
40
|
+
@media (min-width: 1280px) {
|
|
41
|
+
flex-direction: row;
|
|
42
|
+
align-items: flex-start;
|
|
43
|
+
gap: 20px;
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
export const Sidebar = styled.aside`
|
|
48
|
+
min-width: 0;
|
|
49
|
+
|
|
50
|
+
@media (min-width: 1280px) {
|
|
51
|
+
flex: 0 0 290px;
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
export const Listing = styled.div`
|
|
56
|
+
// min-width:0 stops a long stop name inside a card from forcing this
|
|
57
|
+
// column wider than its share of the row
|
|
58
|
+
flex: 1;
|
|
59
|
+
min-width: 0;
|
|
24
60
|
`;
|