@autobusal/routes-order 1.0.0
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/Found/Booking/Booking.tsx +61 -0
- package/Found/Booking/styles.ts +50 -0
- package/Found/Dates/Dates.tsx +82 -0
- package/Found/Dates/Loading.tsx +14 -0
- package/Found/Dates/styles.ts +50 -0
- package/Found/Found.tsx +31 -0
- package/Found/Header/Header.tsx +21 -0
- package/Found/Header/styles.ts +40 -0
- package/Found/Orders/Orders.tsx +40 -0
- package/Found/Orders/styles.ts +12 -0
- package/Found/Route/Route.tsx +112 -0
- package/Found/Route/styles.ts +187 -0
- package/RoutesOrder.tsx +130 -0
- package/Step1/Step1.tsx +63 -0
- package/Step1/prepare.ts +38 -0
- package/Step2.tsx +55 -0
- package/Step3.tsx +56 -0
- package/Step4/Loading.tsx +26 -0
- package/Step4/Passenger/Passenger.tsx +173 -0
- package/Step4/Passenger/styles.ts +10 -0
- package/Step4/Passengers/Display.tsx +50 -0
- package/Step4/Passengers/Passengers.tsx +96 -0
- package/Step4/Passengers/styles.ts +7 -0
- package/Step4/Step4.tsx +96 -0
- package/Step4/checkAge.ts +48 -0
- package/Step5/Billing/Billing.tsx +161 -0
- package/Step5/Billing/Loading.tsx +22 -0
- package/Step5/Billing/styles.ts +13 -0
- package/Step5/Coupons/Coupons.tsx +71 -0
- package/Step5/Coupons/styles.ts +20 -0
- package/Step5/Payments/Payments.tsx +82 -0
- package/Step5/Payments/styles.ts +42 -0
- package/Step5/Step5.tsx +131 -0
- package/Step5/Summary/About/About.tsx +48 -0
- package/Step5/Summary/About/styles.ts +49 -0
- package/Step5/Summary/Amount/Amount.tsx +24 -0
- package/Step5/Summary/Amount/styles.ts +29 -0
- package/Step5/Summary/Summary.tsx +44 -0
- package/Step5/Summary/Tickets/Preview.tsx +41 -0
- package/Step5/Summary/Tickets/Tickets.tsx +34 -0
- package/Step5/Summary/Tickets/styles.ts +36 -0
- package/Step5/Summary/styles.ts +11 -0
- package/Step5/utilities.ts +24 -0
- package/Steps/Steps.tsx +28 -0
- package/Steps/styles.ts +47 -0
- package/index.ts +3 -0
- package/package.json +6 -0
- package/services.ts +149 -0
- package/styles.ts +24 -0
- package/types.ts +32 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { TFunction } from 'i18next';
|
|
2
|
+
import { useForm } from 'react-hook-form';
|
|
3
|
+
import { RiCoupon3Line } from 'react-icons/ri';
|
|
4
|
+
import { Button } from '@autobusal/common';
|
|
5
|
+
import { Validate, Display } from '@autobusal/utilities';
|
|
6
|
+
import { SubTitle } from '../../styles';
|
|
7
|
+
import { Container, ContainerForm, ContainerInput } from './styles';
|
|
8
|
+
import { CouponData } from '@autobusal/providers/types/orders';
|
|
9
|
+
import { FoundData } from '@autobusal/providers/types/routes';
|
|
10
|
+
import { TotalData, CouponForm } from '../../types';
|
|
11
|
+
import { GetCoupon } from '../../services';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
current?: CouponData
|
|
15
|
+
step2: FoundData
|
|
16
|
+
total: TotalData
|
|
17
|
+
t: TFunction<'common'>
|
|
18
|
+
onApplied: (data: CouponData) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const Coupons = ({ current, step2, t, total, onApplied }: Props): (JSX.Element | null) => {
|
|
22
|
+
const { register, handleSubmit, formState: { errors } } = useForm<CouponForm>();
|
|
23
|
+
|
|
24
|
+
const { mutate: SearchCoupons, isPending } = GetCoupon(step2.id, total.value);
|
|
25
|
+
|
|
26
|
+
const onSubmit = (data: CouponForm): void => {
|
|
27
|
+
SearchCoupons(data, {
|
|
28
|
+
onSuccess: (data) => {
|
|
29
|
+
if (data.used) {
|
|
30
|
+
onApplied(data);
|
|
31
|
+
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
alert(t('routes_order.step5.coupon.error'));
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (current) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Container className="box">
|
|
46
|
+
<SubTitle>
|
|
47
|
+
<RiCoupon3Line />
|
|
48
|
+
{ t('routes_order.step5.coupon.title') }
|
|
49
|
+
</SubTitle>
|
|
50
|
+
|
|
51
|
+
<form onSubmit={ handleSubmit(onSubmit) }>
|
|
52
|
+
<ContainerForm>
|
|
53
|
+
<ContainerInput>
|
|
54
|
+
<input type="text" placeholder={ t('routes_order.step5.coupon.code') } { ...register('code', Validate('required|min_length:4', t)) } />
|
|
55
|
+
{ Display(errors.code) }
|
|
56
|
+
</ContainerInput>
|
|
57
|
+
|
|
58
|
+
<Button
|
|
59
|
+
type="submit"
|
|
60
|
+
loading={ isPending }
|
|
61
|
+
text={ t('routes_order.step5.coupon.apply') }
|
|
62
|
+
noMargin
|
|
63
|
+
maxWidth={ 175 }
|
|
64
|
+
/>
|
|
65
|
+
</ContainerForm>
|
|
66
|
+
</form>
|
|
67
|
+
</Container>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default Coupons;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const Container = styled.div`
|
|
4
|
+
margin-top: 25px;
|
|
5
|
+
`;
|
|
6
|
+
|
|
7
|
+
export const ContainerForm = styled.div`
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
gap: 15px;
|
|
11
|
+
max-width: 450px;
|
|
12
|
+
|
|
13
|
+
@media (min-width: 480px) {
|
|
14
|
+
flex-direction: row;
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
export const ContainerInput = styled.div`
|
|
19
|
+
flex: 1;
|
|
20
|
+
`;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { TFunction } from 'i18next';
|
|
3
|
+
import { RiSecurePaymentLine } from 'react-icons/ri';
|
|
4
|
+
import { FaRegHandPaper, FaMoneyBill } from 'react-icons/fa';
|
|
5
|
+
import { AiOutlineCreditCard } from 'react-icons/ai';
|
|
6
|
+
import { Inline } from '@autobusal/common';
|
|
7
|
+
import { SubTitle } from '../../styles';
|
|
8
|
+
import { Container, ContainerPayments, ButtonPayment } from './styles';
|
|
9
|
+
import { GetPayments } from '../../services';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
selected: string
|
|
13
|
+
isPartner: boolean
|
|
14
|
+
t: TFunction<'common'>
|
|
15
|
+
onChange: (action: string) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const Payments = ({ selected, isPartner, t, onChange }: Props): JSX.Element => {
|
|
19
|
+
const { data, isLoading, isSuccess } = GetPayments();
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (isSuccess) {
|
|
23
|
+
const found = Object.keys(data).find(item => {
|
|
24
|
+
return data[item as keyof typeof data] === true;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// we assign the first available action
|
|
28
|
+
onChange(found ?? '');
|
|
29
|
+
}
|
|
30
|
+
}, [isSuccess, data, onChange]);
|
|
31
|
+
|
|
32
|
+
if (isLoading) {
|
|
33
|
+
return (
|
|
34
|
+
<Container className="box">
|
|
35
|
+
<Inline text={ t('routes_order.step5.actions.loading') } />
|
|
36
|
+
</Container>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<>
|
|
42
|
+
<Container className="box">
|
|
43
|
+
<SubTitle>
|
|
44
|
+
<RiSecurePaymentLine />
|
|
45
|
+
{ t('routes_order.step5.actions.title') }
|
|
46
|
+
</SubTitle>
|
|
47
|
+
|
|
48
|
+
<ContainerPayments>
|
|
49
|
+
{ (data?.reserve && !isPartner) && (
|
|
50
|
+
<ButtonPayment type="button" $selected={ selected === 'reserve' } onClick={ () => onChange('reserve') }>
|
|
51
|
+
<FaRegHandPaper />
|
|
52
|
+
{ t('routes_order.step5.actions.reserve') }
|
|
53
|
+
</ButtonPayment>
|
|
54
|
+
) }
|
|
55
|
+
|
|
56
|
+
{ (data?.funds && !isPartner) && (
|
|
57
|
+
<ButtonPayment type="button" $selected={ selected === 'funds' } onClick={ () => onChange('funds') }>
|
|
58
|
+
<FaMoneyBill />
|
|
59
|
+
{ t('routes_order.step5.actions.funds') }
|
|
60
|
+
</ButtonPayment>
|
|
61
|
+
) }
|
|
62
|
+
|
|
63
|
+
{ (data?.funds && !isPartner) && (
|
|
64
|
+
<ButtonPayment type="button" $selected={ selected === 'cash' } onClick={ () => onChange('cash') }>
|
|
65
|
+
<FaMoneyBill />
|
|
66
|
+
{ t('routes_order.step5.actions.cash') }
|
|
67
|
+
</ButtonPayment>
|
|
68
|
+
) }
|
|
69
|
+
|
|
70
|
+
{ (data?.bkt || isPartner) && (
|
|
71
|
+
<ButtonPayment type="button" $selected={ selected === 'bkt' } onClick={ () => onChange('bkt') }>
|
|
72
|
+
<AiOutlineCreditCard />
|
|
73
|
+
{ t('routes_order.step5.actions.card') }
|
|
74
|
+
</ButtonPayment>
|
|
75
|
+
) }
|
|
76
|
+
</ContainerPayments>
|
|
77
|
+
</Container>
|
|
78
|
+
</>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export default Payments;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import styled, { css } from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const Container = styled.div`
|
|
4
|
+
margin-top: 25px;
|
|
5
|
+
`;
|
|
6
|
+
|
|
7
|
+
export const ContainerPayments = styled.div`
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: row;
|
|
10
|
+
flex-wrap: wrap;
|
|
11
|
+
gap: 15px;
|
|
12
|
+
justify-content: center;
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
export const ButtonPayment = styled.button<{ $selected: boolean }>`
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
align-items: center;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
gap: 10px;
|
|
21
|
+
flex-basis: 150px;
|
|
22
|
+
padding: 10px;
|
|
23
|
+
text-align: center;
|
|
24
|
+
font-weight: 700;
|
|
25
|
+
border: 1px solid ${ props => props.theme.background.neutral };
|
|
26
|
+
border-radius: ${ props => props.theme.borderRadius };
|
|
27
|
+
transition: all 0.3s ease;
|
|
28
|
+
|
|
29
|
+
& > svg {
|
|
30
|
+
font-size: 35px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
&:hover {
|
|
34
|
+
background-color: ${ props => props.theme.background.neutral };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
${ props => props.$selected && css`
|
|
38
|
+
color: ${ props => props.theme.primary.contrast } !important;
|
|
39
|
+
background-color: ${ props => props.theme.primary.normal } !important;
|
|
40
|
+
border-color: ${ props => props.theme.primary.normal };
|
|
41
|
+
` }
|
|
42
|
+
`;
|
package/Step5/Step5.tsx
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { 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 Billing from './Billing/Billing';
|
|
13
|
+
import Payments from './Payments/Payments';
|
|
14
|
+
import { getTotal, convertCoupon } from './utilities';
|
|
15
|
+
import { Title, Actions } from '../styles';
|
|
16
|
+
import { PersonData, BillingData } from '@autobusal/providers/types/persons';
|
|
17
|
+
import { FoundData } from '@autobusal/providers/types/routes';
|
|
18
|
+
import { CouponData } from '@autobusal/providers/types/orders';
|
|
19
|
+
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
20
|
+
import { TotalData } from '../types';
|
|
21
|
+
import { useUserStore } from '@autobusal/providers/stores/user';
|
|
22
|
+
import { PostOrder } from '../services';
|
|
23
|
+
|
|
24
|
+
interface Props {
|
|
25
|
+
step1: RoutesSearchForm
|
|
26
|
+
step2: FoundData
|
|
27
|
+
step3?: FoundData
|
|
28
|
+
step4: PersonData
|
|
29
|
+
isPartner?: boolean
|
|
30
|
+
t: TFunction<'common'>
|
|
31
|
+
onBack: (step: number) => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const Step5 = ({ step1, step2, step3, step4, isPartner, t, onBack }: Props): JSX.Element => {
|
|
35
|
+
const [ coupon, setCoupon ] = useState<CouponData | undefined>(undefined);
|
|
36
|
+
const [ total, setTotal ] = useState<TotalData>(
|
|
37
|
+
getTotal(step2, step3)
|
|
38
|
+
);
|
|
39
|
+
const [ action, setAction ] = useState<string | undefined>(undefined);
|
|
40
|
+
|
|
41
|
+
const { data: UserData } = useUserStore();
|
|
42
|
+
|
|
43
|
+
const navigate = useNavigate();
|
|
44
|
+
|
|
45
|
+
const { register, handleSubmit, formState: { errors } } = useForm<BillingData>();
|
|
46
|
+
|
|
47
|
+
const { mutate: OrderSend, isPending } = PostOrder(step1, step2, step3, step4, coupon, action);
|
|
48
|
+
|
|
49
|
+
const onApplyCoupon = (data: CouponData): void => {
|
|
50
|
+
setTotal(
|
|
51
|
+
convertCoupon(data)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
setCoupon(data);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const onAction = (action: string): void => {
|
|
58
|
+
setAction(action);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const onSendForm = async (): Promise<void> => {
|
|
62
|
+
await handleSubmit(onSubmit)();
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const onSubmit = (data: BillingData): void => {
|
|
66
|
+
OrderSend(data, {
|
|
67
|
+
onSuccess: (data) => {
|
|
68
|
+
if (data === undefined) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const hash = step1.type === 'return' ? data.return : data.departure;
|
|
73
|
+
|
|
74
|
+
navigate(`/orders/process/${ action }/${ hash }`);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<>
|
|
81
|
+
<Title>
|
|
82
|
+
<RiShoppingCartLine />
|
|
83
|
+
{ t('routes_order.step5.title') }
|
|
84
|
+
</Title>
|
|
85
|
+
|
|
86
|
+
<Steps value={ 5 } />
|
|
87
|
+
|
|
88
|
+
<Summary
|
|
89
|
+
step1={ step1 }
|
|
90
|
+
step2={ step2 }
|
|
91
|
+
step3={ step3 }
|
|
92
|
+
step4={ step4 }
|
|
93
|
+
coupon={ coupon }
|
|
94
|
+
total={ total }
|
|
95
|
+
t={ t }
|
|
96
|
+
/>
|
|
97
|
+
|
|
98
|
+
<Coupons current={ coupon } step2={ step2 } total={ total } t={ t } onApplied={ onApplyCoupon } />
|
|
99
|
+
|
|
100
|
+
{ UserData === undefined && <Billing t={ t } errors={ errors } refs={ register } /> }
|
|
101
|
+
|
|
102
|
+
<Payments
|
|
103
|
+
selected={ action ?? '' }
|
|
104
|
+
isPartner={ isPartner ?? false }
|
|
105
|
+
t={ t }
|
|
106
|
+
onChange={ onAction }
|
|
107
|
+
/>
|
|
108
|
+
|
|
109
|
+
<Actions>
|
|
110
|
+
<Button
|
|
111
|
+
type="button"
|
|
112
|
+
subtype="secondary"
|
|
113
|
+
text={ <><AiOutlineCaretLeft /> { t('routes_order.step5.back') }</> }
|
|
114
|
+
noMargin
|
|
115
|
+
onClick={ () => onBack(4) }
|
|
116
|
+
/>
|
|
117
|
+
|
|
118
|
+
<Button
|
|
119
|
+
loading={isPending}
|
|
120
|
+
noMargin
|
|
121
|
+
text={ (
|
|
122
|
+
<><MdDone />{ t('routes_order.step5.next') }</>
|
|
123
|
+
) }
|
|
124
|
+
onClick={ onSendForm }
|
|
125
|
+
/>
|
|
126
|
+
</Actions>
|
|
127
|
+
</>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export default Step5;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { TFunction } from 'i18next';
|
|
2
|
+
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';
|
|
3
|
+
import { Container, ContainerIcon, Icon, Information, Title, SubTitle, Operator } from './styles';
|
|
4
|
+
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
5
|
+
import { FoundData } from '@autobusal/providers/types/routes';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
type: 'departure' | 'return'
|
|
9
|
+
step1: RoutesSearchForm
|
|
10
|
+
data?: FoundData
|
|
11
|
+
t: TFunction<'public'>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const About = ({ type, step1, data, t }: Props): (JSX.Element | null) => {
|
|
15
|
+
const icon = type === 'departure' ? <FaArrowRight /> : <FaArrowLeft />;
|
|
16
|
+
|
|
17
|
+
if (!data) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
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
|
+
};
|
|
32
|
+
|
|
33
|
+
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>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default About;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const Container = styled.div`
|
|
4
|
+
flex: 1;
|
|
5
|
+
display: flex;
|
|
6
|
+
`;
|
|
7
|
+
|
|
8
|
+
export const ContainerIcon = styled.div`
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
flex-basis: 54px;
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
export const Icon = styled.div`
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
justify-content: center;
|
|
18
|
+
width: 40px;
|
|
19
|
+
height: 40px;
|
|
20
|
+
font-size: ${ props => props.theme.size.xxl };
|
|
21
|
+
color: ${ props => props.theme.primary.contrast };
|
|
22
|
+
background: ${ props => props.theme.primary.normal };
|
|
23
|
+
border-radius: 100px;
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
export const Information = styled.div`
|
|
27
|
+
flex: 1;
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
export const Title = styled.h2`
|
|
31
|
+
margin: 0;
|
|
32
|
+
font-size: ${ props => props.theme.size.l };
|
|
33
|
+
text-transform: uppercase;
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
export const SubTitle = styled.div`
|
|
37
|
+
display: inline-block;
|
|
38
|
+
padding: 4px 10px;
|
|
39
|
+
margin: 6px 0;
|
|
40
|
+
font-size: calc(${ props => props.theme.size.s } - 1px);
|
|
41
|
+
background: ${ props => props.theme.background.neutral };
|
|
42
|
+
border-radius: ${ props => props.theme.borderRadius };
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
export const Operator = styled.div`
|
|
46
|
+
padding-left: 10px;
|
|
47
|
+
font-size: calc(${ props => props.theme.size.s } - 1px);
|
|
48
|
+
color: ${ props => props.theme.font.faded };
|
|
49
|
+
`;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { TFunction } from 'i18next';
|
|
2
|
+
import { Container, Value, Code } from './styles';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
type: 'total' | 'discount'
|
|
6
|
+
value: string
|
|
7
|
+
code?: string
|
|
8
|
+
t: TFunction<'common'>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const Amount = ({ type, value, code, t }: Props): JSX.Element => (
|
|
12
|
+
<Container $type={ type }>
|
|
13
|
+
{ t(`routes_order.step5.summary.${ type }`) }
|
|
14
|
+
|
|
15
|
+
{ code && <Code>{ code }</Code> }
|
|
16
|
+
|
|
17
|
+
<Value>
|
|
18
|
+
{ type === 'discount' && '-' }
|
|
19
|
+
{ value }
|
|
20
|
+
</Value>
|
|
21
|
+
</Container>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export default Amount;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import styled, { css } from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const Container = styled.div<{ $type: 'total' | 'discount' }>`
|
|
4
|
+
display: flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
padding: 10px 15px;
|
|
7
|
+
margin-top: 15px;
|
|
8
|
+
text-transform: uppercase;
|
|
9
|
+
font-weight: 700;
|
|
10
|
+
background: ${ props => props.theme.background.neutral };
|
|
11
|
+
border-radius: ${ props => props.theme.borderRadius };
|
|
12
|
+
|
|
13
|
+
${ props => props.$type === 'discount' && css`
|
|
14
|
+
color: ${ props => props.theme.font.error };
|
|
15
|
+
` }
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
export const Value = styled.strong`
|
|
19
|
+
margin-left: auto;
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
export const Code = styled.span`
|
|
23
|
+
padding: 3px 5px;
|
|
24
|
+
margin-left: 6px;
|
|
25
|
+
color: #FFF;
|
|
26
|
+
font-size: ${ props => props.theme.size.xs };
|
|
27
|
+
background-color: ${ props => props.theme.font.error };
|
|
28
|
+
border-radius: ${ props => props.theme.borderRadius };
|
|
29
|
+
`;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { TFunction } from 'i18next';
|
|
2
|
+
import { AiOutlineInfoCircle } from 'react-icons/ai';
|
|
3
|
+
import About from './About/About';
|
|
4
|
+
import Tickets from './Tickets/Tickets';
|
|
5
|
+
import Amount from './Amount/Amount';
|
|
6
|
+
import { SubTitle } from '../../styles';
|
|
7
|
+
import { Information } from './styles';
|
|
8
|
+
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
9
|
+
import { FoundData } from '@autobusal/providers/types/routes';
|
|
10
|
+
import { PersonData } from '@autobusal/providers/types/persons';
|
|
11
|
+
import { CouponData } from '@autobusal/providers/types/orders';
|
|
12
|
+
import { TotalData } from '../../types';
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
step1: RoutesSearchForm
|
|
16
|
+
step2: FoundData
|
|
17
|
+
step3?: FoundData
|
|
18
|
+
step4: PersonData
|
|
19
|
+
coupon?: CouponData
|
|
20
|
+
total: TotalData
|
|
21
|
+
t: TFunction<'common'>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const Summary = ({ step1, step2, step3, step4, coupon, total, t }: Props): JSX.Element => (
|
|
25
|
+
<div className="box">
|
|
26
|
+
<SubTitle>
|
|
27
|
+
<AiOutlineInfoCircle />
|
|
28
|
+
{ t('routes_order.step5.summary.title') }
|
|
29
|
+
</SubTitle>
|
|
30
|
+
|
|
31
|
+
<Information>
|
|
32
|
+
<About type="departure" step1={ step1 } data={ step2 } t={ t } />
|
|
33
|
+
<About type="return" step1={ step1 } data={ step3 } t={ t } />
|
|
34
|
+
</Information>
|
|
35
|
+
|
|
36
|
+
<Tickets step1={ step1 } step4={ step4 } t={ t } />
|
|
37
|
+
|
|
38
|
+
{ coupon && <Amount type="discount" code={ coupon.code } value={ coupon.discount } t={ t } /> }
|
|
39
|
+
|
|
40
|
+
<Amount type="total" value={ total.display } t={ t } />
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
export default Summary;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { TFunction } from 'i18next';
|
|
2
|
+
import { MdEventSeat } from 'react-icons/md';
|
|
3
|
+
import { Person, Seats } from './styles';
|
|
4
|
+
import { PersonData } from '@autobusal/providers/types/persons';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
type: 'adult' | 'child' | 'baby'
|
|
8
|
+
data: PersonData
|
|
9
|
+
number: number
|
|
10
|
+
t: TFunction<'public'>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const Preview = ({ type, data, number, t }: Props): JSX.Element[] => {
|
|
14
|
+
const persons: JSX.Element[] = [];
|
|
15
|
+
|
|
16
|
+
for (let i = 1; i <= number; i++) {
|
|
17
|
+
const name = `${ type }${ number }`;
|
|
18
|
+
|
|
19
|
+
const names = {
|
|
20
|
+
firstName: `${ name }_fn`,
|
|
21
|
+
lastName: `${ name }_ln`,
|
|
22
|
+
seatDeparture: `${ name }_sd`,
|
|
23
|
+
seatReturn: `${ name }_sr`
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
persons.push(
|
|
27
|
+
<Person key={ i }>
|
|
28
|
+
{ data[names.firstName] } { data[names.lastName] } ({ t(`data.persons.${ type }.singular`) })
|
|
29
|
+
|
|
30
|
+
<Seats>
|
|
31
|
+
{ t('routes_order.step5.summary.seats.departure') }: <MdEventSeat /> { data[names.seatDeparture] }
|
|
32
|
+
{ data[names.seatReturn] && <>, { t('routes_order.step5.summary.seats.return') }: <MdEventSeat /> { data[names.seatReturn] }</> }
|
|
33
|
+
</Seats>
|
|
34
|
+
</Person>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return persons;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default Preview;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { TFunction } from 'i18next';
|
|
2
|
+
import Preview from './Preview';
|
|
3
|
+
import { Container, About, List } from './styles';
|
|
4
|
+
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
5
|
+
import { PersonData } from '@autobusal/providers/types/persons';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
step1: RoutesSearchForm
|
|
9
|
+
step4: PersonData
|
|
10
|
+
t: TFunction<'public'>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const Tickets = ({ step1, step4, t }: Props): JSX.Element => {
|
|
14
|
+
const total = step1.adults + step1.children + step1.babies;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Container>
|
|
18
|
+
<About>
|
|
19
|
+
{ t('routes_order.step5.summary.tickets') } ({ total }):
|
|
20
|
+
{ step1.adults > 0 && ` ${ step1.adults } x ${ t('data.persons.adult.plural') }` }
|
|
21
|
+
{ step1.children > 0 && `, ${ step1.children } x ${ t('data.persons.child.plural') }` }
|
|
22
|
+
{ step1.babies > 0 && `${ step1.babies } x ${ t('data.persons.baby.plural') }` }
|
|
23
|
+
</About>
|
|
24
|
+
|
|
25
|
+
<List>
|
|
26
|
+
<Preview type="adult" data={ step4 } number={ step1.adults } t={ t } />
|
|
27
|
+
<Preview type="child" data={ step4 } number={ step1.children } t={ t } />
|
|
28
|
+
<Preview type="baby" data={ step4 } number={ step1.babies } t={ t } />
|
|
29
|
+
</List>
|
|
30
|
+
</Container>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default Tickets;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const Container = styled.div`
|
|
4
|
+
padding: 10px 15px;
|
|
5
|
+
margin: 15px 0;
|
|
6
|
+
border: 1px solid ${ props => props.theme.background.neutral };
|
|
7
|
+
border-radius: ${ props => props.theme.borderRadius };
|
|
8
|
+
`;
|
|
9
|
+
|
|
10
|
+
export const About = styled.div`
|
|
11
|
+
padding-bottom: 8px;
|
|
12
|
+
font-weight: 700;
|
|
13
|
+
border-bottom: 1px solid ${ props => props.theme.background.neutral };
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
export const List = styled.div`
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
gap: 10px;
|
|
20
|
+
margin-top: 8px;
|
|
21
|
+
font-size: ${ props => props.theme.size.s };
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
export const Person = styled.div`
|
|
25
|
+
font-weight: 700;
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
export const Seats = styled.div`
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: 2px;
|
|
32
|
+
margin-top: 2px;
|
|
33
|
+
font-weight: 400;
|
|
34
|
+
color: ${ props => props.theme.font.faded };
|
|
35
|
+
font-size: ${ props => props.theme.size.xs };
|
|
36
|
+
`;
|