@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.
Files changed (73) hide show
  1. package/CHANGELOG.md +673 -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 +59 -5
  14. package/Found/Dates/styles.ts +137 -7
  15. package/Found/Empty/Empty.tsx +136 -0
  16. package/Found/Empty/styles.ts +98 -0
  17. package/Found/Filters/Filters.tsx +10 -7
  18. package/Found/Filters/styles.ts +79 -8
  19. package/Found/Found.tsx +128 -29
  20. package/Found/Group/Group.tsx +3 -1
  21. package/Found/Header/styles.ts +20 -4
  22. package/Found/Orders/Orders.tsx +25 -4
  23. package/Found/Picks/Picks.tsx +10 -3
  24. package/Found/Picks/styles.ts +60 -9
  25. package/Found/Route/Route.tsx +189 -33
  26. package/Found/Route/styles.ts +488 -50
  27. package/Found/SameDay/SameDay.tsx +35 -0
  28. package/Found/SameDay/styles.ts +27 -0
  29. package/Found/Sort/styles.ts +52 -7
  30. package/Found/Summary/Summary.tsx +1 -1
  31. package/Found/Summary/styles.ts +12 -0
  32. package/Found/Watch/Watch.tsx +187 -0
  33. package/Found/Watch/styles.ts +118 -0
  34. package/Found/refine.ts +69 -0
  35. package/Links/Links.tsx +91 -0
  36. package/Links/styles.ts +75 -0
  37. package/RoutesOrder.tsx +239 -31
  38. package/Schedule/Schedule.tsx +123 -0
  39. package/Schedule/styles.ts +71 -0
  40. package/Sections/Schema.tsx +186 -0
  41. package/Sections/Sections.tsx +160 -0
  42. package/Sections/styles.ts +44 -0
  43. package/Step1/prepare.ts +21 -2
  44. package/Step2.tsx +34 -1
  45. package/Step4/Passenger/Passenger.tsx +253 -24
  46. package/Step4/Passenger/styles.ts +21 -1
  47. package/Step4/Passengers/Display.tsx +10 -2
  48. package/Step4/Passengers/Passengers.tsx +53 -4
  49. package/Step4/Saved/Choose.tsx +99 -0
  50. package/Step4/Saved/Remember.tsx +58 -0
  51. package/Step4/Saved/styles.ts +87 -0
  52. package/Step4/checkAge.ts +15 -4
  53. package/Step5/Addons/Addons.tsx +146 -4
  54. package/Step5/Addons/styles.ts +94 -0
  55. package/Step5/Billing/Billing.tsx +138 -70
  56. package/Step5/Coupons/Coupons.tsx +28 -18
  57. package/Step5/Coupons/styles.ts +41 -8
  58. package/Step5/Payments/Payments.tsx +36 -5
  59. package/Step5/Payments/styles.ts +16 -7
  60. package/Step5/Summary/About/About.tsx +99 -27
  61. package/Step5/Summary/About/styles.ts +118 -1
  62. package/Step5/Summary/styles.ts +12 -5
  63. package/Step5/utilities.ts +67 -12
  64. package/Steps/Steps.tsx +3 -1
  65. package/commerce.ts +24 -0
  66. package/package.json +1 -1
  67. package/services.ts +295 -12
  68. package/styles.ts +126 -0
  69. package/types.ts +78 -1
  70. package/Found/Booking/Booking.tsx +0 -61
  71. package/Found/Booking/styles.ts +0 -50
  72. package/Step4/Step4.tsx +0 -114
  73. 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
- <div className="column">
44
- <div className="row">
45
- { t('routes_order.step5.billing.first_name') }
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
- <input type="text" { ...refs('bill_fn', Validate('required|min_length:2|max_length:100', t)) } />
96
+ <input type="text" id={ id('bill_fn') } { ...refs('bill_fn', Validate('required|min_length:2|max_length:100', t)) } />
48
97
 
49
- { Display(errors.bill_fn) }
50
- </div>
98
+ { Display(errors.bill_fn) }
99
+ </div>
51
100
 
52
- <div className="row">
53
- { t('routes_order.step5.billing.last_name') }
101
+ <div className="row">
102
+ <Required htmlFor={ id('bill_ln') }>{ t('routes_order.step5.billing.last_name') }</Required>
54
103
 
55
- <input type="text" { ...refs('bill_ln', Validate('required|min_length:2|max_length:100', t)) } />
104
+ <input type="text" id={ id('bill_ln') } { ...refs('bill_ln', Validate('required|min_length:2|max_length:100', t)) } />
56
105
 
57
- { Display(errors.bill_ln) }
106
+ { Display(errors.bill_ln) }
107
+ </div>
58
108
  </div>
59
- </div>
109
+ ) }
60
110
 
61
111
  <div className="column">
62
- <div className="row">
63
- { t('routes_order.step5.billing.email') }
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
- { Display(errors.email) }
116
+ <input type="email" id={ id('email') } { ...refs('email', Validate('required|email|max_length:255', t)) } />
68
117
 
69
- <NoticeEmail>
70
- <AiOutlineInfoCircle /> { t('routes_order.step5.billing.email_info') }
71
- </NoticeEmail>
72
- </div>
118
+ { Display(errors.email) }
73
119
 
74
- <div className="row">
75
- { t('routes_order.step5.billing.phone') }
120
+ <NoticeEmail>
121
+ <AiOutlineInfoCircle /> { t('routes_order.step5.billing.email_info') }
122
+ </NoticeEmail>
123
+ </div>
124
+ ) }
76
125
 
77
- <input type="text" { ...refs('bill_phone', Validate('required|min_length:10|max_length:20', t)) } />
126
+ { asked('bill_phone') && (
127
+ <div className="row">
128
+ <Required htmlFor={ id('bill_phone') }>{ t('routes_order.step5.billing.phone') }</Required>
78
129
 
79
- { Display(errors.bill_phone) }
80
- </div>
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
- <div className="row">
84
- { t('routes_order.step5.billing.address') }
137
+ { asked('bill_address') && (
138
+ <div className="row">
139
+ <Required htmlFor={ id('bill_address') }>{ t('routes_order.step5.billing.address') }</Required>
85
140
 
86
- <textarea { ...refs('bill_address', Validate('required|min_length:2', t)) }></textarea>
141
+ <textarea id={ id('bill_address') } { ...refs('bill_address', Validate(`required|min_length:${ gaps ? 5 : 2 }`, t)) }></textarea>
87
142
 
88
- { Display(errors.bill_address) }
89
- </div>
143
+ { Display(errors.bill_address) }
144
+ </div>
145
+ ) }
90
146
 
91
147
  <div className="column">
92
- <div className="row">
93
- { t('routes_order.step5.billing.city') }
148
+ { asked('bill_city') && (
149
+ <div className="row">
150
+ <Required htmlFor={ id('bill_city') }>{ t('routes_order.step5.billing.city') }</Required>
94
151
 
95
- <input type="text" { ...refs('bill_city', Validate('required|min_length:2|max_length:100', t)) } />
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
- { Display(errors.bill_city) }
98
- </div>
154
+ { Display(errors.bill_city) }
155
+ </div>
156
+ ) }
99
157
 
100
- <div className="row">
101
- { t('routes_order.step5.billing.country') }
158
+ { asked('bill_country') && (
159
+ <div className="row">
160
+ <Required htmlFor={ id('bill_country') }>{ t('routes_order.step5.billing.country') }</Required>
102
161
 
103
- <select { ...refs('bill_country', Validate('required|min:1', t)) }>
104
- <option value={ 0 }>{ t('routes_order.step5.billing.country_choose') }</option>
105
- { items }
106
- </select>
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
- { Display(errors.bill_country) }
109
- </div>
167
+ { Display(errors.bill_country) }
168
+ </div>
169
+ ) }
110
170
  </div>
111
171
 
112
- <div className="row">
113
- <label>
114
- <input type="checkbox" defaultChecked={ false } onClick={ (event: React.MouseEvent<HTMLInputElement>) => onAddCompany(event) } /> { t('routes_order.step5.billing.ask_company') }
115
- </label>
116
- </div>
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
- { t('routes_order.step5.billing.company') }
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
- <div className="row">
139
- { t('routes_order.step5.billing.comments') }
202
+ { asked('comments') && (
203
+ <div className="row">
204
+ <label htmlFor={ id('comments') }>{ t('routes_order.step5.billing.comments') }</label>
140
205
 
141
- <textarea { ...refs('comments') }></textarea>
142
- </div>
206
+ <textarea id={ id('comments') } { ...refs('comments') }></textarea>
207
+ </div>
208
+ ) }
143
209
 
144
- <div className="row row-nomargin">
145
- <label className="single">
146
- <input type="checkbox" value="1" { ...refs('terms', Validate('required', t)) } />&nbsp;
210
+ { asked('terms') && (
211
+ <div className="row row-nomargin">
212
+ <label className="single">
213
+ <input type="checkbox" value="1" { ...refs('terms', Validate('required', t)) } />&nbsp;
147
214
 
148
- <span dangerouslySetInnerHTML={{
149
- __html: t('routes_order.step5.billing.terms', {
150
- terms: `<a href="/page/terms" target="_blank">${ t('routes_order.step5.billing.terms_name') }</a>`
151
- })
152
- }} />
153
- </label>
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
- { Display(errors.terms) }
156
- </div>
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 { SubTitle } from '../../styles';
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 className="box">
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
- <input type="text" placeholder={ t('routes_order.step5.coupon.code') } { ...register('code', Validate('required|min_length:4', t)) } />
60
- { Display(errors.code) }
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
- <Button
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>
@@ -1,20 +1,53 @@
1
1
  import styled from 'styled-components';
2
2
 
3
3
  export const Container = styled.div`
4
- margin-top: 25px;
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: 15px;
11
- max-width: 450px;
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
- @media (min-width: 480px) {
14
- flex-direction: row;
23
+ & > input {
24
+ width: 100%;
25
+ padding-right: 42px;
15
26
  }
16
27
  `;
17
28
 
18
- export const ContainerInput = styled.div`
19
- flex: 1;
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 className="box">
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 className="box">
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 />
@@ -1,14 +1,20 @@
1
1
  import styled, { css } from 'styled-components';
2
2
 
3
3
  export const Container = styled.div`
4
- margin-top: 25px;
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
- flex-wrap: wrap;
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: 10px;
21
- flex-basis: 150px;
22
- padding: 10px;
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: 35px;
39
+ font-size: 22px;
31
40
  }
32
41
 
33
42
  &:hover {