Skip to content

Commit

Permalink
Feature: search bar stops and vehicles (#85)
Browse files Browse the repository at this point in the history
* feature: search bar stops and vehicles

* removed unused styles for search bar

* sashachabin review

* removed search bar for map without controls

* fixed visibility of vehicles of selected direction, typo in search through units

* removed useless media query

* moved form within into form

* fixes for mobile safari

* always show closer for input when there is text
  • Loading branch information
hiba9201 authored Nov 5, 2023
1 parent a5dec32 commit ed626bb
Show file tree
Hide file tree
Showing 33 changed files with 872 additions and 278 deletions.
22 changes: 18 additions & 4 deletions client/common/types/state.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
import { Route, StopInfoItem, Unit } from 'transport-common/types/masstrans';
import { ClientUnit, Route, StopInfoItem, Unit } from 'transport-common/types/masstrans';
import { StrapiStop } from 'transport-common/types/strapi';

export interface State {
publicTransport: {
currentVehicle: Pick<Unit, 'num' | 'routeId' | 'routeDirection' | 'type'> | null;
currentVehicle:
| (Pick<Unit, 'num' | 'routeId' | 'routeDirection' | 'type'> & CurrentVehicleOptions)
| null;
currentStop: string | null;
currentRoute: Route & Pick<Unit, 'type' | 'routeDirection'>;
currentRoute: Route & Pick<Unit, 'type' | 'routeDirection'> & CurrentRouteOptions;
stops: StrapiStop[];
vehicleStops: StrapiStop['attributes']['stopId'][];
stopVehicles: Pick<StopInfoItem, 'route' | 'type' | 'routeDirection'>[];
stopInfo: StopInfoItem[];
units: Record<ClientUnit, Unit[]>;
};
}

export interface CurrentVehicleSettings {
shouldFilterByRouteDirection?: boolean;
}
export type CurrentVehiclePayload = State['publicTransport']['currentVehicle'];
export interface CurrentVehicleOptions {
shouldClear?: boolean;
shouldFlyTo?: boolean;
shouldFilterByRouteDirection?: boolean;
}
export type CurrentVehiclePayloadWithOptions = CurrentVehiclePayload & CurrentVehicleOptions;
export interface CurrentRouteOptions {
shouldFlyTo?: boolean;
}
export interface SetCurrentVehiclePayload extends CurrentVehicleOptions {
currentVehicle: CurrentVehiclePayload;
currentRoute: State['publicTransport']['currentRoute'];
currentRoute: State['publicTransport']['currentRoute'] & CurrentRouteOptions;
}
export type CurrentStopPayload = State['publicTransport']['currentStop'];
export interface CurrentStopOptions {
Expand All @@ -34,3 +45,6 @@ export interface SetCurrentStopPayload extends CurrentStopOptions {
stopInfo: State['publicTransport']['stopInfo'];
}
export type SetStopsPayload = State['publicTransport']['stops'];

export type SetUnitsPayload =
State['publicTransport']['units'][keyof State['publicTransport']['units']];
13 changes: 13 additions & 0 deletions client/components/Map/MainContainer/MapMainContainer.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,16 @@
width: 100%;
height: 100%;
}

.MapSidebar {
position: relative;
max-height: calc(100vh - 100px);
padding: 24px 16px;
max-width: 448px;
width: 100%;

display: grid;
gap: 16px;

grid-template: fit-content(50%) 1fr / 1fr;
}
17 changes: 16 additions & 1 deletion client/components/Map/MainContainer/MapMainContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import React, { useEffect, useState } from 'react';
import { MapContainer, Pane, TileLayer } from 'react-leaflet';
import classNames from 'classnames/bind';
import sidebarStyles from 'styles/leaflet-sidebar.module.css';

import { sidebarService } from 'services/sidebar/sidebar';

import { COORDS_EKATERINBURG, BOUNDS_EKATERINBURG } from 'common/constants/coords';
import { POSITION_CLASSES } from 'common/constants/positions';

import { MapLocation } from 'components/Map/Location/MapLocation';
import { MapTransport } from 'components/Map/Transport/MapTransport';
import { MapZoomControl } from 'components/Map/ZoomControl/MapZoomControl';
import { MapWelcomeMessage } from 'components/Map/WelcomeMessage/MapWelcomeMessage';

import { MapSearchBar } from '../SearchBar/SearchBar';

import styles from './MapMainContainer.module.css';
import 'leaflet/dist/leaflet.css';

Expand Down Expand Up @@ -60,8 +64,19 @@ function MapMainContainer({ zoom = 16, showControls = true }) {
)}

<MapTransport />
{showControls && (
<div
className={cn(
POSITION_CLASSES.topleft,
sidebarStyles.leafletSidebar,
styles.MapSidebar,
)}
>
<MapSearchBar />

{sidebar}
{sidebar}
</div>
)}
</MapContainer>
);
}
Expand Down
18 changes: 18 additions & 0 deletions client/components/Map/SearchBar/SearchBar.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { uniqBy } from 'lodash';
import { Unit } from 'transport-common/types/masstrans';

const uniqUnitsIteratee = (unit: Unit) => `${unit.num}${unit.routeDirection}`;

export function searchThroughUnits(units: Unit[], searchText: string) {
return uniqBy(
units
.filter(
(unit) =>
unit.lastStation.toLowerCase().includes(searchText) ||
unit.firstStation.toLowerCase().includes(searchText) ||
unit.num.toLowerCase().includes(searchText),
)
.sort((a, b) => a.firstStation.localeCompare(b.firstStation, 'ru', { numeric: true })),
uniqUnitsIteratee,
);
}
79 changes: 79 additions & 0 deletions client/components/Map/SearchBar/SearchBar.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
.MapSearchBar {
display: flex;
flex-direction: column;
gap: 2px;

height: max-content;
max-height: calc(45vh - 100px);

border-radius: 8px;
border: 4px solid rgba(230, 228, 224, 0.64);

background-color: rgba(230, 228, 224, 0.64);
}

.MapSearchBar__form {
display: flex;

height: 48px;
width: 100%;

padding: 12px 16px;
border-radius: 4px;

background-color: #fff;
gap: 8px;

&:focus-within {
outline: 2px solid #282828;
}
}

.MapSearchBar__form_withResult {
border-radius: 4px 4px 0px 0px;
}

.MapSearchBar__input {
width: 100%;
padding: 0;
border: none;
outline: none;

font-family: inherit;
font-weight: 400;
font-size: 18px;
line-height: 24px;

background-color: transparent;

&::placeholder {
color: #000;
opacity: 1;
}

&:focus::placeholder {
opacity: 0;
}

&::-webkit-search-cancel-button {
appearance: none;
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYgNkwxOCAxOE02IDE4TDE4IDYiIHN0cm9rZT0iIzlCQUFDMyIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0ic3F1YXJlIi8+Cjwvc3ZnPgoK);
background-size: contain;
opacity: 1;
pointer-events: all;
cursor: pointer;
width: 24px;
height: 24px;
margin: 0;
}
}

.MapSearchBar__results {
display: flex;
flex-direction: column;
gap: 2px;

overflow: auto;

border-radius: 0px 0px 4px 4px;
}
98 changes: 98 additions & 0 deletions client/components/Map/SearchBar/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import classNames from 'classnames/bind';

import { ClientUnit, Unit } from 'transport-common/types/masstrans';
import { StrapiStop } from 'transport-common/types/strapi';

import { useDisablePropagation } from 'hooks/useDisablePropagation';
import { State } from 'common/types/state';

import { MapSearchBarUnitResult } from './UnitResult/UnitResult';
import { MapSearchBarStopResult } from './StopsResult/StopResult';

import styles from './SearchBar.module.css';
import { searchThroughUnits } from './SearchBar.helpers';

const cn = classNames.bind(styles);

const INPUT_PLACEHOLDER = 'Поиск';

export function MapSearchBar() {
const searchBarRef = useRef<HTMLDivElement>(null);
const [searchResult, setSearchResult] = useState<{ stops: StrapiStop[]; units: Unit[] }>({
stops: [],
units: [],
});

const stops = useSelector((state: State) => state.publicTransport.stops);
const units = useSelector((state: State) => state.publicTransport.units);

useDisablePropagation(searchBarRef);

const onSearch = useCallback(
(event) => {
const searchText = (event.target as HTMLInputElement).value.toLowerCase();

if (!searchText) {
setSearchResult({ stops: [], units: [] });

return;
}

const stopsSearch = stops
.filter((stop) => stop.attributes.title.toLowerCase().includes(searchText))
.sort((a, b) =>
a.attributes.title.localeCompare(b.attributes.title, 'ru', { numeric: true }),
);

const trollsSearch = searchThroughUnits(units[ClientUnit.Troll], searchText);
const tramsSearch = searchThroughUnits(units[ClientUnit.Tram], searchText);
const busesSearch = searchThroughUnits(units[ClientUnit.Bus], searchText);

setSearchResult({
stops: stopsSearch,
units: [...trollsSearch, ...tramsSearch, ...busesSearch],
});
},
[stops, units],
) as React.FormEventHandler;

const hasResults = useMemo(
() => Boolean(searchResult.stops.length || searchResult.units.length),
[searchResult],
);

const onSubmit = useCallback((event) => {
event.preventDefault();
}, []) as React.FormEventHandler;

return (
<div className={cn(styles.MapSearchBar)} ref={searchBarRef}>
<form
className={cn(styles.MapSearchBar__form, {
[styles.MapSearchBar__form_withResult]: hasResults,
})}
onSubmit={onSubmit}
>
<img src="/icons/search.svg" alt="" />
<input
type="search"
placeholder={INPUT_PLACEHOLDER}
onInput={onSearch}
className={cn(styles.MapSearchBar__input)}
/>
</form>
{hasResults && (
<div className={cn(styles.MapSearchBar__results)}>
{searchResult.units.map((unit) => (
<MapSearchBarUnitResult {...unit} />
))}
{searchResult.stops.map(({ attributes: stop }) => (
<MapSearchBarStopResult {...stop} />
))}
</div>
)}
</div>
);
}
38 changes: 38 additions & 0 deletions client/components/Map/SearchBar/StopsResult/StopResult.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.MapSearchBarStopResult__wrapper {
display: flex;
align-items: center;
cursor: pointer;
border: none;

background-color: #fff;

font-family: inherit;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: 1.05;

padding: 8px;
gap: 10px;

transition: background-color 150ms ease-out;

& span {
font-size: 28px;
line-height: 1.16;
}

&:hover {
background-color: #f4f4f4;
}
}

.MapSearchBarStopResult__wrapper_selected {
background-color: #f4f4f4;
}

.MapSearchBarStopResult__text {
margin: 0;
text-align: left;
color: #000;
}
Loading

0 comments on commit ed626bb

Please sign in to comment.