diff --git a/package.json b/package.json index 5da2f1714..57922c0ab 100644 --- a/package.json +++ b/package.json @@ -47,4 +47,4 @@ "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.1.0" } -} +} \ No newline at end of file diff --git a/src/app/article/index.js b/src/app/article/index.js index 54f037b64..69cc216c6 100644 --- a/src/app/article/index.js +++ b/src/app/article/index.js @@ -1,60 +1,203 @@ -import { memo, useCallback } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useParams } from 'react-router-dom'; +import { useDispatch, useSelector as useSelectorRedux } from 'react-redux'; + +import shallowequal from 'shallowequal'; + import useStore from '../../hooks/use-store'; import useTranslate from '../../hooks/use-translate'; import useInit from '../../hooks/use-init'; +import useSelector from '../../hooks/use-selector'; + +import articleActions from '../../store-redux/article/actions'; +import commentsActions from '../../store-redux/comments/actions'; +import userCommentsAction from '../../store-redux/user-comment/actions'; + import PageLayout from '../../components/page-layout'; import Head from '../../components/head'; -import Navigation from '../../containers/navigation'; -import Spinner from '../../components/spinner'; import ArticleCard from '../../components/article-card'; +import Spinner from '../../components/spinner'; +import HeadLayout from '../../components/head-layout'; +import ArticleComments from '../../components/article-comments'; +import ArticleForm from '../../components/article-form'; +import ArticleAuthMessage from '../../components/article-auth-message'; +import Textarea from '../../components/textarea'; + +import Navigation from '../../containers/navigation'; import LocaleSelect from '../../containers/locale-select'; import TopHead from '../../containers/top-head'; -import { useDispatch, useSelector } from 'react-redux'; -import shallowequal from 'shallowequal'; -import articleActions from '../../store-redux/article/actions'; -import HeadLayout from '../../components/head-layout'; + +import { textsTreeToList } from '../../utils/texts-tree-to-list'; +import listToTree from '../../utils/list-to-tree'; +import { getLastCommentChildrenId } from '../../utils/get-last-comment-children-id'; function Article() { const store = useStore(); - const dispatch = useDispatch(); - // Параметры из пути /articles/:id - const params = useParams(); + const { t, lang } = useTranslate(); useInit(() => { - //store.actions.article.load(params.id); + dispatch(commentsActions.load(params.id)); dispatch(articleActions.load(params.id)); - }, [params.id]); + if (userComment.isOpenFormInComments) { + dispatch(userCommentsAction.resetForm()); + } + }, [params.id, lang]); - const select = useSelector( - state => ({ - article: state.article.data, - waiting: state.article.waiting, - }), + const selectUser = useSelector(state => ({ + isUserAuth: state.session.exists, + userId: state.session.user._id, + userName: state.session.user.profile?.name, + token: state.session.token, + })); + + const { article, comments, userComment, ...state } = useSelectorRedux( + state => ({ ...state }), shallowequal, - ); // Нужно указать функцию для сравнения свойства объекта, так как хуком вернули объект + ); - const { t } = useTranslate(); + useInit(() => { + dispatch(userCommentsAction.resetUserCommentStore()); + dispatch(userCommentsAction.init(selectUser.userId, selectUser.token, params.id)); + }, [selectUser.token]); const callbacks = { // Добавление в корзину addToBasket: useCallback(_id => store.actions.basket.addToBasket(_id), [store]), + onCloseFormInComments: useCallback(() => { + const commentDefaultData = { + parent: { + _id: params.id, + _type: 'article', + }, + commentAuthorNick: '', + userComment: '', + isOpenFormInComments: false, + lastIdFromCommentTree: '', + }; + dispatch(userCommentsAction.setCommentsData(commentDefaultData)); + }, []), + onChangeCommentData: useCallback( + commentData => { + const { lastItemId, lastChildFromTree } = getLastCommentChildrenId( + commentData.parent._id, + comments.data.items, + ); + const data = { + ...commentData, + isOpenFormInComments: true, + lastIdFromCommentTree: lastChildFromTree, + }; + + dispatch(userCommentsAction.setCommentsData(data)); + }, + [comments.data.items], + ), + onChangeCommentMessage: useCallback(value => { + dispatch(userCommentsAction.setUserMessage(value)); + }, []), + onSubmit: useCallback( + e => { + e.preventDefault(); + const data = { + _id: userComment.userId, + text: userComment.userComment.trim(), + parent: { + ...userComment.parent, + }, + token: userComment.userToken, + }; + + + if (data.text.trim()) { + dispatch(commentsActions.addComment(data, selectUser.userName)); + } + + if (!article.waiting) { + callbacks.onCloseFormInComments(); + } + }, + [userComment], + ), }; + const options = { + comments: useMemo( + () => [ + ...textsTreeToList( + listToTree(comments.data.items || [], '_id', 'parent'), + (item, count) => ({ + ...item, + paddingL: Math.floor(40 * count), + }), + ), + ], + [comments.data.items], + ), + }; + + const disabledBtn = article.waiting || !userComment.userComment.trim(); + return ( <> - + - - + + + + + {userComment.isOpenFormInComments && + (selectUser.isUserAuth ? ( + + + ); +} + +Textarea.propTypes = { + value: PropTypes.string, +}; + +export default Textarea; diff --git a/src/components/textarea/style.css b/src/components/textarea/style.css new file mode 100644 index 000000000..d060a6fa3 --- /dev/null +++ b/src/components/textarea/style.css @@ -0,0 +1,16 @@ +.Textarea { + font-family: var(--font-family); + font-weight: 400; + font-size: 14px; + line-height: 130%; + color: var(--main-text); + border: 1px solid var(--filter-border); + border-radius: 4px; + padding: 8px 12px; + height: 88px; + + &:focus { + outline: none; + border: 0.4px solid var(--primary); + } +} diff --git a/src/config.js b/src/config.js index 67c72f734..2966fefdf 100644 --- a/src/config.js +++ b/src/config.js @@ -18,6 +18,9 @@ const config = { api: { baseUrl: '', }, + i18n: { + defaultLang: JSON.parse(localStorage.getItem('yl-user-lang')) || 'ru', + }, }; export default config; diff --git a/src/containers/catalog-filter/index.js b/src/containers/catalog-filter/index.js index 2eeca7564..2f5a5feb9 100644 --- a/src/containers/catalog-filter/index.js +++ b/src/containers/catalog-filter/index.js @@ -12,6 +12,8 @@ import Button from '../../components/button'; function CatalogFilter() { const store = useStore(); + const { t, lang } = useTranslate(); + const select = useSelector(state => ({ sort: state.catalog.params.sort, query: state.catalog.params.query, @@ -41,28 +43,27 @@ function CatalogFilter() { // Варианты сортировок sort: useMemo( () => [ - { value: 'order', title: 'По порядку' }, - { value: 'title.ru', title: 'По именованию' }, - { value: '-price', title: 'Сначала дорогие' }, - { value: 'edition', title: 'Древние' }, + { value: 'order', title: t( "search.filter-order") }, + { value: 'title.ru', title: t( "search.filter-name") }, + { value: '-price', title: t("search.filter-expensive") }, + { value: 'edition', title: t("search.filter-ancient") }, ], - [], + [lang], ), // Категории для фильтра + categories: useMemo( () => [ - { value: '', title: 'Все' }, + { value: '', title: t('categories.all') }, ...treeToList(listToTree(select.categories), (item, level) => ({ value: item._id, title: '- '.repeat(level) + item.title, })), ], - [select.categories], + [select.categories, lang], ), }; - const { t } = useTranslate(); - return (