@autobusal/map 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.
@@ -0,0 +1,104 @@
1
+ import { useEffect } from 'react';
2
+ import { TFunction } from 'i18next';
3
+ import { useForm } from 'react-hook-form';
4
+ import { useNavigate } from 'react-router-dom';
5
+ import { AiOutlineClose } from 'react-icons/ai';
6
+ import { HiOutlineRefresh, HiOutlineSearch } from 'react-icons/hi';
7
+ import { Button } from '@autobusal/common';
8
+ import { display, validate } from '@autobusal/utilities';
9
+ import { Container, Items, Item, ButtonClear, Actions } from './styles';
10
+ import { CityData } from '@autobusal/providers/types';
11
+ import { SearchForm } from '../types';
12
+
13
+ interface Props {
14
+ top: number
15
+ from: CityData | null
16
+ to: CityData | null
17
+ t: TFunction<'common'>
18
+ onReset: (type: 'from' | 'to') => void
19
+ onResetAll: () => void
20
+ }
21
+
22
+ const Column = ({ top, from, to, t, onReset, onResetAll }: Props): JSX.Element => {
23
+ const { register, handleSubmit, formState: { errors }, setValue } = useForm<SearchForm>();
24
+
25
+ const navigate = useNavigate();
26
+
27
+ useEffect(() => {
28
+ setValue('from', from?.name ?? null);
29
+ }, [from]);
30
+
31
+ useEffect(() => {
32
+ setValue('to', to?.name ?? null)
33
+ }, [to]);
34
+
35
+ const onSubmit = (): void => {
36
+ if (from !== null && to !== null) {
37
+ navigate(`/bus-lines/${ from.slug }/${ to.slug }`);
38
+ }
39
+ };
40
+
41
+ return (
42
+ <Container className="box" $top={ top }>
43
+ <form onSubmit={ handleSubmit(onSubmit) }>
44
+ <Items>
45
+ <Item className="row">
46
+ { t('map.departure') }
47
+
48
+ <input type="text" placeholder={ t('map.choose') } readOnly={ true } { ...register('from', validate('required|min_length:2', t)) } />
49
+
50
+ { display(errors.from) }
51
+
52
+ { from !== null && (
53
+ <ButtonClear onClick={ () => onReset('from') }>
54
+ <AiOutlineClose />
55
+ </ButtonClear>
56
+ ) }
57
+ </Item>
58
+
59
+ <Item className="row">
60
+ { t('map.arrival') }
61
+
62
+ <input type="text" placeholder={ t('map.choose') } readOnly={ true } { ...register('to', validate('required|min_length:2', t)) } />
63
+
64
+ { display(errors.to) }
65
+
66
+ { to !== null && (
67
+ <ButtonClear onClick={ () => onReset('to') }>
68
+ <AiOutlineClose />
69
+ </ButtonClear>
70
+ ) }
71
+ </Item>
72
+ </Items>
73
+
74
+ <Actions>
75
+ <Button
76
+ type="submit"
77
+ text={
78
+ <>
79
+ <HiOutlineSearch />
80
+ { t('map.search') }
81
+ </>
82
+ }
83
+ noMargin
84
+ />
85
+
86
+ <Button
87
+ type="button"
88
+ subtype="secondary"
89
+ text={
90
+ <>
91
+ <HiOutlineRefresh />
92
+ { t('map.reset') }
93
+ </>
94
+ }
95
+ noMargin
96
+ onClick={ onResetAll }
97
+ />
98
+ </Actions>
99
+ </form>
100
+ </Container>
101
+ );
102
+ };
103
+
104
+ export default Column;
@@ -0,0 +1,57 @@
1
+ import styled from 'styled-components';
2
+
3
+ export const Container = styled.div<{ $top: number }>`
4
+ position: absolute;
5
+ left: 0;
6
+ bottom: 0;
7
+ z-index: 6;
8
+ width: 100%;
9
+ height: calc(100% - ${ props => props.$top }px);
10
+ background: ${ props => props.theme.header.transparent };
11
+ backdrop-filter: blur(20px);
12
+ -webkit-backdrop-filter: blur(20px);
13
+ border-radius: 0;
14
+
15
+ @media (min-width: 1024px) {
16
+ width: 300px;
17
+ }
18
+ `;
19
+
20
+ export const Items = styled.div`
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: 10px;
24
+
25
+ @media (min-width: 480px) {
26
+ flex-direction: row;
27
+ }
28
+
29
+ @media (min-width: 1024px) {
30
+ flex-direction: column;
31
+ }
32
+ `;
33
+
34
+ export const Item = styled.div`
35
+ position: relative;
36
+ margin-bottom: 0;
37
+ `;
38
+
39
+ export const ButtonClear = styled.button`
40
+ position: absolute;
41
+ top: 34px;
42
+ right: 8px;
43
+ width: 24px;
44
+ height: 24px;
45
+ display: flex;
46
+ align-items: center;
47
+ justify-content: center;
48
+ background: ${ props => props.theme.primary.neutral };
49
+ border-radius: ${ props => props.theme.borderRadius };
50
+ `;
51
+
52
+ export const Actions = styled.div`
53
+ display: flex;
54
+ gap: 20px;
55
+ justify-content: center;
56
+ margin-top: 20px;
57
+ `;
@@ -0,0 +1,46 @@
1
+ import { MapContainer, TileLayer, Marker, Popup, Polyline } from 'react-leaflet';
2
+ import { getPosition } from '@autobusal/utilities';
3
+ import { Container } from './styles';
4
+ import { CityData } from '@autobusal/providers/types';
5
+
6
+ interface Props {
7
+ top: number
8
+ cities: CityData[]
9
+ from: CityData | null
10
+ to: CityData | null
11
+ onClick: (item: CityData) => void
12
+ }
13
+
14
+ const Display = ({ top, cities, from, to, onClick }: Props): JSX.Element => {
15
+ const markers = cities.map((item, index) => (
16
+ <Marker key={ index } position={ getPosition(item.location) } eventHandlers={{ click: () => { onClick(item) } }}>
17
+ <Popup>{ item.name }</Popup>
18
+ </Marker>
19
+ ));
20
+
21
+ const line = drawLine(from, to);
22
+
23
+ return (
24
+ <Container $top={ top }>
25
+ <MapContainer center={[ 41.32, 19.81 ]} minZoom={ 6 } maxZoom={ 13 } zoom={ 6 } scrollWheelZoom={ true }>
26
+ <TileLayer attribution="&copy; <a href='http://osm.org/copyright'>OpenStreetMap</a> contributors" url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
27
+
28
+ { markers }
29
+
30
+ { line }
31
+ </MapContainer>
32
+ </Container>
33
+ );
34
+ };
35
+
36
+ const drawLine = (from: (CityData | null), to: (CityData | null)) => {
37
+ if (from === null || to === null) {
38
+ return null;
39
+ }
40
+
41
+ return (
42
+ <Polyline positions={[ getPosition(from.location), getPosition(to.location) ]} color="#3388ff" />
43
+ );
44
+ }
45
+
46
+ export default Display;
@@ -0,0 +1,16 @@
1
+ import styled from 'styled-components';
2
+ import 'leaflet/dist/leaflet.css';
3
+
4
+ export const Container = styled.div<{ $top: number }>`
5
+ position: fixed;
6
+ top: ${ props => props.$top }px;
7
+ left: 0;
8
+ z-index: 5;
9
+ width: 100%;
10
+ height: calc(100% - ${ props => props.$top }px);
11
+
12
+ & > .leaflet-container {
13
+ width: 100%;
14
+ height: 100%;
15
+ }
16
+ `;
package/Map.tsx ADDED
@@ -0,0 +1,63 @@
1
+ import { useState } from 'react';
2
+ import { TFunction } from 'i18next';
3
+ import { Meta } from '@autobusal/common';
4
+ import Column from './Column/Column';
5
+ import Display from './Display/Display';
6
+ import { CityData } from '@autobusal/providers/types';
7
+ import { GetLocations } from './services';
8
+
9
+ interface Props {
10
+ top: number
11
+ t: TFunction<'common'>
12
+ }
13
+
14
+ const Map = ({ top, t }: Props): JSX.Element => {
15
+ const [ from, setFrom ] = useState<CityData | null>(null);
16
+ const [ to, setTo ] = useState<CityData | null>(null);
17
+
18
+ const { data } = GetLocations();
19
+
20
+ const onSelected = (selected: CityData): void => {
21
+ if (from === null) {
22
+ setFrom(selected);
23
+ } else if (to === null) {
24
+ setTo(selected);
25
+ }
26
+ };
27
+
28
+ const onReset = (type: 'from' | 'to'): void => {
29
+ if (type === 'from') {
30
+ setFrom(null);
31
+ } else {
32
+ setTo(null);
33
+ }
34
+ };
35
+
36
+ const onResetAll = (): void => {
37
+ onReset('from');
38
+ onReset('to');
39
+ };
40
+
41
+ return (
42
+ <Meta title={ t('map.title') }>
43
+ <Column
44
+ top={ top }
45
+ from={ from }
46
+ to={ to }
47
+ t={ t }
48
+ onReset={ onReset }
49
+ onResetAll={ onResetAll }
50
+ />
51
+
52
+ <Display
53
+ top={ top }
54
+ cities={ data }
55
+ from={ from }
56
+ to={ to }
57
+ onClick={ onSelected }
58
+ />
59
+ </Meta>
60
+ );
61
+ };
62
+
63
+ export default Map;
package/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import Map from './Map';
2
+
3
+ export default Map;
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@autobusal/map",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "main": "index.ts",
6
+ "dependencies": {
7
+ "@autobusal/common": "^0.0.8",
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
+ "react-leaflet": "^4.2.1",
16
+ "react-router-dom": "^6.21.3",
17
+ "styled-components": "^6.1.8"
18
+ }
19
+ }
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';
4
+
5
+ export const GetLocations = (): UseSuspenseQueryResult<CityData[]> => (
6
+ useSuspenseQuery({
7
+ queryKey: ['locations-map'],
8
+ queryFn: async () => (
9
+ await apiClient
10
+ .get('/api/cities/map')
11
+ .then(response => (
12
+ response.data
13
+ ))
14
+ .catch(() => {})
15
+ )
16
+ })
17
+ );
package/types.ts ADDED
@@ -0,0 +1,4 @@
1
+ export interface SearchForm {
2
+ from: string | null
3
+ to: string | null
4
+ }