@autobusal/routes-order 1.13.0 → 1.21.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 +142 -0
- package/Facts/Facts.tsx +119 -0
- package/Facts/services.ts +19 -0
- package/Facts/styles.ts +48 -0
- package/Facts/types.ts +60 -0
- package/Found/Dates/Dates.tsx +59 -5
- package/Found/Dates/styles.ts +109 -7
- package/Found/Found.tsx +9 -0
- package/Found/Picks/Picks.tsx +10 -3
- package/Found/Picks/styles.ts +60 -9
- package/Found/Route/Route.tsx +35 -3
- package/Found/Route/styles.ts +239 -22
- package/Found/SameDay/SameDay.tsx +35 -0
- package/Found/SameDay/styles.ts +27 -0
- package/Found/Sort/styles.ts +52 -7
- package/Links/Links.tsx +67 -0
- package/Links/styles.ts +45 -0
- package/RoutesOrder.tsx +27 -0
- package/Schedule/Schedule.tsx +110 -0
- package/Schedule/styles.ts +53 -0
- package/Sections/Sections.tsx +87 -0
- package/Sections/styles.ts +44 -0
- package/package.json +1 -1
package/Found/Picks/Picks.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TFunction } from 'i18next';
|
|
2
|
-
import { Container, Pick, Label, Value, Meta } from './styles';
|
|
2
|
+
import { Container, Pick, Label, Value, Meta, Company } from './styles';
|
|
3
3
|
import { SortKey, sortItems } from '../refine';
|
|
4
4
|
import { FoundData } from '@autobusal/providers/types/routes';
|
|
5
5
|
|
|
@@ -55,8 +55,15 @@ const Picks = ({ items, sort, t, onSelect }: Props): JSX.Element | null => {
|
|
|
55
55
|
<Value>{ best.price.display }</Value>
|
|
56
56
|
|
|
57
57
|
<Meta>
|
|
58
|
-
{ best.duration !== '-' &&
|
|
59
|
-
|
|
58
|
+
{ best.duration !== '-' && best.duration }
|
|
59
|
+
|
|
60
|
+
{ /* The operator's name is hidden on a phone - see Company in
|
|
61
|
+
styles. The separator goes with it, or a card reads
|
|
62
|
+
"4h ·" with nothing after the dot. */ }
|
|
63
|
+
<Company>
|
|
64
|
+
{ best.duration !== '-' && ' · ' }
|
|
65
|
+
{ best.operator.company }
|
|
66
|
+
</Company>
|
|
60
67
|
</Meta>
|
|
61
68
|
</Pick>
|
|
62
69
|
);
|
package/Found/Picks/styles.ts
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import styled, { css } from 'styled-components';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
5
|
+
*
|
|
6
|
+
* Three across at every width, including phones, where these used to stack.
|
|
7
|
+
* Stacked, they cost three full bands of vertical space before a single
|
|
8
|
+
* result - so the thing that exists to summarise the list was pushing the
|
|
9
|
+
* list itself off the screen. Side by side they are a strip, and comparing
|
|
10
|
+
* three numbers is what they are for; a column of three is a list to read.
|
|
11
|
+
*/
|
|
3
12
|
export const Container = styled.div`
|
|
4
13
|
display: grid;
|
|
5
|
-
gap:
|
|
14
|
+
gap: 6px;
|
|
6
15
|
margin-bottom: 15px;
|
|
7
|
-
grid-template-columns: 1fr;
|
|
16
|
+
grid-template-columns: repeat(3, 1fr);
|
|
8
17
|
|
|
9
18
|
@media (min-width: 640px) {
|
|
10
|
-
|
|
19
|
+
gap: 10px;
|
|
11
20
|
}
|
|
12
21
|
`;
|
|
13
22
|
|
|
@@ -15,9 +24,15 @@ export const Pick = styled.button<{ $active: boolean }>`
|
|
|
15
24
|
display: flex;
|
|
16
25
|
flex-direction: column;
|
|
17
26
|
align-items: flex-start;
|
|
18
|
-
gap:
|
|
19
|
-
padding:
|
|
27
|
+
gap: 2px;
|
|
28
|
+
padding: 8px 9px;
|
|
20
29
|
text-align: left;
|
|
30
|
+
min-width: 0;
|
|
31
|
+
|
|
32
|
+
@media (min-width: 640px) {
|
|
33
|
+
gap: 4px;
|
|
34
|
+
padding: 12px 15px;
|
|
35
|
+
}
|
|
21
36
|
border-radius: ${ props => props.theme.borderRadius };
|
|
22
37
|
border: 1px solid ${ props => props.theme.inputs.border };
|
|
23
38
|
background: ${ props => props.theme.background.normal };
|
|
@@ -34,22 +49,58 @@ export const Pick = styled.button<{ $active: boolean }>`
|
|
|
34
49
|
`;
|
|
35
50
|
|
|
36
51
|
export const Label = styled.span`
|
|
37
|
-
|
|
52
|
+
max-width: 100%;
|
|
53
|
+
overflow: hidden;
|
|
54
|
+
font-size: calc(${ props => props.theme.size.xs } - 1px);
|
|
38
55
|
text-transform: uppercase;
|
|
39
|
-
letter-spacing:
|
|
56
|
+
letter-spacing: 0;
|
|
40
57
|
font-weight: 700;
|
|
41
58
|
color: ${ props => props.theme.font.faded };
|
|
59
|
+
text-overflow: ellipsis;
|
|
60
|
+
white-space: nowrap;
|
|
61
|
+
|
|
62
|
+
@media (min-width: 640px) {
|
|
63
|
+
font-size: ${ props => props.theme.size.xs };
|
|
64
|
+
letter-spacing: .5px;
|
|
65
|
+
}
|
|
42
66
|
`;
|
|
43
67
|
|
|
44
68
|
export const Value = styled.strong`
|
|
45
|
-
font-size: ${ props => props.theme.size.
|
|
69
|
+
font-size: ${ props => props.theme.size.m };
|
|
70
|
+
|
|
71
|
+
@media (min-width: 640px) {
|
|
72
|
+
font-size: ${ props => props.theme.size.l };
|
|
73
|
+
}
|
|
46
74
|
`;
|
|
47
75
|
|
|
48
76
|
export const Meta = styled.span`
|
|
49
77
|
overflow: hidden;
|
|
50
78
|
max-width: 100%;
|
|
51
|
-
font-size: ${ props => props.theme.size.xs };
|
|
79
|
+
font-size: calc(${ props => props.theme.size.xs } - 1px);
|
|
52
80
|
color: ${ props => props.theme.font.faded };
|
|
53
81
|
text-overflow: ellipsis;
|
|
54
82
|
white-space: nowrap;
|
|
83
|
+
|
|
84
|
+
@media (min-width: 640px) {
|
|
85
|
+
font-size: ${ props => props.theme.size.xs };
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The operator's name, dropped on a phone.
|
|
91
|
+
*
|
|
92
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
93
|
+
*
|
|
94
|
+
* At a third of a 375px screen each card gets about 110px, and a company
|
|
95
|
+
* name is the one thing here that will not fit in it - it would ellipsis to
|
|
96
|
+
* two or three characters, which is noise rather than information. The
|
|
97
|
+
* duration stays, because it is short and it is the number being compared;
|
|
98
|
+
* the name is one line down in the results anyway.
|
|
99
|
+
*/
|
|
100
|
+
export const Company = styled.span`
|
|
101
|
+
display: none;
|
|
102
|
+
|
|
103
|
+
@media (min-width: 640px) {
|
|
104
|
+
display: inline;
|
|
105
|
+
}
|
|
55
106
|
`;
|
package/Found/Route/Route.tsx
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import { TFunction } from 'i18next';
|
|
3
|
-
import { HiOutlineArrowNarrowRight, HiOutlineDocumentText } from 'react-icons/hi';
|
|
3
|
+
import { HiOutlineArrowNarrowRight, HiOutlineDocumentText, HiChevronDown, HiChevronUp } from 'react-icons/hi';
|
|
4
4
|
import { BiHourglass } from 'react-icons/bi';
|
|
5
5
|
import { RiBus2Line } from 'react-icons/ri';
|
|
6
6
|
import { formatDuration, splitPrice } from '../refine';
|
|
7
7
|
import { Button, RouteFeature, Policy, Rating } from '@autobusal/common';
|
|
8
|
-
import { Container, SpecialOffer, FavoriteStop, Trip, Company, Location, Stop, Iso, Time, Price, Amount, Decimals, Currency, PriceNotice, LinkCompany, Details, Detail, ButtonPolicy, TitleDetail, LinkRoute, FeaturesItems } from './styles';
|
|
8
|
+
import { Container, ButtonDetails, SpecialOffer, FavoriteStop, Trip, Company, Location, Stop, Iso, Time, Price, Amount, Decimals, Currency, PriceNotice, LinkCompany, Details, Detail, ButtonPolicy, TitleDetail, LinkRoute, FeaturesItems, Leaves } from './styles';
|
|
9
9
|
import { FoundData } from '@autobusal/providers/types/routes';
|
|
10
10
|
|
|
11
11
|
// neutral placeholder so a missing/broken operator logo doesn't render the
|
|
@@ -27,6 +27,14 @@ interface Props {
|
|
|
27
27
|
const Route = ({ data, type, passengers, t, onSave, children }: Props): JSX.Element => {
|
|
28
28
|
const [ policy, setPolicy ] = useState<boolean>(false);
|
|
29
29
|
|
|
30
|
+
// Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
31
|
+
// Closed to begin with. Amenities, route code, transit and policies are
|
|
32
|
+
// supporting detail on a row somebody is scanning, and four bands of it
|
|
33
|
+
// per result pushed the next result off a phone screen. The content stays
|
|
34
|
+
// in the DOM either way (see Details in styles) so the prerendered
|
|
35
|
+
// snapshot a crawler reads is unchanged.
|
|
36
|
+
const [ details, setDetails ] = useState<boolean>(false);
|
|
37
|
+
|
|
30
38
|
const customs = Object.values(data.customs).join(', ');
|
|
31
39
|
const transiting = Object.values(data.transiting).join(', ');
|
|
32
40
|
|
|
@@ -47,11 +55,26 @@ const Route = ({ data, type, passengers, t, onSave, children }: Props): JSX.Elem
|
|
|
47
55
|
? `${ import.meta.env.VITE_API_OBT }/api/external/go/${ data.external.offer_id }`
|
|
48
56
|
: undefined;
|
|
49
57
|
|
|
58
|
+
// Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
59
|
+
// Only today's results carry this - obtapi sends null on every other date,
|
|
60
|
+
// so there is no "in 26 days" badge to suppress here.
|
|
61
|
+
const leaves = data.departs_in ?? null;
|
|
62
|
+
|
|
63
|
+
const soon = leaves !== null && leaves < 60;
|
|
64
|
+
|
|
50
65
|
return (
|
|
51
66
|
<Container className="box" $type={ type }>
|
|
52
67
|
{ data.price.offer === true && <SpecialOffer>{ t('routes_order.step2.route.offer') }</SpecialOffer> }
|
|
53
68
|
{ type === 'favorite' && <FavoriteStop>{ t('routes_order.step2.route.preferred_stop') }</FavoriteStop> }
|
|
54
69
|
|
|
70
|
+
{ leaves !== null && (
|
|
71
|
+
<Leaves $soon={ soon }>
|
|
72
|
+
{ leaves < 60
|
|
73
|
+
? t('routes_order.step2.route.leaves_minutes', { minutes: leaves })
|
|
74
|
+
: t('routes_order.step2.route.leaves_hours', { hours: Math.floor(leaves / 60), minutes: leaves % 60 }) }
|
|
75
|
+
</Leaves>
|
|
76
|
+
) }
|
|
77
|
+
|
|
55
78
|
<Trip>
|
|
56
79
|
<Company>
|
|
57
80
|
<LinkCompany to={ bookOnUrl ?? 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>
|
|
@@ -134,7 +157,16 @@ const Route = ({ data, type, passengers, t, onSave, children }: Props): JSX.Elem
|
|
|
134
157
|
|
|
135
158
|
{ children }
|
|
136
159
|
|
|
137
|
-
<
|
|
160
|
+
<ButtonDetails
|
|
161
|
+
type="button"
|
|
162
|
+
aria-expanded={ details }
|
|
163
|
+
onClick={ () => setDetails(!details) }
|
|
164
|
+
>
|
|
165
|
+
{ details ? <HiChevronUp /> : <HiChevronDown /> }
|
|
166
|
+
{ t('routes_order.step2.route.details') }
|
|
167
|
+
</ButtonDetails>
|
|
168
|
+
|
|
169
|
+
<Details $open={ details }>
|
|
138
170
|
{ /* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
139
171
|
Amenities were a separate full-width block under the card.
|
|
140
172
|
They are the same kind of thing as route, transit and policies
|
package/Found/Route/styles.ts
CHANGED
|
@@ -6,8 +6,20 @@ export const Container = styled.div<{ $type: 'favorite' | 'normal' }>`
|
|
|
6
6
|
position: relative;
|
|
7
7
|
display: flex;
|
|
8
8
|
flex-direction: column;
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
12
|
+
* Tighter on a phone. Every result carried 15px of padding and 15px
|
|
13
|
+
* between each of its bands, which is most of a thumb of screen spent on
|
|
14
|
+
* gaps before anything is read.
|
|
15
|
+
*/
|
|
16
|
+
gap: 8px;
|
|
17
|
+
padding: 10px;
|
|
18
|
+
|
|
19
|
+
@media (min-width: 640px) {
|
|
20
|
+
gap: 15px;
|
|
21
|
+
padding: 15px;
|
|
22
|
+
}
|
|
11
23
|
|
|
12
24
|
${ props => props.$type === 'favorite' && css`
|
|
13
25
|
border: 1px solid ${ props => props.theme.font.success };
|
|
@@ -45,14 +57,32 @@ export const FavoriteStop = styled.div`
|
|
|
45
57
|
border-bottom-right-radius: ${ props => props.theme.borderRadius };
|
|
46
58
|
`;
|
|
47
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
62
|
+
*
|
|
63
|
+
* VERTICAL on a phone, side by side from 640px up.
|
|
64
|
+
*
|
|
65
|
+
* Side by side was tried first and does not survive the content: each end
|
|
66
|
+
* gets about 96px at 375px, while "Thessaloniki (GRC) 16:30" measures 215px
|
|
67
|
+
* - it painted 50px outside the card, and the page never scrolled sideways
|
|
68
|
+
* so nothing looked broken until it was measured against the box it was
|
|
69
|
+
* supposed to be in. Stacked, each end has the full width, so nothing is
|
|
70
|
+
* truncated and nothing escapes.
|
|
71
|
+
*
|
|
72
|
+
* Compact is bought back a different way: tight gaps, a short arrow turned
|
|
73
|
+
* to point down the column, and the price on one row instead of a centred
|
|
74
|
+
* stack. Same information, roughly a third less height than before.
|
|
75
|
+
*/
|
|
48
76
|
export const Trip = styled.div`
|
|
49
77
|
display: flex;
|
|
50
78
|
flex-direction: column;
|
|
51
|
-
gap:
|
|
79
|
+
gap: 6px;
|
|
52
80
|
|
|
53
81
|
@media (min-width: 640px) {
|
|
54
82
|
flex-direction: row;
|
|
55
83
|
flex-wrap: wrap;
|
|
84
|
+
align-items: center;
|
|
85
|
+
gap: 15px;
|
|
56
86
|
}
|
|
57
87
|
|
|
58
88
|
@media (min-width: 1024px) {
|
|
@@ -95,12 +125,49 @@ export const Location = styled.div`
|
|
|
95
125
|
min-width: 0;
|
|
96
126
|
display: flex;
|
|
97
127
|
flex-direction: column;
|
|
98
|
-
align-items: center;
|
|
99
|
-
gap: 3px;
|
|
100
|
-
text-align: center;
|
|
101
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
131
|
+
* Left-aligned while the ends are stacked - centred text in a full-width
|
|
132
|
+
* column leaves the two times floating in the middle of the card with
|
|
133
|
+
* nothing to line them up against. Centred again from 640px, where the
|
|
134
|
+
* two ends sit either side of the arrow and the symmetry is the point.
|
|
135
|
+
*/
|
|
136
|
+
align-items: flex-start;
|
|
137
|
+
gap: 1px;
|
|
138
|
+
text-align: left;
|
|
139
|
+
|
|
140
|
+
@media (min-width: 640px) {
|
|
141
|
+
align-items: center;
|
|
142
|
+
gap: 3px;
|
|
143
|
+
text-align: center;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
148
|
+
*
|
|
149
|
+
* The heading has to be allowed to WRAP now that the two ends share a
|
|
150
|
+
* row. The min-width:0 above lets this column shrink, but the heading
|
|
151
|
+
* inside kept its intrinsic single-line width and simply painted outside
|
|
152
|
+
* the card - "Thessaloniki (GRC) 16:30" measured 215px inside a 96px
|
|
153
|
+
* column and ran 50px past the card's right edge. The page never scrolled
|
|
154
|
+
* sideways, so nothing looked broken until the text was measured against
|
|
155
|
+
* the box it was supposed to be in.
|
|
156
|
+
*
|
|
157
|
+
* A step down in size as well, because three columns of a 375px screen do
|
|
158
|
+
* not have room for a headline at body-copy scale.
|
|
159
|
+
*/
|
|
102
160
|
& > h2 {
|
|
103
161
|
margin-bottom: 0;
|
|
162
|
+
max-width: 100%;
|
|
163
|
+
line-height: 1.25;
|
|
164
|
+
overflow-wrap: anywhere;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@media (min-width: 640px) {
|
|
168
|
+
& > h2 {
|
|
169
|
+
overflow-wrap: normal;
|
|
170
|
+
}
|
|
104
171
|
}
|
|
105
172
|
`;
|
|
106
173
|
|
|
@@ -111,11 +178,30 @@ export const Location = styled.div`
|
|
|
111
178
|
* body copy it competed with the city above it; a step down lets the eye
|
|
112
179
|
* land on the city first and read the stop only when it needs to.
|
|
113
180
|
*/
|
|
181
|
+
/**
|
|
182
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
183
|
+
*
|
|
184
|
+
* Clamped to two lines on a phone. Now that the two ends of the journey
|
|
185
|
+
* share a row, each gets about 96px - and a name like "Terminali Lindor
|
|
186
|
+
* Interurban dhe Nderkombetar i Autobusave (TEG)" ran to five lines and
|
|
187
|
+
* stretched the whole row to match, so one terminal with a long name made
|
|
188
|
+
* every card taller than the screen. Two lines and an ellipsis, which is
|
|
189
|
+
* what the reference layouts do with the same problem.
|
|
190
|
+
*/
|
|
114
191
|
export const Stop = styled.span`
|
|
192
|
+
display: -webkit-box;
|
|
193
|
+
-webkit-line-clamp: 2;
|
|
194
|
+
-webkit-box-orient: vertical;
|
|
195
|
+
overflow: hidden;
|
|
115
196
|
font-size: ${ props => props.theme.size.xs };
|
|
116
197
|
font-weight: 700;
|
|
117
198
|
line-height: 1.3;
|
|
118
199
|
color: ${ props => props.theme.font.faded };
|
|
200
|
+
|
|
201
|
+
@media (min-width: 1024px) {
|
|
202
|
+
display: block;
|
|
203
|
+
overflow: visible;
|
|
204
|
+
}
|
|
119
205
|
`;
|
|
120
206
|
|
|
121
207
|
/**
|
|
@@ -140,13 +226,39 @@ export const Iso = styled.span`
|
|
|
140
226
|
|
|
141
227
|
export const Time = styled.div`
|
|
142
228
|
display: flex;
|
|
143
|
-
flex-direction:
|
|
144
|
-
gap:
|
|
229
|
+
flex-direction: row;
|
|
230
|
+
gap: 6px;
|
|
145
231
|
align-items: center;
|
|
146
232
|
font-weight: 700;
|
|
147
233
|
|
|
234
|
+
@media (min-width: 640px) {
|
|
235
|
+
flex-direction: column;
|
|
236
|
+
gap: 8px;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
241
|
+
* The arrow sat between two stacked blocks at 1.5x the largest type in
|
|
242
|
+
* the theme. Now that the two ends of the journey share a row with it,
|
|
243
|
+
* that size would take the width the city names need - it is a
|
|
244
|
+
* separator, not a headline.
|
|
245
|
+
*/
|
|
246
|
+
/**
|
|
247
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
248
|
+
* Turned to point DOWN while the two ends are stacked - an arrow pointing
|
|
249
|
+
* right between two things sitting one above the other says the wrong
|
|
250
|
+
* thing about which way the journey goes.
|
|
251
|
+
*/
|
|
148
252
|
& > svg {
|
|
149
|
-
font-size:
|
|
253
|
+
font-size: ${ props => props.theme.size.l };
|
|
254
|
+
transform: rotate(90deg);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
@media (min-width: 640px) {
|
|
258
|
+
& > svg {
|
|
259
|
+
font-size: calc(1.5 * ${ props => props.theme.size.xxl });
|
|
260
|
+
transform: none;
|
|
261
|
+
}
|
|
150
262
|
}
|
|
151
263
|
|
|
152
264
|
& > span {
|
|
@@ -176,12 +288,44 @@ export const Time = styled.div`
|
|
|
176
288
|
export const Price = styled.div`
|
|
177
289
|
flex-basis: 100%;
|
|
178
290
|
display: flex;
|
|
179
|
-
|
|
180
|
-
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
294
|
+
*
|
|
295
|
+
* On a phone: the fare and the button share one row, and the tax note sits
|
|
296
|
+
* under them.
|
|
297
|
+
*
|
|
298
|
+
* Reordered in CSS rather than in the markup, because the source order -
|
|
299
|
+
* fare, note, button - is the right one to read aloud and the right one
|
|
300
|
+
* for a crawler: the note explains the number it follows. Only the
|
|
301
|
+
* PAINTING needs to differ, and the order property says exactly that
|
|
302
|
+
* without moving the note away from the price it qualifies.
|
|
303
|
+
*/
|
|
304
|
+
flex-direction: row;
|
|
305
|
+
flex-wrap: wrap;
|
|
306
|
+
justify-content: space-between;
|
|
181
307
|
align-items: center;
|
|
308
|
+
gap: 4px 8px;
|
|
309
|
+
|
|
310
|
+
& > *:nth-child(1) { order: 1; }
|
|
311
|
+
|
|
312
|
+
/* the note drops to its own line under both */
|
|
313
|
+
& > *:nth-child(2) { order: 3; flex-basis: 100%; }
|
|
314
|
+
|
|
315
|
+
& > *:nth-child(3) { order: 2; }
|
|
182
316
|
|
|
183
317
|
@media (min-width: 768px) {
|
|
184
318
|
flex: 0 0 165px;
|
|
319
|
+
flex-direction: column;
|
|
320
|
+
justify-content: initial;
|
|
321
|
+
gap: 8px;
|
|
322
|
+
|
|
323
|
+
& > *:nth-child(1),
|
|
324
|
+
& > *:nth-child(2),
|
|
325
|
+
& > *:nth-child(3) {
|
|
326
|
+
order: initial;
|
|
327
|
+
flex-basis: auto;
|
|
328
|
+
}
|
|
185
329
|
}
|
|
186
330
|
`;
|
|
187
331
|
|
|
@@ -220,11 +364,20 @@ export const Currency = styled.span`
|
|
|
220
364
|
* frees the vertical space the Select button needs.
|
|
221
365
|
*/
|
|
222
366
|
export const PriceNotice = styled.span`
|
|
223
|
-
|
|
367
|
+
/**
|
|
368
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
369
|
+
* Left-aligned under the fare on a phone, where it is a full-width line
|
|
370
|
+
* beneath a price and a button rather than the middle of a centred stack.
|
|
371
|
+
*/
|
|
224
372
|
font-size: ${ props => props.theme.size.xxs };
|
|
225
373
|
line-height: 1.25;
|
|
226
374
|
color: ${ props => props.theme.font.faded };
|
|
227
|
-
text-align:
|
|
375
|
+
text-align: left;
|
|
376
|
+
|
|
377
|
+
@media (min-width: 768px) {
|
|
378
|
+
margin-top: -4px;
|
|
379
|
+
text-align: center;
|
|
380
|
+
}
|
|
228
381
|
`;
|
|
229
382
|
|
|
230
383
|
export const LinkCompany = styled(Link)`
|
|
@@ -239,24 +392,64 @@ export const LinkCompany = styled(Link)`
|
|
|
239
392
|
}
|
|
240
393
|
`;
|
|
241
394
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
395
|
+
/**
|
|
396
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
397
|
+
*
|
|
398
|
+
* Collapsible, and inline at every width including phones.
|
|
399
|
+
*
|
|
400
|
+
* Hidden with CSS rather than unmounted, deliberately. These cards are on a
|
|
401
|
+
* page the prerender crawl snapshots for every route pair in the sitemap, so
|
|
402
|
+
* amenities, the route code, transit countries and the policy link have to
|
|
403
|
+
* be IN the DOM whether or not a reader has opened them - unmounting would
|
|
404
|
+
* take real per-pair content out of exactly the pages Tier 3 exists to
|
|
405
|
+
* fatten.
|
|
406
|
+
*
|
|
407
|
+
* Wrapping instead of stacking on a phone turns four bands of vertical space
|
|
408
|
+
* into one or two, which is the space the results themselves wanted.
|
|
409
|
+
*/
|
|
410
|
+
export const Details = styled.div<{ $open: boolean }>`
|
|
411
|
+
display: ${ props => (props.$open ? 'flex' : 'none') };
|
|
412
|
+
flex-wrap: wrap;
|
|
413
|
+
gap: 6px;
|
|
246
414
|
|
|
247
415
|
@media (min-width: 640px) {
|
|
248
|
-
|
|
249
|
-
flex-wrap: wrap;
|
|
416
|
+
gap: 15px;
|
|
250
417
|
}
|
|
251
418
|
`;
|
|
252
419
|
|
|
253
|
-
|
|
254
|
-
|
|
420
|
+
/**
|
|
421
|
+
* The disclosure. Small and quiet - it is a way to get at supporting detail,
|
|
422
|
+
* not an action competing with "select".
|
|
423
|
+
*/
|
|
424
|
+
export const ButtonDetails = styled.button`
|
|
425
|
+
display: inline-flex;
|
|
426
|
+
align-items: center;
|
|
427
|
+
gap: 5px;
|
|
428
|
+
align-self: flex-start;
|
|
429
|
+
padding: 4px 0;
|
|
255
430
|
font-size: ${ props => props.theme.size.s };
|
|
431
|
+
color: ${ props => props.theme.font.faded };
|
|
432
|
+
|
|
433
|
+
&:hover {
|
|
434
|
+
color: ${ props => props.theme.font.normal };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
svg {
|
|
438
|
+
transition: transform 0.2s ease;
|
|
439
|
+
}
|
|
440
|
+
`;
|
|
441
|
+
|
|
442
|
+
export const Detail = styled.div`
|
|
443
|
+
flex: 1 1 auto;
|
|
444
|
+
min-width: 0;
|
|
445
|
+
padding: 5px 8px;
|
|
446
|
+
font-size: calc(${ props => props.theme.size.s } - 1px);
|
|
256
447
|
background: ${ props => props.theme.background.neutral };
|
|
257
448
|
border-radius: ${ props => props.theme.borderRadius };
|
|
258
449
|
|
|
259
450
|
@media (min-width: 480px) {
|
|
451
|
+
padding: 8px 10px;
|
|
452
|
+
font-size: ${ props => props.theme.size.s };
|
|
260
453
|
flex-basis: calc(50% - 7.5px);
|
|
261
454
|
}
|
|
262
455
|
@media (min-width: 768px) {
|
|
@@ -300,4 +493,28 @@ export const FeaturesItems = styled.div`
|
|
|
300
493
|
display: flex;
|
|
301
494
|
flex-wrap: wrap;
|
|
302
495
|
gap: 6px;
|
|
303
|
-
`;
|
|
496
|
+
`;
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* "Leaves in 40 minutes", on today's departures only.
|
|
500
|
+
*
|
|
501
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
502
|
+
*
|
|
503
|
+
* Same-day seats are sellable now, and a time alone does not tell somebody
|
|
504
|
+
* whether they can make it - "14:00" reads very differently at nine in the
|
|
505
|
+
* morning and at half past one. Under an hour it turns urgent, because that
|
|
506
|
+
* is the point where the answer stops being "yes" and starts being "only if
|
|
507
|
+
* you leave now".
|
|
508
|
+
*/
|
|
509
|
+
export const Leaves = styled.div<{ $soon: boolean }>`
|
|
510
|
+
align-self: flex-start;
|
|
511
|
+
padding: 3px 10px;
|
|
512
|
+
border-radius: 100px;
|
|
513
|
+
font-size: ${ props => props.theme.size.xs };
|
|
514
|
+
font-weight: 700;
|
|
515
|
+
white-space: nowrap;
|
|
516
|
+
|
|
517
|
+
color: ${ props => (props.$soon ? '#FFF' : props.theme.font.normal) };
|
|
518
|
+
background: ${ props => (props.$soon ? props.theme.font.error : props.theme.primary.neutral) };
|
|
519
|
+
`;
|
|
520
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { TFunction } from 'i18next';
|
|
2
|
+
import { HiOutlineExclamationCircle } from 'react-icons/hi';
|
|
3
|
+
import { Container } from './styles';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
t: TFunction<'common'>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The caution that comes with selling today's seats.
|
|
11
|
+
*
|
|
12
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
13
|
+
*
|
|
14
|
+
* Same-day departures are sellable now, and two things about them are worth
|
|
15
|
+
* saying plainly before somebody pays.
|
|
16
|
+
*
|
|
17
|
+
* A coach that has already left is filtered out server-side
|
|
18
|
+
* (Search\Departed), and since countries carry a timezone that filter is now
|
|
19
|
+
* judged against the DEPARTURE STOP's own clock rather than ours - so the
|
|
20
|
+
* warning is no longer about doubt on our side.
|
|
21
|
+
*
|
|
22
|
+
* What remains is the honest thing to say when the margin is minutes: every
|
|
23
|
+
* time on the page is local to its stop, the seat is real, the coach is
|
|
24
|
+
* about to go, and whether somebody can reach the terminal in time is not
|
|
25
|
+
* something we can know for them.
|
|
26
|
+
*/
|
|
27
|
+
const SameDay = ({ t }: Props): JSX.Element => (
|
|
28
|
+
<Container>
|
|
29
|
+
<HiOutlineExclamationCircle />
|
|
30
|
+
|
|
31
|
+
<span>{ t('routes_order.step2.same_day') }</span>
|
|
32
|
+
</Container>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export default SameDay;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
5
|
+
*
|
|
6
|
+
* Shown only on a same-day search. A caution, not an alarm - the whole point
|
|
7
|
+
* of selling today is that these seats are worth selling, and a red banner
|
|
8
|
+
* over every one of them would say the opposite.
|
|
9
|
+
*/
|
|
10
|
+
export const Container = styled.div`
|
|
11
|
+
display: flex;
|
|
12
|
+
gap: 8px;
|
|
13
|
+
align-items: flex-start;
|
|
14
|
+
margin-bottom: 15px;
|
|
15
|
+
padding: 10px 12px;
|
|
16
|
+
border-radius: ${ props => props.theme.borderRadius };
|
|
17
|
+
background: ${ props => props.theme.primary.neutral };
|
|
18
|
+
color: ${ props => props.theme.font.normal };
|
|
19
|
+
font-size: ${ props => props.theme.size.s };
|
|
20
|
+
line-height: 1.4;
|
|
21
|
+
|
|
22
|
+
& > svg {
|
|
23
|
+
flex: 0 0 auto;
|
|
24
|
+
margin-top: 2px;
|
|
25
|
+
font-size: ${ props => props.theme.size.l };
|
|
26
|
+
}
|
|
27
|
+
`;
|