From 77f7715edd1b92c093428166e457c579fe8ec637 Mon Sep 17 00:00:00 2001 From: hiba9201 Date: Sun, 3 Sep 2023 21:18:50 +0500 Subject: [PATCH] Clickable transport + refactor stop modal --- api/service/masstrans/masstrans.ts | 1 + client/common/types/state.ts | 14 ++- .../components/Map/Location/MapLocation.tsx | 1 + .../Map/MainContainer/MapMainContainer.tsx | 21 ++-- .../Map/Stops/Item/MapStopsItem.tsx | 10 +- .../Header/MapStopsSidebarHeader.module.css | 20 ++++ .../Sidebar/Header/MapStopsSidebarHeader.tsx | 40 +++++++ .../Stops/Sidebar/MapStopsSidebar.module.css | 104 ----------------- .../Map/Stops/Sidebar/MapStopsSidebar.tsx | 105 ++---------------- .../Sidebar/Row/MapStopsSidebarRow.module.css | 3 + .../Stops/Sidebar/Row/MapStopsSidebarRow.tsx | 15 +++ .../MapStopsSidebarStopsListItem.module.css | 67 +++++++++++ .../Item/MapStopsSidebarStopsListItem.tsx | 90 +++++++++++++++ .../MapStopsSidebarStopsListItem.utils.ts | 21 ++++ .../MapStopsSidebarStopsList.module.css | 12 ++ .../StopsList/MapStopsSidebarStopsList.tsx | 31 ++++++ .../components/Map/Vehicles/MapVehicles.tsx | 2 +- .../Sidebar/MapVehiclesSidebar.module.css | 11 +- .../Vehicles/Sidebar/MapVehiclesSidebar.tsx | 88 ++++++++++++--- client/state/features/public-transport.ts | 61 +++++++--- common/types/masstrans.ts | 1 + common/types/strapi.ts | 16 +-- 22 files changed, 473 insertions(+), 261 deletions(-) create mode 100644 client/components/Map/Stops/Sidebar/Header/MapStopsSidebarHeader.module.css create mode 100644 client/components/Map/Stops/Sidebar/Header/MapStopsSidebarHeader.tsx create mode 100644 client/components/Map/Stops/Sidebar/Row/MapStopsSidebarRow.module.css create mode 100644 client/components/Map/Stops/Sidebar/Row/MapStopsSidebarRow.tsx create mode 100644 client/components/Map/Stops/Sidebar/StopsList/Item/MapStopsSidebarStopsListItem.module.css create mode 100644 client/components/Map/Stops/Sidebar/StopsList/Item/MapStopsSidebarStopsListItem.tsx create mode 100644 client/components/Map/Stops/Sidebar/StopsList/Item/MapStopsSidebarStopsListItem.utils.ts create mode 100644 client/components/Map/Stops/Sidebar/StopsList/MapStopsSidebarStopsList.module.css create mode 100644 client/components/Map/Stops/Sidebar/StopsList/MapStopsSidebarStopsList.tsx diff --git a/api/service/masstrans/masstrans.ts b/api/service/masstrans/masstrans.ts index 54fe3beb..caab984b 100644 --- a/api/service/masstrans/masstrans.ts +++ b/api/service/masstrans/masstrans.ts @@ -89,6 +89,7 @@ export class MasstransService { arriveTime: item.tc_arrivetime, route: item.mr_num, routeDirection: item.rl_racetype, + routeId: parseInt(item.mr_id, 10), type: item.type, to: item.laststation_title, // TODO: get through stations from GetRoute diff --git a/client/common/types/state.ts b/client/common/types/state.ts index e730524a..a2fdca25 100644 --- a/client/common/types/state.ts +++ b/client/common/types/state.ts @@ -14,12 +14,22 @@ export interface State { } export type CurrentVehiclePayload = State['publicTransport']['currentVehicle']; -export interface SetCurrentVehiclePayload { +export interface CurrentVehicleOptions { + shouldClear?: boolean; +} +export type CurrentVehiclePayloadWithOptions = CurrentVehiclePayload & CurrentVehicleOptions; +export interface SetCurrentVehiclePayload extends CurrentVehicleOptions { currentVehicle: CurrentVehiclePayload; currentRoute: State['publicTransport']['currentRoute']; } export type CurrentStopPayload = State['publicTransport']['currentStop']; -export interface SetCurrentStopPayload { +export interface CurrentStopOptions { + shouldClear?: boolean; +} +export interface CurrentStopPayloadWithOptions extends CurrentStopOptions { + currentStop: CurrentStopPayload; +} +export interface SetCurrentStopPayload extends CurrentStopOptions { currentStop: CurrentStopPayload; stopInfo: State['publicTransport']['stopInfo']; } diff --git a/client/components/Map/Location/MapLocation.tsx b/client/components/Map/Location/MapLocation.tsx index 7d778541..6c6f6b98 100644 --- a/client/components/Map/Location/MapLocation.tsx +++ b/client/components/Map/Location/MapLocation.tsx @@ -79,6 +79,7 @@ export function MapLocation() { userMarkerRef.current = new MovingMarker(COORDS_EKATERINBURG, { icon: USER_ICON, + pane: 'location', }).addTo(map); }, [map]); diff --git a/client/components/Map/MainContainer/MapMainContainer.tsx b/client/components/Map/MainContainer/MapMainContainer.tsx index 4f583960..8d04c70c 100644 --- a/client/components/Map/MainContainer/MapMainContainer.tsx +++ b/client/components/Map/MainContainer/MapMainContainer.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { MapContainer, TileLayer } from 'react-leaflet'; +import { MapContainer, Pane, TileLayer } from 'react-leaflet'; import classNames from 'classnames/bind'; import { sidebarService } from 'services/sidebar/sidebar'; @@ -14,7 +14,9 @@ import { MapWelcomeMessage } from 'components/Map/WelcomeMessage/MapWelcomeMessa import styles from './MapMainContainer.module.css'; import 'leaflet/dist/leaflet.css'; -const tileServerUrl = `https://tiles.ekaterinburg.io/styles/basic-white/{z}/{x}/{y}${devicePixelRatio > 1 ? '@2x' : ''}.png` +const tileServerUrl = `https://tiles.ekaterinburg.io/styles/basic-white/{z}/{x}/{y}${ + devicePixelRatio > 1 ? '@2x' : '' +}.png`; const cn = classNames.bind(styles); @@ -31,7 +33,7 @@ function MapMainContainer({ zoom = 16, showControls = true }) { sidebarService.open({ component: }); localStorage.setItem('hasVisited', '1'); } - } catch (e) { } + } catch (e) {} }, []); return ( @@ -47,12 +49,15 @@ function MapMainContainer({ zoom = 16, showControls = true }) { doubleClickZoom={false} className={cn(styles.Map)} > - + - {showControls && <> - - - } + {showControls && ( + <> + + + + + )} diff --git a/client/components/Map/Stops/Item/MapStopsItem.tsx b/client/components/Map/Stops/Item/MapStopsItem.tsx index d28b7e2b..ca8eb6e5 100644 --- a/client/components/Map/Stops/Item/MapStopsItem.tsx +++ b/client/components/Map/Stops/Item/MapStopsItem.tsx @@ -25,9 +25,10 @@ export function MapStopsItem({ type, id, name, coords }: MapStopsItemProps) { const currentVehicle = useSelector((state: State) => state.publicTransport.currentVehicle); const icon = useMemo(() => { + const hasActiveStop = currentStop !== null; const isVehicleActive = Boolean(currentVehicleStops.length); - if (isVehicleActive) { + if (isVehicleActive && !hasActiveStop) { const isStopActive = currentVehicleStops.includes(id); if (isStopActive) { @@ -46,11 +47,10 @@ export function MapStopsItem({ type, id, name, coords }: MapStopsItemProps) { const iconObject = getIconObjectByTypes(type); - const isActive = currentStop === id; - const hasActiveStop = currentStop !== null; - let icon = iconObject.idle; + const isActive = currentStop === id; + if (!isActive && hasActiveStop) { icon = iconObject.inactive; } @@ -81,7 +81,7 @@ export function MapStopsItem({ type, id, name, coords }: MapStopsItemProps) { onClose: () => dispatch(setCurrentStop(null)), }); - dispatch(setCurrentStop(id)); + dispatch(setCurrentStop({ currentStop: id })); } }, }} diff --git a/client/components/Map/Stops/Sidebar/Header/MapStopsSidebarHeader.module.css b/client/components/Map/Stops/Sidebar/Header/MapStopsSidebarHeader.module.css new file mode 100644 index 00000000..53934347 --- /dev/null +++ b/client/components/Map/Stops/Sidebar/Header/MapStopsSidebarHeader.module.css @@ -0,0 +1,20 @@ +.MapStopsSidebarHeader { + display: flex; + gap: 16px; + border-top-left-radius: 16px; + border-top-right-radius: 16px; + padding-right: 80px; + + background-color: #fff; +} + +.MapStopsSidebarHeaderWrapper { + position: sticky; + top: 0; +} + +.MapStopsSidebarHeaderInfo { + display: flex; + gap: 10px; + flex: 1; +} \ No newline at end of file diff --git a/client/components/Map/Stops/Sidebar/Header/MapStopsSidebarHeader.tsx b/client/components/Map/Stops/Sidebar/Header/MapStopsSidebarHeader.tsx new file mode 100644 index 00000000..af5b2202 --- /dev/null +++ b/client/components/Map/Stops/Sidebar/Header/MapStopsSidebarHeader.tsx @@ -0,0 +1,40 @@ +import React from 'react'; + +import classNames from 'classnames/bind'; + +import { StopType } from 'transport-common/types/masstrans'; + +import { Typography } from 'components/UI/Typography/Typography'; +import { IconFont } from 'components/UI/Typography/IconFont/IconFont'; +import { IconFontCharsNames } from 'common/constants/iconFontChars'; +import { Divider } from 'components/UI/Divider/Divider'; +import t from 'utils/typograph'; + +import { MapStopsSidebarProps } from '../MapStopsSidebar'; +import { MapStopsSidebarRow } from '../Row/MapStopsSidebarRow'; + +import styles from './MapStopsSidebarHeader.module.css'; + +const cn = classNames.bind(styles); + +export interface MapStopsSidebarHeaderProps extends Pick { + types: Exclude[]; +} + +export function MapStopsSidebarHeader({ types, name }: MapStopsSidebarHeaderProps) { + return ( +
+ +
+ + {types.map((t) => ( + + ))} + + {t(name)} +
+
+ +
+ ); +} diff --git a/client/components/Map/Stops/Sidebar/MapStopsSidebar.module.css b/client/components/Map/Stops/Sidebar/MapStopsSidebar.module.css index 12cc9411..1cb07915 100644 --- a/client/components/Map/Stops/Sidebar/MapStopsSidebar.module.css +++ b/client/components/Map/Stops/Sidebar/MapStopsSidebar.module.css @@ -8,107 +8,3 @@ font-family: 'Iset Sans', sans-serif; } - -.MapStopsSidebarRow { - padding: 16px 24px; -} - -.MapStopsSidebarHeaderWrapper { - position: sticky; - top: 0; -} - -.MapStopsSidebarHeader { - display: flex; - gap: 16px; - border-top-left-radius: 16px; - border-top-right-radius: 16px; - padding-right: 80px; - - background-color: #fff; -} - -.MapStopsSidebarHeaderInfo { - display: flex; - gap: 10px; - flex: 1; -} - -.MapStopsSidebarCloseButton { - border: 0; - padding: 0; - - background: none; - cursor: pointer; - z-index: 100; -} - -.MapStopsSidebarCloseIcon > path { - stroke: var(--text-primary); -} - -.MapStopsSidebarVehicles { - display: flex; - flex-direction: column; - gap: 30px; - - -ms-overflow-style: none; - scrollbar-width: none; -} - -.MapStopsSidebarVehicles::-webkit-scrollbar { - display: none; -} - -.MapStopsSidebarVehicle { - display: flex; - gap: 32px; -} - -.MapStopsSidebarVehicleInfo { - display: flex; - gap: 16px; - flex: 1; -} - -.MapStopsSidebarVehicleRoute { - flex-shrink: 0; - - width: 58px; - height: 32px; - border-radius: 3px; - - font-weight: 500; - font-size: 28px; - line-height: 32px; - text-align: center; - - color: white; -} - -.MapStopsSidebarVehicleRouteBus { - background-color: var(--bus); -} - -.MapStopsSidebarVehicleRouteTram { - background-color: var(--tram); -} - -.MapStopsSidebarVehicleRouteTroll { - background-color: var(--troll); -} - -.MapStopsSidebarVehicleEndpoint { - font-size: 20px; - line-height: 22px; -} - -.MapStopsSidebarVehicleKeypoints, -.MapStopsSidebarVehicleArriveTime { - font-size: 16px; - line-height: 18px; -} - -.MapStopsSidebarVehicleKeypoints { - font-weight: 300; -} diff --git a/client/components/Map/Stops/Sidebar/MapStopsSidebar.tsx b/client/components/Map/Stops/Sidebar/MapStopsSidebar.tsx index 3ad31276..10622fff 100644 --- a/client/components/Map/Stops/Sidebar/MapStopsSidebar.tsx +++ b/client/components/Map/Stops/Sidebar/MapStopsSidebar.tsx @@ -1,19 +1,12 @@ -import React, { useCallback } from 'react'; +import React, { useMemo } from 'react'; import classNames from 'classnames/bind'; -import { useSelector } from 'react-redux'; - -import { State } from 'common/types/state'; -import { IconFontCharsNames } from 'common/constants/iconFontChars'; -import { VEHICLE_TYPE_COLORS } from 'common/constants/colors'; - -import { Divider } from 'components/UI/Divider/Divider'; -import { Typography } from 'components/UI/Typography/Typography'; -import { IconFont } from 'components/UI/Typography/IconFont/IconFont'; import { StopType } from 'transport-common/types/masstrans'; +import t from 'utils/typograph'; import styles from './MapStopsSidebar.module.css'; -import t from 'utils/typograph'; +import { MapStopsSidebarHeader } from './Header/MapStopsSidebarHeader'; +import { MapStopsSidebarStopsList } from './StopsList/MapStopsSidebarStopsList'; export type MapStopsSidebarProps = { type: StopType; @@ -23,94 +16,14 @@ export type MapStopsSidebarProps = { const cn = classNames.bind(styles); export function MapStopsSidebar({ type, name }: MapStopsSidebarProps) { - const stopInfo = useSelector((state: State) => state.publicTransport.stopInfo); - - const getTimeToArrive = useCallback((arriveTime: string) => { - const [hours, minutes] = arriveTime.split(':'); - - const arriveDate = new Date(); - arriveDate.setHours(parseInt(hours, 10)); - arriveDate.setMinutes(parseInt(minutes, 10)); - - const now = new Date(); - - if (arriveDate.getHours() === 0 && now.getHours() === 23) { - arriveDate.setDate(now.getDate() + 1); - } - - return Math.max(Math.round((arriveDate.getTime() - now.getTime()) / 1000 / 60), 0); - }, []); + const iconTypes = useMemo(() => { + return type.split('-').reverse() as Exclude[]; + }, [type]); return (
-
-
-
- - {type - .split('-') - .reverse() - .map((t) => ( - - ))} - - {t(name)} -
-
- -
-
- {Boolean(stopInfo) ? ( - <> - {stopInfo.map((vehicle) => ( -
-
-
- {t(vehicle.route)} -
-
- - {t(vehicle.to)} - - {Boolean(vehicle.through.length) && ( - <> -
- - {`через ${vehicle.through.join(', ')}`} - - - )} -
-
-
- {getTimeToArrive(vehicle.arriveTime) > 15 - ? vehicle.arriveTime - : `${getTimeToArrive(vehicle.arriveTime)} мин`} -
-
- ))} - {stopInfo.length === 0 && ( - - О нет! В ближайшее время транспорта тут не будет - - )} - - ) : ( -

Загрузка...

- )} -
+ +
); } diff --git a/client/components/Map/Stops/Sidebar/Row/MapStopsSidebarRow.module.css b/client/components/Map/Stops/Sidebar/Row/MapStopsSidebarRow.module.css new file mode 100644 index 00000000..4dbe03e9 --- /dev/null +++ b/client/components/Map/Stops/Sidebar/Row/MapStopsSidebarRow.module.css @@ -0,0 +1,3 @@ +.MapStopsSidebarRow { + padding: 16px 24px; +} \ No newline at end of file diff --git a/client/components/Map/Stops/Sidebar/Row/MapStopsSidebarRow.tsx b/client/components/Map/Stops/Sidebar/Row/MapStopsSidebarRow.tsx new file mode 100644 index 00000000..15f00002 --- /dev/null +++ b/client/components/Map/Stops/Sidebar/Row/MapStopsSidebarRow.tsx @@ -0,0 +1,15 @@ +import React, { PropsWithChildren } from 'react'; + +import classNames from 'classnames/bind'; + +import styles from './MapStopsSidebarRow.module.css'; + +const cn = classNames.bind(styles); + +export interface MapStopsSidebarRowProps { + mix: string; +} + +export function MapStopsSidebarRow({ children, mix }: PropsWithChildren) { + return
{children}
; +} diff --git a/client/components/Map/Stops/Sidebar/StopsList/Item/MapStopsSidebarStopsListItem.module.css b/client/components/Map/Stops/Sidebar/StopsList/Item/MapStopsSidebarStopsListItem.module.css new file mode 100644 index 00000000..0a326752 --- /dev/null +++ b/client/components/Map/Stops/Sidebar/StopsList/Item/MapStopsSidebarStopsListItem.module.css @@ -0,0 +1,67 @@ +.MapStopsSidebarVehicle { + display: flex; + gap: 32px; + cursor: pointer; + border-radius: 4px; + outline: 8px solid transparent; +} + +.MapStopsSidebarVehicle:not(.MapStopsSidebarVehicle_isSelected):hover { + background-color: #eaeaea; + outline: 8px solid #eaeaea; +} + +.MapStopsSidebarVehicle_isSelected { + background-color: var(--vehicle-color); + color: #fff; + outline: 8px solid var(--vehicle-color); +} + +.MapStopsSidebarVehicleInfo { + display: flex; + gap: 16px; + flex: 1; +} + +.MapStopsSidebarVehicleRoute { + flex-shrink: 0; + + width: 58px; + height: 32px; + border-radius: 3px; + + font-weight: 500; + font-size: 28px; + line-height: 32px; + text-align: center; + + color: #fff; + background-color: var(--vehicle-color); +} + +.MapStopsSidebarVehicleRouteBus { + background-color: var(--bus); +} + +.MapStopsSidebarVehicleRouteTram { + background-color: var(--tram); +} + +.MapStopsSidebarVehicleRouteTroll { + background-color: var(--troll); +} + +.MapStopsSidebarVehicleEndpoint { + font-size: 20px; + line-height: 22px; +} + +.MapStopsSidebarVehicleKeypoints, +.MapStopsSidebarVehicleArriveTime { + font-size: 16px; + line-height: 18px; +} + +.MapStopsSidebarVehicleKeypoints { + font-weight: 300; +} diff --git a/client/components/Map/Stops/Sidebar/StopsList/Item/MapStopsSidebarStopsListItem.tsx b/client/components/Map/Stops/Sidebar/StopsList/Item/MapStopsSidebarStopsListItem.tsx new file mode 100644 index 00000000..6915fa9a --- /dev/null +++ b/client/components/Map/Stops/Sidebar/StopsList/Item/MapStopsSidebarStopsListItem.tsx @@ -0,0 +1,90 @@ +import React, { useCallback, useMemo } from 'react'; +import classNames from 'classnames/bind'; +import { useDispatch, useSelector } from 'react-redux'; + +import { StopInfoItem } from 'transport-common/types/masstrans'; + +import { VEHICLE_TYPE_COLORS } from 'common/constants/colors'; +import { store } from 'state'; +import { setCurrentVehicle } from 'state/features/public-transport'; +import t from 'utils/typograph'; + +import styles from './MapStopsSidebarStopsListItem.module.css'; +import { getTimeToArrive } from './MapStopsSidebarStopsListItem.utils'; +import { State } from 'common/types/state'; + +const cn = classNames.bind(styles); + +export interface MapStopsSidebarStopsListItemProps { + vehicle: StopInfoItem; +} + +export function MapStopsSidebarStopsListItem({ vehicle }: MapStopsSidebarStopsListItemProps) { + const dispatch = useDispatch(); + const currentVehicleRoute = useSelector( + (state: State) => state.publicTransport.currentVehicle?.num, + ); + + const timeToArrive = useMemo(() => { + const minutesToArrive = getTimeToArrive(vehicle.arriveTime); + + return minutesToArrive > 15 ? vehicle.arriveTime : `${minutesToArrive} мин`; + }, [vehicle.arriveTime]); + + const setSelectedVehicle = useCallback(() => { + if (vehicle.route === currentVehicleRoute) { + dispatch(setCurrentVehicle(null)); + + return; + } + + dispatch( + setCurrentVehicle({ + num: vehicle.route, + routeDirection: vehicle.routeDirection, + type: vehicle.type, + routeId: vehicle.routeId, + shouldClear: false, + }), + ); + }, [ + dispatch, + vehicle.route, + vehicle.routeDirection, + vehicle.type, + currentVehicleRoute, + vehicle.routeId, + ]); + + return ( +
+
+
{vehicle.route}
+
+ + {t(vehicle.to)} + + {Boolean(vehicle.through.length) && ( + <> +
+ + {t(`через ${vehicle.through.join(', ')}`)} + + + )} +
+
+
{timeToArrive}
+
+ ); +} diff --git a/client/components/Map/Stops/Sidebar/StopsList/Item/MapStopsSidebarStopsListItem.utils.ts b/client/components/Map/Stops/Sidebar/StopsList/Item/MapStopsSidebarStopsListItem.utils.ts new file mode 100644 index 00000000..4c0a547d --- /dev/null +++ b/client/components/Map/Stops/Sidebar/StopsList/Item/MapStopsSidebarStopsListItem.utils.ts @@ -0,0 +1,21 @@ +function getMinutesToFutureTime(time: Date) { + const now = new Date(); + + return Math.max(Math.round((time.getTime() - now.getTime()) / 1000 / 60), 0); +} + +export function getTimeToArrive(arriveTime: string) { + const [hours, minutes] = arriveTime.split(':'); + + const arriveDate = new Date(); + arriveDate.setHours(parseInt(hours, 10)); + arriveDate.setMinutes(parseInt(minutes, 10)); + + const now = new Date(); + + if (arriveDate.getHours() === 0 && now.getHours() === 23) { + arriveDate.setDate(now.getDate() + 1); + } + + return getMinutesToFutureTime(arriveDate); +} diff --git a/client/components/Map/Stops/Sidebar/StopsList/MapStopsSidebarStopsList.module.css b/client/components/Map/Stops/Sidebar/StopsList/MapStopsSidebarStopsList.module.css new file mode 100644 index 00000000..3d1d30b1 --- /dev/null +++ b/client/components/Map/Stops/Sidebar/StopsList/MapStopsSidebarStopsList.module.css @@ -0,0 +1,12 @@ +.MapStopsSidebarStopsList { + display: flex; + flex-direction: column; + gap: 30px; + + -ms-overflow-style: none; + scrollbar-width: none; +} + +.MapStopsSidebarStopsList::-webkit-scrollbar { + display: none; +} \ No newline at end of file diff --git a/client/components/Map/Stops/Sidebar/StopsList/MapStopsSidebarStopsList.tsx b/client/components/Map/Stops/Sidebar/StopsList/MapStopsSidebarStopsList.tsx new file mode 100644 index 00000000..5439a65f --- /dev/null +++ b/client/components/Map/Stops/Sidebar/StopsList/MapStopsSidebarStopsList.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { State } from 'common/types/state'; +import { Typography } from 'components/UI/Typography/Typography'; +import t from 'utils/typograph'; + +import { MapStopsSidebarRow } from '../Row/MapStopsSidebarRow'; +import { MapStopsSidebarStopsListItem } from './Item/MapStopsSidebarStopsListItem'; + +import styles from './MapStopsSidebarStopsList.module.css'; + +export function MapStopsSidebarStopsList() { + const stopInfo = useSelector((state: State) => state.publicTransport.stopInfo); + + return ( + + {stopInfo.map((vehicle) => ( + + ))} + {stopInfo.length === 0 && ( + + {t('О нет! В ближайшее время транспорта тут не будет')} + + )} + + ); +} diff --git a/client/components/Map/Vehicles/MapVehicles.tsx b/client/components/Map/Vehicles/MapVehicles.tsx index 82311b8e..0b6cd24f 100644 --- a/client/components/Map/Vehicles/MapVehicles.tsx +++ b/client/components/Map/Vehicles/MapVehicles.tsx @@ -50,7 +50,7 @@ export function MapVehicles({ vehicles, type }: MapVehiclesProps) { const isStopActive = currentStop || Boolean(currentStopVehicles.length); - if (isStopActive) { + if (isStopActive && !currentVehicle) { return currentStopVehicles.some( (stopVehicle) => stopVehicle.route === vehicle.num && diff --git a/client/components/Map/Vehicles/Sidebar/MapVehiclesSidebar.module.css b/client/components/Map/Vehicles/Sidebar/MapVehiclesSidebar.module.css index 9093b63b..d13f6096 100644 --- a/client/components/Map/Vehicles/Sidebar/MapVehiclesSidebar.module.css +++ b/client/components/Map/Vehicles/Sidebar/MapVehiclesSidebar.module.css @@ -235,11 +235,16 @@ background-color: var(--tram-translucent); } -.MapVehiclesSidebarStation { +.MapVehiclesSidebarStop { display: flex; gap: 16px; padding: 16px 0 16px 22px; position: relative; + cursor: pointer; +} + +.MapVehiclesSidebarStop:hover .MapVehiclesSidebarStopName { + color: var(--vehicle-color); } .MapVehiclesSidebarStartStation { @@ -288,8 +293,10 @@ bottom: 26px; } -.MapVehiclesSidebarStationName { +.MapVehiclesSidebarStopName { flex: 1; + + transition: color 150ms ease-out; } .MapVehiclesSidebarHiddenStationWrapper { diff --git a/client/components/Map/Vehicles/Sidebar/MapVehiclesSidebar.tsx b/client/components/Map/Vehicles/Sidebar/MapVehiclesSidebar.tsx index b781072d..4ec00abe 100644 --- a/client/components/Map/Vehicles/Sidebar/MapVehiclesSidebar.tsx +++ b/client/components/Map/Vehicles/Sidebar/MapVehiclesSidebar.tsx @@ -1,9 +1,12 @@ // FIXME: cure divatosis /* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable jsx-a11y/no-static-element-interactions */ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import L from 'leaflet'; import classNames from 'classnames/bind'; +import { useDispatch, useSelector } from 'react-redux'; + +import { store } from 'state'; import t from 'utils/typograph'; import { @@ -42,6 +45,10 @@ import { import { getPointsRow } from './MapVehiclesSidebar.utils'; import styles from './MapVehiclesSidebar.module.css'; +import { setCurrentStop } from 'state/features/public-transport'; +import { sidebarService } from 'services/sidebar/sidebar'; +import { MapStopsSidebar } from 'components/Map/Stops/Sidebar/MapStopsSidebar'; +import { State } from 'common/types/state'; export type MapVehiclesSidebarProps = { type: StopType; @@ -73,6 +80,7 @@ export function MapVehiclesSidebar({ depoTitle, coords, }: MapVehiclesItemProps) { + const dispatch = useDispatch(); const [from, to] = [firstStation, lastStation]; const [stops, setStops] = useState([]); const [unitInfo, setUnitInfo] = useState(null); @@ -187,8 +195,40 @@ export function MapVehiclesSidebar({ ); const endStop = useMemo(() => stops[stops.length - 1], [stops]); + const allStops = useSelector((state: State) => state.publicTransport.stops); + const setSelectedStop = useCallback( + (stopId: string) => { + const stop = allStops.find((stopFullData) => stopFullData.attributes.stopId === stopId); + + if (!stop) { + return; + } + + const { attributes: stopData } = stop; + + dispatch( + setCurrentStop({ + currentStop: stopId, + shouldClear: false, + }), + ); + + sidebarService.open({ + component: , + onClose: () => dispatch(setCurrentStop(null)), + }); + }, + [dispatch], + ); + return ( -
+
{unitInfo?.image.data && (
@@ -294,7 +334,10 @@ export function MapVehiclesSidebar({ > {/* TODO: вытащить li в компонент */} {nearestStopIndex !== 0 && ( -
  • +
  • setSelectedStop(stops[0].stopId)} + >
    @@ -348,7 +391,10 @@ export function MapVehiclesSidebar({
    {afterOpened && afterStart.map((stop) => ( -
  • +
  • setSelectedStop(stop.stopId)} + >
    {stop.title} @@ -372,7 +418,10 @@ export function MapVehiclesSidebar({ className={cn(styles.MapVehiclesSidebarActiveBorder)} style={{ backgroundColor: VEHICLE_TYPE_COLORS[type] }} /> -
  • +
  • setSelectedStop(stops[nearestStopIndex].stopId)} + >
    - + {stops[nearestStopIndex]?.title}
  • {afterNearest.map((stop) => ( -
  • +
  • setSelectedStop(stop.stopId)} + >
    - + {t(stop.title)} {'arriveTime' in stop && ( @@ -445,7 +495,8 @@ export function MapVehiclesSidebar({ {beforeOpened && beforeEnd.map((stop) => (
  • setSelectedStop(stop.stopId)} >
    {t(stop.title)} @@ -470,7 +521,10 @@ export function MapVehiclesSidebar({
    )} {stops.length > 0 && nearestStopIndex !== stops.length - 1 && ( -
  • +
  • setSelectedStop(endStop.stopId)} + >
    - + {t(endStop.title)} diff --git a/client/state/features/public-transport.ts b/client/state/features/public-transport.ts index a684d159..e72cac96 100644 --- a/client/state/features/public-transport.ts +++ b/client/state/features/public-transport.ts @@ -1,25 +1,43 @@ import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { uniq } from 'lodash'; +import { isEmpty, uniq } from 'lodash'; import { massTransApi } from 'api/masstrans/masstrans'; import { SetCurrentStopPayload, SetCurrentVehiclePayload, SetStopsPayload, - CurrentVehiclePayload, State, CurrentStopPayload, + CurrentVehiclePayloadWithOptions, + CurrentVehiclePayload, + CurrentStopPayloadWithOptions, } from 'common/types/state'; import { initialState } from '../constants/public-transport'; +const SLICE_NAME = 'publicTransport'; + +function isCurrentVehiclePayload(obj: unknown): obj is CurrentVehiclePayload { + return Boolean( + (obj as CurrentVehiclePayload).num && + (obj as CurrentVehiclePayload).routeId && + (obj as CurrentVehiclePayload).routeDirection && + (obj as CurrentVehiclePayload).type, + ); +} + export const setCurrentVehicle = createAsyncThunk( - 'publicTransport/setCurrentVehicle', - async (currentVehicle: CurrentVehiclePayload): Promise => { - if (!currentVehicle) { + `${SLICE_NAME}/setCurrentVehicle`, + async ( + currentVehiclePayload: CurrentVehiclePayloadWithOptions, + ): Promise => { + const { shouldClear = true, ...currentVehicle } = currentVehiclePayload || {}; + + if (!isCurrentVehiclePayload(currentVehicle)) { return { - currentVehicle, + currentVehicle: null, currentRoute: null, + shouldClear, }; } @@ -34,17 +52,21 @@ export const setCurrentVehicle = createAsyncThunk( type: currentVehicle.type, routeDirection: currentVehicle.routeDirection, }, + shouldClear, }; }, ); export const setCurrentStop = createAsyncThunk( - 'publicTransport/setCurrentStop', - async (currentStop: CurrentStopPayload): Promise => { + `${SLICE_NAME}/setCurrentStop`, + async (currentStopPayload: CurrentStopPayloadWithOptions): Promise => { + const { currentStop, shouldClear = true } = currentStopPayload || {}; + if (!currentStop) { return { currentStop, stopInfo: [], + shouldClear, }; } @@ -53,12 +75,13 @@ export const setCurrentStop = createAsyncThunk( return { currentStop, stopInfo, + shouldClear, }; }, ); const publicTransportSlice = createSlice({ - name: 'publicTransport', + name: SLICE_NAME, initialState, reducers: { setStops(state: State['publicTransport'], action: PayloadAction) { @@ -80,21 +103,25 @@ const publicTransportSlice = createSlice({ builder.addCase( setCurrentVehicle.fulfilled, (state, action: PayloadAction) => { - const { currentVehicle, currentRoute } = action.payload; + const { currentVehicle, currentRoute, shouldClear } = action.payload; state.currentVehicle = currentVehicle; state.currentRoute = currentRoute; - state.currentStop = null; - state.stopInfo = []; - state.stopVehicles = []; - if (!currentRoute) { state.vehicleStops = []; return; } + if (!shouldClear) { + return; + } + + state.currentStop = null; + state.stopInfo = []; + state.stopVehicles = []; + const racesInDirection = currentRoute.races.find( (race) => race.raceType === currentVehicle.routeDirection, ); @@ -107,7 +134,7 @@ const publicTransportSlice = createSlice({ builder.addCase( setCurrentStop.fulfilled, (state, action: PayloadAction) => { - const { currentStop, stopInfo } = action.payload; + const { currentStop, stopInfo, shouldClear } = action.payload; const stopVehicles = uniq( stopInfo?.map((info) => ({ @@ -121,6 +148,10 @@ const publicTransportSlice = createSlice({ state.stopInfo = stopInfo; state.stopVehicles = stopVehicles; + if (!shouldClear) { + return; + } + state.currentVehicle = null; state.currentRoute = null; state.vehicleStops = []; diff --git a/common/types/masstrans.ts b/common/types/masstrans.ts index d7a491d7..d4af25d6 100644 --- a/common/types/masstrans.ts +++ b/common/types/masstrans.ts @@ -39,6 +39,7 @@ export interface Stop { export interface StopInfoItem { arriveTime: string; route: string; + routeId: number; type: ClientUnit; to: string; routeDirection: string; diff --git a/common/types/strapi.ts b/common/types/strapi.ts index e4bc8760..362337f5 100644 --- a/common/types/strapi.ts +++ b/common/types/strapi.ts @@ -6,20 +6,16 @@ export enum StrapiContentTypes { UnitInfo = 'unit-info', } -export interface StrapiStop { +export interface StrapiBase { id: number; - attributes: Stop; + attributes: A; } -export interface StrapiUnitInfo { - id: number; - attributes: UnitInfo; -} +export type StrapiStop = StrapiBase; -export interface StrapiTree { - id: number; - attributes: TransportTreeOfType; -} +export type StrapiUnitInfo = StrapiBase; + +export type StrapiTree = StrapiBase; export interface StrapiLoginResponse { user: string;