From 7948b89047501c5eb18f5071d2c0591316037f6b Mon Sep 17 00:00:00 2001 From: iluvator Date: Fri, 8 Mar 2024 17:08:19 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20=D1=84?= =?UTF-8?q?=D0=B8=D0=BB=D1=8C=D1=82=D1=80=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE?= =?UTF-8?q?=D1=87=D0=B5=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/app.tsx | 13 ++--- src/components/card.tsx | 38 +++++++++------ src/components/list-cards.tsx | 21 +++------ src/components/list-location.tsx | 15 ++++++ src/components/location.tsx | 14 ++++++ src/components/map.tsx | 19 +++++--- src/const.ts | 10 +++- src/hooks/index.ts | 9 ++++ src/hooks/useMap.tsx | 9 ++-- src/index.tsx | 14 ++++-- src/mocks/locations.ts | 1 + src/pages/favorite-page.tsx | 13 +++-- src/pages/main-page.tsx | 81 +++++++++++--------------------- src/pages/offer-page.tsx | 20 ++++---- src/store/action.ts | 6 +++ src/store/index.ts | 6 +++ src/store/reducer.ts | 19 ++++++++ src/types/state.ts | 5 ++ 18 files changed, 189 insertions(+), 124 deletions(-) create mode 100644 src/components/list-location.tsx create mode 100644 src/components/location.tsx create mode 100644 src/hooks/index.ts create mode 100644 src/mocks/locations.ts create mode 100644 src/store/action.ts create mode 100644 src/store/index.ts create mode 100644 src/store/reducer.ts create mode 100644 src/types/state.ts diff --git a/src/components/app.tsx b/src/components/app.tsx index 771b364..bae72fd 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -6,27 +6,22 @@ import { Route, Routes } from 'react-router-dom'; import { AppRoute, authorizationStatus } from '../const'; import NotFoundPage from '../pages/not-found-page'; import ProtectedRoute from './protected-route'; -import { Offer } from '../types/offer'; -type TAppProps = { - offers: Offer[]; -} - -export default function App({ offers }: TAppProps) { +export default function App() { return ( } + element={} /> } + element={} /> } + element={ } /> void; } -export default function Card({ offer, optionCard }: TCardProps) { +export default function Card({ offer, optionCard, handelPointCardMouseOver }: TCardProps) { const { width, height, classCard } = optionCard; + + const onPointCardMouseOver = () => { + handelPointCardMouseOver(offer); + }; + + const onPointCardMouseLeave = () => { + handelPointCardMouseOver(null); + }; + return ( -
+
{offer.isPremium &&
Premium @@ -22,27 +32,27 @@ export default function Card({ offer, optionCard }: TCardProps) {
- Place image + Place image
-
-
-
- €{offer.price} - / night +
+
+
+ €{offer.price} + / night
-
- -

- {offer.title} + +

+ {offer.title}

-

{offer.type}

+

{offer.type}

); diff --git a/src/components/list-cards.tsx b/src/components/list-cards.tsx index c700032..11bd59e 100644 --- a/src/components/list-cards.tsx +++ b/src/components/list-cards.tsx @@ -1,30 +1,23 @@ -import { MouseEvent, useState } from 'react'; -import { optionCard } from '../const'; +import { OptionCard } from '../const'; import { Offer } from '../types/offer'; import Card from './card'; +import { OfferPreviews } from '../types/offer-preview'; type ListOfferNearbyProps = { offers: Offer[]; - onListItemHover: (currentCard: Offer) => void; + onListItemHover: (currentCard: OfferPreviews | null) => void; extraClass: string; } export default function ListCards({ offers, onListItemHover, extraClass }: ListOfferNearbyProps) { - const [cardId, setCardId] = useState(''); - const onPointingCardMouseOver = ({ target }: MouseEvent) => { - setCardId(target.dataset.id); + const handelPointingCardMouseOver = (offer: OfferPreviews | null) => { + onListItemHover(offer); }; - const currentCard = offers.find((offer) => offer.id === cardId); - - if (currentCard !== undefined) { - onListItemHover(currentCard); - } - return ( -
- {offers.map((offer) => )} +
+ {offers.map((offer) => )}
); } diff --git a/src/components/list-location.tsx b/src/components/list-location.tsx new file mode 100644 index 0000000..170815a --- /dev/null +++ b/src/components/list-location.tsx @@ -0,0 +1,15 @@ +import Location from './location'; +import { MouseEvent } from 'react'; + +type ListLocation = { + listLocations: string[]; + handleCurrentCityClick: (evt: MouseEvent) => void; +} + +export default function ListLocation({ listLocations, handleCurrentCityClick }: ListLocation) { + return ( +
    + {listLocations.map((location) => )} +
+ ); +} diff --git a/src/components/location.tsx b/src/components/location.tsx new file mode 100644 index 0000000..e3a9f8b --- /dev/null +++ b/src/components/location.tsx @@ -0,0 +1,14 @@ +type LocationProps = { + city: string; +} + +export default function Location({ city }: LocationProps) { + return ( +
  • + + {city} + +
  • + ); +} + diff --git a/src/components/map.tsx b/src/components/map.tsx index be59b63..894f6b8 100644 --- a/src/components/map.tsx +++ b/src/components/map.tsx @@ -5,11 +5,13 @@ import { useEffect, useRef } from 'react'; import { URL_MARKER_DEFAULT, URL_MARKER_CURRENT } from '../const'; import { City } from '../types/city'; import useMap from '../hooks/useMap'; +import { OfferPreviews } from '../types/offer-preview'; +import { MapSize } from '../const'; type MapProps = { city: City; offers: Offer[]; - selectedOffer: Offer | undefined; + selectedOffer: OfferPreviews | null; }; const defaultCustomIcon = leaflet.icon({ @@ -26,30 +28,35 @@ const currentCustomIcon = leaflet.icon({ export default function Map(props: MapProps): JSX.Element { const { city, offers, selectedOffer } = props; - + console.log(city); const mapRef = useRef(null); const map = useMap(mapRef, city); useEffect(() => { if (map) { + const markerLayer = leaflet.layerGroup().addTo(map); offers.forEach((offer) => { leaflet.marker({ lat: offer.location.latitude, lng: offer.location.longitude }, { - icon: (selectedOffer !== undefined && offer.title === selectedOffer.title) + icon: (selectedOffer !== null && offer.title === selectedOffer.title) ? currentCustomIcon : defaultCustomIcon, }) - .addTo(map); + .addTo(markerLayer); }); + return () => { + map.removeLayer(markerLayer); + mapRef.current = null; + }; } - }, [map, offers, selectedOffer]); + }, [map, offers, selectedOffer, city]); return (
    diff --git a/src/const.ts b/src/const.ts index ec991e4..ccc50f1 100644 --- a/src/const.ts +++ b/src/const.ts @@ -10,7 +10,7 @@ const AuthorizationStatus = { NO_AUTH: false, }; -const optionCard = { +const OptionCard = { CITIES_CARD: { classCard: 'cities__card', width: '260', @@ -23,6 +23,12 @@ const optionCard = { } }; +const MapSize = { + WIDTH: '100%', + HEIGHT: '100%', +}; + + const OptionListCard = { FAVORITES_CARD: 'near-places__list places__list', CITIES_CARD: 'cities__places-list places__list tabs__content', @@ -35,4 +41,4 @@ const URL_MARKER_CURRENT = '../markup/img/pin-active.svg'; const CountStar: number = 5; -export { CountStar, AppRoute, AuthorizationStatus as authorizationStatus, optionCard, URL_MARKER_DEFAULT, URL_MARKER_CURRENT, OptionListCard }; +export { CountStar, AppRoute, AuthorizationStatus as authorizationStatus, OptionCard, URL_MARKER_DEFAULT, URL_MARKER_CURRENT, OptionListCard, MapSize }; diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..9cb3402 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,9 @@ +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import type { State, AppDispatch } from '../types/state'; + +const useAppDispatch = () => useDispatch(); + +const useAppSelector: TypedUseSelectorHook = useSelector; + +export { useAppDispatch, useAppSelector }; + diff --git a/src/hooks/useMap.tsx b/src/hooks/useMap.tsx index 686440a..4251478 100644 --- a/src/hooks/useMap.tsx +++ b/src/hooks/useMap.tsx @@ -8,9 +8,10 @@ function useMap( ): Map | null { const [map, setMap] = useState(null); const isRenderedRef = useRef(false); - + console.log('1 условие:', isRenderedRef.current, '2 условие: ', mapRef.current); useEffect(() => { if (mapRef.current !== null && !isRenderedRef.current) { + console.log('rfhnf', city); const instance = new Map(mapRef.current, { center: { lat: city.location.latitude, @@ -28,10 +29,12 @@ function useMap( ); instance.addLayer(layer); - setMap(instance); } - isRenderedRef.current = true; + + return () => { + isRenderedRef.current = true; + }; }, [mapRef, city]); return map; diff --git a/src/index.tsx b/src/index.tsx index 9ba07ae..7d23b82 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,6 +4,8 @@ import App from './components/app'; import { BrowserRouter } from 'react-router-dom'; import { HelmetProvider } from 'react-helmet-async'; import { offers } from './mocks/offers'; +import { Provider } from 'react-redux'; +import { store } from './store'; const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement @@ -11,10 +13,12 @@ const root = ReactDOM.createRoot( root.render( - - - - - + + + + + + + ); diff --git a/src/mocks/locations.ts b/src/mocks/locations.ts new file mode 100644 index 0000000..a6fd1f8 --- /dev/null +++ b/src/mocks/locations.ts @@ -0,0 +1 @@ +export const locations = ['Paris', 'Cologne', 'Brussels', 'Amsterdam', 'Hamburg', 'Dusseldorf',]; diff --git a/src/pages/favorite-page.tsx b/src/pages/favorite-page.tsx index a08f6d5..ebcf05c 100644 --- a/src/pages/favorite-page.tsx +++ b/src/pages/favorite-page.tsx @@ -2,16 +2,15 @@ import Container from '../components/container'; import Card from '../components/card'; import FavoriteItems from '../components/favorite-items'; import { Helmet } from 'react-helmet-async'; -import { Offer } from '../types/offer'; -import { optionCard } from '../const'; +import { OptionCard } from '../const'; import FavoritesEmpty from '../components/favorites-empty'; import { getFavoritesByLocation } from '../utils/utils'; +import { useAppSelector } from '../hooks'; -type TOfferPageProps = { - offers: Offer[]; -} -export default function FavoritePage({ offers }: TOfferPageProps) { +export default function FavoritePage() { + const offers = useAppSelector((state) => state.offers); + const favorites = getFavoritesByLocation(offers); return ( @@ -26,7 +25,7 @@ export default function FavoritePage({ offers }: TOfferPageProps) {
      {Object.entries(favorites).map(([location, gropedFavorites]) => ( - {gropedFavorites.map((favorite) => )} + {gropedFavorites.map((favorite) => )} ) )} diff --git a/src/pages/main-page.tsx b/src/pages/main-page.tsx index 0ec2d2f..8881fcd 100644 --- a/src/pages/main-page.tsx +++ b/src/pages/main-page.tsx @@ -1,20 +1,25 @@ import Container from '../components/container'; import { City } from '../types/city'; -import { Offer } from '../types/offer'; import ListCards from '../components/list-cards'; import Map from '../components/map'; import { MouseEvent, useState } from 'react'; import MainEmpty from '../components/main-empty'; import { OptionListCard } from '../const'; +import { OfferPreviews } from '../types/offer-preview'; +import { locations } from '../mocks/locations'; +import ListLocation from '../components/list-location'; +import { useAppDispatch, useAppSelector } from '../hooks'; +import { selectCity } from '../store/action'; +import { offers } from '../mocks/offers'; -type TMainPageProps = { - offers: Offer[]; -} -export default function MainPage({ offers }: TMainPageProps) { +export default function MainPage() { + const baseOffers = offers; + const selectOffers = useAppSelector((state) => state.offers); + const dispatch = useAppDispatch(); - const [selectedOffer, setSelectedOffer] = useState( - undefined + const [selectedOffer, setSelectedOffer] = useState( + null ); const [selectedCity, setSelectedCity] = useState({ @@ -26,21 +31,21 @@ export default function MainPage({ offers }: TMainPageProps) { } }); - const handleListItemHover = (currentCard: Offer) => { - const currentPoint = offers.find((offer) => offer.title === currentCard.title); - if (currentPoint !== undefined) { - setSelectedOffer(currentPoint); - } + const handleListItemHover = (currentCard: OfferPreviews | null) => { + setSelectedOffer(currentCard); }; const handleCurrentCityClick = (evt: MouseEvent) => { evt.preventDefault(); - const currentOffer = offers.find((offer) => offer.city.name === evt.target.textContent); + const currentOffer = baseOffers.find((offer) => offer.city.name === evt.target.textContent); + + if (currentOffer !== undefined) { + dispatch(selectCity(currentOffer)); + } if (currentOffer !== undefined) { - setSelectedCity({ ...currentOffer?.city }); - offers.filter((offer) => offer.city.name === currentOffer.city.name); + setSelectedCity({ ...currentOffer.city }); } }; @@ -50,47 +55,16 @@ export default function MainPage({ offers }: TMainPageProps) {

      Cities

      - {offers.length !== 0 ? + {selectOffers.length !== 0 ?

      Places

      - {offers.length} places to stay in Amsterdam + {selectOffers.length} place{selectOffers.length > 1 ? 's' : ''} to stay in Amsterdam
      Sort by @@ -106,13 +80,13 @@ export default function MainPage({ offers }: TMainPageProps) {
    • Top rated first
    - + : } - {offers.length && + {selectOffers.length &&
    -
    - +
    +
    } @@ -121,3 +95,4 @@ export default function MainPage({ offers }: TMainPageProps) {
    ); } + diff --git a/src/pages/offer-page.tsx b/src/pages/offer-page.tsx index 1bf16b2..8b5185e 100644 --- a/src/pages/offer-page.tsx +++ b/src/pages/offer-page.tsx @@ -2,7 +2,6 @@ import Container from '../components/container'; import OfferInside from '../components/offer-inside'; import Rating from '../components/rating'; import { Helmet } from 'react-helmet-async'; -import { Offer } from '../types/offer'; import CommentsTemplate from '../components/comments-template'; import { CountStar } from '../const'; import { useParams } from 'react-router-dom'; @@ -12,20 +11,19 @@ import Map from '../components/map'; import { useState } from 'react'; import { OptionListCard } from '../const'; import ListCards from '../components/list-cards'; +import { OfferPreviews } from '../types/offer-preview'; +import { useAppSelector } from '../hooks'; -type TOfferPageProps = { - offers: Offer[]; -} +export default function OfferPage() { + const offers = useAppSelector((state) => state.offers); -export default function OfferPage({ offers }: TOfferPageProps) { - const [selectedOffer, setSelectedOffer] = useState( - undefined + const [selectedOffer, setSelectedOffer] = useState( + null ); - const handleListItemHover = (currentCard: Offer) => { - const currentPoint = offers.find((offer) => offer.title === currentCard.title); - if (currentPoint !== undefined) { - setSelectedOffer(currentPoint); + const handleListItemHover = (currentCard: OfferPreviews | null) => { + if (currentCard !== null) { + setSelectedOffer(currentCard); } }; diff --git a/src/store/action.ts b/src/store/action.ts new file mode 100644 index 0000000..4a58be8 --- /dev/null +++ b/src/store/action.ts @@ -0,0 +1,6 @@ +import { createAction } from '@reduxjs/toolkit'; +import { Offer } from '../types/offer'; + +const selectCity = createAction('mainPage/selectCity'); + +export { selectCity }; diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..b6e9107 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,6 @@ +import { configureStore } from '@reduxjs/toolkit'; +import { reducer } from './reducer'; + +const store = configureStore({ reducer }); + +export { store }; diff --git a/src/store/reducer.ts b/src/store/reducer.ts new file mode 100644 index 0000000..c634d1b --- /dev/null +++ b/src/store/reducer.ts @@ -0,0 +1,19 @@ +import { createReducer } from '@reduxjs/toolkit'; +import { selectCity } from './action'; +import { offers } from '../mocks/offers'; + +const initialState = { + city: 'Paris', + offers: [...offers].filter((offer) => offer.city.name === 'Paris'), +}; + +const reducer = createReducer(initialState, (builder) => { + builder + .addCase(selectCity, (state, action) => { + state.city = action.payload.city.name; + state.offers = [...offers].filter((offer) => offer.city.name === state.city); + }); +}); + +export { reducer }; + diff --git a/src/types/state.ts b/src/types/state.ts new file mode 100644 index 0000000..70af949 --- /dev/null +++ b/src/types/state.ts @@ -0,0 +1,5 @@ +import { store } from '../store'; + +export type State = ReturnType; + +export type AppDispatch = typeof store.dispatch;