diff --git a/api-examples/comments.http b/api-examples/comments.http index fd797fb83..cba37f5d5 100644 --- a/api-examples/comments.http +++ b/api-examples/comments.http @@ -1,6 +1,6 @@ # Комментрии к товару -GET http://query.rest/api/v1/comments?fields=items(_id,text,dateCreate,author(profile(name)),parent(_id,_type),isDeleted),count&limit=*&search[parent]=670260bb7dd498df5525e5ed +GET http://query.rest/api/v1/comments?fields=items(_id,text,dateCreate,author(profile(name)),parent(_id,_type),isDeleted),count&limit=*&search[parent]=67fb67e98702ec3fc67fef7e # Остальные методы в http://query.rest/api/v1/docs/ (http://localhost:8010/api/v1/docs/) diff --git a/src/app/article/index.js b/src/app/article/index.js index 54f037b64..4f86350c5 100644 --- a/src/app/article/index.js +++ b/src/app/article/index.js @@ -1,4 +1,4 @@ -import { memo, useCallback } from 'react'; +import { memo, useCallback, useEffect, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import useStore from '../../hooks/use-store'; import useTranslate from '../../hooks/use-translate'; @@ -10,10 +10,15 @@ import Spinner from '../../components/spinner'; import ArticleCard from '../../components/article-card'; import LocaleSelect from '../../containers/locale-select'; import TopHead from '../../containers/top-head'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch, useSelector as useSelectorRedux } from 'react-redux'; import shallowequal from 'shallowequal'; import articleActions from '../../store-redux/article/actions'; +import commentActions from '../../store-redux/comment/actions'; import HeadLayout from '../../components/head-layout'; +import Comment from '../../components/comment'; +import listToTree from '../../utils/list-to-tree'; +import treeToList from '../../utils/tree-to-list'; +import useSelector from '../../hooks/use-selector'; function Article() { const store = useStore(); @@ -23,12 +28,15 @@ function Article() { const params = useParams(); + const { t, lang } = useTranslate(); + useInit(() => { //store.actions.article.load(params.id); dispatch(articleActions.load(params.id)); - }, [params.id]); + dispatch(commentActions.load(params.id)); + }, [params.id, lang]); - const select = useSelector( + const select = useSelectorRedux( state => ({ article: state.article.data, waiting: state.article.waiting, @@ -36,7 +44,29 @@ function Article() { shallowequal, ); // Нужно указать функцию для сравнения свойства объекта, так как хуком вернули объект - const { t } = useTranslate(); + const selectComment = useSelectorRedux( + state => ({ + comment: state.comment.data, + postRes: state.comment.postRes, + waitingComment: state.comment.waiting, + }), + shallowequal, + ); + + const selectUser = useSelector(state => ({ + user: state.session.user, + })); + + const commentList = useMemo(() => { + if (!selectComment.waitingComment) { + return [ + ...treeToList(listToTree(selectComment.comment.items, '_id', 'article'), (item, level) => ({ + ...item, + level: level, + })), + ]; + } else return []; + }, [selectComment.comment, selectComment.postRes]); const callbacks = { // Добавление в корзину @@ -55,6 +85,7 @@ function Article() { + diff --git a/src/app/login/index.js b/src/app/login/index.js index 32b86daa4..41d10cb49 100644 --- a/src/app/login/index.js +++ b/src/app/login/index.js @@ -16,14 +16,14 @@ import HeadLayout from '../../components/head-layout'; import Form from '../../components/form'; function Login() { - const { t } = useTranslate(); + const { t, lang } = useTranslate(); const location = useLocation(); const navigate = useNavigate(); const store = useStore(); useInit(() => { store.actions.session.resetErrors(); - }); + }, [lang]); const select = useSelector(state => ({ waiting: state.session.waiting, diff --git a/src/app/main/index.js b/src/app/main/index.js index d9fb1c6c2..bc7e1f6b1 100644 --- a/src/app/main/index.js +++ b/src/app/main/index.js @@ -14,15 +14,15 @@ import HeadLayout from '../../components/head-layout'; function Main() { const store = useStore(); + const { t, lang } = useTranslate(); useInit( async () => { await Promise.all([store.actions.catalog.initParams(), store.actions.categories.load()]); }, - [], + [lang], true, ); - const { t } = useTranslate(); return ( <> diff --git a/src/app/profile/index.js b/src/app/profile/index.js index 166d127fb..d76c21548 100644 --- a/src/app/profile/index.js +++ b/src/app/profile/index.js @@ -15,17 +15,17 @@ import HeadLayout from '../../components/head-layout'; function Profile() { const store = useStore(); + const { t, lang } = useTranslate(); + useInit(() => { store.actions.profile.load(); - }, []); + }, [lang]); const select = useSelector(state => ({ profile: state.profile.data, waiting: state.profile.waiting, })); - const { t } = useTranslate(); - return ( <> diff --git a/src/components/article-card/index.js b/src/components/article-card/index.js index d636d9f88..f6cfe3007 100644 --- a/src/components/article-card/index.js +++ b/src/components/article-card/index.js @@ -7,28 +7,29 @@ import './style.css'; function ArticleCard(props) { const { article, onAdd = () => {}, t = text => text } = props; + const cn = bem('ArticleCard'); return (
{article.description}
-
Страна производитель:
+
{t("article-card.origin-country")}:
{article.madeIn?.title} ({article.madeIn?.code})
-
Категория:
+
{t("article-card.category")}:
{article.category?.title}
-
Год выпуска:
+
{t("article-card.release-year")}:
{article.edition}
-
Цена:
+
{t("article-card.price")}:
{numberFormat(article.price)} ₽
+ + ) : ( +
+ + {t('comment.login-able-to-comment')} +
+ )} + + ); +} + +CommentForm.propTypes = { + children: PropTypes.node, +}; + +export default memo(CommentForm); diff --git a/src/components/comment-form/style.css b/src/components/comment-form/style.css new file mode 100644 index 000000000..0d2a8d2df --- /dev/null +++ b/src/components/comment-form/style.css @@ -0,0 +1,29 @@ +.CommentForm { + display: flex; + flex-direction: column; + gap: 16px; +} + +.CommentForm-buttons { + display: flex; + gap: 16px; +} + +.CommentForm-textarea { + font-family: var(--font-family); + padding: 8px 12px; + color: var(--main-text); + &::placeholder { + color: var(--main-text); + } + &:focus { + outline: none; + } +} + +.CommentForm-link-login { + padding: 0; + color: var(--primary); + font-family: var(--font-family); + font-weight: 400; +} \ No newline at end of file diff --git a/src/components/comment-item/index.js b/src/components/comment-item/index.js new file mode 100644 index 000000000..dceaea69b --- /dev/null +++ b/src/components/comment-item/index.js @@ -0,0 +1,84 @@ +import { memo, useEffect, useRef, useState } from 'react'; +import { cn as bem } from '@bem-react/classname'; +import formatISODateToCustomString from '../../utils/format-iso-date'; + +import './style.css'; +import CommentList from '../comment-list'; +import CommentForm from '../comment-form'; +import { useDispatch } from 'react-redux'; +import commentFormActions from '../../store-redux/comment-form/actions'; +import { useSelector as useSelectorRedux } from 'react-redux'; +import useSelector from '../../hooks/use-selector'; +import shallowequal from 'shallowequal'; +import useTranslate from '../../hooks/use-translate'; + +function CommentItem({ item }) { + const cn = bem('CommentItem'); + const dispatch = useDispatch(); + const { t } = useTranslate(); + const ref = useRef(null); + const [shouldScroll, setShouldScroll] = useState(false); + + useEffect(() => { + if (shouldScroll) { + ref.current?.scrollIntoView({ behavior: 'smooth' }); + setShouldScroll(false); + } + }, [shouldScroll]); + + const select = useSelector(state => ({ + user: state.session.user, + exists: state.session.exists, + })); + + const selectCommentForm = useSelectorRedux( + state => ({ + place: state.commentForm.place, + }), + shallowequal, + ); + + const scrollToTarget = () => { + setShouldScroll(true); + }; + + return ( + <> +
+
+ + {item.author?.profile?.name || select.user.profile.name} + + {formatISODateToCustomString(item.dateCreate)} +
+

{item.text}

+ +
+ {item.children.length !== 0 && ( + ({ ...el, level: item.level + 1 }))} + curLevel={item.level + 1} + /> + )} + {selectCommentForm.place === item._id && ( +
+ +
+ )} + + ); +} + +Comment.propTypes = {}; + +export default memo(CommentItem); diff --git a/src/components/comment-item/style.css b/src/components/comment-item/style.css new file mode 100644 index 000000000..fc69e30a5 --- /dev/null +++ b/src/components/comment-item/style.css @@ -0,0 +1,43 @@ +.CommentItem { + display: flex; + flex-direction: column; + gap: 6px; +} + +.CommentItem-head { + display: flex; + gap: 12px; + font-size: 12px; + line-height: 18px; +} + +.CommentItem-name { + font-weight: 700; +} + +.CommentItem-date { + color: #666666; +} + +.CommentItem-text { + margin: 0; + font-size: 14px; + line-height: 20px; + overflow-wrap: break-word +} + +.CommentItem-button { + padding: 0; + width: fit-content; + color: var(--primary); + font-size: 12px; + line-height: 18px; +} + +.auth-user-name { + color: #4B5563; +} + +.CommentItem-form { + scroll-margin-top: 200px; +} \ No newline at end of file diff --git a/src/components/comment-list/index.js b/src/components/comment-list/index.js new file mode 100644 index 000000000..b94bc3a00 --- /dev/null +++ b/src/components/comment-list/index.js @@ -0,0 +1,23 @@ +import { memo, useState } from 'react'; +import { cn as bem } from '@bem-react/classname'; +import CommentItem from '../comment-item'; + +import './style.css'; + +function CommentList({ comments, curLevel = 0 }) { + const cn = bem('CommentList'); + + return ( +
6 ? 0 : 40}px` }} className={cn()}> + {comments + .filter(item => item.level === curLevel) + .map(item => ( + + ))} +
+ ); +} + +CommentList.propTypes = {}; + +export default memo(CommentList); diff --git a/src/components/comment-list/style.css b/src/components/comment-list/style.css new file mode 100644 index 000000000..4672ad887 --- /dev/null +++ b/src/components/comment-list/style.css @@ -0,0 +1,5 @@ +.CommentList { + display: flex; + flex-direction: column; + gap: 12px; +} \ No newline at end of file diff --git a/src/components/comment/index.js b/src/components/comment/index.js new file mode 100644 index 000000000..91063fe10 --- /dev/null +++ b/src/components/comment/index.js @@ -0,0 +1,39 @@ +import { memo, useState } from 'react'; +import PropTypes from 'prop-types'; +import { cn as bem } from '@bem-react/classname'; +import './style.css'; +import { Link } from 'react-router-dom'; +import CommentForm from '../comment-form'; +import useSelector from '../../hooks/use-selector'; +import { useSelector as useSelectorRedux } from 'react-redux'; +import CommentList from '../../components/comment-list'; +import shallowequal from 'shallowequal'; +import useTranslate from '../../hooks/use-translate'; + +function Comment({ count = 0, comments }) { + const cn = bem('Comment'); + + const {t} = useTranslate(); + + const selectCommentForm = useSelectorRedux( + state => ({ + place: state.commentForm.place, + }), + shallowequal, + ); + + return ( +
+ {/* ДОБАВИТЬ ДИНАМИЧЕСКОЕ ОТОБРАЖЕНИЕ КОЛЛИЧЕСТВА КОММЕНТАРИЕВ */} +

{t('comment')} ({count})

+ + {selectCommentForm.place === 'common' && } +
+ ); +} + +Comment.propTypes = { + children: PropTypes.node, +}; + +export default memo(Comment); diff --git a/src/components/comment/style.css b/src/components/comment/style.css new file mode 100644 index 000000000..43e9bc9ac --- /dev/null +++ b/src/components/comment/style.css @@ -0,0 +1,13 @@ +.Comment { + padding-top: 16px; + display: flex; + flex-direction: column; + gap: 24px; +} + +.Comment-title { + font-family: var(--second-font-family); + font-size: 24px; + line-height: 32px; +} + diff --git a/src/config.js b/src/config.js index 67c72f734..68d525fe8 100644 --- a/src/config.js +++ b/src/config.js @@ -18,6 +18,9 @@ const config = { api: { baseUrl: '', }, + i18n: { + baseLang: 'ru', + }, }; export default config; diff --git a/src/containers/catalog-filter/index.js b/src/containers/catalog-filter/index.js index 2eeca7564..94b95a2a7 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,17 +43,17 @@ function CatalogFilter() { // Варианты сортировок sort: useMemo( () => [ - { value: 'order', title: 'По порядку' }, - { value: 'title.ru', title: 'По именованию' }, - { value: '-price', title: 'Сначала дорогие' }, - { value: 'edition', title: 'Древние' }, + { value: 'order', title: t('sort.in-order') }, + { value: 'title.ru', title: t('sort.by-naming') }, + { value: '-price', title: t('sort.expensive-first') }, + { value: 'edition', title: t('sort.ancient') }, ], - [], + [lang], ), // Категории для фильтра categories: useMemo( () => [ - { value: '', title: 'Все' }, + { value: '', title: t('filter.all') }, ...treeToList(listToTree(select.categories), (item, level) => ({ value: item._id, title: '- '.repeat(level) + item.title, @@ -61,8 +63,6 @@ function CatalogFilter() { ), }; - const { t } = useTranslate(); - return (