@autobusal/routes-order 1.9.3 → 1.11.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/CHANGELOG.md +122 -0
- package/Checkout/Checkout.tsx +280 -0
- package/Checkout/Sidebar/Sidebar.tsx +110 -0
- package/Checkout/Sidebar/styles.ts +52 -0
- package/Found/Empty/Empty.tsx +109 -0
- package/Found/Empty/styles.ts +99 -0
- package/Found/Found.tsx +19 -2
- package/Found/Header/styles.ts +20 -4
- package/Found/Orders/Orders.tsx +13 -4
- package/Found/Route/Route.tsx +25 -14
- package/Found/Route/styles.ts +61 -23
- package/Found/refine.ts +26 -0
- package/RoutesOrder.tsx +69 -23
- package/Step2.tsx +34 -1
- package/Step4/Passenger/Passenger.tsx +10 -10
- package/Step5/Billing/Billing.tsx +8 -7
- package/Step5/Coupons/Coupons.tsx +23 -17
- package/Step5/Coupons/styles.ts +41 -8
- package/Step5/Payments/Payments.tsx +2 -2
- package/Step5/Payments/styles.ts +16 -7
- package/Step5/Summary/About/About.tsx +73 -17
- package/Step5/Summary/About/styles.ts +89 -1
- package/Steps/Steps.tsx +3 -1
- package/package.json +1 -1
- package/services.ts +46 -4
- package/styles.ts +77 -0
- package/types.ts +24 -0
- package/Step4/Step4.tsx +0 -114
- package/Step5/Step5.tsx +0 -185
package/styles.ts
CHANGED
|
@@ -23,6 +23,83 @@ export const Actions = styled.div`
|
|
|
23
23
|
margin-top: 25px;
|
|
24
24
|
`;
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* The search form kept on screen while browsing results.
|
|
28
|
+
*
|
|
29
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
30
|
+
*
|
|
31
|
+
* Changing a date or a city used to mean going Back to Search and losing
|
|
32
|
+
* your place. The form stays available above the results so an adjustment
|
|
33
|
+
* is one edit rather than a round trip.
|
|
34
|
+
*/
|
|
35
|
+
export const SearchAgain = styled.div<{ $background: string }>`
|
|
36
|
+
position: relative;
|
|
37
|
+
margin: -35px calc(50% - 50vw) 20px;
|
|
38
|
+
padding: 18px calc(50vw - 50%);
|
|
39
|
+
background-image: url('${ props => props.$background }');
|
|
40
|
+
background-size: cover;
|
|
41
|
+
background-repeat: no-repeat;
|
|
42
|
+
background-position: center;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Same scrim as the homepage hero, for the same reason: the artwork a
|
|
46
|
+
* brand supplies may be light or dark, and the controls on top of it must
|
|
47
|
+
* read either way.
|
|
48
|
+
*/
|
|
49
|
+
&::before {
|
|
50
|
+
content: '';
|
|
51
|
+
position: absolute;
|
|
52
|
+
inset: 0;
|
|
53
|
+
pointer-events: none;
|
|
54
|
+
background: rgba(0, 0, 0, .45);
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
export const SearchInner = styled.div`
|
|
59
|
+
position: relative;
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
export const SearchToggle = styled.button`
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
gap: 8px;
|
|
66
|
+
padding: 6px 14px;
|
|
67
|
+
font-size: ${ props => props.theme.size.s };
|
|
68
|
+
font-weight: 700;
|
|
69
|
+
color: #FFF;
|
|
70
|
+
border: 1px solid rgba(255, 255, 255, .45);
|
|
71
|
+
border-radius: 100px;
|
|
72
|
+
transition: all 0.2s ease;
|
|
73
|
+
|
|
74
|
+
&:hover {
|
|
75
|
+
border-color: #FFF;
|
|
76
|
+
background: rgba(255, 255, 255, .12);
|
|
77
|
+
}
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
export const SearchSummary = styled.span`
|
|
81
|
+
overflow: hidden;
|
|
82
|
+
text-overflow: ellipsis;
|
|
83
|
+
white-space: nowrap;
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
88
|
+
*
|
|
89
|
+
* Collapsed by default: the form is there for the minority who want to
|
|
90
|
+
* change something, and left open it pushed the results themselves below
|
|
91
|
+
* the fold on the page whose whole job is showing results.
|
|
92
|
+
*/
|
|
93
|
+
export const SearchPanel = styled.div<{ $open: boolean }>`
|
|
94
|
+
display: ${ props => props.$open ? 'block' : 'none' };
|
|
95
|
+
margin-top: 14px;
|
|
96
|
+
padding: 20px;
|
|
97
|
+
background: ${ props => props.theme.header.transparent };
|
|
98
|
+
backdrop-filter: blur(25px);
|
|
99
|
+
-webkit-backdrop-filter: blur(25px);
|
|
100
|
+
border-radius: ${ props => props.theme.borderRadius };
|
|
101
|
+
`;
|
|
102
|
+
|
|
26
103
|
/**
|
|
27
104
|
* Results layout: filters beside the list, not above it.
|
|
28
105
|
*
|
package/types.ts
CHANGED
|
@@ -10,6 +10,30 @@ export interface FoundDay {
|
|
|
10
10
|
from_price_display?: string | null
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
15
|
+
* Alternatives offered when a search finds nothing - see obtapi's
|
|
16
|
+
* Routes\Search\Nearby.
|
|
17
|
+
*/
|
|
18
|
+
export interface NearbyCity {
|
|
19
|
+
city: {
|
|
20
|
+
id: number
|
|
21
|
+
name: string
|
|
22
|
+
slug: string
|
|
23
|
+
}
|
|
24
|
+
/** kilometres from the city the passenger actually asked for */
|
|
25
|
+
distance: number
|
|
26
|
+
routes: number
|
|
27
|
+
from_price: number
|
|
28
|
+
from_price_display: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AlternativesData {
|
|
32
|
+
from: NearbyCity[]
|
|
33
|
+
to: NearbyCity[]
|
|
34
|
+
dates: FoundDay[]
|
|
35
|
+
}
|
|
36
|
+
|
|
13
37
|
export interface CouponForm {
|
|
14
38
|
code: string
|
|
15
39
|
}
|
package/Step4/Step4.tsx
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { TFunction } from 'i18next';
|
|
2
|
-
import { useForm } from 'react-hook-form';
|
|
3
|
-
import { HiUsers } from 'react-icons/hi';
|
|
4
|
-
import { RiShoppingCartLine } from 'react-icons/ri';
|
|
5
|
-
import { AiOutlineCaretLeft } from 'react-icons/ai';
|
|
6
|
-
import { useBusOccupied } from '@autobusal/hooks';
|
|
7
|
-
import { Button } from '@autobusal/common';
|
|
8
|
-
import Steps from '../Steps/Steps';
|
|
9
|
-
import Loading from './Loading';
|
|
10
|
-
import Passengers from './Passengers/Passengers';
|
|
11
|
-
import checkAge from './checkAge';
|
|
12
|
-
import { Title, Actions } from '../styles';
|
|
13
|
-
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
14
|
-
import { PersonData } from '@autobusal/providers/types/persons';
|
|
15
|
-
import { FoundData } from '@autobusal/providers/types/routes';
|
|
16
|
-
|
|
17
|
-
interface Props {
|
|
18
|
-
values?: PersonData
|
|
19
|
-
step1: RoutesSearchForm
|
|
20
|
-
step2: FoundData
|
|
21
|
-
step3?: FoundData
|
|
22
|
-
isPartner?: boolean
|
|
23
|
-
t: TFunction<'common'>
|
|
24
|
-
onSave: (data: PersonData) => void
|
|
25
|
-
onBack: (step: number) => void
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Edited: Ferjolt Ozuni - Date: 2026-07-25
|
|
29
|
-
// Routes without a passenger_fields config keep the pre-feature form
|
|
30
|
-
const DEFAULT_FIELDS = ['sex', 'passport'];
|
|
31
|
-
|
|
32
|
-
const Step4 = ({ values, step1, step2, step3, isPartner, t, onSave, onBack }: Props): JSX.Element => {
|
|
33
|
-
const { register, handleSubmit, formState: { errors }, setValue, setError } = useForm<PersonData>();
|
|
34
|
-
|
|
35
|
-
// Edited: Ferjolt Ozuni - Date: 2026-07-25
|
|
36
|
-
// one form serves both legs of a return trip, so it collects the UNION of
|
|
37
|
-
// what either route requires (mirrored by the backend rules)
|
|
38
|
-
const fields = Array.from(new Set([
|
|
39
|
-
...(step2.trip?.passenger_fields ?? DEFAULT_FIELDS),
|
|
40
|
-
...(step3 !== undefined ? (step3.trip?.passenger_fields ?? DEFAULT_FIELDS) : [])
|
|
41
|
-
]));
|
|
42
|
-
|
|
43
|
-
// Edited: Ferjolt Ozuni - Date: 2026-07-27
|
|
44
|
-
// FoundData.id is widened to (number | string) for external-provider
|
|
45
|
-
// offers (Mode A: search + redirect), but those never reach Step4 -
|
|
46
|
-
// Found/Route.tsx's CTA opens the provider's click-out link for them
|
|
47
|
-
// instead of calling onSave, so a real local numeric id is guaranteed here.
|
|
48
|
-
const busFrom = useBusOccupied(step1.departure, step2.id as number);
|
|
49
|
-
const busTo = useBusOccupied(step1._return, step3?.id as (number | undefined));
|
|
50
|
-
|
|
51
|
-
if (busFrom === undefined || busTo === undefined) {
|
|
52
|
-
return <Loading t={ t } />;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const onSubmit = (data: PersonData): void => {
|
|
56
|
-
// if all the ages are ok, we move forward
|
|
57
|
-
if (checkAge(step1, data, t)) {
|
|
58
|
-
onSave(data);
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const onUpdate = (name: string, value: (string | number)): void => {
|
|
63
|
-
setValue(name, value);
|
|
64
|
-
setError(name, {});
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const onSendForm = (): void => {
|
|
68
|
-
handleSubmit(onSubmit)();
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<>
|
|
73
|
-
<Title>
|
|
74
|
-
<HiUsers />
|
|
75
|
-
{ t('routes_order.step4.title') }
|
|
76
|
-
</Title>
|
|
77
|
-
|
|
78
|
-
<Steps value={ 4 } />
|
|
79
|
-
|
|
80
|
-
<Passengers
|
|
81
|
-
values={ values }
|
|
82
|
-
step1={ step1 }
|
|
83
|
-
fields={ fields }
|
|
84
|
-
busFrom={ busFrom }
|
|
85
|
-
busTo={ busTo }
|
|
86
|
-
t={ t }
|
|
87
|
-
errors={ errors }
|
|
88
|
-
refs={ register }
|
|
89
|
-
onUpdate={ onUpdate }
|
|
90
|
-
/>
|
|
91
|
-
|
|
92
|
-
<Actions>
|
|
93
|
-
{ isPartner === undefined && (
|
|
94
|
-
<Button
|
|
95
|
-
type="button"
|
|
96
|
-
subtype="secondary"
|
|
97
|
-
text={ <><AiOutlineCaretLeft /> { t('routes_order.step4.back') }</> }
|
|
98
|
-
noMargin
|
|
99
|
-
onClick={ () => onBack(step1._return ? 3 : 2) }
|
|
100
|
-
/>
|
|
101
|
-
) }
|
|
102
|
-
|
|
103
|
-
<Button
|
|
104
|
-
type="button"
|
|
105
|
-
text={ <><RiShoppingCartLine /> { t('routes_order.step4.next') }</> }
|
|
106
|
-
noMargin
|
|
107
|
-
onClick={ onSendForm }
|
|
108
|
-
/>
|
|
109
|
-
</Actions>
|
|
110
|
-
</>
|
|
111
|
-
);
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
export default Step4;
|
package/Step5/Step5.tsx
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { useCallback, useState } from 'react';
|
|
2
|
-
import { TFunction } from 'i18next';
|
|
3
|
-
import { useForm } from 'react-hook-form';
|
|
4
|
-
import { useNavigate } from 'react-router-dom';
|
|
5
|
-
import { RiShoppingCartLine } from 'react-icons/ri';
|
|
6
|
-
import { AiOutlineCaretLeft } from 'react-icons/ai';
|
|
7
|
-
import { MdDone } from 'react-icons/md';
|
|
8
|
-
import { Button } from '@autobusal/common';
|
|
9
|
-
import Steps from '../Steps/Steps';
|
|
10
|
-
import Summary from './Summary/Summary';
|
|
11
|
-
import Coupons from './Coupons/Coupons';
|
|
12
|
-
import Addons from './Addons/Addons';
|
|
13
|
-
import Billing from './Billing/Billing';
|
|
14
|
-
import Payments from './Payments/Payments';
|
|
15
|
-
import { getTotal, convertCoupon } from './utilities';
|
|
16
|
-
import { Title, Actions } from '../styles';
|
|
17
|
-
import { PersonData, BillingData } from '@autobusal/providers/types/persons';
|
|
18
|
-
import { FoundData } from '@autobusal/providers/types/routes';
|
|
19
|
-
import { CouponData } from '@autobusal/providers/types/orders';
|
|
20
|
-
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
21
|
-
import { TotalData } from '../types';
|
|
22
|
-
import { useUserStore } from '@autobusal/providers/stores/user';
|
|
23
|
-
import { useGetSettings } from '@autobusal/providers/services';
|
|
24
|
-
import { usePostOrder } from '../services';
|
|
25
|
-
|
|
26
|
-
interface Props {
|
|
27
|
-
step1: RoutesSearchForm
|
|
28
|
-
step2: FoundData
|
|
29
|
-
step3?: FoundData
|
|
30
|
-
step4: PersonData
|
|
31
|
-
isPartner?: boolean
|
|
32
|
-
t: TFunction<'common'>
|
|
33
|
-
onBack: (step: number) => void
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const Step5 = ({ step1, step2, step3, step4, isPartner, t, onBack }: Props): JSX.Element => {
|
|
37
|
-
const [ coupon, setCoupon ] = useState<CouponData | undefined>(undefined);
|
|
38
|
-
const [ total, setTotal ] = useState<TotalData>(
|
|
39
|
-
getTotal(step2, step3)
|
|
40
|
-
);
|
|
41
|
-
const [ action, setAction ] = useState<string | undefined>(undefined);
|
|
42
|
-
|
|
43
|
-
const [ addonWhatsapp, setAddonWhatsapp ] = useState<boolean>(false);
|
|
44
|
-
const [ addonWhatsappNumber, setAddonWhatsappNumber ] = useState<string>('');
|
|
45
|
-
const [ addonTelegram, setAddonTelegram ] = useState<boolean>(false);
|
|
46
|
-
|
|
47
|
-
const { data: UserData } = useUserStore();
|
|
48
|
-
const { data: SettingsData } = useGetSettings();
|
|
49
|
-
|
|
50
|
-
const navigate = useNavigate();
|
|
51
|
-
|
|
52
|
-
const { register, handleSubmit, formState: { errors } } = useForm<BillingData>();
|
|
53
|
-
|
|
54
|
-
// Edited: Ferjolt Ozuni - Date: 2026-07-25
|
|
55
|
-
// Paid addons are priced/verified server-side (see obtapi's
|
|
56
|
-
// App\Libraries\Orders\Make\Addons) - this is only for the checkout
|
|
57
|
-
// summary display and the grand total shown before payment.
|
|
58
|
-
const currency = total.display.split(' ')[1] ?? '';
|
|
59
|
-
|
|
60
|
-
const addonItems = [
|
|
61
|
-
addonWhatsapp && SettingsData?.addons?.whatsapp.available ? {
|
|
62
|
-
label: t('routes_order.step5.addons.whatsapp.label'),
|
|
63
|
-
price: SettingsData.addons.whatsapp.price
|
|
64
|
-
} : null,
|
|
65
|
-
addonTelegram && SettingsData?.addons?.telegram.available ? {
|
|
66
|
-
label: t('routes_order.step5.addons.telegram.label'),
|
|
67
|
-
price: SettingsData.addons.telegram.price
|
|
68
|
-
} : null
|
|
69
|
-
].filter((item): item is { label: string, price: number } => item !== null);
|
|
70
|
-
|
|
71
|
-
const addonsValue = addonItems.reduce((sum, item) => sum + item.price, 0);
|
|
72
|
-
|
|
73
|
-
const grandTotal: TotalData = {
|
|
74
|
-
display: `${ total.value + addonsValue } ${ currency }`,
|
|
75
|
-
value: total.value + addonsValue
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const { mutate: OrderSend, isPending } = usePostOrder(step1, step2, step3, step4, coupon, action, {
|
|
79
|
-
whatsapp: addonWhatsapp,
|
|
80
|
-
whatsappNumber: addonWhatsappNumber,
|
|
81
|
-
telegram: addonTelegram
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
const onApplyCoupon = (data: CouponData): void => {
|
|
85
|
-
setTotal(
|
|
86
|
-
convertCoupon(data)
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
setCoupon(data);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const onAction = useCallback((action: string): void => {
|
|
93
|
-
setAction(action);
|
|
94
|
-
}, []);
|
|
95
|
-
|
|
96
|
-
const onSendForm = async (): Promise<void> => {
|
|
97
|
-
if (addonWhatsapp && addonWhatsappNumber.trim().length < 5) {
|
|
98
|
-
alert(t('routes_order.step5.addons.whatsapp.phone_error'));
|
|
99
|
-
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
await handleSubmit(onSubmit)();
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const onSubmit = (data: BillingData): void => {
|
|
107
|
-
OrderSend(data, {
|
|
108
|
-
onSuccess: (data) => {
|
|
109
|
-
if (data === undefined) {
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const hash = step1.type === 'return' ? data.return : data.departure;
|
|
114
|
-
|
|
115
|
-
navigate(`/orders/process/${ action }/${ hash }`);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
return (
|
|
121
|
-
<>
|
|
122
|
-
<Title>
|
|
123
|
-
<RiShoppingCartLine />
|
|
124
|
-
{ t('routes_order.step5.title') }
|
|
125
|
-
</Title>
|
|
126
|
-
|
|
127
|
-
<Steps value={ 5 } />
|
|
128
|
-
|
|
129
|
-
<Summary
|
|
130
|
-
step1={ step1 }
|
|
131
|
-
step2={ step2 }
|
|
132
|
-
step3={ step3 }
|
|
133
|
-
step4={ step4 }
|
|
134
|
-
coupon={ coupon }
|
|
135
|
-
addons={ addonItems.map(item => ({ label: item.label, value: `${ item.price } ${ currency }` })) }
|
|
136
|
-
total={ grandTotal }
|
|
137
|
-
t={ t }
|
|
138
|
-
/>
|
|
139
|
-
|
|
140
|
-
<Coupons current={ coupon } step2={ step2 } total={ total } t={ t } onApplied={ onApplyCoupon } />
|
|
141
|
-
|
|
142
|
-
<Addons
|
|
143
|
-
data={ SettingsData?.addons }
|
|
144
|
-
currency={ currency }
|
|
145
|
-
whatsapp={ addonWhatsapp }
|
|
146
|
-
whatsappNumber={ addonWhatsappNumber }
|
|
147
|
-
telegram={ addonTelegram }
|
|
148
|
-
t={ t }
|
|
149
|
-
onChangeWhatsapp={ setAddonWhatsapp }
|
|
150
|
-
onChangeWhatsappNumber={ setAddonWhatsappNumber }
|
|
151
|
-
onChangeTelegram={ setAddonTelegram }
|
|
152
|
-
/>
|
|
153
|
-
|
|
154
|
-
{ UserData === undefined && <Billing t={ t } errors={ errors } refs={ register } /> }
|
|
155
|
-
|
|
156
|
-
<Payments
|
|
157
|
-
selected={ action ?? '' }
|
|
158
|
-
isPartner={ isPartner ?? false }
|
|
159
|
-
t={ t }
|
|
160
|
-
onChange={ onAction }
|
|
161
|
-
/>
|
|
162
|
-
|
|
163
|
-
<Actions>
|
|
164
|
-
<Button
|
|
165
|
-
type="button"
|
|
166
|
-
subtype="secondary"
|
|
167
|
-
text={ <><AiOutlineCaretLeft /> { t('routes_order.step5.back') }</> }
|
|
168
|
-
noMargin
|
|
169
|
-
onClick={ () => onBack(4) }
|
|
170
|
-
/>
|
|
171
|
-
|
|
172
|
-
<Button
|
|
173
|
-
loading={isPending}
|
|
174
|
-
noMargin
|
|
175
|
-
text={ (
|
|
176
|
-
<><MdDone />{ t('routes_order.step5.next') }</>
|
|
177
|
-
) }
|
|
178
|
-
onClick={ onSendForm }
|
|
179
|
-
/>
|
|
180
|
-
</Actions>
|
|
181
|
-
</>
|
|
182
|
-
);
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
export default Step5;
|