From 06df0b5365004f2bc3ba80d2d99ff6cbe382e515 Mon Sep 17 00:00:00 2001 From: Viktoria Shlamova Date: Sat, 19 Apr 2025 20:35:36 +0400 Subject: [PATCH 1/2] lecture 5 --- api-examples/comments.http | 2 +- src/app/article/index.js | 31 ++++++++- src/app/login/index.js | 4 +- src/app/main/index.js | 4 +- src/app/profile/index.js | 6 +- src/components/article-card/index.js | 9 +-- src/components/comment-form/index.js | 69 +++++++++++++++++++ src/components/comment-form/style.css | 22 ++++++ src/components/comment-item/index.js | 54 +++++++++++++++ src/components/comment-item/style.css | 34 ++++++++++ src/components/comment-list/index.js | 23 +++++++ src/components/comment-list/style.css | 5 ++ src/components/comment/index.js | 52 ++++++++++++++ src/components/comment/style.css | 16 +++++ src/config.js | 3 + src/containers/catalog-filter/index.js | 18 ++--- src/hooks/use-translate.js | 26 ++++++- src/i18n/context.js | 30 --------- src/i18n/index.js | 54 +++++++++++++++ src/i18n/translate.js | 21 ------ src/i18n/translations/en.json | 34 +++++++++- src/i18n/translations/ru.json | 34 +++++++++- src/index.js | 5 +- src/services.js | 13 ++++ src/store-redux/comment-form/actions.js | 13 ++++ src/store-redux/comment-form/reducer.js | 19 ++++++ src/store-redux/comment/actions.js | 90 +++++++++++++++++++++++++ src/store-redux/comment/reducer.js | 37 ++++++++++ src/store-redux/exports.js | 2 + src/utils/format-iso-date.js | 32 +++++++++ src/utils/list-to-tree/index.js | 5 +- 31 files changed, 682 insertions(+), 85 deletions(-) create mode 100644 src/components/comment-form/index.js create mode 100644 src/components/comment-form/style.css create mode 100644 src/components/comment-item/index.js create mode 100644 src/components/comment-item/style.css create mode 100644 src/components/comment-list/index.js create mode 100644 src/components/comment-list/style.css create mode 100644 src/components/comment/index.js create mode 100644 src/components/comment/style.css delete mode 100644 src/i18n/context.js create mode 100644 src/i18n/index.js delete mode 100644 src/i18n/translate.js create mode 100644 src/store-redux/comment-form/actions.js create mode 100644 src/store-redux/comment-form/reducer.js create mode 100644 src/store-redux/comment/actions.js create mode 100644 src/store-redux/comment/reducer.js create mode 100644 src/utils/format-iso-date.js 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..e713886b6 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, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import useStore from '../../hooks/use-store'; import useTranslate from '../../hooks/use-translate'; @@ -13,7 +13,11 @@ import TopHead from '../../containers/top-head'; import { useDispatch, useSelector } 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'; function Article() { const store = useStore(); @@ -22,11 +26,14 @@ function Article() { // Параметры из пути /articles/:id 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( state => ({ @@ -36,7 +43,24 @@ function Article() { shallowequal, ); // Нужно указать функцию для сравнения свойства объекта, так как хуком вернули объект - const { t } = useTranslate(); + const selectComment = useSelector( + state => ({ + comment: state.comment.data, + waitingComment: state.comment.waiting, + }), + shallowequal, + ); + + const commentList = useMemo(() => { + if (!selectComment.waitingComment) { + return [ + ...treeToList(listToTree(selectComment.comment.items, '_id', 'article'), (item, level) => ({ + ...item, + level: level, + })), + ]; + } else return []; + }, [selectComment.comment]); const callbacks = { // Добавление в корзину @@ -55,6 +79,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)} ₽
+ + ); +} + +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..6a24273ae --- /dev/null +++ b/src/components/comment-form/style.css @@ -0,0 +1,22 @@ +.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; + } +} diff --git a/src/components/comment-item/index.js b/src/components/comment-item/index.js new file mode 100644 index 000000000..7e50d674b --- /dev/null +++ b/src/components/comment-item/index.js @@ -0,0 +1,54 @@ +import { memo, 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 } from 'react-redux'; +import shallowequal from 'shallowequal'; +import useTranslate from '../../hooks/use-translate'; + +function CommentItem({ item }) { + const cn = bem('CommentItem'); + const dispatch = useDispatch(); + const { t } = useTranslate(); + + const selectCommentForm = useSelector( + state => ({ + place: state.commentForm.place, + }), + shallowequal, + ); + + return ( + <> +
+
+ {item.author.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..a9e391915 --- /dev/null +++ b/src/components/comment-item/style.css @@ -0,0 +1,34 @@ +.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; +} + +.CommentItem-button { + padding: 0; + width: fit-content; + color: var(--primary); + font-size: 12px; + line-height: 18px; +} diff --git a/src/components/comment-list/index.js b/src/components/comment-list/index.js new file mode 100644 index 000000000..3218e7c07 --- /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 ( +
+ {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..fb49abfc9 --- /dev/null +++ b/src/components/comment/index.js @@ -0,0 +1,52 @@ +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, + ); + + const select = useSelector(state => ({ + exists: state.session.exists, + })); + + return ( +
+ {/* ДОБАВИТЬ ДИНАМИЧЕСКОЕ ОТОБРАЖЕНИЕ КОЛЛИЧЕСТВА КОММЕНТАРИЕВ */} +

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

+ + {select.exists ? ( + selectCommentForm.place.match(/^common$/) && + ) : ( +
+ + {t('comment.login')} + + {t('comment.login-able-to-comment')} +
+ )} +
+ ); +} + +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..07ee7702c --- /dev/null +++ b/src/components/comment/style.css @@ -0,0 +1,16 @@ +.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; +} + +.Comment-link-login { + color: var(--primary); +} \ No newline at end of file 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 ( -
-
+
+ {select.exists ? ( + <> + + {isCommonPlace ? t('comment-form.new-comment') : t('comment-form.new-reply')} + + +
+
+ + ) : ( +
+ + {t('comment.login-able-to-comment')} +
+ )}
); } diff --git a/src/components/comment-form/style.css b/src/components/comment-form/style.css index 6a24273ae..0d2a8d2df 100644 --- a/src/components/comment-form/style.css +++ b/src/components/comment-form/style.css @@ -20,3 +20,10 @@ 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 index 7e50d674b..dceaea69b 100644 --- a/src/components/comment-item/index.js +++ b/src/components/comment-item/index.js @@ -1,4 +1,4 @@ -import { memo, useState } from 'react'; +import { memo, useEffect, useRef, useState } from 'react'; import { cn as bem } from '@bem-react/classname'; import formatISODateToCustomString from '../../utils/format-iso-date'; @@ -7,7 +7,8 @@ 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 } from 'react-redux'; +import { useSelector as useSelectorRedux } from 'react-redux'; +import useSelector from '../../hooks/use-selector'; import shallowequal from 'shallowequal'; import useTranslate from '../../hooks/use-translate'; @@ -15,27 +16,52 @@ function CommentItem({ item }) { const cn = bem('CommentItem'); const dispatch = useDispatch(); const { t } = useTranslate(); + const ref = useRef(null); + const [shouldScroll, setShouldScroll] = useState(false); - const selectCommentForm = useSelector( + 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} + + {item.author?.profile?.name || select.user.profile.name} + {formatISODateToCustomString(item.dateCreate)}

{item.text}

{item.children.length !== 0 && ( @@ -44,7 +70,11 @@ function CommentItem({ item }) { curLevel={item.level + 1} /> )} - {selectCommentForm.place === item._id && } + {selectCommentForm.place === item._id && ( +
+ +
+ )} ); } diff --git a/src/components/comment-item/style.css b/src/components/comment-item/style.css index a9e391915..fc69e30a5 100644 --- a/src/components/comment-item/style.css +++ b/src/components/comment-item/style.css @@ -23,6 +23,7 @@ margin: 0; font-size: 14px; line-height: 20px; + overflow-wrap: break-word } .CommentItem-button { @@ -32,3 +33,11 @@ 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 index 3218e7c07..b94bc3a00 100644 --- a/src/components/comment-list/index.js +++ b/src/components/comment-list/index.js @@ -8,7 +8,7 @@ function CommentList({ comments, curLevel = 0 }) { const cn = bem('CommentList'); return ( -
+
6 ? 0 : 40}px` }} className={cn()}> {comments .filter(item => item.level === curLevel) .map(item => ( diff --git a/src/components/comment/index.js b/src/components/comment/index.js index fb49abfc9..91063fe10 100644 --- a/src/components/comment/index.js +++ b/src/components/comment/index.js @@ -22,25 +22,12 @@ function Comment({ count = 0, comments }) { shallowequal, ); - const select = useSelector(state => ({ - exists: state.session.exists, - })); - return (
{/* ДОБАВИТЬ ДИНАМИЧЕСКОЕ ОТОБРАЖЕНИЕ КОЛЛИЧЕСТВА КОММЕНТАРИЕВ */}

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

- {select.exists ? ( - selectCommentForm.place.match(/^common$/) && - ) : ( -
- - {t('comment.login')} - - {t('comment.login-able-to-comment')} -
- )} + {selectCommentForm.place === 'common' && }
); } diff --git a/src/components/comment/style.css b/src/components/comment/style.css index 07ee7702c..43e9bc9ac 100644 --- a/src/components/comment/style.css +++ b/src/components/comment/style.css @@ -11,6 +11,3 @@ line-height: 32px; } -.Comment-link-login { - color: var(--primary); -} \ No newline at end of file diff --git a/src/store-redux/comment/actions.js b/src/store-redux/comment/actions.js index 951b4b499..2728137d6 100644 --- a/src/store-redux/comment/actions.js +++ b/src/store-redux/comment/actions.js @@ -45,7 +45,7 @@ export default { }, body: requestBody, }); - console.log(res.data); + dispatch({ type: 'comment/post-success', payload: { data: res.data.result }}); } catch (e) { //Ошибка загрузки console.log(e); @@ -78,7 +78,7 @@ export default { }, body: requestBody, }); - console.log(res.data); + dispatch({ type: 'comment/reply-success', payload: { data: res.data.result }}); } catch (e) { //Ошибка загрузки console.log(e); diff --git a/src/store-redux/comment/reducer.js b/src/store-redux/comment/reducer.js index c6ef1199a..1a8189418 100644 --- a/src/store-redux/comment/reducer.js +++ b/src/store-redux/comment/reducer.js @@ -3,6 +3,7 @@ export const initialState = { data: [], waiting: true, // признак ожидания загрузки error: '', + postRes: {}, }; // Обработчик действий @@ -20,12 +21,32 @@ function reducer(state = initialState, action) { case 'comment/post-start': return { ...state, waiting: true }; + case 'comment/post-success': + return { + ...state, + data: { + ...state.data, + items: [ ...state.data.items, action.payload.data], + }, + waiting: false, + }; + case 'comment/post-error': return { ...state, waiting: false, error: action.payload.error }; case 'comment/reply-start': return { ...state, waiting: true }; + case 'comment/reply-success': + return { + ...state, + data: { + ...state.data, + items: [ ...state.data.items, action.payload.data], + }, + waiting: false, + }; + case 'comment/reply-error': return { ...state, waiting: false, error: action.payload.error }; default: