Repository: htmlacademy-univer-javascript-3/2579773-six-cities-5
Pull request #8: Контроль и ограничения (часть 2)
Author: login: VlasovaNatalya
Created: 2025-04-07T10:49:07Z | Merged: 2025-04-08T15:12:02Z



---
:mortar_board: [Контроль и ограничения (часть 2)](https://up.htmlacademy.ru/univer-js3/5/user/2579773/tasks/9)


Diff:
diff --git a/src/components/main-page/main-page.tsx b/src/components/main-page/main-page.tsx
index 78175e3..2c134de 100644
--- a/src/components/main-page/main-page.tsx
+++ b/src/components/main-page/main-page.tsx
@@ -10,6 +10,8 @@ import {useAppDispatch, useAppSelector } from '../../hooks';
 import { useEffect, useState } from 'react';
 import { fillOffersList } from '../../store/action';
 import { offers as mockOffers } from '../../mocks/offers';
+import Sorting from '../sorting/sorting';
+import { sorting } from '../../utils';
 
 type MainPageProps = {
   cities: CityType[];
@@ -30,6 +32,8 @@ const MainPage = ({cities}: MainPageProps): JSX.Element => {
   const selectedCityName = useAppSelector((state) => state.city);
   const filteredOffers = offers.filter((offer) => offer.city.name === selectedCityName);
   const selectedCityData = cities.find((city) => city.name === selectedCityName) ?? cities[0];
+  const selectedSort = useAppSelector((state) => state.sortOption);
+  const sortedOffers = sorting[selectedSort](filteredOffers);
 
   return (
     <div className="page page--gray page--main">
@@ -70,22 +74,9 @@ const MainPage = ({cities}: MainPageProps): JSX.Element => {
             <section className="cities__places places">
               <h2 className="visually-hidden">Places</h2>
               <b className="places__found">{filteredOffers.length} places to stay in {selectedCityName}</b>
-              <form className="places__sorting" action="#" method="get">
-                <span className="places__sorting-caption">Sort by</span>
-                <span className="places__sorting-type" tabIndex={0}>
-                  Popular
-                  <svg className="places__sorting-arrow" width="7" height="4">
-                    <use xlinkHref="#icon-arrow-select"></use>
-                  </svg>
-                </span>
-                <ul className="places__options places__options--custom places__options--opened">
-                  <li className="places__option places__option--active" tabIndex={0}>Popular</li>
-                  <li className="places__option" tabIndex={0}>Price: low to high</li>
-                  <li className="places__option" tabIndex={0}>Price: high to low</li>
-                  <li className="places__option" tabIndex={0}>Top rated first</li>
-                </ul>
-              </form>
-              <OfferList offers={filteredOffers} setActiveOffer={setActiveOffer} block='cities' />
+
+              <Sorting />
+              <OfferList offers={sortedOffers} setActiveOffer={setActiveOffer} block='cities' />
             </section>
             <div className="cities__right-section">
               <section className="cities__map map">
diff --git a/src/components/sorting/sorting.tsx b/src/components/sorting/sorting.tsx
new file mode 100644
index 0000000..5babadb
--- /dev/null
+++ b/src/components/sorting/sorting.tsx
@@ -0,0 +1,41 @@
+import { SortingType } from '../../const';
+import { useState } from 'react';
+import { useAppDispatch, useAppSelector } from '../../hooks';
+import { setSortOption } from '../../store/action';
+
+const sortingOptions = Object.values(SortingType);
+
+const Sorting = (): JSX.Element => {
+  const dispatch = useAppDispatch();
+  const selectedSort = useAppSelector((state) => state.sortOption);
+
+  const [isOpen, setIsOpen] = useState(false);
+
+  const handleOptionClick = (option: SortingType) => {
+    if (option !== selectedSort) {
+      dispatch(setSortOption(option));
+    }
+    setIsOpen(false);
+  };
+
+  return (
+    <form className="places__sorting" action="#" method="get">
+      <span className="places__sorting-caption">Sort by</span>
+      <span className="places__sorting-type" tabIndex={0} onClick={() => setIsOpen((prev) => !prev)}>
+        { selectedSort}
+        <svg className="places__sorting-arrow" width="7" height="4">
+          <use xlinkHref="#icon-arrow-select"></use>
+        </svg>
+      </span>
+      <ul className={`places__options places__options--custom ${isOpen ? 'places__options--opened' : ''}`}>
+        {sortingOptions.map((option) => (
+          <li key={option} className={`places__option ${selectedSort === option ? 'places__option--active' : ''}`} tabIndex={0} onClick={() => handleOptionClick(option)} >
+            {option}
+          </li>
+        ))}
+      </ul>
+    </form>
+  );
+};
+
+export default Sorting;
diff --git a/src/const.ts b/src/const.ts
index 98b7c5d..429f1d5 100644
--- a/src/const.ts
+++ b/src/const.ts
@@ -18,4 +18,11 @@ const enum CityName {
 const URL_MARKER_DEFAULT = 'img/pin.svg';
 const URL_MARKER_CURRENT = 'img/pin-active.svg';
 
-export {AppRoute, CityName, URL_MARKER_CURRENT, URL_MARKER_DEFAULT};
+enum SortingType {
+  Popular = 'Popular',
+  LowToHigh = 'Price: low to high',
+  HighToLow = 'Price: high to low',
+  TopRated = 'Top rated first'
+}
+
+export {AppRoute, CityName, URL_MARKER_CURRENT, URL_MARKER_DEFAULT, SortingType};
diff --git a/src/store/action.ts b/src/store/action.ts
index 4ccb057..c5f8bf0 100644
--- a/src/store/action.ts
+++ b/src/store/action.ts
@@ -1,8 +1,9 @@
 import { createAction } from '@reduxjs/toolkit';
-import { CityName } from '../const';
+import { CityName, SortingType } from '../const';
 import { OfferPreviewType } from '../types/offer-preview';
 
 const changeCity = createAction<CityName>('changeCity');
 const fillOffersList = createAction<OfferPreviewType[]>('fillOffersList');
+const setSortOption = createAction<SortingType>('sort');
 
-export {changeCity, fillOffersList};
+export {changeCity, fillOffersList, setSortOption};
diff --git a/src/store/reducer.ts b/src/store/reducer.ts
index 9d5350e..5778910 100644
--- a/src/store/reducer.ts
+++ b/src/store/reducer.ts
@@ -1,16 +1,18 @@
 import { createReducer } from '@reduxjs/toolkit';
-import { changeCity, fillOffersList } from './action';
-import { CityName } from '../const';
+import { changeCity, fillOffersList, setSortOption } from './action';
+import { CityName, SortingType } from '../const';
 import { OfferPreviewType } from '../types/offer-preview';
 
 type StateType = {
   city: CityName;
   offers: OfferPreviewType[];
+  sortOption: SortingType;
 };
 
 const initialState: StateType = {
   city: CityName.Paris,
   offers: [],
+  sortOption: SortingType.Popular,
 };
 
 const reducer = createReducer(initialState, (builder) => {
@@ -20,6 +22,9 @@ const reducer = createReducer(initialState, (builder) => {
     })
     .addCase(fillOffersList, (state, action) => {
       state.offers = action.payload;
+    })
+    .addCase(setSortOption, (state, action) => {
+      state.sortOption = action.payload;
     });
 });
 
diff --git a/src/utils.ts b/src/utils.ts
index 42e1b44..9f20fe9 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,5 +1,5 @@
 import { OfferPreviewType } from './types/offer-preview';
-import { AppRoute } from './const';
+import { AppRoute, SortingType } from './const';
 
 function getFavorites(favorites:OfferPreviewType[]) {
   return favorites.reduce<{ [key: string]: OfferPreviewType[] }>((acc, curr) => {
@@ -27,4 +27,23 @@ function getRatingWidth(rating: number): string {
   return `${(rating / 5) * 100}%`;
 }
 
-export {getFavorites, getOfferLink, formatDate, getRatingWidth};
+function sortByRating(a: OfferPreviewType, b: OfferPreviewType) {
+  return b.rating - a.rating;
+}
+
+function sortLowToHigh(a: OfferPreviewType, b: OfferPreviewType) {
+  return a.price - b.price;
+}
+
+function sortHighToLow(a: OfferPreviewType, b: OfferPreviewType) {
+  return b.price - a.price;
+}
+
+const sorting: Record<SortingType, (offers: OfferPreviewType[]) => OfferPreviewType[]> = {
+  [SortingType.Popular]: (offers) => offers ,
+  [SortingType.LowToHigh]: (offers) => offers.toSorted(sortLowToHigh),
+  [SortingType.HighToLow]: (offers) => offers.toSorted(sortHighToLow),
+  [SortingType.TopRated]: (offers) => offers.toSorted(sortByRating),
+};
+
+export {getFavorites, getOfferLink, formatDate, getRatingWidth, sorting};


Reviews:
user: login: BotaniQQQuser
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-04-07T15:15:36Z
user: login: VlasovaNatalya
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T17:58:52Z
user: login: BotaniQQQuser
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-08T14:59:33Z

Review comments:
diff_hunk: @@ -27,4 +27,23 @@ function getRatingWidth(rating: number): string {
   return `${(rating / 5) * 100}%`;
 }
 
-export {getFavorites, getOfferLink, formatDate, getRatingWidth};
+function sortByRating(a: OfferPreviewType, b: OfferPreviewType) {
+  return b.rating - a.rating;
+}
+
+function sortLowToHigh(a: OfferPreviewType, b: OfferPreviewType) {
+  return a.price - b.price;
+}
+
+function sortHighToLow(a: OfferPreviewType, b: OfferPreviewType) {
+  return b.price - a.price;
+}
+
+const sorting: Record<SortingType, (offers: OfferPreviewType[]) => OfferPreviewType[]> = {
+  [SortingType.Popular]: (offers) => offers.slice(),
path: src/utils.ts
user: login: BotaniQQQuser
body: А зачем копию тут просто так создавать?  
P.S: за использование метода `toSorted` из относительно нового стандарта респект👍
created_at: 2025-04-07T15:13:34Z
author_association: NONE
diff_hunk: @@ -31,6 +33,9 @@ const MainPage = ({cities}: MainPageProps): JSX.Element => {
   const filteredOffers = offers.filter((offer) => offer.city.name === selectedCityName);
   const selectedCityData = cities.find((city) => city.name === selectedCityName) ?? cities[0];
 
+  const [selectedSort, setSelectedSort] = useState<SortingType>(SortingType.Popular);
path: src/components/main-page/main-page.tsx
user: login: BotaniQQQuser
body: В текущем исполнении ранее выбранный вариант сортировки будет сбрасываться при каждом "покидании" главной страницы
created_at: 2025-04-07T15:14:54Z
author_association: NONE
diff_hunk: @@ -31,6 +33,9 @@ const MainPage = ({cities}: MainPageProps): JSX.Element => {
   const filteredOffers = offers.filter((offer) => offer.city.name === selectedCityName);
   const selectedCityData = cities.find((city) => city.name === selectedCityName) ?? cities[0];
 
+  const [selectedSort, setSelectedSort] = useState<SortingType>(SortingType.Popular);
path: src/components/main-page/main-page.tsx
user: login: VlasovaNatalya
body: перенесла хранение сортировки в redux, чтобы выбранное значение не сбрасывалось
created_at: 2025-04-07T17:58:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,39 @@
+import { SortingType } from '../../const';
+import { useState } from 'react';
+
+type SortingProps = {
+  selectedSort: SortingType;
+  onSortChange: (type: SortingType) => void;
+}
+
+const sortingOptions = Object.values(SortingType);
+
+const Sorting = ({selectedSort, onSortChange}: SortingProps): JSX.Element => {
path: src/components/sorting/sorting.tsx
user: login: BotaniQQQuser
body: Компоненту можно больше в пропах ничего не тащить - текущий тип сортировки он достанет из стора и обработчик может сам "приготовить"  
К тому же при выборе нового типа сортировки менюшка закрывается - это тоже можно в обработчик перенести
created_at: 2025-04-08T14:59:33Z
author_association: NONE

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-07T10:49:55Z
body: Ваш пулреквест опубликован. Посмотреть можно [здесь](https://htmlacademy-univer-javascript-3.github.io/2579773-six-cities-5/8/)
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-07T17:58:19Z
body: Ваш пулреквест опубликован. Посмотреть можно [здесь](https://htmlacademy-univer-javascript-3.github.io/2579773-six-cities-5/8/)
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-08T15:11:11Z
body: Ваш пулреквест опубликован. Посмотреть можно [здесь](https://htmlacademy-univer-javascript-3.github.io/2579773-six-cities-5/8/)
author_association: NONE