diff --git a/package-lock.json b/package-lock.json index ca8e237..993628a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "foodwagon-online-shop", - "version": "1.1.4", + "version": "1.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "foodwagon-online-shop", - "version": "1.1.4", + "version": "1.1.5", "dependencies": { "@types/jest": "^27.5.2", "@types/node": "^16.18.68", @@ -26,6 +26,7 @@ "@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/react-fontawesome": "^0.2.0", "@hookform/resolvers": "^3.3.2", + "@pbe/react-yandex-maps": "^1.2.5", "@reduxjs/toolkit": "^1.9.3", "@types/lodash.debounce": "^4.0.9", "@types/react": "^18.2.43", @@ -3908,6 +3909,21 @@ "node": ">= 8" } }, + "node_modules/@pbe/react-yandex-maps": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@pbe/react-yandex-maps/-/react-yandex-maps-1.2.5.tgz", + "integrity": "sha512-cBojin5e1fPx9XVCAqHQJsCnHGMeBNsP0TrNfpWCrPFfxb30ye+JgcGr2mn767Gbr1d+RufBLRiUcX2kaiAwjQ==", + "dev": true, + "dependencies": { + "@types/yandex-maps": "2.1.29" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x" + } + }, "node_modules/@pkgr/utils": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", @@ -4837,6 +4853,12 @@ "@types/node": "*" } }, + "node_modules/@types/yandex-maps": { + "version": "2.1.29", + "resolved": "https://registry.npmjs.org/@types/yandex-maps/-/yandex-maps-2.1.29.tgz", + "integrity": "sha512-nuibRWj3RU/9KXlCzTrRtDE+n6V9l7NbT9JakicqZ5OXIdwyb6blvV2Uwn6lB58WYm3DSUDP2I2AWlnWMc8z2w==", + "dev": true + }, "node_modules/@types/yargs": { "version": "16.0.9", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", diff --git a/package.json b/package.json index 5ff5ed1..fb00df1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "foodwagon-online-shop", - "version": "1.1.4", + "version": "1.1.5", "private": true, "dependencies": { "@types/jest": "^27.5.2", @@ -50,6 +50,7 @@ "@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/react-fontawesome": "^0.2.0", "@hookform/resolvers": "^3.3.2", + "@pbe/react-yandex-maps": "^1.2.5", "@reduxjs/toolkit": "^1.9.3", "@types/lodash.debounce": "^4.0.9", "@types/react": "^18.2.43", diff --git a/public/images/find-food/preloader.svg b/public/images/find-food/preloader.svg new file mode 100644 index 0000000..a470d0f --- /dev/null +++ b/public/images/find-food/preloader.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/elements/FeaturedRestaurants/FeaturedRestaurants.tsx b/src/components/elements/FeaturedRestaurants/FeaturedRestaurants.tsx index 7fc7727..eba1010 100644 --- a/src/components/elements/FeaturedRestaurants/FeaturedRestaurants.tsx +++ b/src/components/elements/FeaturedRestaurants/FeaturedRestaurants.tsx @@ -53,7 +53,7 @@ export const FeaturedRestaurants: FC = () => { dispatch( fetchRestaurants({ category: categoryNames[category], - limit, + limit: 20, orderType, sortType, }), diff --git a/src/components/elements/FindFood/FindFood.tsx b/src/components/elements/FindFood/FindFood.tsx index edb9c9c..0443967 100644 --- a/src/components/elements/FindFood/FindFood.tsx +++ b/src/components/elements/FindFood/FindFood.tsx @@ -1,20 +1,110 @@ import { faLocationDot } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { FC, useRef, useState } from 'react'; +import { FC, KeyboardEvent, useEffect, useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import { useAppDispatch } from '../../../store'; +import { + fetchLocation, + isLoadedSelector, + locationListSelector, + setLocation, +} from '../../../store/slices/location/slice'; +import { Coords, LocationItem } from '../../../store/slices/location/types'; +import { placemarkSelector, restaurantListSelector, setPlacemarks } from '../../../store/slices/restaurants/slice'; import { TextInput } from '../../ui/TextInput'; import { SearchButton } from '../../ui/buttons/SearchButton'; import { DeliveryMethod } from './DeliveryMethod'; +import { Maps } from './Maps'; +import { Popup } from './Popup'; import style from './findFood.module.scss'; export const FindFood: FC = () => { const searchRef = useRef(null); + const popupRef = useRef(null); + + const dispatch = useAppDispatch(); + + const list = useSelector(locationListSelector); + const isLoaded = useSelector(isLoadedSelector); + const placemarks = useSelector(placemarkSelector); + const listRest = useSelector(restaurantListSelector); + + const [place, setPlace] = useState(''); + const [coord, setCoord] = useState([30.35151817345885, 59.94971367493227]); const [searchValue, setSearchValue] = useState(''); + const [visiblePopup, setVisiblePopup] = useState(false); + + const navigate = useNavigate(); + + useEffect(() => { + if (listRest.length) { + dispatch(setPlacemarks()); + } + }, [listRest]); + + const handleSearch = () => { + dispatch(setLocation({ address: place, coords: coord })); + navigate('search'); + }; const handleSearchValue = (text: string) => { setSearchValue(text); }; + const handleChangeCoord = (coord: Coords) => { + setCoord(coord); + }; + + const handleChangeAddress = (address: string) => { + setPlace(address); + }; + + const handleChangeLocation = ({ address, coords }: LocationItem) => { + setPlace(address); + setCoord(coords); + setVisiblePopup(false); + }; + + useEffect(() => { + if (searchValue) { + dispatch(fetchLocation({ searchValue })); + setVisiblePopup(true); + } + }, [searchValue]); + + const handleKeyDown = (event: KeyboardEvent) => { + if (popupRef.current && event.key === 'ArrowDown') { + event.preventDefault(); + popupRef.current?.focus(); + } + + if (event.key === 'Enter' && list.length) { + event.preventDefault(); + handleChangeLocation(list[0]); + } + }; + + const handleChangeStatus = (status: boolean) => { + setVisiblePopup(status); + }; + + useEffect(() => { + const handleOutsideClick = (e: MouseEvent) => { + if (popupRef.current?.contains(e.target as Node) || searchRef.current?.contains(e.target as Node)) { + setVisiblePopup(true); + } else { + setVisiblePopup(false); + } + return; + }; + + document.body.addEventListener('mousedown', handleOutsideClick); + + return () => document.body.removeEventListener('mousedown', handleOutsideClick); + }, []); + return (
@@ -22,15 +112,47 @@ export const FindFood: FC = () => {

Are you starving?

Within a few clicks, find meals that are accessible near you

-
+
+
+
+ + + + +
-
- - - - +
+ + {place && ( + + )}
diff --git a/src/components/elements/FindFood/Maps.tsx b/src/components/elements/FindFood/Maps.tsx new file mode 100644 index 0000000..f46da37 --- /dev/null +++ b/src/components/elements/FindFood/Maps.tsx @@ -0,0 +1,170 @@ +/* eslint-disable max-len */ +import { Map, ObjectManager, Placemark, YMaps } from '@pbe/react-yandex-maps'; +import cn from 'classnames'; +import debounce from 'lodash.debounce'; +import { FC, useCallback, useEffect, useRef, useState } from 'react'; +import { ReactSVG } from 'react-svg'; +import { Event } from 'yandex-maps'; + +import { PlacemarkType } from '../../../store/slices/restaurants/types'; +import './balloon.css'; +import { deliveryZones } from './deliveryZones'; + +type MapsProps = { + coord: [number, number]; + handleChangeAddress: (address: string) => void; + handleChangeCoord: (coord: [number, number]) => void; + place: string; + placemarks: PlacemarkType[]; +}; + +export const Maps: FC = ({ coord, handleChangeAddress, handleChangeCoord, place, placemarks }) => { + const [maps, setMaps] = useState(); + const [status, setStatus] = useState(true); + const [zone, setZone] = useState(null); + const [isActive, setIsActive] = useState(false); + const [visibleBalloon, setVisibleBalloon] = useState(false); + const [isLoaded, setIsLoaded] = useState(true); + + const mapRef = useRef(); + const placemarkRef = useRef(); + + const updateSearchValue = useCallback( + debounce((coord) => { + handleChangeCoord(coord); + }, 500), + [], + ); + + const getGeoLocation = (e: Event) => { + const coord = e.get('target').getCenter(); + updateSearchValue(coord); + }; + + const onLoad = (map: any) => { + setMaps(map); + + if (map && mapRef.current) { + const deliveryZone = map?.geoQuery(deliveryZones).addToMap(mapRef.current); + deliveryZone.each(function (obj: any) { + obj.options.set({ + fillColor: obj.properties.get('fill'), + fillOpacity: obj.properties.get('fill-opacity'), + strokeColor: obj.properties.get('stroke'), + strokeOpacity: obj.properties.get('stroke-opacity'), + strokeWidth: obj.properties.get('stroke-width'), + }); + obj.properties.set('balloonContent', obj.properties.get('description')); + }); + + setZone(deliveryZone); + } + }; + + useEffect(() => { + if (zone && placemarkRef.current) { + const targetZone = zone.searchContaining(placemarkRef.current).get(0); + + if (targetZone) { + setStatus(true); + } else { + setStatus(false); + } + } + + if (maps && coord?.length) { + setIsLoaded(false); + + const resp = maps?.geocode(coord); + resp + .then((res: any) => { + setIsLoaded(true); + handleChangeAddress(res.geoObjects.get(0).getAddressLine()); + }) + .catch((error: any) => { + console.error('The Promise is rejected!', error); + }); + } + }, [coord]); + + const handleActionBegin = () => { + setIsActive(true); + }; + + const handleActionEnd = () => { + setIsActive(false); + }; + + const handleVisibleBalloon = () => { + setVisibleBalloon(true); + }; + + return ( + + +
+ + {!isLoaded && ( + + )} +
+ + + + + +
+
Your location
+
{place}
+
{status ? 'Delivery available' : 'No delivery'}
+
+
+
+ ); +}; diff --git a/src/components/elements/FindFood/Popup.tsx b/src/components/elements/FindFood/Popup.tsx new file mode 100644 index 0000000..8cb7daf --- /dev/null +++ b/src/components/elements/FindFood/Popup.tsx @@ -0,0 +1,86 @@ +import classNames from 'classnames'; +import { KeyboardEvent, forwardRef, useEffect, useRef, useState } from 'react'; +import { CSSTransition } from 'react-transition-group'; +import { v4 as uuidv4 } from 'uuid'; + +import { LocationItem } from '../../../store/slices/location/types'; +import style from './popup.module.scss'; + +type PopupProps = { + handleChangeLocation: (el: LocationItem) => void; + handleChangeStatus: (status: boolean) => void; + isLoaded: boolean; + isOpen: boolean; + list: LocationItem[]; +}; + +export const Popup = forwardRef( + ({ handleChangeLocation, handleChangeStatus, isLoaded, isOpen, list }, ref) => { + const buttonRef = useRef(null); + const [activeIndex, setActiveIndex] = useState(-1); + + const handleListKeyDown = (event: KeyboardEvent) => { + if (event.key === 'ArrowDown') { + event.preventDefault(); + setActiveIndex((prevIndex) => { + if (list && prevIndex < list.length - 1) { + return prevIndex + 1; + } + return 0; + }); + } + + if (event.key === 'ArrowUp') { + event.preventDefault(); + + setActiveIndex((prevIndex) => { + if (list && prevIndex > 0) { + return prevIndex - 1; + } + return 0; + }); + } + + if (event.key === 'Escape') { + event.preventDefault(); + handleChangeStatus(false); + setActiveIndex(-1); + } + }; + + const handleButtonKeyDown = (event: KeyboardEvent, el: any) => { + if (event.key === 'Enter') { + event.preventDefault(); + handleChangeLocation(el); + setActiveIndex(-1); + } + }; + + useEffect(() => { + buttonRef.current?.focus(); + }, [activeIndex]); + + return ( + +
    + {list.map((el: LocationItem, index) => { + return ( +
  • handleChangeLocation(el)} + onKeyDown={(e) => handleButtonKeyDown(e, el)} + ref={index === activeIndex ? buttonRef : undefined} + tabIndex={0} + > + {el.address} +
  • + ); + })} +
+
+ ); + }, +); diff --git a/src/components/elements/FindFood/balloon.css b/src/components/elements/FindFood/balloon.css new file mode 100644 index 0000000..035706e --- /dev/null +++ b/src/components/elements/FindFood/balloon.css @@ -0,0 +1,86 @@ +.balloon { + background-color: white; + position: absolute; + z-index: 10; + max-width: 300px; + bottom: 57%; + left: 54%; + display: none; + padding: 10px 20px; + box-shadow: -5px 5px 5px #808080; +} + +.balloon__link {} + +.balloon__logo { + width: 100px; + height: auto; +} + +.balloon__image { + max-width: 100%; + height: auto; +} + +.balloon__rest {} + +.balloon__contact {} + +.balloon__contact-link {} + +.balloon__address {} + +.map { + max-width: 856px; + min-height: 390px; + border-radius: 15px; + padding: 10px 0; + position: relative; +} + +.pointer { + position: absolute; + top: 40.55%; + left: 48.4%; + transition: all 0.3s ease; + z-index: 20; + cursor: pointer; +} + +.placemark { + + transition: all 0.3s ease; + + path { + fill: #f75900; + } + + svg { + width: 32px; + height: 40px; + } +} + +.pointer.active { + + transition: all 0.3s ease; + transform: translateY(-15px); +} + +.placemark.active { + + path { + fill: #757575; + transition: all 0.3s ease; + } +} + +.visible { + display: block; +} + +.preloader { + position: absolute; + top: 4%; + left: 7%; +} \ No newline at end of file diff --git a/src/components/elements/FindFood/balloon.module.scss b/src/components/elements/FindFood/balloon.module.scss new file mode 100644 index 0000000..67e589c --- /dev/null +++ b/src/components/elements/FindFood/balloon.module.scss @@ -0,0 +1,23 @@ +.balloon__logo { + width: 150px; + height: auto; +} + +.balloon__image { + max-width: 100%; + height: auto; +} + +.map { + max-width: 856px; + height: 390px; + border-radius: 15px; + padding: 10px 0; + position: relative; +} + +.pointer { + position: absolute; + left: 0; + top: 0; +} \ No newline at end of file diff --git a/src/components/elements/FindFood/deliveryZones.js b/src/components/elements/FindFood/deliveryZones.js new file mode 100644 index 0000000..44ce0cd --- /dev/null +++ b/src/components/elements/FindFood/deliveryZones.js @@ -0,0 +1,223 @@ +export const deliveryZones = { + features: [ + { + geometry: { + coordinates: [ + [ + [30.317557572666534, 59.926709209445455], + [30.31853845407954, 59.908747560199146], + [30.321055173222035, 59.908938009542545], + [30.340903519932148, 59.91628143603289], + [30.34536671573281, 59.91628143603289], + [30.35171818668041, 59.915290195578685], + [30.358960151020604, 59.91422350144856], + [30.363372384850113, 59.91401608470413], + [30.369533418957324, 59.914293537976185], + [30.37464570933971, 59.91517167570822], + [30.392961632061247, 59.91834604836555], + [30.39731933906929, 59.919484054040396], + [30.398184775654276, 59.9200198692472], + [30.393667935673072, 59.922497560421434], + [30.389172553364254, 59.928723286809166], + [30.390685319248792, 59.932223396685586], + [30.396736382786283, 59.93795200760245], + [30.39802384311343, 59.94080517457784], + [30.398098944965877, 59.94271073757233], + [30.400298356358253, 59.94926632680774], + [30.399311303440733, 59.95175797069334], + [30.397208451572986, 59.953366945394], + [30.39354455405859, 59.95496238971217], + [30.38979482585575, 59.9562564456284], + [30.38710188800474, 59.956434004442656], + [30.38559189304071, 59.95623160851031], + [30.38412481342097, 59.9557063724889], + [30.37209260120287, 59.95124962471725], + [30.363305833449683, 59.94974430066047], + [30.349025752654, 59.95013177685424], + [30.345925119032987, 59.949453690528266], + [30.341064956298194, 59.948937043993276], + [30.331859614958994, 59.94685961278085], + [30.33683217855989, 59.93892322881185], + [30.335067033115635, 59.93401100208173], + [30.317557572666534, 59.926709209445455], + ], + ], + type: 'Polygon', + }, + id: 0, + properties: { + description: + '
Стоимость доставки: 500 р.
\nм. КУ, Маршала Новикова 1 корпус 3
тел: +7(123)456-789
Часы работы: с 09-00 до 02-00
Служба доставки: +7(123)456-789', + fill: '#ed4543', + 'fill-opacity': 0.3, + stroke: '#b3b3b3', + 'stroke-opacity': 0, + 'stroke-width': '0', + }, + type: 'Feature', + }, + { + geometry: { + coordinates: [ + [ + [30.264981955459618, 59.9567962610097], + [30.199646026065437, 59.968023448258606], + [30.169090300967973, 59.95080722529315], + [30.205997497012916, 59.90936544563101], + [30.31854298727787, 59.908729629378556], + [30.31755056994245, 59.92671635778719], + [30.335070759227836, 59.93400738050088], + [30.336846381595894, 59.93891741426239], + [30.331862837246344, 59.946856988818254], + [30.293615877798302, 59.94700499658315], + [30.264981955459618, 59.9567962610097], + ], + ], + type: 'Polygon', + }, + id: 1, + properties: { + description: + '
Стоимость доставки: 400 р.
\nм. ОБ, Итальянская, д. 4
тел: +7(123)456-789
Часы работы: с 09-00 до 02-00
Служба доставки: +7(123)456-789', + fill: '#b51eff', + 'fill-opacity': 0.3, + stroke: '#b3b3b3', + 'stroke-opacity': 0, + 'stroke-width': '0', + }, + type: 'Feature', + }, + { + geometry: { + coordinates: [ + [ + [30.331857919739864, 59.9468556967079], + [30.341084718751066, 59.948946582544735], + [30.345918059395903, 59.94945784723438], + [30.349024057435095, 59.950135933461446], + [30.363304138230347, 59.94974307563543], + [30.37208032612708, 59.95125259269921], + [30.385574850015093, 59.956232065300675], + [30.387098344735556, 59.9564392173057], + [30.389802011422514, 59.95625896822289], + [30.393567832879437, 59.95496222199212], + [30.39722100155763, 59.95336408710427], + [30.399323853425273, 59.951755112255405], + [30.400310906342714, 59.94926346813624], + [30.398111494950417, 59.94273479220603], + [30.398047121934024, 59.94079693197237], + [30.396763633197057, 59.937951021273946], + [30.390697300272862, 59.932224968122135], + [30.38918478985355, 59.92871417601343], + [30.391899185376563, 59.924987496898986], + [30.420985059934033, 59.93079274799345], + [30.435619192319283, 59.93346346737091], + [30.442485647397394, 59.93499257245471], + [30.445489721494084, 59.936392395379656], + [30.45093997021234, 59.93789983069446], + [30.4782341291479, 59.944897736946885], + [30.541319685178124, 59.94623255377958], + [30.55711253185781, 59.96241829775388], + [30.550246076779697, 59.97240128977449], + [30.522780256467193, 59.98152103953561], + [30.501150922971096, 59.98427367628628], + [30.48192484875235, 59.994422039546485], + [30.477118330197673, 60.01195930333351], + [30.46578867931872, 60.02158363824368], + [30.491194563107765, 60.06228408109402], + [30.43626292248278, 60.07927063274563], + [30.365538435178056, 60.095048248449004], + [30.26254160900617, 60.10190571826372], + [30.166067915158507, 60.0751534867464], + [29.95011790295141, 60.038934706821934], + [29.962477522092037, 60.01109985092908], + [30.011724782523764, 59.9909448999639], + [30.199672532615285, 59.968018224624764], + [30.264984322127837, 59.95679641593396], + [30.29361958557071, 59.947005151458676], + [30.331857919739864, 59.9468556967079], + ], + ], + type: 'Polygon', + }, + id: 2, + properties: { + description: + '
Стоимость доставки: 200 р.
\nм. ГП, Ул. Тисовая, 4
тел. +7(123)456-789
Часы работы: пн, вт, ср, чт, вс с 09:00 до 00:00 пт и сб с 09:00 до 02:00
Служба доставки: +7(123)456-789', + fill: '#56db40', + 'fill-opacity': 0.3, + stroke: '#b3b3b3', + 'stroke-opacity': 0, + 'stroke-width': '0', + }, + type: 'Feature', + }, + { + geometry: { + coordinates: [ + [ + [30.39188845654023, 59.92497672537026], + [30.39366407890809, 59.92249921990695], + [30.398186283307165, 59.920018835586696], + [30.397324385562865, 59.919479579892105], + [30.392968478122633, 59.91834568641537], + [30.36956888667663, 59.91428651398849], + [30.363410534778456, 59.91400097944435], + [30.358979525485854, 59.914205702562214], + [30.345364632526156, 59.91626902534273], + [30.34091216556145, 59.916274412431086], + [30.32107454768721, 59.90891481949039], + [30.318553271213226, 59.908726228614185], + [30.20600778094817, 59.909329715580654], + [30.12131434909393, 59.86495487622657], + [30.011451067843932, 59.86426425113543], + [29.949652972140804, 59.88980779816408], + [29.79996425143767, 59.91671076431287], + [29.76563197604705, 59.93532308713607], + [29.67362147800017, 59.94152486691147], + [29.65576869479705, 59.907055805288635], + [29.679114642062665, 59.88359631539802], + [29.76700526706267, 59.86357361165538], + [29.793097796359557, 59.857357208798135], + [29.831549944797043, 59.81588472664123], + [29.90433436862518, 59.80965938322088], + [29.971625628390797, 59.82003430743956], + [30.10758143893768, 59.81519307943185], + [30.15427333346893, 59.796513159668955], + [30.272376360812665, 59.83386249918776], + [30.325934710422054, 59.80758400946622], + [30.33005458346892, 59.772282814787644], + [30.390479388156425, 59.75288582455442], + [30.494849505343932, 59.78959200546058], + [30.51132899753143, 59.85459398892704], + [30.531928362765797, 59.87047935897711], + [30.526435198703307, 59.89463814605622], + [30.540854754367267, 59.945830973760465], + [30.478026690402388, 59.944883678389736], + [30.445507588305833, 59.936383717008], + [30.44249278537309, 59.93498389368851], + [30.39188845654023, 59.92497672537026], + ], + ], + type: 'Polygon', + }, + id: 3, + properties: { + description: + '
Стоимость доставки: 300 р.
\nм. ШХ, Бейкер-стрит, дом 221-б
тел. +7(987)654-321
Часы работы: с 09:00 до 00:00
Служба доставки: +7(987)654-321\n', + fill: '#ffd21e', + 'fill-opacity': 0.3, + stroke: '#b3b3b3', + 'stroke-opacity': 0, + 'stroke-width': '0', + }, + type: 'Feature', + }, + ], + metadata: { + creator: 'Yandex Map Constructor', + name: 'delivery', + }, + type: 'FeatureCollection', +}; + diff --git a/src/components/elements/FindFood/findFood.module.scss b/src/components/elements/FindFood/findFood.module.scss index 25e4c07..7335426 100644 --- a/src/components/elements/FindFood/findFood.module.scss +++ b/src/components/elements/FindFood/findFood.module.scss @@ -4,11 +4,17 @@ background-image: url('../../../../public/images/find-food/background.png'); background-repeat: no-repeat; background-position: 50% 100%; - + background-size: cover; padding: 132px + 78px 0 132px 0; } .findFood { + &__search { + position: relative; + + max-width: 856px; + } + &__title { font-size: 88px; font-weight: 700; @@ -113,6 +119,10 @@ } } +.hidden { + display: none; +} + @media(min-width:1921px) { .findFoodWrapper { background-size: cover; diff --git a/src/components/elements/FindFood/popup.module.scss b/src/components/elements/FindFood/popup.module.scss new file mode 100644 index 0000000..eb1271e --- /dev/null +++ b/src/components/elements/FindFood/popup.module.scss @@ -0,0 +1,68 @@ +@import "../../../styles/variables"; + +.popup { + position: absolute; + + z-index: 999; + + left: 24px; + top: 86px; + + background: $white-color; + border-radius: 8px; + + min-width: 199px; + width: calc(100% - 199px - 16px - 48px); + + box-shadow: 2px 2px 2px 0 rgba(0, 0, 0, .15); + border: 2px solid transparent; + + transition: all $transition-short ease; + + &:focus-visible { + outline: none; + } + + &:focus { + border: 2px solid rgba(255, 116, 116, 0.5647058824); + } + + &__item { + &__active { + color: $base-color; + + transition: all $transition-short ease; + } + + &:focus-visible { + outline: none; + } + + @include setTextStyle(400, 18px, 100%); + color: $text-color; + + padding: 10px 20px; + + transition: all $transition-short ease; + + border-bottom: 1px solid rgba(0, 0, 0, 0.19); + + cursor: pointer; + + &:hover { + color: $base-color; + + transition: all $transition-short ease; + } + + &:last-child { + border: none; + } + } +} + +@media(max-width:555px) { + .popup { + width: 100%; + } +} \ No newline at end of file diff --git a/src/components/elements/Header/DeliverAddress.tsx b/src/components/elements/Header/DeliverAddress.tsx index b28a2de..e44176b 100644 --- a/src/components/elements/Header/DeliverAddress.tsx +++ b/src/components/elements/Header/DeliverAddress.tsx @@ -6,18 +6,17 @@ import { FC } from 'react'; import style from './deliverAddress.module.scss'; type DeliverAddressProps = { + address: string; classNames?: string; }; -export const DeliverAddress: FC = ({ classNames }) => { +export const DeliverAddress: FC = ({ address, classNames }) => { return (

Deliver to:

Current Location - - Lakeshore Road East, Mississauga - + {address}
); }; diff --git a/src/components/elements/Header/Header.tsx b/src/components/elements/Header/Header.tsx index 3321fe1..58e6c11 100644 --- a/src/components/elements/Header/Header.tsx +++ b/src/components/elements/Header/Header.tsx @@ -1,11 +1,12 @@ import cn from 'classnames'; import { FC } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { Link } from 'react-router-dom'; import { useNavigate } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import { useLocation } from 'react-router-dom'; import { ReactSVG } from 'react-svg'; +import { addressSelector } from '../../../store/slices/location/slice'; import { isAuthSelector, removeUser } from '../../../store/slices/user/slice'; import { LogoType } from '../../ui/LogoType'; import { CartButton } from '../../ui/buttons/CartButton'; @@ -23,6 +24,8 @@ export const Header: FC = () => { const dispatch = useDispatch(); + const address = useSelector(addressSelector); + const handleLogOut = () => { dispatch(removeUser()); }; @@ -39,7 +42,7 @@ export const Header: FC = () => { - +
diff --git a/src/components/elements/Header/MobileMenu.tsx b/src/components/elements/Header/MobileMenu.tsx index 28307d1..41abcbc 100644 --- a/src/components/elements/Header/MobileMenu.tsx +++ b/src/components/elements/Header/MobileMenu.tsx @@ -3,6 +3,7 @@ import { FC, useEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { Link, NavLink, useLocation } from 'react-router-dom'; +import { addressSelector } from '../../../store/slices/location/slice'; // import { useOutsideClick } from '../../../hooks/useOutsideClick'; import { isAuthSelector } from '../../../store/slices/user/slice'; import { CartButton } from '../../ui/buttons/CartButton'; @@ -15,6 +16,7 @@ type MobileMenuProps = { export const MobileMenu: FC = ({ handleLogOut }) => { const { pathname } = useLocation(); + const address = useSelector(addressSelector); useEffect(() => setMenuIsVisible(false), [pathname]); @@ -84,7 +86,7 @@ export const MobileMenu: FC = ({ handleLogOut }) => {
  • - +
  • diff --git a/src/components/elements/Header/deliverAddress.module.scss b/src/components/elements/Header/deliverAddress.module.scss index 48c5624..b788cd4 100644 --- a/src/components/elements/Header/deliverAddress.module.scss +++ b/src/components/elements/Header/deliverAddress.module.scss @@ -6,6 +6,7 @@ justify-content: stretch; align-items: center; row-gap: 5px; + max-width: 560px; &__deliver { margin-right: 12px; diff --git a/src/components/pages/SearchPage/SearchPanel.tsx b/src/components/pages/SearchPage/SearchPanel.tsx index 03db11b..113ad89 100644 --- a/src/components/pages/SearchPage/SearchPanel.tsx +++ b/src/components/pages/SearchPage/SearchPanel.tsx @@ -54,9 +54,9 @@ export const SearchPanel: FC = () => { return; }; - document.body.addEventListener('click', handleOutsideClick); + document.body.addEventListener('mousedown', handleOutsideClick); - return () => document.body.removeEventListener('click', handleOutsideClick); + return () => document.body.removeEventListener('mousedown', handleOutsideClick); }, []); useEffect(() => { @@ -93,6 +93,7 @@ export const SearchPanel: FC = () => { handleKeyDown={handleKeyDown} handleSearchValue={handleSearchValue} iconUrl={'/images/header/search.svg'} + placeholder={'Enter Your Request'} ref={searchRef} /> diff --git a/src/components/ui/TextInput/TextInput.tsx b/src/components/ui/TextInput/TextInput.tsx index ebcfc21..605c9ea 100644 --- a/src/components/ui/TextInput/TextInput.tsx +++ b/src/components/ui/TextInput/TextInput.tsx @@ -1,31 +1,43 @@ /* eslint-disable max-len */ import cn from 'classnames'; import debounce from 'lodash.debounce'; -import { KeyboardEvent, forwardRef, useCallback, useRef, useState } from 'react'; -import { ChangeEvent, PropsWithChildren } from 'react'; +import { + ChangeEvent, + FocusEvent, + KeyboardEvent, + PropsWithChildren, + forwardRef, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; import { ReactSVG } from 'react-svg'; import style from './textInput.module.scss'; type TextInputProps = { + address?: string; classNames?: string; handleKeyDown?: (event: KeyboardEvent) => void; handleSearchValue: (text: string) => void; iconUrl?: string; + placeholder: string; }; export const TextInput = forwardRef>( - ({ children, classNames, handleKeyDown, handleSearchValue, iconUrl }, ref) => { + ({ address, children, classNames, handleKeyDown, handleSearchValue, iconUrl, placeholder }, ref) => { const inputRef = useRef(null); const [value, setValue] = useState(''); const handleKeyDeleteDown = (event: KeyboardEvent) => { if (event.key === 'Delete') { + event.preventDefault(); handleClickClear(); } }; - + const handleClickClear = () => { setValue(''); handleSearchValue(''); @@ -44,6 +56,16 @@ export const TextInput = forwardRef { + if (address) { + setValue(address); + } + }, [address]); + + useEffect(() => { + inputRef.current?.focus(); + }, [value]); + return (
    ( + `https://geocode-maps.yandex.ru/1.x?apikey=${process.env.REACT_APP_YANDEX_API_KEY}&geocode=${searchValue}&sco=longlat&format=json&lang=en_RU&results=5`, + ); + + if (data.length === 0) { + return rejectWithValue(CustomErrors.ERROR_NOTHING_FOUND); + } + return getGeolocationCoordinates(data); + } catch (error: any) { + if (error.toJSON().status === 404) { + return rejectWithValue(CustomErrors.ERROR_NOTHING_FOUND); + } + return rejectWithValue('Error: ' + error?.message); + } +}; + +interface Params { + searchValue: string; +} + +export const fetchLocation = createAsyncThunk( + 'location/fetchLocation', + fetchData, +); + +const initialState: LocationSliceState = { + error: null, + isLoaded: false, + list: [], + location: { + address: 'Saint Petersburg, Shpalernaya Street, 26', + coords: [30.35151817345885, 59.94971367493227], + }, + status: Status.LOADING, +}; + +const locationSlice = createSlice({ + extraReducers: (builder) => getExtraReducers(builder)(fetchLocation), + + initialState, + name: 'location', + + reducers: { + setLoaded(state, action) { + state.isLoaded = action.payload; + }, + setLocation(state, action) { + state.location = action.payload; + }, + }, +}); + +export const locationListSelector = (state: RootStore) => state.location.list; +export const errorSelector = (state: RootStore) => state.location.error; +export const isLoadedSelector = (state: RootStore) => state.location.isLoaded; +export const statusSelector = (state: RootStore) => state.location.status; +export const addressSelector = (state: RootStore) => state.location.location.address; +export const coordsSelector = (state: RootStore) => state.location.location.coords; + +export const { setLoaded, setLocation } = locationSlice.actions; +export default locationSlice.reducer; diff --git a/src/store/slices/location/types.ts b/src/store/slices/location/types.ts new file mode 100644 index 0000000..934d230 --- /dev/null +++ b/src/store/slices/location/types.ts @@ -0,0 +1,16 @@ +import { Status } from '../../utils/getExtraReducers'; + +export type Coords = [number, number]; + +export interface LocationItem { + address: string; + coords: Coords; +} + +export interface LocationSliceState { + error: null; + isLoaded: false; + list: LocationItem[]; + location: LocationItem; + status: Status.LOADING; +} diff --git a/src/store/slices/restaurants/slice.ts b/src/store/slices/restaurants/slice.ts index 2cb79b1..3e4b3fb 100644 --- a/src/store/slices/restaurants/slice.ts +++ b/src/store/slices/restaurants/slice.ts @@ -1,6 +1,7 @@ import { PayloadAction, createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { RootStore } from '../..'; +import { getBalloon } from '../../../utils/getBalloon'; import { fetchRestaurantsData } from '../../utils/fetchRestaurantsData'; import { MyAsyncThunkConfig, Restaurant, Status, getExtraReducers } from '../../utils/getExtraReducers'; import { FiltersForRestaurants } from '../../utils/getFilterForRestaurants'; @@ -15,6 +16,7 @@ const initialState: RestaurantSliceState = { error: null, isLoaded: false, list: [], + placemarks: [], status: Status.LOADING, }; @@ -28,6 +30,40 @@ const restaurantsSlice = createSlice({ setLoaded(state, action: PayloadAction) { state.isLoaded = action.payload; }, + setPlacemarks(state) { + state.placemarks = state.list.map((item) => { + const { + address: { city, latitude, longitude, street_addr }, + backgroundId, + id, + logo_photos, + name, + phone_number, + } = item; + + return { + geometry: { + coordinates: [longitude, latitude], + type: 'Point', + }, + id, + properties: { + balloonContent: getBalloon( + id, + name, + logo_photos, + phone_number, + street_addr, + latitude, + longitude, + backgroundId, + city, + ), + }, + type: 'Feature', + }; + }); + }, }, }); @@ -35,6 +71,7 @@ export const restaurantListSelector = (state: RootStore) => state.restaurants.li export const errorSelector = (state: RootStore) => state.restaurants.error; export const isLoadedSelector = (state: RootStore) => state.restaurants.isLoaded; export const statusSelector = (state: RootStore) => state.restaurants.status; +export const placemarkSelector = (state: RootStore) => state.restaurants.placemarks; -export const { setLoaded } = restaurantsSlice.actions; +export const { setLoaded, setPlacemarks } = restaurantsSlice.actions; export default restaurantsSlice.reducer; diff --git a/src/store/slices/restaurants/types.ts b/src/store/slices/restaurants/types.ts index 9da809a..5e838e9 100644 --- a/src/store/slices/restaurants/types.ts +++ b/src/store/slices/restaurants/types.ts @@ -4,5 +4,21 @@ export interface RestaurantSliceState { error: ErrorType; isLoaded: boolean; list: Restaurant[]; + placemarks: PlacemarkType[]; status: Status; } +export interface PlacemarkType { + geometry: Geometry; + id: string; + properties: Properties; + type: string; +} + +export type Geometry = { + coordinates: [number, number]; + type: string; +}; + +export type Properties = { + balloonContent: string; +}; diff --git a/src/store/utils/getExtraReducers.ts b/src/store/utils/getExtraReducers.ts index 9b0bebe..1e1d827 100644 --- a/src/store/utils/getExtraReducers.ts +++ b/src/store/utils/getExtraReducers.ts @@ -1,5 +1,7 @@ import type { ActionReducerMapBuilder, PayloadAction } from '@reduxjs/toolkit'; +import { LocationItem } from '../slices/location/types'; + export enum CustomErrors { ERROR_EMPTY_REQUEST = 'Are you ready to order with the best deals?', ERROR_NOTHING_FOUND = 'Nothing was found according to request.', @@ -70,7 +72,7 @@ export interface Product { interface ProductSliceState { error: ErrorType; isLoaded: boolean; - list: Product[] | Restaurant[]; + list: LocationItem[] | Product[] | Restaurant[]; status: Status; } diff --git a/src/utils/getAddress.js b/src/utils/getAddress.js new file mode 100644 index 0000000..2aadd03 --- /dev/null +++ b/src/utils/getAddress.js @@ -0,0 +1,5 @@ +export const getExactAddress = (response) => { + return response?.response?.GeoObjectCollection?.featureMember[0]['GeoObject']['metaDataProperty']['GeocoderMetaData'][ + 'Address' + ]['formatted']; +}; diff --git a/src/utils/getBalloon.js b/src/utils/getBalloon.js new file mode 100644 index 0000000..22b40ad --- /dev/null +++ b/src/utils/getBalloon.js @@ -0,0 +1,33 @@ +export const getBalloon = ( + id, + name, + logo_photos, + phone_number, + street_addr, + latitude, + longitude, + backgroundId, + city, +) => { + return `
    + + +
    ${name}
    +
    + + +
    ${city}, ${street_addr}
    +
    ${latitude.toFixed(5)}, ${longitude.toFixed(5)}
    +
    `; +}; diff --git a/src/utils/getCurrentPosition.js b/src/utils/getCurrentPosition.js new file mode 100644 index 0000000..719faee --- /dev/null +++ b/src/utils/getCurrentPosition.js @@ -0,0 +1,10 @@ +export const getCoords = async () => { + const pos = await new Promise((resolve, reject) => { + navigator.geolocation.getCurrentPosition(resolve, reject); + }); + + return { + lat: pos.coords.latitude, + long: pos.coords.longitude, + }; +}; diff --git a/src/utils/getGeolocationCoordinates.js b/src/utils/getGeolocationCoordinates.js new file mode 100644 index 0000000..9407dfd --- /dev/null +++ b/src/utils/getGeolocationCoordinates.js @@ -0,0 +1,10 @@ +export const getGeolocationCoordinates = (response) => { + // return response?.response?.GeoObjectCollection?.featureMember[0]['GeoObject']['Point']['pos'].split(' '); + return response?.response?.GeoObjectCollection?.featureMember.map((el) => { + const geoObject = el?.GeoObject; + return { + address: geoObject?.metaDataProperty?.GeocoderMetaData?.Address?.formatted, + coords: geoObject?.Point?.pos.split(' '), + }; + }); +};