From 06a673e586c49e88b34ffe2c7ced315917b9181d Mon Sep 17 00:00:00 2001 From: sashtje Date: Sun, 10 Sep 2023 15:04:08 +0300 Subject: [PATCH] feat: add recommendations --- extractedTranslations/en/translation.json | 1 + extractedTranslations/ru/translation.json | 1 + public/locales/en/articles.json | 3 +- public/locales/ru/articles.json | 3 +- .../StoreProvider/config/StateSchema.ts | 6 ++- src/app/styles/index.scss | 20 +++++++++ src/app/styles/reset.scss | 4 ++ src/app/styles/themes/orange.scss | 2 +- .../Article/ui/ArticleList/ArticleList.tsx | 7 ++- .../ui/ArticleListItem/ArticleListItem.tsx | 32 +++++++------ src/pages/ArticleDetailsPage/index.ts | 2 +- .../model/selectors/comments/comments.ts | 4 +- .../recommendations/recommendations.ts | 9 ++++ .../fetchArticleRecommendations.ts | 27 +++++++++++ .../slices/articleDetailsCommentsSlice.ts | 4 +- .../articleDetailsRecommendationsSlice.ts | 45 +++++++++++++++++++ .../ArticleDetailsPage/model/slices/index.ts | 12 +++++ .../ArticleDetailsRecommendationsSchema.ts | 8 ++++ .../ArticleDetailsPage/model/types/index.ts | 11 +++++ .../ArticleDetailsPage.module.scss | 8 ++++ .../ArticleDetailsPage/ArticleDetailsPage.tsx | 41 ++++++++++++----- .../StoreDecorator/StoreDecorator.tsx | 2 + .../DynamicModuleLoader.tsx | 4 +- 23 files changed, 217 insertions(+), 39 deletions(-) create mode 100644 src/pages/ArticleDetailsPage/model/selectors/recommendations/recommendations.ts create mode 100644 src/pages/ArticleDetailsPage/model/services/fetchArticleRecommendations/fetchArticleRecommendations.ts create mode 100644 src/pages/ArticleDetailsPage/model/slices/articleDetailsRecommendationsSlice.ts create mode 100644 src/pages/ArticleDetailsPage/model/slices/index.ts create mode 100644 src/pages/ArticleDetailsPage/model/types/ArticleDetailsRecommendationsSchema.ts create mode 100644 src/pages/ArticleDetailsPage/model/types/index.ts diff --git a/extractedTranslations/en/translation.json b/extractedTranslations/en/translation.json index a3e34b5b..fdc6deaf 100644 --- a/extractedTranslations/en/translation.json +++ b/extractedTranslations/en/translation.json @@ -49,6 +49,7 @@ }, "Профиль": "Профиль", "Редактировать": "Редактировать", + "Рекомендуем": "Рекомендуем", "Сортировать ПО": "Сортировать ПО", "Сохранить": "Сохранить", "Список статей": "Список статей", diff --git a/extractedTranslations/ru/translation.json b/extractedTranslations/ru/translation.json index 25a119a2..597e96e3 100644 --- a/extractedTranslations/ru/translation.json +++ b/extractedTranslations/ru/translation.json @@ -49,6 +49,7 @@ }, "Профиль": "Профиль", "Редактировать": "Редактировать", + "Рекомендуем": "Рекомендуем", "Сортировать ПО": "Сортировать ПО", "Сохранить": "Сохранить", "Список статей": "Список статей", diff --git a/public/locales/en/articles.json b/public/locales/en/articles.json index a1734d98..8362141f 100644 --- a/public/locales/en/articles.json +++ b/public/locales/en/articles.json @@ -17,5 +17,6 @@ "IT": "IT", "Экономика": "Economy", "Наука": "Science", - "Статьи не найдены": "No articles found" + "Статьи не найдены": "No articles found", + "Рекомендуем": "We recommend" } \ No newline at end of file diff --git a/public/locales/ru/articles.json b/public/locales/ru/articles.json index 6db4ccf0..982ff33d 100644 --- a/public/locales/ru/articles.json +++ b/public/locales/ru/articles.json @@ -17,5 +17,6 @@ "IT": "IT", "Экономика": "Экономика", "Наука": "Наука", - "Статьи не найдены": "Статьи не найдены" + "Статьи не найдены": "Статьи не найдены", + "Рекомендуем": "Рекомендуем" } \ 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 9223bb1c..9725d817 100644 --- a/src/app/providers/StoreProvider/config/StateSchema.ts +++ b/src/app/providers/StoreProvider/config/StateSchema.ts @@ -9,7 +9,9 @@ import { UserSchema } from 'entities/User'; import { LoginSchema } from 'features/AuthByUsername'; import { ProfileSchema } from 'entities/Profile'; 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'; @@ -23,7 +25,7 @@ export interface StateSchema { loginForm?: LoginSchema, profile?: ProfileSchema, articleDetails?: ArticleDetailsSchema, - articleDetailsComments?: ArticleDetailsCommentsSchema, + articleDetailsPage?: ArticleDetailsPageSchema, addCommentForm?: AddCommentFormSchema, articlesPage?: ArticlesPageSchema, } diff --git a/src/app/styles/index.scss b/src/app/styles/index.scss index b0d1a121..4a72b699 100644 --- a/src/app/styles/index.scss +++ b/src/app/styles/index.scss @@ -9,6 +9,11 @@ body { color: var(--primary-color); } +* { + scrollbar-color: var(--card-bg) var(--inverted-primary-color); + scrollbar-width: 10px; +} + .app { background: var(--bg-color); min-height: 100vh; @@ -17,3 +22,18 @@ body { .content-page { display: flex; } + +*::-webkit-scrollbar { + width: 10px; + height: 8px; +} + +*::-webkit-scrollbar-track { + background: var(--card-bg); +} + +*::-webkit-scrollbar-thumb { + background-color: var(--inverted-primary-color); + border-radius: 20px; + border: 3px solid var(--primary-color); +} diff --git a/src/app/styles/reset.scss b/src/app/styles/reset.scss index 4054425c..3bbfd088 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/app/styles/themes/orange.scss b/src/app/styles/themes/orange.scss index 53830840..2df9cbd6 100644 --- a/src/app/styles/themes/orange.scss +++ b/src/app/styles/themes/orange.scss @@ -3,7 +3,7 @@ --inverted-bg-color: #bd5012; --primary-color: #9a1a0e; --secondary-color: #d01f0e; - --inverted-primary-color: #dbd5dc; + --inverted-primary-color: #f7f5dc; --inverted-secondary-color: #faf4fb; // skeleton diff --git a/src/entities/Article/ui/ArticleList/ArticleList.tsx b/src/entities/Article/ui/ArticleList/ArticleList.tsx index 2b17d618..9a404c09 100644 --- a/src/entities/Article/ui/ArticleList/ArticleList.tsx +++ b/src/entities/Article/ui/ArticleList/ArticleList.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback } from 'react'; +import { HTMLAttributeAnchorTarget, memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Text, TextSize } from 'shared/ui/Text'; @@ -14,6 +14,7 @@ interface ArticleListProps { articles: Article[]; isLoading?: boolean; view?: ArticleView; + target?: HTMLAttributeAnchorTarget; } const getSkeletons = (view: ArticleView) => (new Array(view === ArticleView.SMALL ? 9 : 3) @@ -32,6 +33,7 @@ export const ArticleList = memo((props: ArticleListProps) => { articles, isLoading, view = ArticleView.SMALL, + target, } = props; const { t } = useTranslation('articles'); @@ -42,8 +44,9 @@ export const ArticleList = memo((props: ArticleListProps) => { key={article.id} article={article} view={view} + target={target} /> - ), [view]); + ), [view, target]); if (!isLoading && !articles.length) { return ( diff --git a/src/entities/Article/ui/ArticleListItem/ArticleListItem.tsx b/src/entities/Article/ui/ArticleListItem/ArticleListItem.tsx index 6c21012f..1c9a1c75 100644 --- a/src/entities/Article/ui/ArticleListItem/ArticleListItem.tsx +++ b/src/entities/Article/ui/ArticleListItem/ArticleListItem.tsx @@ -1,5 +1,4 @@ -import { memo, useCallback } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { HTMLAttributeAnchorTarget, memo } from 'react'; import { useTranslation } from 'react-i18next'; import { classNames } from 'shared/lib/classNames'; @@ -9,7 +8,8 @@ import { Avatar } from 'shared/ui/Avatar'; import { Button } from 'shared/ui/Button'; import { Icon } from 'shared/ui/Icon'; import EyeIcon from 'shared/assets/icons/eye-20-20.svg'; -import { AppRoutes, RoutePath } from 'shared/config/routerConfig/routerConfig'; +import { AppLink } from 'shared/ui/AppLink'; +import { RoutePath } from 'shared/config/routerConfig/routerConfig'; import { Article, ArticleBlockType, ArticleTextBlock, ArticleView, @@ -21,6 +21,7 @@ interface ArticleListItemProps { className?: string; article: Article; view: ArticleView; + target?: HTMLAttributeAnchorTarget; } export const ArticleListItem = memo((props: ArticleListItemProps) => { @@ -28,6 +29,7 @@ export const ArticleListItem = memo((props: ArticleListItemProps) => { className, article, view, + target, } = props; const { t } = useTranslation('articles'); @@ -48,11 +50,6 @@ export const ArticleListItem = memo((props: ArticleListItemProps) => { ); - const navigate = useNavigate(); - const onOpenArticle = useCallback(() => { - navigate(RoutePath[AppRoutes.ARTICLE_DETAILS] + article.id); - }, [article.id, navigate]); - if (view === ArticleView.BIG) { const textBlock = article.blocks.find((block) => block.type === ArticleBlockType.TEXT) as ArticleTextBlock; @@ -83,9 +80,14 @@ export const ArticleListItem = memo((props: ArticleListItemProps) => { )}
- + + + {views}
@@ -95,10 +97,12 @@ export const ArticleListItem = memo((props: ArticleListItemProps) => { } return ( -
- +
{ text={article.title} /> -
+ ); }); diff --git a/src/pages/ArticleDetailsPage/index.ts b/src/pages/ArticleDetailsPage/index.ts index 717b668f..cae482af 100644 --- a/src/pages/ArticleDetailsPage/index.ts +++ b/src/pages/ArticleDetailsPage/index.ts @@ -1,2 +1,2 @@ export { ArticleDetailsPageAsync as ArticleDetailsPage } from './ui/ArticleDetailsPage/ArticleDetailsPage.async'; -export { ArticleDetailsCommentsSchema } from './model/types/ArticleDetailsCommentsSchema'; +export { ArticleDetailsPageSchema } from './model/types'; diff --git a/src/pages/ArticleDetailsPage/model/selectors/comments/comments.ts b/src/pages/ArticleDetailsPage/model/selectors/comments/comments.ts index e04070a0..ca2099fc 100644 --- a/src/pages/ArticleDetailsPage/model/selectors/comments/comments.ts +++ b/src/pages/ArticleDetailsPage/model/selectors/comments/comments.ts @@ -1,4 +1,4 @@ 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/recommendations.ts b/src/pages/ArticleDetailsPage/model/selectors/recommendations/recommendations.ts new file mode 100644 index 00000000..f2f457b7 --- /dev/null +++ b/src/pages/ArticleDetailsPage/model/selectors/recommendations/recommendations.ts @@ -0,0 +1,9 @@ +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 00000000..6925e3bc --- /dev/null +++ b/src/pages/ArticleDetailsPage/model/services/fetchArticleRecommendations/fetchArticleRecommendations.ts @@ -0,0 +1,27 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; + +import { ThunkConfig } from 'app/providers/StoreProvider'; +import { Article } from 'entities/Article'; + +export const fetchArticleRecommendations = createAsyncThunk>( + 'articleDetailsPage/fetchArticleRecommendations', + async (args, 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 b45d8fdf..5645b0cd 100644 --- a/src/pages/ArticleDetailsPage/model/slices/articleDetailsCommentsSlice.ts +++ b/src/pages/ArticleDetailsPage/model/slices/articleDetailsCommentsSlice.ts @@ -5,7 +5,7 @@ import { StateSchema } from 'app/providers/StoreProvider'; import { fetchCommentsByArticleId, -} from 'pages/ArticleDetailsPage/model/services/fetchCommentsByArticleId/fetchCommentsByArticleId'; +} from '../../model/services/fetchCommentsByArticleId/fetchCommentsByArticleId'; import { ArticleDetailsCommentsSchema } from '../types/ArticleDetailsCommentsSchema'; const commentsAdapter = createEntityAdapter({ @@ -13,7 +13,7 @@ const commentsAdapter = createEntityAdapter({ }); export const getArticleComments = commentsAdapter.getSelectors( - (state) => state.articleDetailsComments || commentsAdapter.getInitialState(), + (state) => state.articleDetailsPage?.comments || commentsAdapter.getInitialState(), ); const articleDetailsCommentsSlice = createSlice({ diff --git a/src/pages/ArticleDetailsPage/model/slices/articleDetailsRecommendationsSlice.ts b/src/pages/ArticleDetailsPage/model/slices/articleDetailsRecommendationsSlice.ts new file mode 100644 index 00000000..ed646ced --- /dev/null +++ b/src/pages/ArticleDetailsPage/model/slices/articleDetailsRecommendationsSlice.ts @@ -0,0 +1,45 @@ +import { createEntityAdapter, createSlice, PayloadAction } from '@reduxjs/toolkit'; + +import { StateSchema } from 'app/providers/StoreProvider'; +import { Article } from 'entities/Article'; + +import { + fetchArticleRecommendations, +} from '../../model/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 articleDetailsRecommendationsSlice = createSlice({ + name: 'articleDetailsRecommendationsSlice', + 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: PayloadAction) => { + state.isLoading = false; + recommendationsAdapter.setAll(state, action.payload); + }) + .addCase(fetchArticleRecommendations.rejected, (state, action) => { + state.isLoading = false; + state.error = action.payload; + }); + }, +}); + +export const { reducer: articleDetailsRecommendationsReducer } = articleDetailsRecommendationsSlice; diff --git a/src/pages/ArticleDetailsPage/model/slices/index.ts b/src/pages/ArticleDetailsPage/model/slices/index.ts new file mode 100644 index 00000000..e3a6ab2b --- /dev/null +++ b/src/pages/ArticleDetailsPage/model/slices/index.ts @@ -0,0 +1,12 @@ +import { combineReducers } from '@reduxjs/toolkit'; + +import { articleDetailsCommentsReducer } from '../../model/slices/articleDetailsCommentsSlice'; +import { + articleDetailsRecommendationsReducer, +} from '../../model/slices/articleDetailsRecommendationsSlice'; +import { ArticleDetailsPageSchema } from '../types'; + +export const articleDetailsPageReducer = combineReducers({ + comments: articleDetailsCommentsReducer, + recommendations: articleDetailsRecommendationsReducer, +}); diff --git a/src/pages/ArticleDetailsPage/model/types/ArticleDetailsRecommendationsSchema.ts b/src/pages/ArticleDetailsPage/model/types/ArticleDetailsRecommendationsSchema.ts new file mode 100644 index 00000000..510fb2dd --- /dev/null +++ b/src/pages/ArticleDetailsPage/model/types/ArticleDetailsRecommendationsSchema.ts @@ -0,0 +1,8 @@ +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 00000000..3ff4738e --- /dev/null +++ b/src/pages/ArticleDetailsPage/model/types/index.ts @@ -0,0 +1,11 @@ +import { + ArticleDetailsCommentsSchema, +} from '../types/ArticleDetailsCommentsSchema'; +import { + ArticleDetailsRecommendationsSchema, +} from '../types/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 f03272f9..f71650bd 100644 --- a/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.module.scss +++ b/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.module.scss @@ -1,3 +1,11 @@ .commentTitle { margin-top: 20px; } + +.recommendations { + margin-top: 20px; + flex-wrap: nowrap; + overflow-x: auto; + overflow-y: hidden; + justify-content: flex-start; +} diff --git a/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.tsx b/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.tsx index 9ee92655..2f89845a 100644 --- a/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.tsx +++ b/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.tsx @@ -4,9 +4,9 @@ import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { classNames } from 'shared/lib/classNames'; -import { Text } from 'shared/ui/Text'; +import { Text, TextSize } from 'shared/ui/Text'; import { Button } from 'shared/ui'; -import { ArticleDetails } from 'entities/Article'; +import { ArticleDetails, ArticleList } from 'entities/Article'; import { CommentList } from 'entities/Comment'; import { DynamicModuleLoader, ReducersList } from 'shared/lib/components/DynamicModuleLoader/DynamicModuleLoader'; import { useInitialEffect } from 'shared/lib/hooks/useInitialEffect/useInitialEffect'; @@ -15,15 +15,18 @@ import { AddCommentForm } from 'features/addCommentForm'; import { AppRoutes, RoutePath } from 'shared/config/routerConfig/routerConfig'; import { Page } from 'widgets/Page'; +import { articleDetailsPageReducer } from '../../model/slices'; import { - fetchCommentsByArticleId, -} from '../../model/services/fetchCommentsByArticleId/fetchCommentsByArticleId'; + fetchArticleRecommendations, +} from '../../model/services/fetchArticleRecommendations/fetchArticleRecommendations'; +import { getArticleRecommendationsIsLoading } from '../../model/selectors/recommendations/recommendations'; +import { fetchCommentsByArticleId } from '../../model/services/fetchCommentsByArticleId/fetchCommentsByArticleId'; import { addCommentForArticle } from '../../model/services/addCommentForArticle/addCommentForArticle'; +import { getArticleCommentsIsLoading } from '../../model/selectors/comments/comments'; +import { getArticleComments } from '../../model/slices/articleDetailsCommentsSlice'; import { - // getArticleCommentsError, - getArticleCommentsIsLoading, -} from '../../model/selectors/comments/comments'; -import { articleDetailsCommentsReducer, getArticleComments } from '../../model/slices/articleDetailsCommentsSlice'; + getArticleRecommendations, +} from '../../model/slices/articleDetailsRecommendationsSlice'; import cls from './ArticleDetailsPage.module.scss'; interface ArticleDetailsPageProps { @@ -31,7 +34,7 @@ interface ArticleDetailsPageProps { } const reducersList: ReducersList = { - articleDetailsComments: articleDetailsCommentsReducer, + articleDetailsPage: articleDetailsPageReducer, }; export const ArticleDetailsPage = memo((props: ArticleDetailsPageProps) => { @@ -44,8 +47,12 @@ export const ArticleDetailsPage = memo((props: ArticleDetailsPageProps) => { const commentsIsLoading = useSelector(getArticleCommentsIsLoading); // const error = useSelector(getArticleCommentsError); + const recommendations = useSelector(getArticleRecommendations.selectAll); + const recommendationsIsLoading = useSelector(getArticleRecommendationsIsLoading); + useInitialEffect(() => { dispatch(fetchCommentsByArticleId(id)); + dispatch(fetchArticleRecommendations()); }, [id]); const onSendComment = useCallback((textComment: string) => { @@ -73,12 +80,24 @@ export const ArticleDetailsPage = memo((props: ArticleDetailsPageProps) => { + + - >; } interface DynamicModuleLoaderProps {