Repository: htmlacademy-univer-javascript-3/2579369-six-cities-5
Pull request #8: Контроль и ограничения (часть 2)
Author: login: Suninall
Created: 2025-04-07T07:15:41Z | Merged: 2025-04-07T16:13:58Z



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


Diff:
diff --git a/src/components/main-page/main-page.tsx b/src/components/main-page/main-page.tsx
index 1084e15..fda9e07 100644
--- a/src/components/main-page/main-page.tsx
+++ b/src/components/main-page/main-page.tsx
@@ -4,7 +4,7 @@ import Cards from '../cards-list/cards-list';
 import { OfferPreview } from '../../types/offers-preview';
 import { addPluralEnding } from '../../utils/common';
 import { AppRoute} from '../../const/const';
-import { useState } from 'react';
+import { useState, useMemo } from 'react';
 import Map from '../map/map';
 import CityList from './city-list';
 import { Cities } from '../../mock/cities';
@@ -12,22 +12,26 @@ import { useAppSelector } from '../hooks';
 import { offersPreview } from '../../mock/offers-preview';
 import { useEffect } from 'react';
 import { useAppDispatch } from '../hooks';
-import { fillingOffers } from '../store/action';
-
+import { fillingOffers} from '../store/action';
+import Sorting from './sorting';
+import sort from '../../utils/sort';
 
 const MainPage = (): JSX.Element => {
   const dispatch = useAppDispatch();
+
   useEffect(() => {
     dispatch(fillingOffers(offersPreview));
   }, [dispatch]);
 
   const activeCity = useAppSelector((state) => state.city);
   const allOffers = useAppSelector((state) => state.offers);
+  const activeSort = useAppSelector((state) => state.sort);
   const filteredOffers = allOffers.filter((offer) => offer.city.name === activeCity);
   const allFavoritesOffers = allOffers.filter((offer) => offer.isFavorite);
   const cityInfotmation = Cities.find((city) => city.name === activeCity) || Cities[0];
 
   const [activeCard, setActiveCard] = useState<OfferPreview['id'] | null>(null);
+  const sortedOffers = useMemo(() => sort[activeSort](filteredOffers), [filteredOffers,activeSort]);
 
   return(
     <div className="page page--gray page--main">
@@ -66,23 +70,9 @@ const MainPage = (): JSX.Element => {
             <section className="cities__places places">
               <h2 className="visually-hidden">Places</h2>
               <b className="places__found">{filteredOffers.length} place{addPluralEnding(filteredOffers.length)} to stay in Amsterdam</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>
+              <Sorting />
               <div className="cities__places-list places__list tabs__content">
-                <Cards offers = {filteredOffers} setActiveCard={setActiveCard}/>
+                <Cards offers = {sortedOffers} setActiveCard={setActiveCard}/>
               </div>
             </section>
             <div className="cities__right-section">
diff --git a/src/components/main-page/sorting.tsx b/src/components/main-page/sorting.tsx
new file mode 100644
index 0000000..b12d727
--- /dev/null
+++ b/src/components/main-page/sorting.tsx
@@ -0,0 +1,56 @@
+import { useState, KeyboardEvent } from 'react';
+import { SortingMap } from '../../const/const-sort';
+import { Sort } from '../../types/sort';
+import { useAppSelector,useAppDispatch } from '../hooks';
+import { changeSort } from '../store/action';
+
+
+const Sorting = ():JSX.Element => {
+  const dispatch = useAppDispatch();
+
+  const [isOpened, setIsOpened] = useState(false);
+  const activeSort = useAppSelector((state) => state.sort);
+
+  const onChange = (newSort:Sort) => dispatch(changeSort(newSort));
+
+  function handleKeyDown(evt: KeyboardEvent) {
+    if(evt.key === 'Escape' && isOpened) {
+      evt.preventDefault();
+      setIsOpened(false);
+    }
+  }
+
+  function handleSortingClick (type:Sort) {
+    onChange(type);
+    setIsOpened(false);
+  }
+
+  function handleTypeClick() {
+    setIsOpened((prevIsOpened) => !prevIsOpened);
+  }
+
+  return (
+    <form className="places__sorting" action="#" method="get" onKeyDown={handleKeyDown}>
+      <span className="places__sorting-caption">Sort by</span>
+      <span className="places__sorting-type" tabIndex={0} onClick={handleTypeClick}>
+        {SortingMap[activeSort]}
+        <svg className="places__sorting-arrow" width="7" height="4">
+          <use xlinkHref="#icon-arrow-select"></use>
+        </svg>
+      </span>
+      <ul className={`places__options places__options--custom ${isOpened ? 'places__options--opened' : ''}`}>
+        {Object.entries(SortingMap).map(([key, type]) => (
+          <li
+            key={key}
+            className={`places__option ${activeSort === key ? 'places__option--active' : ''}`}
+            tabIndex={0}
+            onClick={() =>handleSortingClick(key as Sort)}
+          >
+            {type}
+          </li>
+        ))}
+      </ul>
+    </form>
+  );
+};
+export default Sorting;
diff --git a/src/components/store/action.ts b/src/components/store/action.ts
index c1d40ab..c76e341 100644
--- a/src/components/store/action.ts
+++ b/src/components/store/action.ts
@@ -1,7 +1,9 @@
 import { createAction } from '@reduxjs/toolkit';
 import { CityName } from '../../const/const';
 import { OfferPreview } from '../../types/offers-preview';
+import { Sort } from '../../types/sort';
 
 
 export const changeCity = createAction<CityName>('changeCity');
 export const fillingOffers = createAction<OfferPreview[]>('fillingOffers');
+export const changeSort = createAction<Sort>('changeSort');
diff --git a/src/components/store/reducer.ts b/src/components/store/reducer.ts
index da9ddb6..5752239 100644
--- a/src/components/store/reducer.ts
+++ b/src/components/store/reducer.ts
@@ -1,14 +1,17 @@
 import { createReducer } from '@reduxjs/toolkit';
 import { OfferPreview } from '../../types/offers-preview';
 import { CityName } from '../../const/const';
-import { changeCity, fillingOffers } from './action';
+import { Sort } from '../../types/sort';
+import { changeCity, fillingOffers, changeSort } from './action';
 
 const initialState:{
   city:CityName;
   offers:OfferPreview[];
+  sort:Sort;
 } = {
   city: CityName.Paris,
   offers: [],
+  sort: 'Popular',
 };
 
 const reducer = createReducer(initialState, (builder) => {
@@ -18,7 +21,11 @@ const reducer = createReducer(initialState, (builder) => {
     })
     .addCase(fillingOffers, (state, action) => {
       state.offers = action.payload;
+    })
+    .addCase(changeSort, (state,action) =>{
+      state.sort = action.payload;
     });
+
 });
 
 export default reducer;
diff --git a/src/const/const-sort.tsx b/src/const/const-sort.tsx
new file mode 100644
index 0000000..e4e9456
--- /dev/null
+++ b/src/const/const-sort.tsx
@@ -0,0 +1,6 @@
+export const SortingMap = {
+  Popular: 'Popular',
+  LowToHigh: 'Price: low to high',
+  HighToLow: 'Price: high to low',
+  TopRated: 'TopRatedFirst'
+};
diff --git a/src/types/sort.ts b/src/types/sort.ts
new file mode 100644
index 0000000..4286a25
--- /dev/null
+++ b/src/types/sort.ts
@@ -0,0 +1,3 @@
+import { SortingMap } from '../const/const-sort';
+
+export type Sort = keyof typeof SortingMap;
diff --git a/src/utils/sort.tsx b/src/utils/sort.tsx
new file mode 100644
index 0000000..ffc7efa
--- /dev/null
+++ b/src/utils/sort.tsx
@@ -0,0 +1,23 @@
+import { Sort } from '../types/sort';
+import { OfferPreview} from '../types/offers-preview';
+
+function sortByRating (a:OfferPreview, b:OfferPreview) {
+  return b.rating - a.rating;
+}
+
+function sortLowToHIgh (a:OfferPreview, b:OfferPreview) {
+  return a.price - b.price;
+}
+
+function sortHIghToLow (a:OfferPreview, b:OfferPreview) {
+  return b.price - a.price;
+}
+
+const sort: Record<Sort,(offers: OfferPreview[]) => OfferPreview[]> = {
+  Popular:(offers: OfferPreview[]) => offers,
+  HighToLow:(offers: OfferPreview[]) => offers.sort(sortHIghToLow),
+  LowToHigh:(offers: OfferPreview[]) => offers.sort(sortLowToHIgh),
+  TopRated:(offers: OfferPreview[]) => offers.sort(sortByRating),
+};
+
+export default sort;


Reviews:
user: login: BotaniQQQuser
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-04-07T15:10:54Z
user: login: Suninall
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T15:14:36Z
user: login: BotaniQQQuser
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-07T15:20:24Z
user: login: Suninall
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T15:23:35Z
user: login: Suninall
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T15:24:54Z
user: login: Suninall
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T15:47:40Z
user: login: BotaniQQQuser
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-07T16:00:50Z
user: login: Suninall
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T16:11:17Z

Review comments:
diff_hunk: @@ -0,0 +1,23 @@
+import { Sort } from '../types/sort';
+import { OfferPreview} from '../types/offers-preview';
+
+function sortByRating (a:OfferPreview, b:OfferPreview) {
+  return b.rating - a.rating;
+}
+
+function sortLowToHIgh (a:OfferPreview, b:OfferPreview) {
+  return a.price - b.price;
+}
+
+function sortHIghToLow (a:OfferPreview, b:OfferPreview) {
+  return b.price - a.price;
+}
+
+const sort: Record<Sort,(offers: OfferPreview[]) => OfferPreview[]> = {
+  Popular:(offers: OfferPreview[]) => offers.slice(),
path: src/utils/sort.tsx
user: login: BotaniQQQuser
body: А зачем каждый раз создавать копию предложений при сортировке?
created_at: 2025-04-07T15:00:17Z
author_association: NONE
diff_hunk: @@ -0,0 +1,53 @@
+import { useState, KeyboardEvent } from 'react';
+import { SortingMap } from '../../const/const-sort';
+import { Sort } from '../../types/sort';
+
+type SortingProps = {
+  activeSort: Sort;
+  onChange: (newSort: Sort) => void;
+}
+
+const Sorting = ({activeSort, onChange}:SortingProps):JSX.Element => {
+  const [isOpened, setIsOpened] = useState(false);
+
+  function handleKeyDown(evt: KeyboardEvent) {
path: src/components/main-page/sorting.tsx
user: login: BotaniQQQuser
body: Круто, что постаралась подумать над улучшением цифровой доступности переключателя типа сортировки👍  
Возможно более удачным вариантом было бы повесить обработчик на весь документ, чтобы даже если форма не в фокусе, она бы закрывалась по нажатию на `Escape`  
(P.S: править этот момент не нужно, на досуге можешь поискать где-нибудь похожие примеры и посмотреть, как они работают - а то может я вообще не прав)
created_at: 2025-04-07T15:04:55Z
author_association: NONE
diff_hunk: @@ -28,6 +31,9 @@ const MainPage = (): JSX.Element => {
   const cityInfotmation = Cities.find((city) => city.name === activeCity) || Cities[0];
 
   const [activeCard, setActiveCard] = useState<OfferPreview['id'] | null>(null);
+  const [activeSort, setActiveSort] = useState<Sort>('Popular');
path: src/components/main-page/main-page.tsx
user: login: BotaniQQQuser
body: В текущем исполнении при каждом переходе с главной страницы куда-либо и возврате на неё ранее выбранный вариант сортировки будет сбрасываться
created_at: 2025-04-07T15:05:59Z
author_association: NONE
diff_hunk: @@ -0,0 +1,3 @@
+import { SortingMap } from '../const/const-sort';
+
+export type Sort = keyof typeof SortingMap;
path: src/types/sort.ts
user: login: BotaniQQQuser
body: Можно было пойти в обратном направлении - описать тип, содержащий варианты сортировки, а уже из него потом сконструировать константу (тоже править не нужно - just for your information, как говорится)  
```
type Sort = "Popular" | "LowToHigh" | "HighToLow" | "TopRated";

const SortingMap: Record<Sort, string> = {
  Popular: 'Popular',
  LowToHigh: 'Price: low to high',
  HighToLow: 'Price: high to low',
  TopRated: 'TopRatedFirst'
};
```
created_at: 2025-04-07T15:08:36Z
author_association: NONE
diff_hunk: @@ -0,0 +1,3 @@
+import { SortingMap } from '../const/const-sort';
+
+export type Sort = keyof typeof SortingMap;
path: src/types/sort.ts
user: login: Suninall
body: А как лучше описывать? Или это зависит от договоренностей в команде? 
created_at: 2025-04-07T15:14:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,3 @@
+import { SortingMap } from '../const/const-sort';
+
+export type Sort = keyof typeof SortingMap;
path: src/types/sort.ts
user: login: BotaniQQQuser
body: Да, в основном всё зависит от договоренностей в команде
created_at: 2025-04-07T15:20:24Z
author_association: NONE
diff_hunk: @@ -0,0 +1,23 @@
+import { Sort } from '../types/sort';
+import { OfferPreview} from '../types/offers-preview';
+
+function sortByRating (a:OfferPreview, b:OfferPreview) {
+  return b.rating - a.rating;
+}
+
+function sortLowToHIgh (a:OfferPreview, b:OfferPreview) {
+  return a.price - b.price;
+}
+
+function sortHIghToLow (a:OfferPreview, b:OfferPreview) {
+  return b.price - a.price;
+}
+
+const sort: Record<Sort,(offers: OfferPreview[]) => OfferPreview[]> = {
+  Popular:(offers: OfferPreview[]) => offers.slice(),
path: src/utils/sort.tsx
user: login: Suninall
body: Думала лучше создавать копию, чтобы не мутировать офферы. Сейчас поправила, теперь офферы просто сортируются   
created_at: 2025-04-07T15:23:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,53 @@
+import { useState, KeyboardEvent } from 'react';
+import { SortingMap } from '../../const/const-sort';
+import { Sort } from '../../types/sort';
+
+type SortingProps = {
+  activeSort: Sort;
+  onChange: (newSort: Sort) => void;
+}
+
+const Sorting = ({activeSort, onChange}:SortingProps):JSX.Element => {
+  const [isOpened, setIsOpened] = useState(false);
+
+  function handleKeyDown(evt: KeyboardEvent) {
path: src/components/main-page/sorting.tsx
user: login: Suninall
body: Поняла, спасибо, посмотрю про это!
created_at: 2025-04-07T15:24:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,6 +31,9 @@ const MainPage = (): JSX.Element => {
   const cityInfotmation = Cities.find((city) => city.name === activeCity) || Cities[0];
 
   const [activeCard, setActiveCard] = useState<OfferPreview['id'] | null>(null);
+  const [activeSort, setActiveSort] = useState<Sort>('Popular');
path: src/components/main-page/main-page.tsx
user: login: Suninall
body: Исправила! Решила закинуть в хранилище сортировку, чтобы состояние сохранялось даже при переходе на другую страницу.
created_at: 2025-04-07T15:47:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,23 +72,9 @@ const MainPage = (): JSX.Element => {
             <section className="cities__places places">
               <h2 className="visually-hidden">Places</h2>
               <b className="places__found">{filteredOffers.length} place{addPluralEnding(filteredOffers.length)} to stay in Amsterdam</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>
+              <Sorting activeSort={activeSort} onChange={(newSort:Sort) => dispatch(changeSort(newSort))}/>
path: src/components/main-page/main-page.tsx
user: login: BotaniQQQuser
body: За счет того, что тип сортировки переехал в стор, нам сейчас нет необходимости передавать какие-либо пропы в компонент `Sorting`: текущее значение он может достать из стора, а обработчик для переключения соберет сам (тем более список закрывать нужно при переключении)
created_at: 2025-04-07T16:00:50Z
author_association: NONE
diff_hunk: @@ -66,23 +72,9 @@ const MainPage = (): JSX.Element => {
             <section className="cities__places places">
               <h2 className="visually-hidden">Places</h2>
               <b className="places__found">{filteredOffers.length} place{addPluralEnding(filteredOffers.length)} to stay in Amsterdam</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>
+              <Sorting activeSort={activeSort} onChange={(newSort:Sort) => dispatch(changeSort(newSort))}/>
path: src/components/main-page/main-page.tsx
user: login: Suninall
body: Исправила! 
created_at: 2025-04-07T16:11:17Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-07T07:16:29Z
body: Ваш пулреквест опубликован. Посмотреть можно [здесь](https://htmlacademy-univer-javascript-3.github.io/2579369-six-cities-5/8/)
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-07T07:33:10Z
body: Ваш пулреквест опубликован. Посмотреть можно [здесь](https://htmlacademy-univer-javascript-3.github.io/2579369-six-cities-5/8/)
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-07T15:24:46Z
body: Ваш пулреквест опубликован. Посмотреть можно [здесь](https://htmlacademy-univer-javascript-3.github.io/2579369-six-cities-5/8/)
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-07T15:47:56Z
body: Ваш пулреквест опубликован. Посмотреть можно [здесь](https://htmlacademy-univer-javascript-3.github.io/2579369-six-cities-5/8/)
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-07T16:11:52Z
body: Ваш пулреквест опубликован. Посмотреть можно [здесь](https://htmlacademy-univer-javascript-3.github.io/2579369-six-cities-5/8/)
author_association: NONE