@autobusal/routes-order 1.9.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.
- package/CHANGELOG.md +673 -0
- package/Checkout/Checkout.tsx +519 -0
- package/Checkout/Sidebar/Sidebar.tsx +124 -0
- package/Checkout/Sidebar/styles.ts +52 -0
- package/Facts/Facts.tsx +271 -0
- package/Facts/questions.ts +250 -0
- package/Facts/service.ts +111 -0
- package/Facts/services.ts +19 -0
- package/Facts/styles.ts +112 -0
- package/Facts/types.ts +167 -0
- package/Faq/Faq.tsx +52 -0
- package/Faq/styles.ts +68 -0
- package/Found/Dates/Dates.tsx +59 -5
- package/Found/Dates/styles.ts +137 -7
- package/Found/Empty/Empty.tsx +136 -0
- package/Found/Empty/styles.ts +98 -0
- package/Found/Filters/Filters.tsx +10 -7
- package/Found/Filters/styles.ts +79 -8
- package/Found/Found.tsx +128 -29
- package/Found/Group/Group.tsx +3 -1
- package/Found/Header/styles.ts +20 -4
- package/Found/Orders/Orders.tsx +25 -4
- package/Found/Picks/Picks.tsx +10 -3
- package/Found/Picks/styles.ts +60 -9
- package/Found/Route/Route.tsx +189 -33
- package/Found/Route/styles.ts +488 -50
- package/Found/SameDay/SameDay.tsx +35 -0
- package/Found/SameDay/styles.ts +27 -0
- package/Found/Sort/styles.ts +52 -7
- package/Found/Summary/Summary.tsx +1 -1
- package/Found/Summary/styles.ts +12 -0
- package/Found/Watch/Watch.tsx +187 -0
- package/Found/Watch/styles.ts +118 -0
- package/Found/refine.ts +69 -0
- package/Links/Links.tsx +91 -0
- package/Links/styles.ts +75 -0
- package/RoutesOrder.tsx +239 -31
- package/Schedule/Schedule.tsx +123 -0
- package/Schedule/styles.ts +71 -0
- package/Sections/Schema.tsx +186 -0
- package/Sections/Sections.tsx +160 -0
- package/Sections/styles.ts +44 -0
- package/Step1/prepare.ts +21 -2
- package/Step2.tsx +34 -1
- package/Step4/Passenger/Passenger.tsx +253 -24
- package/Step4/Passenger/styles.ts +21 -1
- package/Step4/Passengers/Display.tsx +10 -2
- package/Step4/Passengers/Passengers.tsx +53 -4
- package/Step4/Saved/Choose.tsx +99 -0
- package/Step4/Saved/Remember.tsx +58 -0
- package/Step4/Saved/styles.ts +87 -0
- package/Step4/checkAge.ts +15 -4
- package/Step5/Addons/Addons.tsx +146 -4
- package/Step5/Addons/styles.ts +94 -0
- package/Step5/Billing/Billing.tsx +138 -70
- package/Step5/Coupons/Coupons.tsx +28 -18
- package/Step5/Coupons/styles.ts +41 -8
- package/Step5/Payments/Payments.tsx +36 -5
- package/Step5/Payments/styles.ts +16 -7
- package/Step5/Summary/About/About.tsx +99 -27
- package/Step5/Summary/About/styles.ts +118 -1
- package/Step5/Summary/styles.ts +12 -5
- package/Step5/utilities.ts +67 -12
- package/Steps/Steps.tsx +3 -1
- package/commerce.ts +24 -0
- package/package.json +1 -1
- package/services.ts +295 -12
- package/styles.ts +126 -0
- package/types.ts +78 -1
- package/Found/Booking/Booking.tsx +0 -61
- package/Found/Booking/styles.ts +0 -50
- package/Step4/Step4.tsx +0 -114
- package/Step5/Step5.tsx +0 -185
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
1
|
+
import { useState, useId } from 'react';
|
|
2
2
|
import { TFunction } from 'i18next';
|
|
3
|
+
import { Required } from '@autobusal/common';
|
|
3
4
|
import { UseFormRegister, FieldErrors } from 'react-hook-form';
|
|
4
5
|
import { RiPencilFill } from 'react-icons/ri';
|
|
5
6
|
import { AiOutlineInfoCircle } from 'react-icons/ai';
|
|
@@ -14,11 +15,58 @@ interface Props {
|
|
|
14
15
|
t: TFunction<'common'>
|
|
15
16
|
errors: FieldErrors
|
|
16
17
|
refs: UseFormRegister<BillingData>
|
|
18
|
+
/**
|
|
19
|
+
* Edited: Claude - Date: 2026-08-21
|
|
20
|
+
*
|
|
21
|
+
* Which fields to ask for. Undefined is the whole block, which is what a
|
|
22
|
+
* guest has always been shown and stays the default.
|
|
23
|
+
*
|
|
24
|
+
* A SIGNED-IN CUSTOMER gets a list instead, holding only the fields their
|
|
25
|
+
* profile cannot answer. `orders_contact`.`phone`/`address`/`city` are
|
|
26
|
+
* NOT NULL, the same three on `visitors` are nullable, and registration
|
|
27
|
+
* never collects them - so a customer could reach this screen unable to
|
|
28
|
+
* fill three columns that refuse to be empty, which the API used to
|
|
29
|
+
* answer with a 500 and then with a booking carrying no phone number.
|
|
30
|
+
* They are asked here instead, in the same boxes and under the same
|
|
31
|
+
* labels a guest sees, rather than being sent out of the checkout to
|
|
32
|
+
* their account page and back.
|
|
33
|
+
*/
|
|
34
|
+
only?: string[]
|
|
17
35
|
}
|
|
18
36
|
|
|
19
|
-
const Billing = ({ errors, t, refs }: Props): JSX.Element => {
|
|
37
|
+
const Billing = ({ errors, t, refs, only }: Props): JSX.Element => {
|
|
20
38
|
const [ company, setCompany ] = useState<boolean>(false);
|
|
21
39
|
|
|
40
|
+
// A field is asked for when nothing narrowed the block, or when it is on
|
|
41
|
+
// the list of gaps.
|
|
42
|
+
const asked = (field: string): boolean => only === undefined || only.includes(field);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Edited: Claude - Date: 2026-08-21
|
|
46
|
+
*
|
|
47
|
+
* Two of these fields are validated differently depending on WHO is
|
|
48
|
+
* filling them in, and the difference is not cosmetic - it mirrors the
|
|
49
|
+
* two sets of rules the API applies. A guest's answers go to
|
|
50
|
+
* `orders_contact` and nowhere else; a signed-in customer's are also
|
|
51
|
+
* written back onto their `visitors` profile, whose `city` column holds
|
|
52
|
+
* 40 characters and whose address rule has always asked for 5. A form
|
|
53
|
+
* that accepted more than the API will store would just move the refusal
|
|
54
|
+
* to after the pay button.
|
|
55
|
+
*/
|
|
56
|
+
const gaps = only !== undefined;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Edited: Claude - Date: 2026-08-19
|
|
60
|
+
* Same fix as the passenger block: the labels here were text sitting
|
|
61
|
+
* beside the field rather than attached to it, so the billing form - the
|
|
62
|
+
* last thing between a buyer and paying - announced as unnamed edit
|
|
63
|
+
* boxes. Prefixed with useId rather than using the field name directly,
|
|
64
|
+
* because ids are page-global and these names are not guaranteed to be.
|
|
65
|
+
*/
|
|
66
|
+
const uid = useId();
|
|
67
|
+
|
|
68
|
+
const id = (field: string): string => `${ uid }${ field }`;
|
|
69
|
+
|
|
22
70
|
const countries = useCountries('all');
|
|
23
71
|
|
|
24
72
|
if (countries.isLoading) {
|
|
@@ -40,120 +88,140 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
|
|
|
40
88
|
{ t('routes_order.step5.billing.title') }
|
|
41
89
|
</SubTitle>
|
|
42
90
|
|
|
43
|
-
|
|
44
|
-
<div className="
|
|
45
|
-
|
|
91
|
+
{ asked('bill_fn') && (
|
|
92
|
+
<div className="column">
|
|
93
|
+
<div className="row">
|
|
94
|
+
<Required htmlFor={ id('bill_fn') }>{ t('routes_order.step5.billing.first_name') }</Required>
|
|
46
95
|
|
|
47
|
-
|
|
96
|
+
<input type="text" id={ id('bill_fn') } { ...refs('bill_fn', Validate('required|min_length:2|max_length:100', t)) } />
|
|
48
97
|
|
|
49
|
-
|
|
50
|
-
|
|
98
|
+
{ Display(errors.bill_fn) }
|
|
99
|
+
</div>
|
|
51
100
|
|
|
52
|
-
|
|
53
|
-
|
|
101
|
+
<div className="row">
|
|
102
|
+
<Required htmlFor={ id('bill_ln') }>{ t('routes_order.step5.billing.last_name') }</Required>
|
|
54
103
|
|
|
55
|
-
|
|
104
|
+
<input type="text" id={ id('bill_ln') } { ...refs('bill_ln', Validate('required|min_length:2|max_length:100', t)) } />
|
|
56
105
|
|
|
57
|
-
|
|
106
|
+
{ Display(errors.bill_ln) }
|
|
107
|
+
</div>
|
|
58
108
|
</div>
|
|
59
|
-
|
|
109
|
+
) }
|
|
60
110
|
|
|
61
111
|
<div className="column">
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
<input type="email" { ...refs('email', Validate('required|email|max_length:255', t)) } />
|
|
112
|
+
{ asked('email') && (
|
|
113
|
+
<div className="row">
|
|
114
|
+
<Required htmlFor={ id('email') }>{ t('routes_order.step5.billing.email') }</Required>
|
|
66
115
|
|
|
67
|
-
|
|
116
|
+
<input type="email" id={ id('email') } { ...refs('email', Validate('required|email|max_length:255', t)) } />
|
|
68
117
|
|
|
69
|
-
|
|
70
|
-
<AiOutlineInfoCircle /> { t('routes_order.step5.billing.email_info') }
|
|
71
|
-
</NoticeEmail>
|
|
72
|
-
</div>
|
|
118
|
+
{ Display(errors.email) }
|
|
73
119
|
|
|
74
|
-
|
|
75
|
-
|
|
120
|
+
<NoticeEmail>
|
|
121
|
+
<AiOutlineInfoCircle /> { t('routes_order.step5.billing.email_info') }
|
|
122
|
+
</NoticeEmail>
|
|
123
|
+
</div>
|
|
124
|
+
) }
|
|
76
125
|
|
|
77
|
-
|
|
126
|
+
{ asked('bill_phone') && (
|
|
127
|
+
<div className="row">
|
|
128
|
+
<Required htmlFor={ id('bill_phone') }>{ t('routes_order.step5.billing.phone') }</Required>
|
|
78
129
|
|
|
79
|
-
|
|
80
|
-
|
|
130
|
+
<input type="text" id={ id('bill_phone') } { ...refs('bill_phone', Validate('required|min_length:10|max_length:20', t)) } />
|
|
131
|
+
|
|
132
|
+
{ Display(errors.bill_phone) }
|
|
133
|
+
</div>
|
|
134
|
+
) }
|
|
81
135
|
</div>
|
|
82
136
|
|
|
83
|
-
|
|
84
|
-
|
|
137
|
+
{ asked('bill_address') && (
|
|
138
|
+
<div className="row">
|
|
139
|
+
<Required htmlFor={ id('bill_address') }>{ t('routes_order.step5.billing.address') }</Required>
|
|
85
140
|
|
|
86
|
-
|
|
141
|
+
<textarea id={ id('bill_address') } { ...refs('bill_address', Validate(`required|min_length:${ gaps ? 5 : 2 }`, t)) }></textarea>
|
|
87
142
|
|
|
88
|
-
|
|
89
|
-
|
|
143
|
+
{ Display(errors.bill_address) }
|
|
144
|
+
</div>
|
|
145
|
+
) }
|
|
90
146
|
|
|
91
147
|
<div className="column">
|
|
92
|
-
|
|
93
|
-
|
|
148
|
+
{ asked('bill_city') && (
|
|
149
|
+
<div className="row">
|
|
150
|
+
<Required htmlFor={ id('bill_city') }>{ t('routes_order.step5.billing.city') }</Required>
|
|
94
151
|
|
|
95
|
-
|
|
152
|
+
<input type="text" id={ id('bill_city') } { ...refs('bill_city', Validate(`required|min_length:2|max_length:${ gaps ? 40 : 100 }`, t)) } />
|
|
96
153
|
|
|
97
|
-
|
|
98
|
-
|
|
154
|
+
{ Display(errors.bill_city) }
|
|
155
|
+
</div>
|
|
156
|
+
) }
|
|
99
157
|
|
|
100
|
-
|
|
101
|
-
|
|
158
|
+
{ asked('bill_country') && (
|
|
159
|
+
<div className="row">
|
|
160
|
+
<Required htmlFor={ id('bill_country') }>{ t('routes_order.step5.billing.country') }</Required>
|
|
102
161
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
162
|
+
<select id={ id('bill_country') } { ...refs('bill_country', Validate('required|min:1', t)) }>
|
|
163
|
+
<option value={ 0 }>{ t('routes_order.step5.billing.country_choose') }</option>
|
|
164
|
+
{ items }
|
|
165
|
+
</select>
|
|
107
166
|
|
|
108
|
-
|
|
109
|
-
|
|
167
|
+
{ Display(errors.bill_country) }
|
|
168
|
+
</div>
|
|
169
|
+
) }
|
|
110
170
|
</div>
|
|
111
171
|
|
|
112
|
-
|
|
113
|
-
<
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
172
|
+
{ asked('bill_company') && (
|
|
173
|
+
<div className="row">
|
|
174
|
+
<label>
|
|
175
|
+
<input type="checkbox" defaultChecked={ false } onClick={ (event: React.MouseEvent<HTMLInputElement>) => onAddCompany(event) } /> { t('routes_order.step5.billing.ask_company') }
|
|
176
|
+
</label>
|
|
177
|
+
</div>
|
|
178
|
+
) }
|
|
117
179
|
|
|
118
|
-
{ company && (
|
|
180
|
+
{ asked('bill_company') && company && (
|
|
119
181
|
<div className="column">
|
|
120
182
|
<div className="row">
|
|
121
|
-
{
|
|
183
|
+
{ /* not Required - these are optional, so a plain <label>
|
|
184
|
+
rather than the asterisked one, but still associated */ }
|
|
185
|
+
<label htmlFor={ id('bill_company') }>{ t('routes_order.step5.billing.company') }</label>
|
|
122
186
|
|
|
123
|
-
<input type="text" { ...refs('bill_company') } />
|
|
187
|
+
<input type="text" id={ id('bill_company') } { ...refs('bill_company') } />
|
|
124
188
|
|
|
125
189
|
{ Display(errors.bill_company) }
|
|
126
190
|
</div>
|
|
127
191
|
|
|
128
192
|
<div className="row">
|
|
129
|
-
{ t('routes_order.step5.billing.nipt') }
|
|
193
|
+
<label htmlFor={ id('bill_nipt') }>{ t('routes_order.step5.billing.nipt') }</label>
|
|
130
194
|
|
|
131
|
-
<input type="text" { ...refs('bill_nipt') } />
|
|
195
|
+
<input type="text" id={ id('bill_nipt') } { ...refs('bill_nipt') } />
|
|
132
196
|
|
|
133
197
|
{ Display(errors.bill_nipt) }
|
|
134
198
|
</div>
|
|
135
199
|
</div>
|
|
136
200
|
) }
|
|
137
201
|
|
|
138
|
-
|
|
139
|
-
|
|
202
|
+
{ asked('comments') && (
|
|
203
|
+
<div className="row">
|
|
204
|
+
<label htmlFor={ id('comments') }>{ t('routes_order.step5.billing.comments') }</label>
|
|
140
205
|
|
|
141
|
-
|
|
142
|
-
|
|
206
|
+
<textarea id={ id('comments') } { ...refs('comments') }></textarea>
|
|
207
|
+
</div>
|
|
208
|
+
) }
|
|
143
209
|
|
|
144
|
-
|
|
145
|
-
<
|
|
146
|
-
<
|
|
210
|
+
{ asked('terms') && (
|
|
211
|
+
<div className="row row-nomargin">
|
|
212
|
+
<label className="single">
|
|
213
|
+
<input type="checkbox" value="1" { ...refs('terms', Validate('required', t)) } />
|
|
147
214
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
215
|
+
<span dangerouslySetInnerHTML={{
|
|
216
|
+
__html: t('routes_order.step5.billing.terms', {
|
|
217
|
+
terms: `<a href="/page/terms" target="_blank">${ t('routes_order.step5.billing.terms_name') }</a>`
|
|
218
|
+
})
|
|
219
|
+
}} />
|
|
220
|
+
</label>
|
|
154
221
|
|
|
155
|
-
|
|
156
|
-
|
|
222
|
+
{ Display(errors.terms) }
|
|
223
|
+
</div>
|
|
224
|
+
) }
|
|
157
225
|
</Container>
|
|
158
226
|
);
|
|
159
227
|
};
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { TFunction } from 'i18next';
|
|
2
2
|
import { useForm } from 'react-hook-form';
|
|
3
3
|
import { RiCoupon3Line } from 'react-icons/ri';
|
|
4
|
-
import { Button } from '@autobusal/common';
|
|
5
4
|
import { Validate, Display } from '@autobusal/utilities';
|
|
6
|
-
import {
|
|
7
|
-
import { Container, ContainerForm, ContainerInput } from './styles';
|
|
5
|
+
import { Container, ContainerForm, ContainerInput, Apply } from './styles';
|
|
8
6
|
import { CouponData } from '@autobusal/providers/types/orders';
|
|
9
7
|
import { FoundData } from '@autobusal/providers/types/routes';
|
|
10
8
|
import { TotalData, CouponForm } from '../../types';
|
|
@@ -46,27 +44,39 @@ const Coupons = ({ current, step2, t, total, onApplied }: Props): (JSX.Element |
|
|
|
46
44
|
return null;
|
|
47
45
|
}
|
|
48
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
49
|
+
*
|
|
50
|
+
* One field with the apply control tucked inside it. In the checkout
|
|
51
|
+
* sidebar this used to be a card of its own, with a heading and a
|
|
52
|
+
* full-width button, taking more room than the totals it exists to
|
|
53
|
+
* change - for something most buyers do not have.
|
|
54
|
+
*
|
|
55
|
+
* The button keeps its accessible name via aria-label and title, so
|
|
56
|
+
* dropping the visible text costs a screen reader nothing.
|
|
57
|
+
*/
|
|
49
58
|
return (
|
|
50
|
-
<Container
|
|
51
|
-
<SubTitle>
|
|
52
|
-
<RiCoupon3Line />
|
|
53
|
-
{ t('routes_order.step5.coupon.title') }
|
|
54
|
-
</SubTitle>
|
|
55
|
-
|
|
59
|
+
<Container>
|
|
56
60
|
<form onSubmit={ handleSubmit(onSubmit) }>
|
|
57
61
|
<ContainerForm>
|
|
58
62
|
<ContainerInput>
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
{ /* Edited: Claude - Date: 2026-08-19
|
|
64
|
+
aria-label rather than a <label for>, because this field has
|
|
65
|
+
no visible label to attach - the placeholder is the only
|
|
66
|
+
wording, and a placeholder disappears the moment you type. */ }
|
|
67
|
+
<input type="text" aria-label={ t('routes_order.step5.coupon.code') } placeholder={ t('routes_order.step5.coupon.code') } { ...register('code', Validate('required|min_length:4', t)) } />
|
|
68
|
+
|
|
69
|
+
<Apply
|
|
70
|
+
type="submit"
|
|
71
|
+
disabled={ isPending }
|
|
72
|
+
aria-label={ t('routes_order.step5.coupon.apply') }
|
|
73
|
+
title={ t('routes_order.step5.coupon.apply') }
|
|
74
|
+
>
|
|
75
|
+
<RiCoupon3Line />
|
|
76
|
+
</Apply>
|
|
61
77
|
</ContainerInput>
|
|
62
78
|
|
|
63
|
-
|
|
64
|
-
type="submit"
|
|
65
|
-
loading={ isPending }
|
|
66
|
-
text={ t('routes_order.step5.coupon.apply') }
|
|
67
|
-
noMargin
|
|
68
|
-
maxWidth={ 175 }
|
|
69
|
-
/>
|
|
79
|
+
{ Display(errors.code) }
|
|
70
80
|
</ContainerForm>
|
|
71
81
|
</form>
|
|
72
82
|
</Container>
|
package/Step5/Coupons/styles.ts
CHANGED
|
@@ -1,20 +1,53 @@
|
|
|
1
1
|
import styled from 'styled-components';
|
|
2
2
|
|
|
3
3
|
export const Container = styled.div`
|
|
4
|
-
margin-top:
|
|
4
|
+
margin-top: 0;
|
|
5
5
|
`;
|
|
6
6
|
|
|
7
7
|
export const ContainerForm = styled.div`
|
|
8
8
|
display: flex;
|
|
9
9
|
flex-direction: column;
|
|
10
|
-
gap:
|
|
11
|
-
|
|
10
|
+
gap: 4px;
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
15
|
+
*
|
|
16
|
+
* The apply control sits INSIDE the field rather than beside it. Beside it
|
|
17
|
+
* in a narrow sidebar, the two either fought for width or stacked into two
|
|
18
|
+
* full-width rows for something most buyers never use.
|
|
19
|
+
*/
|
|
20
|
+
export const ContainerInput = styled.div`
|
|
21
|
+
position: relative;
|
|
12
22
|
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
& > input {
|
|
24
|
+
width: 100%;
|
|
25
|
+
padding-right: 42px;
|
|
15
26
|
}
|
|
16
27
|
`;
|
|
17
28
|
|
|
18
|
-
export const
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
export const Apply = styled.button`
|
|
30
|
+
position: absolute;
|
|
31
|
+
top: 50%;
|
|
32
|
+
right: 6px;
|
|
33
|
+
transform: translateY(-50%);
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
width: 30px;
|
|
38
|
+
height: 30px;
|
|
39
|
+
font-size: ${ props => props.theme.size.l };
|
|
40
|
+
color: ${ props => props.theme.primary.contrast };
|
|
41
|
+
background: ${ props => props.theme.primary.normal };
|
|
42
|
+
border-radius: ${ props => props.theme.borderRadius };
|
|
43
|
+
transition: all 0.2s ease;
|
|
44
|
+
|
|
45
|
+
&:hover {
|
|
46
|
+
opacity: .85;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
&:disabled {
|
|
50
|
+
opacity: .5;
|
|
51
|
+
cursor: default;
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
2
|
import { TFunction } from 'i18next';
|
|
3
3
|
import { RiSecurePaymentLine } from 'react-icons/ri';
|
|
4
|
-
import { FaRegHandPaper, FaMoneyBill } from 'react-icons/fa';
|
|
4
|
+
import { FaRegHandPaper, FaMoneyBill, FaGift } from 'react-icons/fa';
|
|
5
5
|
import { AiOutlineCreditCard } from 'react-icons/ai';
|
|
6
6
|
import { Inline } from '@autobusal/common';
|
|
7
7
|
import { SubTitle } from '../../styles';
|
|
8
8
|
import { Container, ContainerPayments, ButtonPayment } from './styles';
|
|
9
9
|
import { useGetPayments } from '../../services';
|
|
10
|
+
import { getAvailablePayments } from '../utilities';
|
|
10
11
|
|
|
11
12
|
interface Props {
|
|
12
13
|
selected: string
|
|
13
14
|
isPartner: boolean
|
|
15
|
+
routeId?: number | string
|
|
14
16
|
t: TFunction<'common'>
|
|
15
17
|
onChange: (action: string) => void
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
const Payments = ({ selected, isPartner, t, onChange }: Props): JSX.Element => {
|
|
19
|
-
const { data, isLoading, isSuccess } = useGetPayments();
|
|
20
|
+
const Payments = ({ selected, isPartner, routeId, t, onChange }: Props): JSX.Element | null => {
|
|
21
|
+
const { data, isLoading, isSuccess } = useGetPayments(routeId);
|
|
22
|
+
|
|
23
|
+
const available = getAvailablePayments(data, isPartner);
|
|
20
24
|
|
|
21
25
|
useEffect(() => {
|
|
22
26
|
if (isSuccess) {
|
|
@@ -31,15 +35,35 @@ const Payments = ({ selected, isPartner, t, onChange }: Props): JSX.Element => {
|
|
|
31
35
|
|
|
32
36
|
if (isLoading) {
|
|
33
37
|
return (
|
|
34
|
-
<Container
|
|
38
|
+
<Container>
|
|
35
39
|
<Inline text={ t('routes_order.step5.actions.loading') } />
|
|
36
40
|
</Container>
|
|
37
41
|
);
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
/*
|
|
45
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-07
|
|
46
|
+
*
|
|
47
|
+
* One method is not a choice, so there is nothing to draw.
|
|
48
|
+
*
|
|
49
|
+
* A heading reading "Payment Method" over a single button the buyer must
|
|
50
|
+
* press before pressing the real one is a step that only ever has one
|
|
51
|
+
* answer - and it is the last screen before paying, which is the worst
|
|
52
|
+
* place to spend a click. The effect above has already selected it, so the
|
|
53
|
+
* button beside this can simply say "Pay Now".
|
|
54
|
+
*
|
|
55
|
+
* Rendering nothing rather than hiding with CSS: the picker is a control,
|
|
56
|
+
* and a control nobody can act on should not be in the page at all - it
|
|
57
|
+
* would still be reachable by keyboard and still announced by a screen
|
|
58
|
+
* reader as a thing to decide.
|
|
59
|
+
*/
|
|
60
|
+
if (available.length <= 1) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
40
64
|
return (
|
|
41
65
|
<>
|
|
42
|
-
<Container
|
|
66
|
+
<Container>
|
|
43
67
|
<SubTitle>
|
|
44
68
|
<RiSecurePaymentLine />
|
|
45
69
|
{ t('routes_order.step5.actions.title') }
|
|
@@ -67,6 +91,13 @@ const Payments = ({ selected, isPartner, t, onChange }: Props): JSX.Element => {
|
|
|
67
91
|
</ButtonPayment>
|
|
68
92
|
) }
|
|
69
93
|
|
|
94
|
+
{ (data?.complimentary && !isPartner) && (
|
|
95
|
+
<ButtonPayment type="button" $selected={ selected === 'complimentary' } onClick={ () => onChange('complimentary') }>
|
|
96
|
+
<FaGift />
|
|
97
|
+
{ t('routes_order.step5.actions.complimentary') }
|
|
98
|
+
</ButtonPayment>
|
|
99
|
+
) }
|
|
100
|
+
|
|
70
101
|
{ (data?.bkt || isPartner) && (
|
|
71
102
|
<ButtonPayment type="button" $selected={ selected === 'bkt' } onClick={ () => onChange('bkt') }>
|
|
72
103
|
<AiOutlineCreditCard />
|
package/Step5/Payments/styles.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import styled, { css } from 'styled-components';
|
|
2
2
|
|
|
3
3
|
export const Container = styled.div`
|
|
4
|
-
margin-top:
|
|
4
|
+
margin-top: 0;
|
|
5
5
|
`;
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
9
|
+
*
|
|
10
|
+
* All options on one row inside the checkout sidebar. They are a choice of
|
|
11
|
+
* three short labels, not three products, so stacking them into tall cards
|
|
12
|
+
* gave a minor decision more visual weight than the total above it.
|
|
13
|
+
*/
|
|
7
14
|
export const ContainerPayments = styled.div`
|
|
8
15
|
display: flex;
|
|
9
16
|
flex-direction: row;
|
|
10
|
-
|
|
11
|
-
gap: 15px;
|
|
17
|
+
gap: 6px;
|
|
12
18
|
justify-content: center;
|
|
13
19
|
`;
|
|
14
20
|
|
|
@@ -17,9 +23,12 @@ export const ButtonPayment = styled.button<{ $selected: boolean }>`
|
|
|
17
23
|
flex-direction: column;
|
|
18
24
|
align-items: center;
|
|
19
25
|
justify-content: center;
|
|
20
|
-
gap:
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
gap: 4px;
|
|
27
|
+
// equal shares of one row, never wrapping - min-width:0 lets them shrink
|
|
28
|
+
// below their label width instead of pushing each other onto new lines
|
|
29
|
+
flex: 1 1 0;
|
|
30
|
+
min-width: 0;
|
|
31
|
+
padding: 8px 4px;
|
|
23
32
|
text-align: center;
|
|
24
33
|
font-weight: 700;
|
|
25
34
|
border: 1px solid ${ props => props.theme.background.neutral };
|
|
@@ -27,7 +36,7 @@ export const ButtonPayment = styled.button<{ $selected: boolean }>`
|
|
|
27
36
|
transition: all 0.3s ease;
|
|
28
37
|
|
|
29
38
|
& > svg {
|
|
30
|
-
font-size:
|
|
39
|
+
font-size: 22px;
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
&:hover {
|