Repository: htmlacademy-univer-javascript-3/2579433-six-cities-5
Pull request #5: Больше подробностей (часть 1)
Author: login: Imb0duck
Created: 2025-03-26T13:56:50Z | Merged: 2025-03-29T19:57:54Z



---
:mortar_board: [Больше подробностей (часть 1)](https://up.htmlacademy.ru/univer-js3/5/user/2579433/tasks/6)


Diff:
diff --git a/src/components/favorite-card-group/favorite-card-group.tsx b/src/components/favorite-card-group/favorite-card-group.tsx
new file mode 100644
index 0000000..b9ecba9
--- /dev/null
+++ b/src/components/favorite-card-group/favorite-card-group.tsx
@@ -0,0 +1,26 @@
+import FavoriteCard from '../favorite-card/favorite-card';
+import { OfferInfo } from '../../types/offer';
+
+type CardGroupProps = {
+  city: string;
+  currentFavoriteOffers: OfferInfo[];
+}
+
+function FavoriteCardGroup({city, currentFavoriteOffers}: CardGroupProps): JSX.Element{
+  return (
+    <li className="favorites__locations-items">
+      <div className="favorites__locations locations locations--current">
+        <div className="locations__item">
+          <a className="locations__item-link" href="#">
+            <span>{city}</span>
+          </a>
+        </div>
+      </div>
+      <div className="favorites__places">
+        {currentFavoriteOffers.map((offer) => <FavoriteCard key={`${offer.id}-card`} offerInfo={offer}/>)}
+      </div>
+    </li>
+  );
+}
+
+export default FavoriteCardGroup;
diff --git a/src/components/favorite-card-list/favorite-card-list.tsx b/src/components/favorite-card-list/favorite-card-list.tsx
index d963497..42f94e4 100644
--- a/src/components/favorite-card-list/favorite-card-list.tsx
+++ b/src/components/favorite-card-list/favorite-card-list.tsx
@@ -1,38 +1,25 @@
 import { OfferInfo } from '../../types/offer';
-import FavoriteCard from '../favorite-card/favorite-card';
+import FavoriteCardGroup from '../favorite-card-group/favorite-card-group';
 
 type FavoriteCardListProps = {
   offers: OfferInfo[];
 }
 
 function FavoriteCardList({offers}: FavoriteCardListProps): JSX.Element {
-  const cities: string[] = Array.from(new Set<string>(offers.map((offer) => offer.city.name)));
+  const cityOfferMap = new Map<string, OfferInfo[]>();
+
+  offers.forEach((offer) => {
+    if(!cityOfferMap.has(offer.city.name)){
+      cityOfferMap.set(offer.city.name, []);
+    }
+    cityOfferMap.get(offer.city.name)?.push(offer);
+  });
 
   return (
     <ul className="favorites__list">
-      {cities.map((city) => {
-        const currentFavoriteOffers: OfferInfo[] = offers.filter((offer) => offer.city.name === city);
-        offers.forEach((offer, index) => {
-          if(offer.city.name === city){
-            offers.splice(index, 1);
-          }
-        });
-        return (
-          <li className="favorites__locations-items" key={city}>
-            <div className="favorites__locations locations locations--current">
-              <div className="locations__item">
-                <a className="locations__item-link" href="#">
-                  <span>{city}</span>
-                </a>
-              </div>
-            </div>
-            <div className="favorites__places">
-              {currentFavoriteOffers.map((offer) => <FavoriteCard key={`${offer.id}-card`} offerInfo={offer}/>)}
-            </div>
-          </li>
-        );
-      }
-      )}
+      {[...cityOfferMap.entries()].map(([city, currentFavoriteOffers]) => (
+        <FavoriteCardGroup key={city} city={city} currentFavoriteOffers={currentFavoriteOffers}/>
+      ))}
     </ul>
   );
 }
diff --git a/src/components/map/map.tsx b/src/components/map/map.tsx
new file mode 100644
index 0000000..ed5e7a9
--- /dev/null
+++ b/src/components/map/map.tsx
@@ -0,0 +1,49 @@
+import 'leaflet/dist/leaflet.css';
+import { OfferCity, PointInfo } from '../../types/offer';
+import { useRef, useEffect } from 'react';
+import {layerGroup, Marker} from 'leaflet';
+import useMap from '../../hooks/use-map';
+import { defaultCustomIcon, currentCustomIcon } from '../../const';
+
+type MapProps = {
+  city: OfferCity;
+  points: PointInfo[];
+  selectedPoint: string | null;
+};
+
+function Map({city, points, selectedPoint}: MapProps): JSX.Element {
+  const mapRef = useRef(null);
+  const map = useMap(mapRef, city);
+
+  useEffect(() => {
+    if (map) {
+      const markerLayer = layerGroup().addTo(map);
+      points.forEach((point) => {
+        const marker = new Marker({
+          lat: point.location.latitude,
+          lng: point.location.longitude
+        });
+
+        marker
+          .setIcon(
+            selectedPoint !== null && point.id === selectedPoint
+              ? currentCustomIcon
+              : defaultCustomIcon
+          )
+          .addTo(markerLayer);
+      });
+
+      return () => {
+        map.removeLayer(markerLayer);
+      };
+    }
+  }, [map, points, selectedPoint]);
+
+  return (
+    <div id="map" style={{width: '100%', height: '100%'}} ref={mapRef}>
+
+    </div>
+  );
+}
+
+export default Map;
diff --git a/src/const.ts b/src/const.ts
index b3088d7..779cf7d 100644
--- a/src/const.ts
+++ b/src/const.ts
@@ -1,3 +1,5 @@
+import { Icon } from 'leaflet';
+
 export const SETTING = {
   places: 321
 };
@@ -14,3 +16,15 @@ export enum AuthorizationStatus {
   NoAuth = 'NO_AUTH',
   Unknown = 'UNKNOWN',
 }
+
+export const defaultCustomIcon = new Icon({
+  iconUrl: 'https://assets.htmlacademy.ru/content/intensive/javascript-1/demo/interactive-map/pin.svg',
+  iconSize: [40, 40],
+  iconAnchor: [20, 40]
+});
+
+export const currentCustomIcon = new Icon({
+  iconUrl: 'https://assets.htmlacademy.ru/content/intensive/javascript-1/demo/interactive-map/main-pin.svg',
+  iconSize: [40, 40],
+  iconAnchor: [20, 40]
+});
diff --git a/src/hooks/use-map.tsx b/src/hooks/use-map.tsx
new file mode 100644
index 0000000..1241ffb
--- /dev/null
+++ b/src/hooks/use-map.tsx
@@ -0,0 +1,36 @@
+import {useEffect, useState, MutableRefObject, useRef} from 'react';
+import {Map, TileLayer} from 'leaflet';
+import { OfferCity } from '../types/offer';
+
+function useMap(mapRef: MutableRefObject<HTMLElement | null>, city: OfferCity): Map | null {
+  const [map, setMap] = useState<Map | null>(null);
+  const isRenderedRef = useRef<boolean>(false);
+
+  useEffect(() => {
+    const url = 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png';
+    const attribution = { attribution:
+      '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
+    };
+
+    if (mapRef.current !== null && !isRenderedRef.current) {
+      const instance = new Map(mapRef.current, {
+        center: {
+          lat: city.location.latitude,
+          lng: city.location.longitude
+        },
+        zoom: 10
+      });
+
+      const layer = new TileLayer(url, attribution);
+
+      instance.addLayer(layer);
+
+      setMap(instance);
+      isRenderedRef.current = true;
+    }
+  }, [mapRef, city]);
+
+  return map;
+}
+
+export default useMap;
diff --git a/src/mock/offers.ts b/src/mock/offers.ts
index ae62e22..5616729 100644
--- a/src/mock/offers.ts
+++ b/src/mock/offers.ts
@@ -23,5 +23,28 @@ export const offers: OfferInfo[] = [
     isPremium: true,
     rating: 4,
     previewImage: 'https://url-to-image/image.png'
+  },
+  {
+    id: '6af6f711-c28d-4121-82cd-e0b462a27f00',
+    title: 'Beautiful & luxurious studio at great location',
+    type: 'apartment',
+    price: 120,
+    city: {
+      name: 'Amsterdam',
+      location: {
+        latitude: 52.35514938496378,
+        longitude: 4.673877537499948,
+        zoom: 8
+      }
+    },
+    location: {
+      latitude: 50.1,
+      longitude: 7.1,
+      zoom: 8
+    },
+    isFavorite: true,
+    isPremium: true,
+    rating: 4,
+    previewImage: 'https://url-to-image/image.png'
   }
 ];
diff --git a/src/pages/main/main.tsx b/src/pages/main/main.tsx
index 2efa64e..3181ecb 100644
--- a/src/pages/main/main.tsx
+++ b/src/pages/main/main.tsx
@@ -1,11 +1,13 @@
 import CardList from '../../components/card-list/card-list';
-import { OfferInfo } from '../../types/offer';
+import { OfferInfo, PointInfo } from '../../types/offer';
+import Map from '../../components/map/map';
 
 type MainProps = {
   offersInfo: OfferInfo[];
 }
 
 function Main({ offersInfo }: MainProps): JSX.Element {
+  const points: PointInfo[] = offersInfo.map((offer) => ({id: offer.id, location: offer.location}));
   return (
     <div className="page page--gray page--main">
       <header className="header">
@@ -98,7 +100,9 @@ function Main({ offersInfo }: MainProps): JSX.Element {
               <CardList offers={offersInfo}/>
             </section>
             <div className="cities__right-section">
-              <section className="cities__map map"></section>
+              <section className="cities__map map">
+                <Map city={offersInfo[0].city} points={points} selectedPoint={null} />
+              </section>
             </div>
           </div>
         </div>
diff --git a/src/types/offer.ts b/src/types/offer.ts
index 1c24269..186dcfe 100644
--- a/src/types/offer.ts
+++ b/src/types/offer.ts
@@ -4,7 +4,7 @@ type CityLocation = {
   zoom: number;
 };
 
-type OfferCity = {
+export type OfferCity = {
   name: string;
   location: CityLocation;
 };
@@ -51,3 +51,8 @@ export type FullOfferInfo = {
   images: string[];
   maxAdults: number;
 };
+
+export type PointInfo = {
+  id: string;
+  location: OfferLocation;
+}


Reviews:
user: login: nesen-kontur
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-29T17:18:57Z

Review comments:
diff_hunk: @@ -0,0 +1,61 @@
+import 'leaflet/dist/leaflet.css';
+import { OfferCity, PointInfo } from '../../types/offer';
+import { useRef, useEffect } from 'react';
+import {layerGroup, Marker, Icon} from 'leaflet';
+import useMap from '../../hooks/use-map';
+import { URL_MARKER_CURRENT, URL_MARKER_DEFAULT } from '../../const';
+
+type MapProps = {
+  city: OfferCity;
+  points: PointInfo[];
+  selectedPoint: string | null;
+};
+
+const defaultCustomIcon = new Icon({
path: src/components/map/map.tsx
user: login: nesen-kontur
body: иконки лучше вынести в константы или хелперы из файла с компонентом
created_at: 2025-03-29T17:15:11Z
author_association: NONE
diff_hunk: @@ -6,33 +6,31 @@ type FavoriteCardListProps = {
 }
 
 function FavoriteCardList({offers}: FavoriteCardListProps): JSX.Element {
-  const cities: string[] = Array.from(new Set<string>(offers.map((offer) => offer.city.name)));
+  const cityOfferMap = new Map<string, OfferInfo[]>();
+
+  offers.forEach((offer) => {
+    if(!cityOfferMap.has(offer.city.name)){
+      cityOfferMap.set(offer.city.name, []);
+    }
+    cityOfferMap.get(offer.city.name)?.push(offer);
+  });
 
   return (
     <ul className="favorites__list">
-      {cities.map((city) => {
-        const currentFavoriteOffers: OfferInfo[] = offers.filter((offer) => offer.city.name === city);
-        offers.forEach((offer, index) => {
-          if(offer.city.name === city){
-            offers.splice(index, 1);
-          }
-        });
-        return (
-          <li className="favorites__locations-items" key={city}>
-            <div className="favorites__locations locations locations--current">
-              <div className="locations__item">
-                <a className="locations__item-link" href="#">
-                  <span>{city}</span>
-                </a>
-              </div>
-            </div>
-            <div className="favorites__places">
-              {currentFavoriteOffers.map((offer) => <FavoriteCard key={`${offer.id}-card`} offerInfo={offer}/>)}
+      {[...cityOfferMap.entries()].map(([city, currentFavoriteOffers]) => (
+        <li className="favorites__locations-items" key={city}>
path: src/components/favorite-card-list/favorite-card-list.tsx
user: login: nesen-kontur
body: тут напрашивается отдельный компонент
created_at: 2025-03-29T17:17:58Z
author_association: NONE
diff_hunk: @@ -0,0 +1,37 @@
+import {useEffect, useState, MutableRefObject, useRef} from 'react';
+import {Map, TileLayer} from 'leaflet';
+import { OfferCity } from '../types/offer';
+
+function useMap(mapRef: MutableRefObject<HTMLElement | null>, city: OfferCity): Map | null {
+  const [map, setMap] = useState<Map | null>(null);
+  const isRenderedRef = useRef<boolean>(false);
+
+  useEffect(() => {
+    if (mapRef.current !== null && !isRenderedRef.current) {
+      const instance = new Map(mapRef.current, {
+        center: {
+          lat: city.location.latitude,
+          lng: city.location.longitude
+        },
+        zoom: 10
+      });
+
+      const layer = new TileLayer(
+        'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png',
path: src/hooks/use-map.tsx
user: login: nesen-kontur
body: эти длинные строчки в переменные бы вынести
created_at: 2025-03-29T17:18:54Z
author_association: NONE

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-26T13:57:34Z
body: Ваш пулреквест опубликован. Посмотреть можно [здесь](https://htmlacademy-univer-javascript-3.github.io/2579433-six-cities-5/5/)
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-28T20:07:55Z
body: Ваш пулреквест опубликован. Посмотреть можно [здесь](https://htmlacademy-univer-javascript-3.github.io/2579433-six-cities-5/5/)
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-29T18:36:07Z
body: Ваш пулреквест опубликован. Посмотреть можно [здесь](https://htmlacademy-univer-javascript-3.github.io/2579433-six-cities-5/5/)
author_association: NONE