@autobusal/routes-order 1.40.1 → 1.40.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/Checkout/Checkout.tsx +6 -0
- package/Checkout/LocalPrice.ts +78 -0
- package/Checkout/Sidebar/Sidebar.tsx +11 -2
- package/Checkout/Sidebar/styles.ts +8 -0
- package/Found/Route/Route.tsx +21 -2
- package/Found/Route/styles.ts +46 -0
- package/RoutesOrder.tsx +5 -0
- package/package.json +1 -1
package/Checkout/Checkout.tsx
CHANGED
|
@@ -16,6 +16,7 @@ import Addons from '../Step5/Addons/Addons';
|
|
|
16
16
|
import Billing from '../Step5/Billing/Billing';
|
|
17
17
|
import Payments from '../Step5/Payments/Payments';
|
|
18
18
|
import Sidebar from './Sidebar/Sidebar';
|
|
19
|
+
import { useLocalPrice } from './LocalPrice';
|
|
19
20
|
import { getTotal, convertCoupon, getAvailablePayments, currencyOf, surchargeOf } from '../Step5/utilities';
|
|
20
21
|
import { Title, Actions, Results, Listing } from '../styles';
|
|
21
22
|
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
@@ -396,6 +397,10 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
|
|
|
396
397
|
value: owed
|
|
397
398
|
} : grandTotal;
|
|
398
399
|
|
|
400
|
+
// Claude - 2026-09-26: the same total in the currency Google quoted, for
|
|
401
|
+
// a traveller who arrived from Google Travel on this route (LocalPrice)
|
|
402
|
+
const local = useLocalPrice(step2.id as number | undefined, payable.value);
|
|
403
|
+
|
|
399
404
|
/**
|
|
400
405
|
* Edited: Ferjolt Ozuni - Date: 2026-08-05
|
|
401
406
|
*
|
|
@@ -634,6 +639,7 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
|
|
|
634
639
|
addons={ addonItems.map(item => ({ label: item.label, value: `${ item.price } ${ currency }` })) }
|
|
635
640
|
coupon={ coupon }
|
|
636
641
|
total={ payable }
|
|
642
|
+
local={ local }
|
|
637
643
|
surcharge={ surcharge > 0 ? `${ surcharge } ${ currency }` : undefined }
|
|
638
644
|
/* the seller's own cut, taken off what they settle - the same line
|
|
639
645
|
their invoice carries (audit A4-17), so the two documents for one
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
import { apiClient } from '@autobusal/providers';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The price Google showed, beside the price we charge.
|
|
6
|
+
*
|
|
7
|
+
* Claude - 2026-09-26 (Ferjolt: "build the checkout price display"). Google
|
|
8
|
+
* Travel quotes a departure in its origin's currency (obtapi Google\Transport
|
|
9
|
+
* - lekë from Albania) and requires the landing page to show the price the
|
|
10
|
+
* traveller picked. The ticket link names that currency (`local=ALL`, next
|
|
11
|
+
* to `route=`); this remembers it for the session, and the checkout shows
|
|
12
|
+
* "≈ 1,840 ALL" under the euro total for THAT route only - a later booking
|
|
13
|
+
* from Greece in the same tab shows nothing. Display only: the charge stays
|
|
14
|
+
* in the brand's currency. No rate known: no line.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const KEY = 'checkout_local_currency';
|
|
18
|
+
|
|
19
|
+
// whole units where prices are quoted whole - the same rule as the quote
|
|
20
|
+
const WHOLE = ['ALL', 'MKD', 'RSD', 'HUF', 'ISK'];
|
|
21
|
+
|
|
22
|
+
interface Stored {
|
|
23
|
+
currency: string
|
|
24
|
+
route: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** called on every wizard render: keeps the currency the landing URL named */
|
|
28
|
+
export const rememberLocalCurrency = (params: URLSearchParams): void => {
|
|
29
|
+
const currency = params.get('local');
|
|
30
|
+
const route = Number(params.get('route'));
|
|
31
|
+
|
|
32
|
+
if (!currency || !/^[A-Z]{3}$/.test(currency) || !route) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
sessionStorage.setItem(KEY, JSON.stringify({ currency, route }));
|
|
38
|
+
} catch {
|
|
39
|
+
// private mode / storage blocked: the line is a nicety, not a feature
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const stored = (): Stored | null => {
|
|
44
|
+
try {
|
|
45
|
+
const value = JSON.parse(sessionStorage.getItem(KEY) ?? 'null');
|
|
46
|
+
|
|
47
|
+
return value && typeof value.currency === 'string' && typeof value.route === 'number' ? value : null;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** "1,840 ALL" for this route's total, or undefined */
|
|
54
|
+
export const useLocalPrice = (routeId: number | undefined, total: number): string | undefined => {
|
|
55
|
+
const local = stored();
|
|
56
|
+
|
|
57
|
+
const currency = local && routeId && local.route === routeId ? local.currency : null;
|
|
58
|
+
|
|
59
|
+
const { data: rate } = useQuery({
|
|
60
|
+
queryKey: ['exchange-rate-local', currency],
|
|
61
|
+
queryFn: async () => (
|
|
62
|
+
await apiClient.get('/api/exchange-rates/local', { params: { currency } }).then(response => response.data?.rate as number | null)
|
|
63
|
+
),
|
|
64
|
+
enabled: currency !== null,
|
|
65
|
+
staleTime: 10 * 60 * 1000,
|
|
66
|
+
retry: false
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (!currency || !rate || !(total > 0)) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const decimals = WHOLE.includes(currency) ? 0 : 2;
|
|
74
|
+
|
|
75
|
+
const amount = Number((total * rate).toFixed(decimals));
|
|
76
|
+
|
|
77
|
+
return `${ amount.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) } ${ currency }`;
|
|
78
|
+
};
|
|
@@ -4,7 +4,7 @@ import { AiOutlineInfoCircle } from 'react-icons/ai';
|
|
|
4
4
|
import { Button } from '@autobusal/common';
|
|
5
5
|
import About from '../../Step5/Summary/About/About';
|
|
6
6
|
import Amount from '../../Step5/Summary/Amount/Amount';
|
|
7
|
-
import { Container, Panel, Actions, Notice } from './styles';
|
|
7
|
+
import { Container, Panel, Actions, Notice, Local } from './styles';
|
|
8
8
|
import { SubTitle } from '../../styles';
|
|
9
9
|
import { Information } from '../../Step5/Summary/styles';
|
|
10
10
|
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
@@ -25,6 +25,11 @@ interface Props {
|
|
|
25
25
|
addons: AddonItem[]
|
|
26
26
|
coupon?: CouponData
|
|
27
27
|
total: TotalData
|
|
28
|
+
/**
|
|
29
|
+
* Claude - 2026-09-26: the total in the currency Google Travel quoted
|
|
30
|
+
* ("1,840 ALL"), shown under it for a Google arrival - display only.
|
|
31
|
+
*/
|
|
32
|
+
local?: string
|
|
28
33
|
/**
|
|
29
34
|
* The card surcharge already included in `total`, formatted with its
|
|
30
35
|
* currency - undefined when the chosen method carries none.
|
|
@@ -65,7 +70,7 @@ interface Props {
|
|
|
65
70
|
* page and scrolled away while add-ons were being ticked - so the number
|
|
66
71
|
* changed out of sight of the person it was changing for.
|
|
67
72
|
*/
|
|
68
|
-
const Sidebar = ({ step1, step2, step3, fare, addons, coupon, total, surcharge, comission, action, single, pending, t, onPay, children, payments }: Props): JSX.Element => (
|
|
73
|
+
const Sidebar = ({ step1, step2, step3, fare, addons, coupon, total, local, surcharge, comission, action, single, pending, t, onPay, children, payments }: Props): JSX.Element => (
|
|
69
74
|
<Container>
|
|
70
75
|
<Panel className="box">
|
|
71
76
|
<SubTitle>
|
|
@@ -116,6 +121,10 @@ const Sidebar = ({ step1, step2, step3, fare, addons, coupon, total, surcharge,
|
|
|
116
121
|
) }
|
|
117
122
|
|
|
118
123
|
<Amount type="total" value={ total.display } t={ t } />
|
|
124
|
+
|
|
125
|
+
{ local && (
|
|
126
|
+
<Local>{ t('routes_order.step5.summary.local', { ns: 'common', amount: local }) }</Local>
|
|
127
|
+
) }
|
|
119
128
|
</div>
|
|
120
129
|
|
|
121
130
|
<Actions>
|
|
@@ -50,3 +50,11 @@ export const Notice = styled.p`
|
|
|
50
50
|
color: ${ props => props.theme.font.faded };
|
|
51
51
|
text-align: center;
|
|
52
52
|
`;
|
|
53
|
+
|
|
54
|
+
// Claude - 2026-09-26: the total in the currency Google Travel quoted
|
|
55
|
+
export const Local = styled.div`
|
|
56
|
+
margin-top: -4px;
|
|
57
|
+
text-align: right;
|
|
58
|
+
color: ${ props => props.theme.font.faded };
|
|
59
|
+
font-size: ${ props => props.theme.size.s };
|
|
60
|
+
`;
|
package/Found/Route/Route.tsx
CHANGED
|
@@ -3,12 +3,12 @@ import { TFunction } from 'i18next';
|
|
|
3
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
|
-
import { MdOutlineAltRoute } from 'react-icons/md';
|
|
6
|
+
import { MdOutlineAltRoute, MdEventSeat } from 'react-icons/md';
|
|
7
7
|
import { formatDuration, splitPrice } from '../refine';
|
|
8
8
|
import { Button, RouteFeature, Information, Rating } from '@autobusal/common';
|
|
9
9
|
import { useGetSettings } from '@autobusal/providers/services';
|
|
10
10
|
import { createDate, getDayParts } from '@autobusal/utilities';
|
|
11
|
-
import { Container, ButtonDetails, SpecialOffer, FavoriteStop, Trip, Company, Location, Stop, Iso, Time, Price, Amount, Decimals, Currency, PriceNotice, SeatsLeft, LinkCompany, CodeCompany, Details, Detail, ButtonPolicy, TitleDetail, FeaturesItems, Leaves, Day, NextDay, Standard, Stops, StopsItems, StopRow, StopTime, StopDay, StopBorder } from './styles';
|
|
11
|
+
import { Container, ButtonDetails, SpecialOffer, FavoriteStop, Trip, Company, Location, Stop, Iso, Time, Price, Amount, Decimals, Currency, PriceNotice, SeatsLeft, SeatsCompact, LinkCompany, CodeCompany, Details, Detail, ButtonPolicy, TitleDetail, FeaturesItems, Leaves, Day, NextDay, Standard, Stops, StopsItems, StopRow, StopTime, StopDay, StopBorder } from './styles';
|
|
12
12
|
import { FoundData } from '@autobusal/providers/types/routes';
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -195,6 +195,25 @@ const Route = ({ data, type, date, passengers, t, onSave }: Props): JSX.Element
|
|
|
195
195
|
<MdOutlineAltRoute />
|
|
196
196
|
{ data.code }
|
|
197
197
|
</CodeCompany>
|
|
198
|
+
|
|
199
|
+
{ /* Claude - 2026-09-27 (Ferjolt: "on small screens, can we show the
|
|
200
|
+
Seats left inline with operator logo and route code, on the
|
|
201
|
+
right side of the row?"). A seat and the number rather than the
|
|
202
|
+
sentence: the row is short on a phone, and a seat icon ALONE
|
|
203
|
+
could be read as "choose your seat" (a paid add-on) - the
|
|
204
|
+
number is what makes it a count. The full sentence stays for
|
|
205
|
+
screen readers and as the tooltip. Phones only; wider screens
|
|
206
|
+
keep the sentence under the price. */ }
|
|
207
|
+
{ seats !== null && (
|
|
208
|
+
<SeatsCompact
|
|
209
|
+
$low={ seats <= LOW_SEATS }
|
|
210
|
+
title={ t('routes_order.step2.route.seats_left', { count: seats }) }
|
|
211
|
+
aria-label={ t('routes_order.step2.route.seats_left', { count: seats }) }
|
|
212
|
+
>
|
|
213
|
+
<MdEventSeat aria-hidden="true" />
|
|
214
|
+
{ seats }
|
|
215
|
+
</SeatsCompact>
|
|
216
|
+
) }
|
|
198
217
|
</Company>
|
|
199
218
|
|
|
200
219
|
<Location $side="from">
|
package/Found/Route/styles.ts
CHANGED
|
@@ -157,6 +157,18 @@ export const Company = styled.div`
|
|
|
157
157
|
flex-wrap: wrap;
|
|
158
158
|
gap: 8px;
|
|
159
159
|
margin-bottom: 4px;
|
|
160
|
+
|
|
161
|
+
/*
|
|
162
|
+
* Claude - 2026-09-27: the operator's rating as ONE star and the number
|
|
163
|
+
* ("★ 4.3 (3)") on a phone. Measured at 375px: logo 96 + five stars and
|
|
164
|
+
* the score 120 + route code 77 + the seats count 48 is 365px on a 315px
|
|
165
|
+
* row, so the seats count wrapped to a line of its own. The full five
|
|
166
|
+
* stars stay on wider screens, and the whole summary is the tooltip
|
|
167
|
+
* (Rating's title). The rating is the only span before the seats count.
|
|
168
|
+
*/
|
|
169
|
+
& > span:first-of-type svg:not(:first-child) {
|
|
170
|
+
display: none;
|
|
171
|
+
}
|
|
160
172
|
}
|
|
161
173
|
|
|
162
174
|
/**
|
|
@@ -1051,7 +1063,41 @@ export const SeatsLeft = styled.span<{ $low: boolean }>`
|
|
|
1051
1063
|
font-weight: ${ props => (props.$low ? 700 : 400) };
|
|
1052
1064
|
color: ${ props => (props.$low ? props.theme.font.error : props.theme.font.faded) };
|
|
1053
1065
|
|
|
1066
|
+
// Claude - 2026-09-27: on a phone the count moves up to the operator row
|
|
1067
|
+
// (SeatsCompact) - one place to read it, not two
|
|
1068
|
+
@media (max-width: 639px) {
|
|
1069
|
+
display: none;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1054
1072
|
@media (min-width: 768px) {
|
|
1055
1073
|
text-align: center;
|
|
1056
1074
|
}
|
|
1057
1075
|
`;
|
|
1076
|
+
|
|
1077
|
+
/*
|
|
1078
|
+
* Claude - 2026-09-27 (Ferjolt): the seats count on a phone - a seat and the
|
|
1079
|
+
* number, pushed to the right end of the operator row (logo, rating, route
|
|
1080
|
+
* code). Same emphasis as SeatsLeft when few are left.
|
|
1081
|
+
*/
|
|
1082
|
+
export const SeatsCompact = styled.span<{ $low: boolean }>`
|
|
1083
|
+
display: none;
|
|
1084
|
+
|
|
1085
|
+
@media (max-width: 639px) {
|
|
1086
|
+
display: inline-flex;
|
|
1087
|
+
align-items: center;
|
|
1088
|
+
gap: 4px;
|
|
1089
|
+
margin-left: auto;
|
|
1090
|
+
padding: 2px 8px;
|
|
1091
|
+
border-radius: 100px;
|
|
1092
|
+
background: ${ props => (props.$low ? 'rgba(204, 51, 51, .1)' : props.theme.background.neutral) };
|
|
1093
|
+
color: ${ props => (props.$low ? props.theme.font.error : props.theme.font.faded) };
|
|
1094
|
+
font-size: ${ props => props.theme.size.xs };
|
|
1095
|
+
font-weight: 700;
|
|
1096
|
+
line-height: 1.4;
|
|
1097
|
+
white-space: nowrap;
|
|
1098
|
+
|
|
1099
|
+
svg {
|
|
1100
|
+
font-size: 1.15em;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
`;
|
package/RoutesOrder.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useMemo, useRef } from 'react';
|
|
2
2
|
import { useParams, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
|
|
3
|
+
import { rememberLocalCurrency } from './Checkout/LocalPrice';
|
|
3
4
|
import { TFunction } from 'i18next';
|
|
4
5
|
import { AiOutlineSearch, AiOutlineCaretDown, AiOutlineCaretUp } from 'react-icons/ai';
|
|
5
6
|
import { Meta, PageBreadcrumbs } from '@autobusal/common';
|
|
@@ -109,6 +110,10 @@ const RoutesOrder = ({ t }: Props): JSX.Element => {
|
|
|
109
110
|
*/
|
|
110
111
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
|
111
112
|
|
|
113
|
+
// Claude - 2026-09-26: a Google Travel arrival names the currency Google
|
|
114
|
+
// quoted (`local=ALL`); the checkout shows the price in it too
|
|
115
|
+
rememberLocalCurrency(searchParams);
|
|
116
|
+
|
|
112
117
|
const navigate = useNavigate();
|
|
113
118
|
|
|
114
119
|
const location = useLocation();
|