@autobusal/routes-search 0.0.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/RoutesSearch.tsx +187 -0
- package/index.ts +3 -0
- package/package.json +17 -0
- package/services.ts +17 -0
- package/styles.ts +49 -0
- package/types.ts +10 -0
package/RoutesSearch.tsx
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { TFunction } from 'i18next';
|
|
3
|
+
import { useForm } from 'react-hook-form';
|
|
4
|
+
import { FaArrowUp, FaHistory } from 'react-icons/fa';
|
|
5
|
+
import { AiOutlineSearch } from 'react-icons/ai';
|
|
6
|
+
import { Button } from '@autobusal/common';
|
|
7
|
+
import { display, createDate } from '@autobusal/utilities';
|
|
8
|
+
import { Autocomplete, Calendar, Passengers } from '@autobusal/common';
|
|
9
|
+
import { Options, Columns, Column, Item, Title, Action } from './styles';
|
|
10
|
+
import { RoutesSearchForm } from './types';
|
|
11
|
+
import { AutocompleteData } from '@autobusal/common/Autocomplete/types';
|
|
12
|
+
import { GetSuggestions } from './services';
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
type: 'departure' | 'return'
|
|
16
|
+
from: string
|
|
17
|
+
to: string
|
|
18
|
+
departure: string
|
|
19
|
+
_return: string
|
|
20
|
+
adults: number
|
|
21
|
+
children: number
|
|
22
|
+
babies: number
|
|
23
|
+
t: TFunction<'common'>
|
|
24
|
+
onSearch: (data: RoutesSearchForm) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const RoutesSearch = ({ type, from, to, departure, _return, adults, children, babies, t, onSearch }: Props): JSX.Element => {
|
|
28
|
+
const [ selected, setSelected ] = useState<'departure' | 'return'>(type);
|
|
29
|
+
|
|
30
|
+
const { register, handleSubmit, formState: { errors }, setValue } = useForm<RoutesSearchForm>();
|
|
31
|
+
|
|
32
|
+
const { data } = GetSuggestions();
|
|
33
|
+
|
|
34
|
+
const onClick = (type: ('departure' | 'return')): void => {
|
|
35
|
+
setSelected(type);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const onSubmit = (data: RoutesSearchForm): void => {
|
|
39
|
+
const departureDate = createDate(data.departure);
|
|
40
|
+
const _returnDate = createDate(data._return ?? '');
|
|
41
|
+
|
|
42
|
+
data.type = selected;
|
|
43
|
+
|
|
44
|
+
// arrange the data, as some get transformed to strings
|
|
45
|
+
data.adults = Number(data.adults);
|
|
46
|
+
data.babies = Number(data.babies);
|
|
47
|
+
data.children = Number(data.children);
|
|
48
|
+
|
|
49
|
+
if (data.adults == 0) {
|
|
50
|
+
alert(t('general:search_routes.errors.adults'));
|
|
51
|
+
} else if (data.adults + data.children > 5) {
|
|
52
|
+
alert(t('general:search_routes.errors.max_tickets', { max: 5 }));
|
|
53
|
+
} else if (data.babies > data.adults) {
|
|
54
|
+
alert(t('general:search_routes.errors.babies'));
|
|
55
|
+
} else if (selected === 'return' && departureDate > _returnDate) {
|
|
56
|
+
alert(t('general:search_routes.errors.dates'));
|
|
57
|
+
} else {
|
|
58
|
+
onSearch(data);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const cities: AutocompleteData[] = data.map(city => ({
|
|
63
|
+
name: `${ city.name }, ${ city.country?.name ?? '' }`,
|
|
64
|
+
value: city.slug
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<form onSubmit={ handleSubmit(onSubmit) }>
|
|
69
|
+
<Options>
|
|
70
|
+
<Button
|
|
71
|
+
type="button"
|
|
72
|
+
size="medium"
|
|
73
|
+
active={ selected === 'departure' }
|
|
74
|
+
text={ <><FaArrowUp /> { t('routes_search.options.departure', { ns: 'common' }) }</> }
|
|
75
|
+
noMargin
|
|
76
|
+
onClick={ () => onClick('departure') }
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
<Button
|
|
80
|
+
type="button"
|
|
81
|
+
size="medium"
|
|
82
|
+
active={ selected === 'return' }
|
|
83
|
+
text={ <><FaHistory />{ t('routes_search.options.return', { ns: 'common' }) }</> }
|
|
84
|
+
noMargin
|
|
85
|
+
onClick={ () => onClick('return') }
|
|
86
|
+
/>
|
|
87
|
+
</Options>
|
|
88
|
+
|
|
89
|
+
<Columns>
|
|
90
|
+
<Column>
|
|
91
|
+
<Item>
|
|
92
|
+
<Title>{ t('routes_search.from', { ns: 'common' }) }</Title>
|
|
93
|
+
|
|
94
|
+
<Autocomplete
|
|
95
|
+
name="from"
|
|
96
|
+
defaultValue={ from }
|
|
97
|
+
data={ cities }
|
|
98
|
+
validation="required"
|
|
99
|
+
t={ t }
|
|
100
|
+
refs={ register }
|
|
101
|
+
onUpdate={ setValue }
|
|
102
|
+
/>
|
|
103
|
+
|
|
104
|
+
{ display(errors.from) }
|
|
105
|
+
</Item>
|
|
106
|
+
|
|
107
|
+
<Item>
|
|
108
|
+
<Title>{ t('routes_search.to', { ns: 'common' }) }</Title>
|
|
109
|
+
|
|
110
|
+
<Autocomplete
|
|
111
|
+
name="to"
|
|
112
|
+
defaultValue={ to }
|
|
113
|
+
data={ cities }
|
|
114
|
+
validation="required"
|
|
115
|
+
t={ t }
|
|
116
|
+
refs={ register }
|
|
117
|
+
onUpdate={ setValue }
|
|
118
|
+
/>
|
|
119
|
+
|
|
120
|
+
{ display(errors.to) }
|
|
121
|
+
</Item>
|
|
122
|
+
</Column>
|
|
123
|
+
|
|
124
|
+
<Column>
|
|
125
|
+
<Item>
|
|
126
|
+
<Title>{ t('routes_search.departure', { ns: 'common' }) }</Title>
|
|
127
|
+
|
|
128
|
+
<Calendar
|
|
129
|
+
type="picker"
|
|
130
|
+
name="departure"
|
|
131
|
+
defaultValue={ departure }
|
|
132
|
+
t={ t }
|
|
133
|
+
validation="required"
|
|
134
|
+
refs={ register }
|
|
135
|
+
onUpdate={ setValue }
|
|
136
|
+
/>
|
|
137
|
+
|
|
138
|
+
{ display(errors.departure) }
|
|
139
|
+
</Item>
|
|
140
|
+
|
|
141
|
+
{ selected === 'return' && (
|
|
142
|
+
<Item>
|
|
143
|
+
<Title>{ t('routes_search.return', { ns: 'common' }) }</Title>
|
|
144
|
+
|
|
145
|
+
<Calendar
|
|
146
|
+
type="picker"
|
|
147
|
+
name="_return"
|
|
148
|
+
defaultValue={ _return }
|
|
149
|
+
t={ t }
|
|
150
|
+
validation="required"
|
|
151
|
+
refs={ register }
|
|
152
|
+
onUpdate={ setValue }
|
|
153
|
+
/>
|
|
154
|
+
|
|
155
|
+
{ display(errors._return) }
|
|
156
|
+
</Item>
|
|
157
|
+
) }
|
|
158
|
+
</Column>
|
|
159
|
+
|
|
160
|
+
<Column>
|
|
161
|
+
<Item>
|
|
162
|
+
<Title>{ t('routes_search.passengers', { ns: 'common' }) }</Title>
|
|
163
|
+
|
|
164
|
+
<Passengers
|
|
165
|
+
defaultAdults={ adults }
|
|
166
|
+
defaultChildren={ children }
|
|
167
|
+
defaultBabies={ babies }
|
|
168
|
+
t={ t }
|
|
169
|
+
refs={ register }
|
|
170
|
+
onUpdate={ setValue }
|
|
171
|
+
/>
|
|
172
|
+
</Item>
|
|
173
|
+
|
|
174
|
+
<Action>
|
|
175
|
+
<Button
|
|
176
|
+
type="submit"
|
|
177
|
+
noMargin
|
|
178
|
+
text={ <><AiOutlineSearch /> { t('routes_search.action', { ns: 'common' }) }</> }
|
|
179
|
+
/>
|
|
180
|
+
</Action>
|
|
181
|
+
</Column>
|
|
182
|
+
</Columns>
|
|
183
|
+
</form>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export default RoutesSearch;
|
package/index.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@autobusal/routes-search",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@autobusal/common": "^0.0.18",
|
|
8
|
+
"@autobusal/providers": "^1.0.16",
|
|
9
|
+
"@autobusal/utilities": "^0.0.2",
|
|
10
|
+
"@tanstack/react-query": "^5.17.19",
|
|
11
|
+
"i18next": "^23.7.18",
|
|
12
|
+
"react": "^18.2.0",
|
|
13
|
+
"react-hook-form": "^7.49.3",
|
|
14
|
+
"react-icons": "^5.0.1",
|
|
15
|
+
"styled-components": "^6.1.8"
|
|
16
|
+
}
|
|
17
|
+
}
|
package/services.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useSuspenseQuery, UseSuspenseQueryResult } from '@tanstack/react-query';
|
|
2
|
+
import { apiClient } from '@autobusal/providers';
|
|
3
|
+
import { CityData } from '@autobusal/providers/types/locations';
|
|
4
|
+
|
|
5
|
+
export const GetSuggestions = (): UseSuspenseQueryResult<CityData[]> => (
|
|
6
|
+
useSuspenseQuery({
|
|
7
|
+
queryKey: ['search-suggestions'],
|
|
8
|
+
queryFn: async () => (
|
|
9
|
+
await apiClient
|
|
10
|
+
.get('/api/cities/browse')
|
|
11
|
+
.then(response => (
|
|
12
|
+
response?.data ?? []
|
|
13
|
+
))
|
|
14
|
+
.catch(() => {})
|
|
15
|
+
)
|
|
16
|
+
})
|
|
17
|
+
);
|
package/styles.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const Options = styled.div`
|
|
4
|
+
display: flex;
|
|
5
|
+
gap: 15px;
|
|
6
|
+
margin-bottom: 20px;
|
|
7
|
+
`;
|
|
8
|
+
|
|
9
|
+
export const Columns = styled.div`
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-wrap: wrap;
|
|
12
|
+
gap: 15px;
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
export const Column = styled.div`
|
|
16
|
+
flex-basis: 100%;
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
gap: 15px;
|
|
20
|
+
|
|
21
|
+
@media (min-width: 480px) {
|
|
22
|
+
flex-direction: row;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@media (min-width: 768px) {
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
flex-basis: calc(33.3333% - 10px);
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
export const Item = styled.div`
|
|
32
|
+
flex: 1;
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
export const Title = styled.h4`
|
|
36
|
+
margin-bottom: 8px;
|
|
37
|
+
font-size: ${ props => props.theme.size.s };
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
export const Action = styled.div`
|
|
41
|
+
flex: 1;
|
|
42
|
+
display: flex;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
align-items: end;
|
|
45
|
+
|
|
46
|
+
@media (min-width: 640px) {
|
|
47
|
+
justify-content: right;
|
|
48
|
+
}
|
|
49
|
+
`;
|