@autobusal/routes-order 1.8.0 → 1.9.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.
Files changed (76) hide show
  1. package/CHANGELOG.md +731 -0
  2. package/Checkout/Checkout.tsx +519 -0
  3. package/Checkout/Sidebar/Sidebar.tsx +124 -0
  4. package/Checkout/Sidebar/styles.ts +52 -0
  5. package/Facts/Facts.tsx +271 -0
  6. package/Facts/questions.ts +250 -0
  7. package/Facts/service.ts +111 -0
  8. package/Facts/services.ts +19 -0
  9. package/Facts/styles.ts +112 -0
  10. package/Facts/types.ts +167 -0
  11. package/Faq/Faq.tsx +52 -0
  12. package/Faq/styles.ts +68 -0
  13. package/Found/Dates/Dates.tsx +95 -5
  14. package/Found/Dates/styles.ts +173 -9
  15. package/Found/Empty/Empty.tsx +136 -0
  16. package/Found/Empty/styles.ts +98 -0
  17. package/Found/Filters/Filters.tsx +194 -0
  18. package/Found/Filters/styles.ts +188 -0
  19. package/Found/Found.tsx +207 -17
  20. package/Found/Group/Group.tsx +68 -0
  21. package/Found/Group/styles.ts +44 -0
  22. package/Found/Header/styles.ts +20 -4
  23. package/Found/Orders/Orders.tsx +89 -27
  24. package/Found/Orders/styles.ts +14 -0
  25. package/Found/Picks/Picks.tsx +75 -0
  26. package/Found/Picks/styles.ts +106 -0
  27. package/Found/Route/Route.tsx +211 -33
  28. package/Found/Route/styles.ts +491 -46
  29. package/Found/SameDay/SameDay.tsx +35 -0
  30. package/Found/SameDay/styles.ts +27 -0
  31. package/Found/Sort/Sort.tsx +40 -0
  32. package/Found/Sort/styles.ts +87 -0
  33. package/Found/Summary/Summary.tsx +49 -0
  34. package/Found/Summary/styles.ts +42 -0
  35. package/Found/Watch/Watch.tsx +187 -0
  36. package/Found/Watch/styles.ts +118 -0
  37. package/Found/refine.ts +652 -0
  38. package/Links/Links.tsx +91 -0
  39. package/Links/styles.ts +75 -0
  40. package/RoutesOrder.tsx +239 -31
  41. package/Schedule/Schedule.tsx +123 -0
  42. package/Schedule/styles.ts +71 -0
  43. package/Sections/Schema.tsx +186 -0
  44. package/Sections/Sections.tsx +160 -0
  45. package/Sections/styles.ts +44 -0
  46. package/Step1/prepare.ts +21 -2
  47. package/Step2.tsx +34 -1
  48. package/Step4/Passenger/Passenger.tsx +253 -24
  49. package/Step4/Passenger/styles.ts +21 -1
  50. package/Step4/Passengers/Display.tsx +10 -2
  51. package/Step4/Passengers/Passengers.tsx +53 -4
  52. package/Step4/Saved/Choose.tsx +99 -0
  53. package/Step4/Saved/Remember.tsx +58 -0
  54. package/Step4/Saved/styles.ts +87 -0
  55. package/Step4/checkAge.ts +15 -4
  56. package/Step5/Addons/Addons.tsx +146 -4
  57. package/Step5/Addons/styles.ts +94 -0
  58. package/Step5/Billing/Billing.tsx +138 -70
  59. package/Step5/Coupons/Coupons.tsx +28 -18
  60. package/Step5/Coupons/styles.ts +41 -8
  61. package/Step5/Payments/Payments.tsx +36 -5
  62. package/Step5/Payments/styles.ts +16 -7
  63. package/Step5/Summary/About/About.tsx +99 -27
  64. package/Step5/Summary/About/styles.ts +118 -1
  65. package/Step5/Summary/styles.ts +12 -5
  66. package/Step5/utilities.ts +67 -12
  67. package/Steps/Steps.tsx +3 -1
  68. package/commerce.ts +24 -0
  69. package/package.json +1 -1
  70. package/services.ts +302 -13
  71. package/styles.ts +126 -0
  72. package/types.ts +84 -1
  73. package/Found/Booking/Booking.tsx +0 -61
  74. package/Found/Booking/styles.ts +0 -50
  75. package/Step4/Step4.tsx +0 -114
  76. package/Step5/Step5.tsx +0 -185
@@ -1,6 +1,7 @@
1
1
  import { TFunction } from 'i18next';
2
2
  import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';
3
- import { Container, ContainerIcon, Icon, Information, Title, SubTitle, Operator } from './styles';
3
+ import { FaCircleDot, FaLocationDot } from 'react-icons/fa6';
4
+ import { Trip, TripLabel, Container, ContainerIcon, Icon, Information, Title, Operator, Carrier, Logo, Leg, Point, PointIcon, PointCity, Country, PointMeta } from './styles';
4
5
  import { RoutesSearchForm } from '@autobusal/routes-search/types';
5
6
  import { FoundData } from '@autobusal/providers/types/routes';
6
7
 
@@ -8,41 +9,112 @@ interface Props {
8
9
  type: 'departure' | 'return'
9
10
  step1: RoutesSearchForm
10
11
  data?: FoundData
12
+
13
+ /**
14
+ * Which leg of the journey this is, 1-based.
15
+ *
16
+ * Ferjolt Ozuni - Date: 2026-08-06
17
+ * Only passed on a ROUND TRIP - the caller is the one that knows whether
18
+ * there is a second leg, and a one-way has nothing to number.
19
+ */
20
+ number?: number
21
+
11
22
  t: TFunction<'public'>
12
23
  }
13
24
 
14
- const About = ({ type, step1, data, t }: Props): (JSX.Element | null) => {
15
- const icon = type === 'departure' ? <FaArrowRight /> : <FaArrowLeft />;
16
-
25
+ /**
26
+ * One leg of the trip, both ends of it.
27
+ *
28
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
29
+ *
30
+ * This used to render a single line - the departure point on the outbound
31
+ * leg, the arrival point on the return one - so a one-way buyer reached the
32
+ * payment screen having never been shown where the bus actually drops them.
33
+ * Both ends are now stated, origin above destination.
34
+ *
35
+ * The country is named in full rather than coded: this is the last screen
36
+ * before someone pays, and "Thessaloniki, Greece" settles a doubt that
37
+ * "(GRC)" only half answers.
38
+ */
39
+ const About = ({ type, step1, data, number, t }: Props): (JSX.Element | null) => {
17
40
  if (!data) {
18
41
  return null;
19
42
  }
20
43
 
21
- const details = type === 'departure' ? {
22
- date: step1.departure,
23
- hour: data.locations.from.departure,
24
- stop: data.locations.from.stop?.name,
25
- city: data.locations.from.city.name
26
- } : {
27
- date: step1._return,
28
- hour: data.locations.to.arrival,
29
- stop: data.locations.to.stop?.name,
30
- city: data.locations.to.city.name
31
- };
44
+ const icon = type === 'departure' ? <FaArrowRight /> : <FaArrowLeft />;
45
+
46
+ const date = type === 'departure' ? step1.departure : step1._return;
32
47
 
33
48
  return (
34
- <Container>
35
- <ContainerIcon>
36
- <Icon>{ icon }</Icon>
37
- </ContainerIcon>
38
-
39
- <Information>
40
- <Title>{ data.name } ({ data.code })</Title>
41
- <SubTitle>{ t(`routes_order.step5.summary.${ type }`, details) }</SubTitle>
42
- <Operator>{ data.operator.company }</Operator>
43
- </Information>
44
- </Container>
49
+ <Trip>
50
+ { number !== undefined && (
51
+ <TripLabel>{ t('routes_order.step5.summary.trip', { number }) }</TripLabel>
52
+ ) }
53
+
54
+ <Container>
55
+ <ContainerIcon>
56
+ <Icon>{ icon }</Icon>
57
+ </ContainerIcon>
58
+
59
+ <Information>
60
+ <Title>{ data.name } ({ data.code })</Title>
61
+
62
+ { /* Edited: Ferjolt Ozuni - Date: 2026-08-01
63
+ Mark and name together, ranged right. The name used to sit
64
+ alone at the foot of the block, so the logo read as decoration
65
+ rather than as the operator being identified. */ }
66
+ <Carrier>
67
+ { data.operator.logo_url && (
68
+ <Logo src={ data.operator.logo_url } alt={ data.operator.display_name } />
69
+ ) }
70
+
71
+ <Operator>{ data.operator.display_name }</Operator>
72
+ </Carrier>
73
+
74
+ <Leg>
75
+ <Point>
76
+ <PointIcon><FaCircleDot /></PointIcon>
77
+
78
+ <PointCity>
79
+ { data.locations.from.city.name }
80
+ { data.locations.from.city.country?.name && (
81
+ <Country>, { data.locations.from.city.country.name }</Country>
82
+ ) }
83
+ </PointCity>
84
+
85
+ { /* the date and time are what a buyer double-checks before
86
+ paying, so they carry the weight here */ }
87
+ <PointMeta $strong>
88
+ { t('routes_order.step5.summary.departs', {
89
+ date,
90
+ hour: data.locations.from.departure,
91
+ stop: data.locations.from.stop?.name
92
+ }) }
93
+ </PointMeta>
94
+ </Point>
95
+
96
+ <Point>
97
+ <PointIcon><FaLocationDot /></PointIcon>
98
+
99
+ <PointCity>
100
+ { data.locations.to.city.name }
101
+ { data.locations.to.city.country?.name && (
102
+ <Country>, { data.locations.to.city.country.name }</Country>
103
+ ) }
104
+ </PointCity>
105
+
106
+ <PointMeta>
107
+ { t('routes_order.step5.summary.arrives', {
108
+ hour: data.locations.to.arrival,
109
+ stop: data.locations.to.stop?.name
110
+ }) }
111
+ </PointMeta>
112
+ </Point>
113
+ </Leg>
114
+ </Information>
115
+ </Container>
116
+ </Trip>
45
117
  );
46
118
  };
47
119
 
48
- export default About;
120
+ export default About;
@@ -1,5 +1,34 @@
1
1
  import styled from 'styled-components';
2
2
 
3
+ /**
4
+ * One leg, with its number above it.
5
+ *
6
+ * Ferjolt Ozuni - Date: 2026-08-06
7
+ * This wrapper exists so the label can sit ACROSS the leg rather than beside
8
+ * the arrow: Container below is the icon-and-content row, and a heading
9
+ * placed inside it would indent into the content column and read as part of
10
+ * the route name.
11
+ */
12
+ export const Trip = styled.div`
13
+ flex: 1;
14
+ `;
15
+
16
+ /**
17
+ * "Trip 1", "Trip 2" - only rendered when there are two.
18
+ *
19
+ * Ferjolt Ozuni - Date: 2026-08-06
20
+ * A one-way booking has nothing to enumerate, and numbering a lone leg would
21
+ * invite the reader to go looking for the other one.
22
+ */
23
+ export const TripLabel = styled.div`
24
+ margin-bottom: 8px;
25
+ color: ${ props => props.theme.font.faded };
26
+ font-size: ${ props => props.theme.size.xs };
27
+ font-weight: 700;
28
+ text-transform: uppercase;
29
+ letter-spacing: 0.06em;
30
+ `;
31
+
3
32
  export const Container = styled.div`
4
33
  flex: 1;
5
34
  display: flex;
@@ -42,8 +71,96 @@ export const SubTitle = styled.div`
42
71
  border-radius: ${ props => props.theme.borderRadius };
43
72
  `;
44
73
 
74
+ /**
75
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
76
+ *
77
+ * Origin above destination, each with a city line and a detail line.
78
+ * Replaces the single pill that carried only one end of the leg.
79
+ */
80
+ /**
81
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
82
+ *
83
+ * The line runs down the Leg itself, not down each Point, so it reads as
84
+ * one continuous journey rather than two separate bars. Each point's icon
85
+ * then sits ON that line - centred over it with a background matching the
86
+ * card, so the line appears to pass behind the icon.
87
+ */
88
+ export const Leg = styled.div`
89
+ position: relative;
90
+ display: flex;
91
+ flex-direction: column;
92
+ gap: 12px;
93
+ margin: 10px 0;
94
+ padding-left: 26px;
95
+
96
+ &::before {
97
+ content: '';
98
+ position: absolute;
99
+ left: 8px;
100
+ top: 6px;
101
+ bottom: 6px;
102
+ width: 2px;
103
+ background: ${ props => props.theme.background.neutral };
104
+ }
105
+ `;
106
+
107
+ export const Point = styled.div`
108
+ position: relative;
109
+ display: flex;
110
+ flex-direction: column;
111
+ `;
112
+
113
+ export const PointIcon = styled.span`
114
+ position: absolute;
115
+ left: -26px;
116
+ top: 0;
117
+ display: flex;
118
+ align-items: center;
119
+ justify-content: center;
120
+ width: 18px;
121
+ height: 18px;
122
+ font-size: 11px;
123
+ color: ${ props => props.theme.primary.contrast };
124
+ background: ${ props => props.theme.primary.normal };
125
+ border-radius: 100px;
126
+ `;
127
+
128
+ export const PointCity = styled.div`
129
+ font-weight: 700;
130
+ `;
131
+
132
+ export const Country = styled.span`
133
+ font-weight: 400;
134
+ color: ${ props => props.theme.font.faded };
135
+ `;
136
+
137
+ export const PointMeta = styled.div<{ $strong?: boolean }>`
138
+ font-size: calc(${ props => props.theme.size.s } - 1px);
139
+ font-weight: ${ props => props.$strong ? 700 : 400 };
140
+ color: ${ props => props.$strong ? props.theme.font.normal : props.theme.font.faded };
141
+ `;
142
+
143
+ // Edited: Ferjolt Ozuni - Date: 2026-08-01
144
+ // The operator's mark under the route name - who the buyer is actually
145
+ // travelling with, which a company name alone conveys less quickly.
146
+ export const Carrier = styled.div`
147
+ display: flex;
148
+ align-items: center;
149
+ justify-content: flex-end;
150
+ gap: 8px;
151
+ margin: 6px 0;
152
+ `;
153
+
154
+ export const Logo = styled.img`
155
+ display: block;
156
+ max-width: 90px;
157
+ max-height: 30px;
158
+ object-fit: contain;
159
+ `;
160
+
45
161
  export const Operator = styled.div`
46
- padding-left: 10px;
47
162
  font-size: calc(${ props => props.theme.size.s } - 1px);
163
+ font-weight: 700;
164
+ text-align: right;
48
165
  color: ${ props => props.theme.font.faded };
49
166
  `;
@@ -1,11 +1,18 @@
1
1
  import styled from 'styled-components';
2
2
 
3
+ /**
4
+ * The legs of the booking, stacked.
5
+ *
6
+ * Edited: Ferjolt Ozuni - Date: 2026-08-06
7
+ * They used to sit SIDE BY SIDE from 768px up. That was written when the
8
+ * summary spanned the page; it now lives in the checkout sidebar, a narrow
9
+ * column, where two legs abreast left each one about half the room a single
10
+ * line like "Terminali Lindor Interurban dhe Nderkombetar i Autobusave
11
+ * (TEG)" needs. Vertical at every width, so each leg gets the panel's full
12
+ * width and the two read in the order they are travelled.
13
+ */
3
14
  export const Information = styled.div`
4
15
  display: flex;
5
16
  flex-direction: column;
6
- gap: 15px;
7
-
8
- @media (min-width: 768px) {
9
- flex-direction: row;
10
- }
17
+ gap: 20px;
11
18
  `;
@@ -1,24 +1,79 @@
1
1
  import { FoundData } from '@autobusal/providers/types/routes';
2
2
  import { CouponData } from '@autobusal/providers/types/orders';
3
- import { TotalData } from '../types';
3
+ import { ActionData, TotalData } from '../types';
4
4
 
5
- export const getTotal = (step2: FoundData, step3?: FoundData): TotalData => {
6
- // we get the currency
7
- const currency = step2.price.display.split(' ');
5
+ /*
6
+ * Edited: Claude - Date: 2026-08-28 (sweep, low)
7
+ *
8
+ * The server formats thousands with a SPACE ('1 250.00 EUR', Fundable
9
+ * 2026-08-19), so split(' ')[0]/[1] read the wrong pieces the moment a
10
+ * basket crosses 1000 - the currency became '250.00' and a couponed total
11
+ * became the number 1, corrupting the rendered total and the GA4 values.
12
+ * The currency is the LAST token; the amount is everything else with the
13
+ * grouping spaces stripped.
14
+ */
15
+ export const currencyOf = (display: string): string => (
16
+ display.trim().split(' ').pop() ?? ''
17
+ );
18
+
19
+ export const amountOf = (display: string): number => (
20
+ Number(display.trim().split(' ').slice(0, -1).join(''))
21
+ );
8
22
 
23
+ export const getTotal = (step2: FoundData, step3?: FoundData): TotalData => {
9
24
  const total = step2.price.value + (step3?.price.value ?? 0);
10
25
 
11
26
  return {
12
- display: `${ total } ${ currency[1] }`,
27
+ display: `${ total } ${ currencyOf(step2.price.display) }`,
13
28
  value: total
14
29
  };
15
30
  };
16
31
 
17
- export const convertCoupon = (data: CouponData): TotalData => {
18
- const currency = data.total.split(' ');
19
-
20
- return {
32
+ export const convertCoupon = (data: CouponData): TotalData => (
33
+ {
21
34
  display: data.total,
22
- value: Number(currency[0])
23
- };
24
- };
35
+ value: amountOf(data.total)
36
+ }
37
+ );
38
+
39
+ /**
40
+ * The payment methods actually offered on this checkout, in the order the
41
+ * picker shows them.
42
+ *
43
+ * Ferjolt Ozuni - Date: 2026-08-07
44
+ *
45
+ * Pulled out of Payments because two places need the same answer and they
46
+ * must not disagree: the picker decides what to draw, and the button decides
47
+ * whether it is asking the buyer to choose ("Make Payment", after picking a
48
+ * method) or simply to pay ("Pay Now", when there is nothing to pick). A
49
+ * second copy of these conditions would eventually drift and produce a
50
+ * button offering a choice that is not on screen.
51
+ *
52
+ * The conditions themselves are unchanged, including their oddities: `cash`
53
+ * rides on the `funds` flag rather than one of its own, and a partner is
54
+ * offered every card gateway regardless of what the label enabled. Both were
55
+ * already true; this is a move, not a rewrite.
56
+ */
57
+ export const getAvailablePayments = (data?: ActionData, isPartner = false): string[] => {
58
+ if (!data) {
59
+ return [];
60
+ }
61
+
62
+ const available: string[] = [];
63
+
64
+ if (data.reserve && !isPartner) { available.push('reserve'); }
65
+ if (data.funds && !isPartner) { available.push('funds', 'cash'); }
66
+
67
+ // Claude - 2026-08-22 (audit decision BOOK-07): an operator/employee on
68
+ // their own operator's route pays nothing - the flag is route-aware
69
+ // (services.ts) and re-checked server-side at payment time.
70
+ if (data.complimentary && !isPartner) { available.push('complimentary'); }
71
+
72
+ if (data.bkt || isPartner) { available.push('bkt'); }
73
+ if (data.stripe || isPartner) { available.push('stripe'); }
74
+ if (data.mollie || isPartner) { available.push('mollie'); }
75
+ if (data.raiffeisen || isPartner) { available.push('raiffeisen'); }
76
+ if (data.nexi || isPartner) { available.push('nexi'); }
77
+
78
+ return available;
79
+ };
package/Steps/Steps.tsx CHANGED
@@ -8,7 +8,9 @@ interface Props {
8
8
  const Steps = ({ value }: Props): JSX.Element => {
9
9
  const items: JSX.Element[] = [];
10
10
 
11
- for (let i = 1; i <= 5; i++) {
11
+ // Edited: Ferjolt Ozuni - Date: 2026-08-01
12
+ // Four, not five: passenger details and checkout are one screen now.
13
+ for (let i = 1; i <= 4; i++) {
12
14
  items.push(
13
15
  <Item key={ i }>
14
16
  <Content $selected={ value > i }>
package/commerce.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { CommerceItem } from '@autobusal/providers/Setup/commerce';
2
+ import { FoundData } from '@autobusal/providers/types/routes';
3
+
4
+ /**
5
+ * A coach, as the analytics layer sees it.
6
+ *
7
+ * Edited: Ferjolt Ozuni - Date: 2026-08-05
8
+ *
9
+ * One mapping used by every event, so a route cannot be identified one way
10
+ * in `select_item` and another in `purchase` - which is exactly how a funnel
11
+ * ends up unable to join its own steps together.
12
+ *
13
+ * The ROUTE CODE is the id rather than the numeric key: it is what appears
14
+ * on the ticket, in the timetable and on the operator's own paperwork, so a
15
+ * report built on it can be reconciled against something a human recognises.
16
+ */
17
+ export const item = (route: FoundData): CommerceItem => ({
18
+ item_id: String(route.code ?? route.id),
19
+ item_name: `${ route.locations?.from?.city?.name ?? '' } - ${ route.locations?.to?.city?.name ?? '' }`.trim(),
20
+ item_brand: route.operator?.company,
21
+ price: Number(route.price?.value ?? 0),
22
+ quantity: 1
23
+ });
24
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/routes-order",
3
- "version": "1.8.0",
3
+ "version": "1.9.1",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"