diff --git a/.eslintrc.js b/.eslintrc.js
index 69ab8a6..0b5fee5 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -43,7 +43,7 @@ module.exports = {
'error',
{
markupOnly: true,
- ignoreAttribute: ['data-testid', 'to'],
+ ignoreAttribute: ['data-testid', 'to', 'target'],
},
],
'max-len': ['error', { ignoreComments: true, code: 150 }],
diff --git a/json-server/db.json b/json-server/db.json
index 8eabb8e..a27ff04 100644
--- a/json-server/db.json
+++ b/json-server/db.json
@@ -14,10 +14,10 @@
"articles": [
{
"id": "1",
- "title": "Javascript news",
+ "title": "Javascript news ...",
"subtitle": "Что нового в JS за 2024 год?",
"img": "https://teknotower.com/wp-content/uploads/2020/11/js.png",
- "views": 1022,
+ "views": 805,
"createdAt": "26.02.2022",
"userId": "1",
"type": [
@@ -89,8 +89,8 @@
"title": "Python news",
"subtitle": "Что нового в Python за 2024 год?",
"img": "https://micros.uz/upload/iblock/85a/nwx4nuozlvjg8a4h57kqtpdqginf5ovg/python_programming_language.png",
- "views": 1022,
- "createdAt": "26.02.2022",
+ "views": 750,
+ "createdAt": "26.02.2023",
"userId": "1",
"type": [
"IT"
@@ -113,8 +113,8 @@
"title": "Kotlin news",
"subtitle": "Что нового в Kotlin за 2024 год?",
"img": "https://maxilect.ru/wp-content/uploads/sites/1/2021/11/c344c59f81efa338f5dd4324a8411696.jpeg",
- "views": 1022,
- "createdAt": "26.02.2022",
+ "views": 850,
+ "createdAt": "26.02.2021",
"userId": "1",
"type": [
"IT"
@@ -137,8 +137,8 @@
"title": "Python news",
"subtitle": "Что нового в Python за 2024 год?",
"img": "https://datascientest.com/en/wp-content/uploads/sites/9/2024/02/python.png",
- "views": 1022,
- "createdAt": "26.02.2022",
+ "views": 456,
+ "createdAt": "26.08.2022",
"userId": "1",
"type": [
"IT"
@@ -161,8 +161,8 @@
"title": "Kotlin news",
"subtitle": "Что нового в JS за 2024 год?",
"img": "https://maspex.com/wp-content/uploads/2021/05/MAS2MA-PL-WWW-KOMPRO-KOTLIN-870X755PX-V02-870x755.jpg",
- "views": 1022,
- "createdAt": "26.02.2022",
+ "views": 20,
+ "createdAt": "26.12.2022",
"userId": "1",
"type": [
"IT"
@@ -185,8 +185,8 @@
"title": "Python news",
"subtitle": "Что нового в Python за 2024 год?",
"img": "https://dlacademy.ru/media/blog/image_7.jpg",
- "views": 1022,
- "createdAt": "26.02.2022",
+ "views": 74,
+ "createdAt": "17.02.2024",
"userId": "1",
"type": [
"IT"
@@ -209,8 +209,8 @@
"title": "Kotlin news",
"subtitle": "Что нового в Kotlin за 2024 год?",
"img": "https://habrastorage.org/getpro/habr/upload_files/784/040/572/784040572c499a4b59cbf7ce8d06e31e.png",
- "views": 1022,
- "createdAt": "26.02.2022",
+ "views": 898,
+ "createdAt": "26.01.2023",
"userId": "1",
"type": [
"IT"
@@ -233,8 +233,8 @@
"title": "Python news",
"subtitle": "Что нового в Python за 2024 год?",
"img": "https://datascientest.com/en/wp-content/uploads/sites/9/2024/02/python.png",
- "views": 1022,
- "createdAt": "26.02.2022",
+ "views": 636,
+ "createdAt": "01.02.2024",
"userId": "1",
"type": [
"IT"
@@ -257,8 +257,8 @@
"title": "Kotlin news",
"subtitle": "Что нового в Kotlin за 2024 год?",
"img": "https://habrastorage.org/getpro/habr/upload_files/784/040/572/784040572c499a4b59cbf7ce8d06e31e.png",
- "views": 1022,
- "createdAt": "26.02.2022",
+ "views": 225,
+ "createdAt": "18.04.2024",
"userId": "1",
"type": [
"IT"
@@ -281,8 +281,8 @@
"title": "Python news",
"subtitle": "Что нового в JS за 2024 год?",
"img": "https://datascientest.com/en/wp-content/uploads/sites/9/2024/02/python.png",
- "views": 1022,
- "createdAt": "26.02.2022",
+ "views": 21,
+ "createdAt": "25.02.2022",
"userId": "1",
"type": [
"IT"
@@ -305,8 +305,8 @@
"title": "Kotlin news",
"subtitle": "Что нового в JS за 2024 год?",
"img": "https://habrastorage.org/getpro/habr/upload_files/784/040/572/784040572c499a4b59cbf7ce8d06e31e.png",
- "views": 1022,
- "createdAt": "26.02.2022",
+ "views": 321,
+ "createdAt": "08.02.2022",
"userId": "1",
"type": [
"IT"
@@ -329,8 +329,8 @@
"title": "Python news",
"subtitle": "Что нового в JS за 2024 год?",
"img": "https://datascientest.com/en/wp-content/uploads/sites/9/2024/02/python.png",
- "views": 1022,
- "createdAt": "26.02.2022",
+ "views": 889,
+ "createdAt": "26.06.2022",
"userId": "1",
"type": [
"IT"
@@ -353,8 +353,8 @@
"title": "Kotlin news",
"subtitle": "Что нового в JS за 2024 год?",
"img": "https://habrastorage.org/getpro/habr/upload_files/784/040/572/784040572c499a4b59cbf7ce8d06e31e.png",
- "views": 1022,
- "createdAt": "26.02.2022",
+ "views": 864,
+ "createdAt": "08.02.2024",
"userId": "1",
"type": [
"IT"
@@ -378,7 +378,7 @@
"subtitle": "Что нового в JS за 2024 год?",
"img": "https://datascientest.com/en/wp-content/uploads/sites/9/2024/02/python.png",
"views": 1022,
- "createdAt": "26.02.2022",
+ "createdAt": "27.12.2024",
"userId": "1",
"type": [
"IT"
@@ -401,8 +401,8 @@
"title": "Kotlin news",
"subtitle": "Что нового в JS за 2024 год?",
"img": "https://habrastorage.org/getpro/habr/upload_files/784/040/572/784040572c499a4b59cbf7ce8d06e31e.png",
- "views": 1022,
- "createdAt": "26.02.2022",
+ "views": 877,
+ "createdAt": "05.02.2023",
"userId": "1",
"type": [
"IT"
diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json
index 82da6ab..b585740 100644
--- a/public/locales/en/translation.json
+++ b/public/locales/en/translation.json
@@ -22,12 +22,25 @@
"incorrect_username_or_password": "Incorrect username or password",
"profile_page": "Profile page",
"error_loading_an_article": "Error loading an article",
- "error_loading_articles_page": "Error loading the articles page",
+ "articles_not_found": "Articles not found",
"article_not_found": "Article not found",
"comments": "Comments",
+ "we_recommend": "We recommend",
"start_of_discussion": "Start of discussion",
"send": "Send",
"enter_comment_text": "Enter comment text",
"read_more": "Read more...",
- "go_back": "Go back"
+ "go_back": "Go back",
+ "search": "Search",
+ "sort_software": "Sort Software",
+ "by": "by",
+ "ascending": "ascending",
+ "descending": "descending",
+ "date_of_creation": "date of creation",
+ "name": "name",
+ "views": "views",
+ "all_articles": "All Articles",
+ "it": "IT",
+ "economics": "Economy",
+ "science": "Science"
}
\ No newline at end of file
diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json
index 45f77c0..1e17b22 100644
--- a/public/locales/ru/translation.json
+++ b/public/locales/ru/translation.json
@@ -22,12 +22,25 @@
"incorrect_username_or_password": "Неверное имя пользователя или пароль",
"profile_page": "Страница профиля",
"error_loading_an_article": "Ошибка при загрузке статьи",
- "error_loading_articles_page": "Ошибка при загрузке страницы статей",
+ "articles_not_found": "Статьи не найдены",
"article_not_found": "Статья не найдена",
"comments": "Комментарии",
+ "we_recommend": "Мы рекомендуем",
"start_of_discussion": "Начало обсуждения",
"send": "Отправить",
"enter_comment_text": "Введите текст комментария",
"read_more": "Читать далее...",
- "go_back": "Вернуться назад"
+ "go_back": "Вернуться назад",
+ "search": "Поиск",
+ "sort_software": "Сортировать по",
+ "by": "по",
+ "ascending": "возрастанию",
+ "descending": "убыванию",
+ "date_of_creation": "дате создания",
+ "name": "названию",
+ "views": "просмотрам",
+ "all_articles": "Все статьи",
+ "it": "IT",
+ "economics": "Экономика",
+ "science": "Наука"
}
\ No newline at end of file
diff --git a/src/app/providers/StoreProvider/config/StateSchema.ts b/src/app/providers/StoreProvider/config/StateSchema.ts
index 0dfc6c3..b91bd02 100644
--- a/src/app/providers/StoreProvider/config/StateSchema.ts
+++ b/src/app/providers/StoreProvider/config/StateSchema.ts
@@ -8,7 +8,7 @@ import { CombinedState } from 'redux';
import { ProfileSchema } from 'entities/Profile';
import { AxiosInstance } from 'axios';
import { ArticleDetailsSchema } from 'entities/Article';
-import { ArticleDetailsCommentsSchema } from 'pages/ArticleDetailsPage';
+import { ArticleDetailsPageSchema } from 'pages/ArticleDetailsPage';
import { AddCommentFormSchema } from 'features/addCommentForm';
import { ArticlesPageSchema } from 'pages/ArticlesPage';
import { UISchema } from 'features/UI';
@@ -22,9 +22,9 @@ export interface StateSchema {
loginForm?: LoginSchema;
profile?: ProfileSchema;
articleDetails?: ArticleDetailsSchema;
- articleDetailsComments?: ArticleDetailsCommentsSchema;
addCommentForm?: AddCommentFormSchema;
articlesPage?: ArticlesPageSchema;
+ articleDetailsPage?: ArticleDetailsPageSchema;
}
export type StateSchemaKey = keyof StateSchema;
diff --git a/src/app/styles/index.scss b/src/app/styles/index.scss
index 048515e..04bb0ae 100644
--- a/src/app/styles/index.scss
+++ b/src/app/styles/index.scss
@@ -18,3 +18,18 @@ body {
display: flex;
}
+*::-webkit-scrollbar {
+ width: 12px; /* ширина scrollbar */
+ height: 8px;
+}
+
+*::-webkit-scrollbar-track {
+ background: var(--card-bg); /* цвет дорожки */
+}
+
+*::-webkit-scrollbar-thumb {
+ background-color: var(--inverted-primary-color); /* цвет плашки */
+ border-radius: 20px; /* закругления плашки */
+ border: 2px solid var(--primary-color); /* padding вокруг плашки */
+}
+
diff --git a/src/app/styles/reset.scss b/src/app/styles/reset.scss
index 4054425..3bbfd08 100644
--- a/src/app/styles/reset.scss
+++ b/src/app/styles/reset.scss
@@ -11,3 +11,7 @@ select {
margin: 0;
font: inherit;
}
+
+a {
+ text-decoration: none;
+}
diff --git a/src/entities/Article/index.ts b/src/entities/Article/index.ts
index b337cd0..93727b1 100644
--- a/src/entities/Article/index.ts
+++ b/src/entities/Article/index.ts
@@ -2,8 +2,12 @@ export {
ArticleDetails,
} from './ui/ArticleDetails/ArticleDetails';
-export { Article, ArticleView } from './model/types/article';
+export {
+ Article, ArticleView, ArticleSortField, ArticleType,
+} from './model/types/article';
export type { ArticleDetailsSchema } from './model/types/articleDetailsSchema';
export { ArticleList } from './ui/ArticleList/ArticleList';
export { ArticleViewSelector } from './ui/ArticleViewSelector/ArticleViewSelector';
+export { ArticleSortSelector } from './ui/ArticleSortSelector/ArticleSortSelector';
+export { ArticleTypeTabs } from './ui/ArticleTypeTabs/ArticleTypeTabs';
diff --git a/src/entities/Article/model/types/article.ts b/src/entities/Article/model/types/article.ts
index 5355b29..c9fbc18 100644
--- a/src/entities/Article/model/types/article.ts
+++ b/src/entities/Article/model/types/article.ts
@@ -1,5 +1,11 @@
import { User } from 'entities/User';
+export enum ArticleSortField {
+ VIEWS = 'views',
+ TITLE = 'title',
+ CREATED = 'createdAt',
+}
+
export enum ArticleBlockType {
CODE = 'CODE',
IMAGE = 'IMAGE',
@@ -31,6 +37,7 @@ export interface ArticleTextBlock extends ArticleBlockBase {
export type ArticleBlock = ArticleCodeBlock | ArticleImageBlock | ArticleTextBlock;
export enum ArticleType {
+ ALL = 'ALL',
IT = 'IT',
SCIENCE = 'SCIENCE',
ECONOMICS = 'ECONOMICS'
diff --git a/src/entities/Article/ui/ArticleList/ArticleList.module.scss b/src/entities/Article/ui/ArticleList/ArticleList.module.scss
index 2205caa..9114423 100644
--- a/src/entities/Article/ui/ArticleList/ArticleList.module.scss
+++ b/src/entities/Article/ui/ArticleList/ArticleList.module.scss
@@ -7,9 +7,5 @@
.TILE {
display: flex;
flex-wrap: wrap;
-
- .card {
- margin-right: 30px;
- margin-bottom: 30px;
- }
+ gap: 30px;
}
diff --git a/src/entities/Article/ui/ArticleList/ArticleList.tsx b/src/entities/Article/ui/ArticleList/ArticleList.tsx
index ac3d6a4..083f161 100644
--- a/src/entities/Article/ui/ArticleList/ArticleList.tsx
+++ b/src/entities/Article/ui/ArticleList/ArticleList.tsx
@@ -1,7 +1,8 @@
import { classNames } from 'shared/lib/classNames/classNames';
import { useTranslation } from 'react-i18next';
-import { memo } from 'react';
+import { HTMLAttributeAnchorTarget, memo } from 'react';
import { ArticleListItemSkeleton } from 'entities/Article/ui/ArticleListItem/ArticleListItemSkeleton';
+import { Text, TextSize } from 'shared/ui/Text/Text';
import { ArticleListItem } from '../ArticleListItem/ArticleListItem';
import cls from './ArticleList.module.scss';
import { Article, ArticleView } from '../../model/types/article';
@@ -10,6 +11,7 @@ interface ArticleListProps {
className?: string;
articles: Article[]
isLoading?: boolean;
+ target?: HTMLAttributeAnchorTarget;
view?: ArticleView;
}
@@ -24,6 +26,7 @@ export const ArticleList = memo((props: ArticleListProps) => {
className,
articles,
isLoading,
+ target,
view = ArticleView.TILE,
} = props;
const { t } = useTranslation();
@@ -34,9 +37,21 @@ export const ArticleList = memo((props: ArticleListProps) => {
className={cls.card}
article={article}
view={view}
+ target={target}
/>
);
+ if (!isLoading && !articles.length) {
+ return (
+
+
+
+ );
+ }
+
return (
{articles.length > 0
diff --git a/src/entities/Article/ui/ArticleListItem/ArticleListItem.tsx b/src/entities/Article/ui/ArticleListItem/ArticleListItem.tsx
index 7f376be..df0720f 100644
--- a/src/entities/Article/ui/ArticleListItem/ArticleListItem.tsx
+++ b/src/entities/Article/ui/ArticleListItem/ArticleListItem.tsx
@@ -1,14 +1,14 @@
import { classNames } from 'shared/lib/classNames/classNames';
import { useTranslation } from 'react-i18next';
-import { memo, useCallback } from 'react';
+import { HTMLAttributeAnchorTarget, memo } from 'react';
import { Text } from 'shared/ui/Text/Text';
import { Icon } from 'shared/ui/Icon/Icon';
import EyeIcon from 'shared/assets/icon/eye-20-20.svg';
import { Card } from 'shared/ui/Card/Card';
import { Avatar } from 'shared/ui/Avatar/Avatar';
import { Button, ButtonTheme } from 'shared/ui/Button/Button';
-import { useNavigate } from 'react-router-dom';
import { RoutePath } from 'shared/config/routeConfig/routeConfig';
+import { AppLink } from 'shared/ui/AppLink/AppLink';
import cls from './ArticleListItem.module.scss';
import {
Article, ArticleBlockType, ArticleTextBlock, ArticleView,
@@ -18,17 +18,18 @@ import { ArticleTextBlockComponent } from '../ArticleTextBlockComponent/ArticleT
interface ArticleListItemProps {
className?: string;
article: Article;
+ target?: HTMLAttributeAnchorTarget;
view: ArticleView;
}
export const ArticleListItem = memo((props: ArticleListItemProps) => {
- const { className, article, view } = props;
+ const {
+ className,
+ article,
+ target,
+ view,
+ } = props;
const { t } = useTranslation();
- const navigate = useNavigate();
-
- const onOpenArticle = useCallback(() => {
- navigate(RoutePath.article_details + article.id);
- }, [article.id, navigate]);
const types =
;
const views = (
@@ -58,9 +59,14 @@ export const ArticleListItem = memo((props: ArticleListItemProps) => {
)}
-
+
+
+
{views}
@@ -69,8 +75,12 @@ export const ArticleListItem = memo((props: ArticleListItemProps) => {
}
return (
-
-
+
+
@@ -81,6 +91,6 @@ export const ArticleListItem = memo((props: ArticleListItemProps) => {
-
+
);
});
diff --git a/src/entities/Article/ui/ArticleSortSelector/ArticleSortSelector.module.scss b/src/entities/Article/ui/ArticleSortSelector/ArticleSortSelector.module.scss
new file mode 100644
index 0000000..2944e27
--- /dev/null
+++ b/src/entities/Article/ui/ArticleSortSelector/ArticleSortSelector.module.scss
@@ -0,0 +1,8 @@
+.ArticleSortSelector {
+ display: flex;
+ align-items: center;
+}
+
+.order {
+ margin-left: 8px;
+}
diff --git a/src/entities/Article/ui/ArticleSortSelector/ArticleSortSelector.stories.tsx b/src/entities/Article/ui/ArticleSortSelector/ArticleSortSelector.stories.tsx
new file mode 100644
index 0000000..f7b9356
--- /dev/null
+++ b/src/entities/Article/ui/ArticleSortSelector/ArticleSortSelector.stories.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { ArticleSortSelector } from './ArticleSortSelector';
+
+export default {
+ title: 'entities/Article/ArticleSortSelector',
+ component: ArticleSortSelector,
+ argTypes: {
+ backgroundColor: { control: 'color' },
+ },
+} as ComponentMeta
;
+
+const Template: ComponentStory = (args) => ;
+
+export const Normal = Template.bind({});
+Normal.args = {};
diff --git a/src/entities/Article/ui/ArticleSortSelector/ArticleSortSelector.tsx b/src/entities/Article/ui/ArticleSortSelector/ArticleSortSelector.tsx
new file mode 100644
index 0000000..b654b8a
--- /dev/null
+++ b/src/entities/Article/ui/ArticleSortSelector/ArticleSortSelector.tsx
@@ -0,0 +1,70 @@
+import { classNames } from 'shared/lib/classNames/classNames';
+import { useTranslation } from 'react-i18next';
+import { memo, useMemo } from 'react';
+import { Select, SelectOption } from 'shared/ui/Select/Select';
+import { ArticleSortField } from 'entities/Article/model/types/article';
+import { SortOrder } from 'shared/types';
+import cls from './ArticleSortSelector.module.scss';
+
+interface ArticleSortSelectorProps {
+ className?: string;
+ order: SortOrder;
+ sort: ArticleSortField;
+ onChangeOrder: (newOrder: SortOrder) => void;
+ onChangeSort: (newSort: ArticleSortField) => void;
+}
+
+export const ArticleSortSelector = memo((props: ArticleSortSelectorProps) => {
+ const {
+ className,
+ onChangeOrder,
+ onChangeSort,
+ order,
+ sort,
+ } = props;
+ const { t } = useTranslation();
+
+ const orderOptions = useMemo[]>(() => [
+ {
+ value: 'asc',
+ content: t('ascending'),
+ },
+ {
+ value: 'desc',
+ content: t('descending'),
+ },
+ ], [t]);
+
+ const sortFieldOptions = useMemo[]>(() => [
+ {
+ value: ArticleSortField.CREATED,
+ content: t('date_of_creation'),
+ },
+ {
+ value: ArticleSortField.TITLE,
+ content: t('name'),
+ },
+ {
+ value: ArticleSortField.VIEWS,
+ content: t('views'),
+ },
+ ], [t]);
+
+ return (
+
+
+
+
+ );
+});
diff --git a/src/entities/Article/ui/ArticleTypeTabs/ArticleTypeTabs.stories.tsx b/src/entities/Article/ui/ArticleTypeTabs/ArticleTypeTabs.stories.tsx
new file mode 100644
index 0000000..330ab98
--- /dev/null
+++ b/src/entities/Article/ui/ArticleTypeTabs/ArticleTypeTabs.stories.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+
+import { ArticleTypeTabs } from './ArticleTypeTabs';
+
+export default {
+ title: 'entities/Article/ArticleTypeTabs',
+ component: ArticleTypeTabs,
+ argTypes: {
+ backgroundColor: { control: 'color' },
+ },
+} as ComponentMeta;
+
+const Template: ComponentStory = (args) => ;
+
+export const Normal = Template.bind({});
+Normal.args = {};
diff --git a/src/entities/Article/ui/ArticleTypeTabs/ArticleTypeTabs.tsx b/src/entities/Article/ui/ArticleTypeTabs/ArticleTypeTabs.tsx
new file mode 100644
index 0000000..6d8a379
--- /dev/null
+++ b/src/entities/Article/ui/ArticleTypeTabs/ArticleTypeTabs.tsx
@@ -0,0 +1,53 @@
+import { classNames } from 'shared/lib/classNames/classNames';
+import { useTranslation } from 'react-i18next';
+import { memo, useCallback, useMemo } from 'react';
+import { TabItem, Tabs } from 'shared/ui/Tabs/Tabs';
+import { ArticleType } from 'entities/Article';
+
+interface ArticleTypeTabsProps {
+ className?: string;
+ value: ArticleType;
+ onChangeType: (type: ArticleType) => void;
+}
+
+export const ArticleTypeTabs = memo((props: ArticleTypeTabsProps) => {
+ const {
+ className,
+ value,
+ onChangeType,
+ } = props;
+ const { t } = useTranslation();
+
+ // TODO: Когда будет больше типов, переделать в цикл
+ const typeTabs = useMemo(() => [
+ {
+ value: ArticleType.ALL,
+ content: t('all_articles'),
+ },
+ {
+ value: ArticleType.IT,
+ content: t('it'),
+ },
+ {
+ value: ArticleType.ECONOMICS,
+ content: t('economics'),
+ },
+ {
+ value: ArticleType.SCIENCE,
+ content: t('science'),
+ },
+ ], [t]);
+
+ const onTabClick = useCallback((tab: TabItem) => {
+ onChangeType(tab.value as ArticleType);
+ }, [onChangeType]);
+
+ return (
+
+ );
+});
diff --git a/src/pages/ArticleDetailsPage/index.ts b/src/pages/ArticleDetailsPage/index.ts
index a958908..9fd9162 100644
--- a/src/pages/ArticleDetailsPage/index.ts
+++ b/src/pages/ArticleDetailsPage/index.ts
@@ -1,5 +1,7 @@
export {
ArticleDetailsPageLazy as ArticleDetailsPage,
-} from 'pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.lazy';
+} from './ui/ArticleDetailsPage/ArticleDetailsPage.lazy';
-export { ArticleDetailsCommentsSchema } from 'pages/ArticleDetailsPage/model/types/ArticleDetailsCommentsSchema';
+export { ArticleDetailsCommentsSchema } from './model/types/ArticleDetailsCommentsSchema';
+export { ArticleDetailsRecommendationsSchema } from './model/types/ArticleDetailsRecommendationsSchema';
+export { ArticleDetailsPageSchema } from './model/types';
diff --git a/src/pages/ArticleDetailsPage/model/selectors/comments.ts b/src/pages/ArticleDetailsPage/model/selectors/comments.ts
index e04070a..da9a625 100644
--- a/src/pages/ArticleDetailsPage/model/selectors/comments.ts
+++ b/src/pages/ArticleDetailsPage/model/selectors/comments.ts
@@ -1,4 +1,5 @@
import { StateSchema } from 'app/providers/StoreProvider';
-export const getArticleCommentsIsLoading = (state: StateSchema) => state.articleDetailsComments?.isLoading;
-export const getArticleCommentsError = (state: StateSchema) => state.articleDetailsComments?.error;
+export const getArticleCommentsIsLoading = (state: StateSchema) => state.articleDetailsPage?.comments?.isLoading;
+
+export const getArticleCommentsError = (state: StateSchema) => state.articleDetailsPage?.comments?.error;
diff --git a/src/pages/ArticleDetailsPage/model/selectors/recommendations.ts b/src/pages/ArticleDetailsPage/model/selectors/recommendations.ts
new file mode 100644
index 0000000..5a58006
--- /dev/null
+++ b/src/pages/ArticleDetailsPage/model/selectors/recommendations.ts
@@ -0,0 +1,5 @@
+import { StateSchema } from 'app/providers/StoreProvider';
+
+export const getArticleRecommendationsIsLoading = (state: StateSchema) => state.articleDetailsPage?.recommendations?.isLoading;
+
+export const getArticleRecommendationsError = (state: StateSchema) => state.articleDetailsPage?.recommendations?.error;
diff --git a/src/pages/ArticleDetailsPage/model/services/fetchArticleRecommendations/fetchArticleRecommendations.ts b/src/pages/ArticleDetailsPage/model/services/fetchArticleRecommendations/fetchArticleRecommendations.ts
new file mode 100644
index 0000000..9631219
--- /dev/null
+++ b/src/pages/ArticleDetailsPage/model/services/fetchArticleRecommendations/fetchArticleRecommendations.ts
@@ -0,0 +1,30 @@
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { ThunkConfig } from 'app/providers/StoreProvider';
+import { Article } from 'entities/Article';
+
+export const fetchArticleRecommendations = createAsyncThunk<
+ Article[],
+ void,
+ ThunkConfig
+>(
+ 'articleDetailsPage/fetchArticleRecommendations',
+ async (props, thunkApi) => {
+ const { extra, rejectWithValue } = thunkApi;
+
+ try {
+ const response = await extra.api.get('/articles', {
+ params: {
+ _limit: 4,
+ },
+ });
+
+ if (!response.data) {
+ throw new Error();
+ }
+
+ return response.data;
+ } catch (e) {
+ return rejectWithValue('error');
+ }
+ },
+);
diff --git a/src/pages/ArticleDetailsPage/model/slices/articleDetailsCommentsSlice.ts b/src/pages/ArticleDetailsPage/model/slices/articleDetailsCommentsSlice.ts
index 6effc6c..338ef18 100644
--- a/src/pages/ArticleDetailsPage/model/slices/articleDetailsCommentsSlice.ts
+++ b/src/pages/ArticleDetailsPage/model/slices/articleDetailsCommentsSlice.ts
@@ -1,20 +1,18 @@
-import {
- createEntityAdapter,
- createSlice, PayloadAction,
-} from '@reduxjs/toolkit';
+import { createEntityAdapter, createSlice, PayloadAction } from '@reduxjs/toolkit';
+
import { Comment } from 'entities/Comment';
import { StateSchema } from 'app/providers/StoreProvider';
import {
fetchCommentsByArticleId,
-} from 'pages/ArticleDetailsPage/model/services/fetchCommentsByArticleId/fetchCommentsByArticleId';
-import { ArticleDetailsCommentsSchema } from 'pages/ArticleDetailsPage/model/types/ArticleDetailsCommentsSchema';
+} from '../../model/services/fetchCommentsByArticleId/fetchCommentsByArticleId';
+import { ArticleDetailsCommentsSchema } from '../types/ArticleDetailsCommentsSchema';
const commentsAdapter = createEntityAdapter({
selectId: (comment) => comment.id,
});
export const getArticleComments = commentsAdapter.getSelectors(
- (state) => state.articleDetailsComments || commentsAdapter.getInitialState(),
+ (state) => state.articleDetailsPage?.comments || commentsAdapter.getInitialState(),
);
const articleDetailsCommentsSlice = createSlice({
@@ -22,7 +20,7 @@ const articleDetailsCommentsSlice = createSlice({
initialState: commentsAdapter.getInitialState({
isLoading: false,
error: undefined,
- ids: ['1', '2'],
+ ids: [],
entities: {},
}),
reducers: {},
diff --git a/src/pages/ArticleDetailsPage/model/slices/articleDetailsPageRecommendationsSlice.ts b/src/pages/ArticleDetailsPage/model/slices/articleDetailsPageRecommendationsSlice.ts
new file mode 100644
index 0000000..cd97ce7
--- /dev/null
+++ b/src/pages/ArticleDetailsPage/model/slices/articleDetailsPageRecommendationsSlice.ts
@@ -0,0 +1,46 @@
+import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
+import { StateSchema } from 'app/providers/StoreProvider';
+import { Article } from 'entities/Article';
+import { fetchArticleRecommendations } from '../services/fetchArticleRecommendations/fetchArticleRecommendations';
+import { ArticleDetailsRecommendationsSchema } from '../types/ArticleDetailsRecommendationsSchema';
+
+const recommendationsAdapter = createEntityAdapter({
+ selectId: (article) => article.id,
+});
+
+export const getArticleRecommendations = recommendationsAdapter.getSelectors(
+ (state) => state.articleDetailsPage?.recommendations || recommendationsAdapter.getInitialState(),
+);
+
+const articleDetailsPageRecommendationsSlice = createSlice({
+ name: 'articleDetailsPageRecommendationsSlice',
+ initialState: recommendationsAdapter.getInitialState({
+ isLoading: false,
+ error: undefined,
+ ids: [],
+ entities: {},
+ }),
+ reducers: {},
+ extraReducers: (builder) => {
+ builder
+ .addCase(fetchArticleRecommendations.pending, (state) => {
+ state.error = undefined;
+ state.isLoading = true;
+ })
+ .addCase(fetchArticleRecommendations.fulfilled, (
+ state,
+ action,
+ ) => {
+ state.isLoading = false;
+ recommendationsAdapter.setAll(state, action.payload);
+ })
+ .addCase(fetchArticleRecommendations.rejected, (state, action) => {
+ state.isLoading = false;
+ state.error = action.payload;
+ });
+ },
+});
+
+export const {
+ reducer: articleDetailsPageRecommendationsReducer,
+} = articleDetailsPageRecommendationsSlice;
diff --git a/src/pages/ArticleDetailsPage/model/slices/index.ts b/src/pages/ArticleDetailsPage/model/slices/index.ts
new file mode 100644
index 0000000..6b22384
--- /dev/null
+++ b/src/pages/ArticleDetailsPage/model/slices/index.ts
@@ -0,0 +1,11 @@
+import { combineReducers } from '@reduxjs/toolkit';
+import { ArticleDetailsPageSchema } from '../types';
+import {
+ articleDetailsPageRecommendationsReducer,
+} from './articleDetailsPageRecommendationsSlice';
+import { articleDetailsCommentsReducer } from './articleDetailsCommentsSlice';
+
+export const articleDetailsPageReducer = combineReducers({
+ recommendations: articleDetailsPageRecommendationsReducer,
+ comments: articleDetailsCommentsReducer,
+});
diff --git a/src/pages/ArticleDetailsPage/model/types/ArticleDetailsRecommendationsSchema.ts b/src/pages/ArticleDetailsPage/model/types/ArticleDetailsRecommendationsSchema.ts
new file mode 100644
index 0000000..c8c7d78
--- /dev/null
+++ b/src/pages/ArticleDetailsPage/model/types/ArticleDetailsRecommendationsSchema.ts
@@ -0,0 +1,7 @@
+import { EntityState } from '@reduxjs/toolkit';
+import { Article } from 'entities/Article';
+
+export interface ArticleDetailsRecommendationsSchema extends EntityState{
+ isLoading?: boolean;
+ error?: string;
+}
diff --git a/src/pages/ArticleDetailsPage/model/types/index.ts b/src/pages/ArticleDetailsPage/model/types/index.ts
new file mode 100644
index 0000000..7895b4c
--- /dev/null
+++ b/src/pages/ArticleDetailsPage/model/types/index.ts
@@ -0,0 +1,7 @@
+import { ArticleDetailsCommentsSchema } from './ArticleDetailsCommentsSchema';
+import { ArticleDetailsRecommendationsSchema } from './ArticleDetailsRecommendationsSchema';
+
+export interface ArticleDetailsPageSchema {
+ comments: ArticleDetailsCommentsSchema;
+ recommendations: ArticleDetailsRecommendationsSchema;
+}
diff --git a/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.module.scss b/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.module.scss
index 62505f4..8d04a20 100644
--- a/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.module.scss
+++ b/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.module.scss
@@ -5,3 +5,10 @@
.commentTitle {
margin-top: 20px;
}
+
+.recommendations {
+ margin-top: 20px;
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ overflow-y: hidden;
+}
diff --git a/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.tsx b/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.tsx
index a4dbf7a..6586a29 100644
--- a/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.tsx
+++ b/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.tsx
@@ -1,9 +1,9 @@
import { classNames } from 'shared/lib/classNames/classNames';
import { useTranslation } from 'react-i18next';
import { memo, useCallback } from 'react';
-import { ArticleDetails } from 'entities/Article';
+import { ArticleDetails, ArticleList } from 'entities/Article';
import { useNavigate, useParams } from 'react-router-dom';
-import { Text } from 'shared/ui/Text/Text';
+import { Text, TextSize } from 'shared/ui/Text/Text';
import { CommentList } from 'entities/Comment';
import { DynamicModuleLoader, ReducersList } from 'shared/lib/components/DynamicModuleLoader/DynamicModuleLoader';
import { useSelector } from 'react-redux';
@@ -13,8 +13,14 @@ import { AddCommentForm } from 'features/addCommentForm';
import { Button, ButtonTheme } from 'shared/ui/Button/Button';
import { RoutePath } from 'shared/config/routeConfig/routeConfig';
import { Page } from 'widgets/Page/Page';
+import { articleDetailsPageReducer } from '../../model/slices';
+import { getArticleRecommendations } from '../../model/slices/articleDetailsPageRecommendationsSlice';
+import { getArticleRecommendationsIsLoading } from '../../model/selectors/recommendations';
+import {
+ fetchArticleRecommendations,
+} from '../../model/services/fetchArticleRecommendations/fetchArticleRecommendations';
import { fetchCommentsByArticleId } from '../../model/services/fetchCommentsByArticleId/fetchCommentsByArticleId';
-import { articleDetailsCommentsReducer, getArticleComments } from '../../model/slices/articleDetailsCommentsSlice';
+import { getArticleComments } from '../../model/slices/articleDetailsCommentsSlice';
import { addCommentForArticle } from '../../model/services/addCommentForArticle/addCommentForArticle';
import { getArticleCommentsIsLoading } from '../../model/selectors/comments';
import cls from './ArticleDetailsPage.module.scss';
@@ -24,7 +30,7 @@ interface ArticleDetailsPageProps {
}
const reducers: ReducersList = {
- articleDetailsComments: articleDetailsCommentsReducer,
+ articleDetailsPage: articleDetailsPageReducer,
};
const ArticleDetailsPage = (props: ArticleDetailsPageProps) => {
@@ -33,6 +39,8 @@ const ArticleDetailsPage = (props: ArticleDetailsPageProps) => {
const { id } = useParams<{ id: string }>();
const dispatch = useAppDispatch();
const comments = useSelector(getArticleComments.selectAll);
+ const recommendations = useSelector(getArticleRecommendations.selectAll);
+ const recommendationsIsLoading = useSelector(getArticleRecommendationsIsLoading);
const commentsIsLoading = useSelector(getArticleCommentsIsLoading);
const navigate = useNavigate();
@@ -46,6 +54,7 @@ const ArticleDetailsPage = (props: ArticleDetailsPageProps) => {
useInitialEffect(() => {
dispatch(fetchCommentsByArticleId(id));
+ dispatch(fetchArticleRecommendations());
});
if (!id) {
@@ -57,16 +66,28 @@ const ArticleDetailsPage = (props: ArticleDetailsPageProps) => {
}
return (
-
+
-