Skip to content

Commit

Permalink
th-216: Truck filters functionality (BinaryStudioAcademy#380)
Browse files Browse the repository at this point in the history
* th-216: * moves the interval in a separate function

* th-216: * updates getAllOpenedWithTrucks method to return only active trucks

* th-216: * updated towTruckCard and adds helpers

* th-216: * adds the ability to add truck markers and user location to the hook

* th-216: * adds libraries and methods for autocomplete and zoom control

* th-216: + hooks and helpers for homepage

* th-216: + map component for homepage

* th-216: + autocomplete component

* th-216: + adds functionality to the filter

* th-216: + adds the ability to control the display of dropdown value

* th-216: + adds filters to homepage

* th-216: * updates map component

* th-216: * updates createIcon helper

* th-216: * updates types for choosenTruck

* th-216: + adds possibility to set location from navigator

* th-216: + custom autocomplete

* th-216: + autocomplete component

* th-216: * updates truck filter

* th-216: * updates homepage

* th-216: * updates imports

* th-216: * moves setZoom to a separate useEffect

* th-216: - removed @react-google-maps/api package

* th-216: - removes unused types

* th-216: * updates type name

---------

Co-authored-by: Alina Kupchyk <[email protected]>
  • Loading branch information
h0wter and kalinkaaaa14 committed Sep 29, 2023
1 parent 4e85f4f commit 217e07f
Show file tree
Hide file tree
Showing 48 changed files with 595 additions and 210 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class GeolocationCacheSocketService {

private appConfig: IConfig;

private isTruckLocationUpdateIntervalActive = false;

public constructor({
geolocationCacheService,
shiftService,
Expand Down Expand Up @@ -55,15 +57,16 @@ class GeolocationCacheSocketService {
this.truckLocationUpdate(payload);
},
);
setInterval(() => {
void getTrucksList(this.shiftService, this.geolocationCacheService).then(
(result) => {
this.io
.to(SocketRoom.HOME_ROOM)
.emit(ServerToClientEvent.TRUCKS_LIST_UPDATE, result);
},

this.sendTrucksList();

if (!this.isTruckLocationUpdateIntervalActive) {
this.isTruckLocationUpdateIntervalActive = true;
setInterval(
() => this.sendTrucksList(),
this.appConfig.ENV.SOCKET.TRUCK_LIST_UPDATE_INTERVAL,
);
}, this.appConfig.ENV.SOCKET.TRUCK_LIST_UPDATE_INTERVAL);
}
}

private truckLocationUpdate(
Expand All @@ -74,6 +77,16 @@ class GeolocationCacheSocketService {
const { truckId, latLng } = payload;
this.geolocationCacheService.setCache(truckId, latLng);
}

private sendTrucksList(): void {
void getTrucksList(this.shiftService, this.geolocationCacheService).then(
(result) => {
this.io
.to(SocketRoom.HOME_ROOM)
.emit(ServerToClientEvent.TRUCKS_LIST_UPDATE, result);
},
);
}
}

export { GeolocationCacheSocketService };
15 changes: 13 additions & 2 deletions backend/src/packages/shifts/shift.repository.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { and, desc, eq, isNull } from 'drizzle-orm';
import { and, desc, eq, inArray, isNull } from 'drizzle-orm';

import { type IRepository } from '~/libs/interfaces/interfaces.js';
import { type IDatabase } from '~/libs/packages/database/libs/interfaces/database.interface.js';
Expand All @@ -7,6 +7,7 @@ import {
schema,
} from '~/libs/packages/database/schema/schema.js';

import { TruckStatus } from '../trucks/libs/enums/enums.js';
import { type TruckEntityT } from '../trucks/libs/types/types.js';
import { TruckEntity } from '../trucks/truck.entity.js';
import { type ShiftDatabaseModel } from './libs/types/types.js';
Expand Down Expand Up @@ -106,7 +107,17 @@ class ShiftRepository implements IRepository {

public async getAllOpenedWithTrucks(): Promise<TruckEntityT[]> {
const result = await this.db.driver().query.shifts.findMany({
where: isNull(this.shiftSchema.endDate),
where: and(
isNull(this.shiftSchema.endDate),
inArray(
this.shiftSchema.truckId,
this.db
.driver()
.select({ id: schema.trucks.id })
.from(schema.trucks)
.where(eq(schema.trucks.status, TruckStatus.ACTIVE)),
),
),
with: { truck: true },
});

Expand Down
1 change: 0 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"@fortawesome/react-fontawesome": "0.2.0",
"@googlemaps/js-api-loader": "1.16.2",
"@hookform/resolvers": "2.9.11",
"@react-google-maps/api": "2.19.2",
"@reduxjs/toolkit": "1.9.3",
"@rollup/pluginutils": "5.0.4",
"@svgr/core": "8.1.0",
Expand Down
75 changes: 75 additions & 0 deletions frontend/src/libs/components/autocomplete/autocomplete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { type FieldValues } from 'react-hook-form/dist/types';

import { getValidClassNames } from '~/libs/helpers/helpers.js';
import { useCallback, useEffect, useRef } from '~/libs/hooks/hooks.js';
import { type MapService } from '~/libs/packages/map/map.js';
import { MapConnector } from '~/libs/packages/map/map-connector.package.js';
import { type LocationChangeHandler } from '~/libs/types/types.js';

type Properties = {
placeholder?: string;
field?: FieldValues;
isDisabled?: boolean;
inputStyles?: string | string[];
onPlaceChanged: LocationChangeHandler;
};

const PLACE_CHANGED_EVENT = 'place_changed';

const Autocomplete = ({
placeholder,
field,
isDisabled = false,
inputStyles = [],
onPlaceChanged,
}: Properties): JSX.Element => {
const inputReference = useRef<HTMLInputElement>(null);
const mapService = useRef<MapService | null>(null);
const autocomplete = useRef<google.maps.places.Autocomplete | null>(null);

const onPlaceChange = useCallback(() => {
const place = autocomplete.current?.getPlace();

if (place?.geometry?.location) {
onPlaceChanged(
{
lat: place.geometry.location.lat(),
lng: place.geometry.location.lng(),
},
place.formatted_address as string,
);
}
}, [onPlaceChanged]);

useEffect(() => {
const getMapService = async (): Promise<void> => {
if (inputReference.current) {
await MapConnector.getInstance();

mapService.current = new MapConnector().getMapService({
mapElement: null,
});

autocomplete.current = mapService.current.createAutocomplete(
inputReference.current,
);

autocomplete.current.addListener(PLACE_CHANGED_EVENT, onPlaceChange);
}
};
void getMapService();
}, [onPlaceChange]);

return (
<input
{...field}
type="text"
placeholder={placeholder}
className={getValidClassNames(inputStyles)}
disabled={isDisabled}
ref={inputReference}
/>
);
};

export { Autocomplete };
3 changes: 3 additions & 0 deletions frontend/src/libs/components/dropdown/dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type Properties<T extends FieldValues> = {
field?: ControllerRenderProps<T, FieldPath<T>>;
className?: string;
isCustomValueContainer?: boolean;
controlShouldRenderValue?: boolean;
};

type GetClassNamesArguments = {
Expand Down Expand Up @@ -78,6 +79,7 @@ const Dropdown = <T extends FieldValues>({
placeholder,
isCustomValueContainer = false,
label,
controlShouldRenderValue = true,
}: Properties<T>): JSX.Element => {
const [isMenuOpen, setIsMenuOpen] = useState(false);

Expand Down Expand Up @@ -115,6 +117,7 @@ const Dropdown = <T extends FieldValues>({
defaultValue={defaultValue}
value={findOptionByValue(field ? field.value : label)}
placeholder={placeholder}
controlShouldRenderValue={controlShouldRenderValue}
/>
);
};
Expand Down
1 change: 1 addition & 0 deletions frontend/src/libs/components/dropdown/styles.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
.container {
.control {
height: 20px;
min-height: auto;
background: transparent;
border: none;
outline: none;
Expand Down
59 changes: 19 additions & 40 deletions frontend/src/libs/components/location-input/location-input.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Autocomplete } from '@react-google-maps/api';
import {
type Control,
type FieldErrors,
Expand All @@ -7,13 +6,10 @@ import {
} from 'react-hook-form';

import { getValidClassNames } from '~/libs/helpers/helpers.js';
import {
useCallback,
useFormController,
useState,
} from '~/libs/hooks/hooks.js';
import { type LocationChangeHandler } from '~/libs/types/location-change-handler.type';
import { useCallback, useFormController } from '~/libs/hooks/hooks.js';
import { type LocationChangeHandler } from '~/libs/types/types.js';

import { Autocomplete } from '../autocomplete/autocomplete.js';
import styles from './styles.module.scss';

type Properties<T extends FieldValues> = {
Expand All @@ -36,8 +32,6 @@ const LocationInput = <T extends FieldValues>({
onLocationChange,
}: Properties<T>): JSX.Element => {
const { field } = useFormController({ name, control });
const [location, setLocation] =
useState<google.maps.places.Autocomplete | null>(null);

const error = errors[name]?.message;
const hasError = Boolean(error);
Expand All @@ -50,47 +44,32 @@ const LocationInput = <T extends FieldValues>({
hasError && styles.error,
];

const handleLoad = useCallback(
(instance: google.maps.places.Autocomplete) => {
setLocation(instance);
const handlePlaceChanged = useCallback(
(location: google.maps.LatLngLiteral, address: string) => {
field.onChange(address);

if (onLocationChange) {
onLocationChange(location, address);
}
},
[],
[onLocationChange, field],
);

const handlePlaceChanged = useCallback(() => {
const place = location?.getPlace();
field.onChange(place?.formatted_address);

if (onLocationChange && place?.geometry?.location) {
onLocationChange(
{
lat: place.geometry.location.lat(),
lng: place.geometry.location.lng(),
},
place.formatted_address as string,
);
}
}, [location, onLocationChange, field]);

return (
<label className={styles.inputComponentWrapper}>
{hasLabel && <span className={styles.label}>{label}</span>}
<span className={styles.inputWrapper}>
<Autocomplete
options={{
types: ['address'],
field={{
ref: field.ref,
name: field.name,
onBlur: field.onBlur,
}}
onLoad={handleLoad}
placeholder={placeholder}
inputStyles={getValidClassNames(...inputStyles)}
isDisabled={isDisabled}
onPlaceChanged={handlePlaceChanged}
>
<input
{...field}
type="text"
placeholder={placeholder}
className={getValidClassNames(...inputStyles)}
disabled={isDisabled}
/>
</Autocomplete>
/>
</span>

<span
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/libs/components/map/map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ const Map: React.FC<Properties> = ({
}

if (markers.length > 0) {
mapService.current.removeMarkers();

for (const marker of markers) {
mapService.current.addMarker(marker, true);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const METERS_IN_KILOMETER = 1000;
const DECIMAL_POINTS = 1;

export { DECIMAL_POINTS, METERS_IN_KILOMETER };
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { DECIMAL_POINTS, METERS_IN_KILOMETER } from '../constants/constants.js';

const formatDistanceIntoKilometers = (distance: number): string => {
return (distance / METERS_IN_KILOMETER).toFixed(DECIMAL_POINTS);
};

export { formatDistanceIntoKilometers };
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { formatDistanceIntoKilometers } from './format-distance-into-kilometers.helper.js';
export { getTowTruckImage } from './get-tow-truck-image.helper.js';
15 changes: 10 additions & 5 deletions frontend/src/libs/components/tow-truck-card/tow-truck-card.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import { IconName } from '~/libs/enums/icon-name.enum.js';
import { getValidClassNames } from '~/libs/helpers/helpers.js';
import { type TruckEntityT } from '~/libs/types/types.js';
import { manufacturerKeyToReadableName } from '~/packages/trucks/libs/maps/maps.js';
import { type TruckWithDistance } from '~/pages/homepage/libs/types/types.js';

import { Badge } from '../badge/badge.js';
import { Button } from '../button/button.js';
import { Icon } from '../icon/icon.js';
import { getTowTruckImage } from './lib/helpers/helpers.js';
import {
formatDistanceIntoKilometers,
getTowTruckImage,
} from './libs/helpers/helpers.js';
import styles from './styles.module.scss';

type Properties = {
truck: TruckEntityT;
truck: TruckWithDistance;
distance?: number;
hasButton?: boolean;
onOrderButtonClick?: () => void;
};

const TowTruckCard: React.FC<Properties> = ({
truck,
distance,
hasButton = true,
onOrderButtonClick,
}: Properties) => {
Expand All @@ -27,9 +29,12 @@ const TowTruckCard: React.FC<Properties> = ({
capacity,
pricePerKm,
towType,
distance,
} = truck;
const img = getTowTruckImage(towType);
const manufacturer = manufacturerKeyToReadableName[manufacturerRaw];
const distanceInKilometers =
distance && formatDistanceIntoKilometers(distance);

return (
<div className={getValidClassNames(styles.container)}>
Expand All @@ -54,7 +59,7 @@ const TowTruckCard: React.FC<Properties> = ({
</div>
<Badge color="grey">
<Icon iconName={IconName.LOCATION_DOT} />
<span className={styles.km}>{distance} km</span>
<span className={styles.km}>{distanceInKilometers} km</span>
</Badge>
</div>
{hasButton && <Button label="order now" onClick={onOrderButtonClick} />}
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/libs/components/truck-filter/libs/enums/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { FilterOption } from './filter-option.enum.js';
export { FilterValue } from './filter-value.enum.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const FilterOption = {
LOW_TO_HIGH: 'Low to High',
HIGH_TO_LOW: 'High to Low',
} as const;

export { FilterOption };
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const FilterValue = {
ASC: 'asc',
DESC: 'desc',
} as const;

export { FilterValue };
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Fields } from './filter-fields.js';
Loading

0 comments on commit 217e07f

Please sign in to comment.