From 5451a5a918a2efad0f6ab2b03ee5ab5e3e819efb Mon Sep 17 00:00:00 2001 From: Ilya Pridanov Date: Sat, 19 Apr 2025 17:07:21 +0300 Subject: [PATCH 1/6] feat: ArticleCommentList --- .gitignore | 2 + .qodo/history.sqlite | Bin 0 -> 143360 bytes src/app/article/index.js | 2 + src/components/article-comment-list/index.js | 36 ++++++++++++++++++ src/components/article-comment-list/style.css | 9 +++++ src/components/side-layout/style.css | 2 +- 6 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 .qodo/history.sqlite create mode 100644 src/components/article-comment-list/index.js create mode 100644 src/components/article-comment-list/style.css diff --git a/.gitignore b/.gitignore index 6ebaad808..49b190e0e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ /.idea/ /node_modules/ /dist/ +/.godo/ +.qodo diff --git a/.qodo/history.sqlite b/.qodo/history.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..2d1348aef65a07081b0e5dceee100c873665278f GIT binary patch literal 143360 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 literal 0 HcmV?d00001 diff --git a/src/app/article/index.js b/src/app/article/index.js index 54f037b64..784c7007e 100644 --- a/src/app/article/index.js +++ b/src/app/article/index.js @@ -8,6 +8,7 @@ import Head from '../../components/head'; import Navigation from '../../containers/navigation'; import Spinner from '../../components/spinner'; import ArticleCard from '../../components/article-card'; +import ArticleCommentList from '../../components/article-comment-list'; import LocaleSelect from '../../containers/locale-select'; import TopHead from '../../containers/top-head'; import { useDispatch, useSelector } from 'react-redux'; @@ -55,6 +56,7 @@ function Article() { + diff --git a/src/components/article-comment-list/index.js b/src/components/article-comment-list/index.js new file mode 100644 index 000000000..c926f6a12 --- /dev/null +++ b/src/components/article-comment-list/index.js @@ -0,0 +1,36 @@ +import { memo, useCallback } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import useSelector from '../../hooks/use-selector'; +import Button from '../../components/button'; +import useStore from '../../hooks/use-store'; +import { cn as bem } from '@bem-react/classname'; +import './style.css'; + +function ArticleCommentList(props) { + const cn = bem('ArticleCommentList'); + const navigate = useNavigate(); + const location = useLocation(); + const store = useStore(); + + const select = useSelector(state => ({ + user: state.session.user, + exists: state.session.exists, + })); + + const callbacks = { + // Переход к авторизации + onSignIn: useCallback(() => { + navigate('/login', { state: { back: location.pathname } }); + }, [location.pathname]), + }; + + return ( +
+

Комментарии (0)

+ {select.exists ? '' :

+ ); +} + + +export default memo(ArticleCommentList); diff --git a/src/components/article-comment-list/style.css b/src/components/article-comment-list/style.css new file mode 100644 index 000000000..b2a45bf12 --- /dev/null +++ b/src/components/article-comment-list/style.css @@ -0,0 +1,9 @@ +.ArticleCommentList-stranger .Button { + display: inline; +} + +.ArticleCommentList-stranger button { + font-weight: 400; + color: var(--primary); + font-family: var(--font-family); +} diff --git a/src/components/side-layout/style.css b/src/components/side-layout/style.css index 763e99af2..b9d812064 100644 --- a/src/components/side-layout/style.css +++ b/src/components/side-layout/style.css @@ -1,6 +1,6 @@ .SideLayout { display: flex; - flex-wrap: nowrap; + flex-wrap: wrap; justify-content: flex-start; padding: 0; gap: 16px; From e5d3c0d33f405b44148dd21873b26bb3bf46c65c Mon Sep 17 00:00:00 2001 From: Ilya Pridanov Date: Sat, 19 Apr 2025 22:09:50 +0300 Subject: [PATCH 2/6] feat: NewComment --- src/components/article-comment-list/index.js | 20 ++++++++--- src/components/comment/index.js | 35 ++++++++++++++++++++ src/components/comment/style.css | 3 ++ src/components/new-comment/index.js | 22 ++++++++++++ src/components/new-comment/style.css | 21 ++++++++++++ 5 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 src/components/comment/index.js create mode 100644 src/components/comment/style.css create mode 100644 src/components/new-comment/index.js create mode 100644 src/components/new-comment/style.css diff --git a/src/components/article-comment-list/index.js b/src/components/article-comment-list/index.js index c926f6a12..7dbc77eb5 100644 --- a/src/components/article-comment-list/index.js +++ b/src/components/article-comment-list/index.js @@ -1,8 +1,10 @@ import { memo, useCallback } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import useSelector from '../../hooks/use-selector'; -import Button from '../../components/button'; -import useStore from '../../hooks/use-store'; +import Button from '../button'; +import NewComment from '../new-comment'; +import Comment from '../comment'; + import { cn as bem } from '@bem-react/classname'; import './style.css'; @@ -10,7 +12,6 @@ function ArticleCommentList(props) { const cn = bem('ArticleCommentList'); const navigate = useNavigate(); const location = useLocation(); - const store = useStore(); const select = useSelector(state => ({ user: state.session.user, @@ -27,7 +28,18 @@ function ArticleCommentList(props) { return (

Комментарии (0)

- {select.exists ? '' :

); } diff --git a/src/components/comment/index.js b/src/components/comment/index.js new file mode 100644 index 000000000..57e6877e9 --- /dev/null +++ b/src/components/comment/index.js @@ -0,0 +1,35 @@ +import { memo } from 'react'; +import Button from '../button'; +import NewComment from '../new-comment'; +import { cn as bem } from '@bem-react/classname'; +import PropTypes from 'prop-types'; +import './style.css'; + +function Comment({children}) { + const cn = bem('Comment'); + + return ( +
+
+
+

User №1

+ +
+

+ Текст комментрия о том какой товар. Комментатор может оставить большой комментрий и он весь показывается. Текст комментрия о том какой товар. +

+
+
+ {children} + +
+
+ ); +} + +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..abdf6eabf --- /dev/null +++ b/src/components/comment/style.css @@ -0,0 +1,3 @@ +.Comment-children { + padding-left: 40px; +} diff --git a/src/components/new-comment/index.js b/src/components/new-comment/index.js new file mode 100644 index 000000000..4080d52b0 --- /dev/null +++ b/src/components/new-comment/index.js @@ -0,0 +1,22 @@ +import { memo } from 'react'; +import Button from '../button'; +import { cn as bem } from '@bem-react/classname'; +import './style.css'; + +function NewComment() { + const cn = bem('NewComment'); + + return ( +
+

Новый комментарий

+ +
+
+
+ ); +} + + +export default memo(NewComment); diff --git a/src/components/new-comment/style.css b/src/components/new-comment/style.css new file mode 100644 index 000000000..f8801ff66 --- /dev/null +++ b/src/components/new-comment/style.css @@ -0,0 +1,21 @@ +.NewComment textarea { + resize: none; + margin-bottom: 16px; + padding: 8px 12px; + height: 88px; + display: block; + width: 100%; + + font-size: 14px; + line-height: 1.3; + font-family: var(--font-family); + + border-radius: 4px; + border: 1px solid var(--filter-border); +} + +.NewComment-row { + display: flex; + align-items: center; + gap: 16px; +} From dde4d3c3834842378f60c2b4c34c7e7988f64da6 Mon Sep 17 00:00:00 2001 From: Ilya Pridanov Date: Sun, 20 Apr 2025 19:00:12 +0300 Subject: [PATCH 3/6] feat: first-comment --- src/api/index.js | 59 ++++++++-- src/app/article/index.js | 61 +++++++---- src/components/article-comment-list/index.js | 99 +++++++++++++---- src/components/comment/index.js | 99 ++++++++++++++--- src/components/new-comment/index.js | 78 ++++++++++++-- src/store-redux/comments/actions.js | 53 +++++++++ src/store-redux/comments/reducer.js | 56 ++++++++++ src/store-redux/exports.js | 2 + src/store-redux/session/actions.js | 107 +++++++++++++++++++ src/store-redux/session/reducer.js | 34 ++++++ 10 files changed, 575 insertions(+), 73 deletions(-) create mode 100644 src/store-redux/comments/actions.js create mode 100644 src/store-redux/comments/reducer.js create mode 100644 src/store-redux/session/actions.js create mode 100644 src/store-redux/session/reducer.js diff --git a/src/api/index.js b/src/api/index.js index a5f073295..9947030ef 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -20,13 +20,22 @@ class APIService { * @returns {Promise<{}>} */ async request({ url, method = 'GET', headers = {}, ...options }) { - if (!url.match(/^(http|\/\/)/)) url = this.config.baseUrl + url; - const res = await fetch(url, { - method, - headers: { ...this.defaultHeaders, ...headers }, - ...options, - }); - return { data: await res.json(), status: res.status, headers: res.headers }; + const fullUrl = url.match(/^(http|\/\/)/) ? url : this.config.baseUrl + url; + console.log('API Request:', { fullUrl, method, headers }); + + try { + const res = await fetch(fullUrl, { + method, + headers: { ...this.defaultHeaders, ...headers }, + ...options + }); + const data = await res.json(); + console.log('API Response:', { status: res.status, data }); + return { data, status: res.status }; + } catch (e) { + console.error('API Error:', e); + throw e; + } } /** @@ -41,6 +50,42 @@ class APIService { delete this.defaultHeaders[name]; } } + + /** + * Загрузка комментариев + * @param parentId {String} ID родителя (товара или комментария) + */ + async getComments(parentId) { + const res = await this.request({ + url: `/api/v1/comments?fields=items(_id,text,dateCreate,author(profile(name)),parent(_id,_type),isDeleted),count&limit=*&search[parent]=${parentId}` + }); + // Явно возвращаем result или дефолтные значения + return { + items: res.data?.result?.items || [], + count: res.data?.result?.count || 0 + }; + } + + /** + * Отправка комментария + * @param parentId {String} ID родителя + * @param parentType {'article'|'comment'} Тип родителя + * @param text {String} Текст комментария + */ + async addComment(parentId, parentType, text) { + const res = await this.request({ + url: `/api/v1/comments`, + method: 'POST', + headers: { + 'X-Token': localStorage.getItem('token') + }, + body: JSON.stringify({ + text, + parent: { _id: parentId, _type: parentType } + }) + }); + return res.data?.result; // Учитываем вложенность + } } export default APIService; diff --git a/src/app/article/index.js b/src/app/article/index.js index 784c7007e..e67dee11c 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, useState, useEffect } from 'react'; import { useParams } from 'react-router-dom'; import useStore from '../../hooks/use-store'; import useTranslate from '../../hooks/use-translate'; @@ -11,36 +11,48 @@ import ArticleCard from '../../components/article-card'; import ArticleCommentList from '../../components/article-comment-list'; import LocaleSelect from '../../containers/locale-select'; import TopHead from '../../containers/top-head'; -import { useDispatch, useSelector } from 'react-redux'; -import shallowequal from 'shallowequal'; +import { useDispatch, useSelector, shallowEqual } from 'react-redux'; import articleActions from '../../store-redux/article/actions'; +import commentsActions from '../../store-redux/comments/actions'; import HeadLayout from '../../components/head-layout'; function Article() { const store = useStore(); - + const { id: articleId } = useParams(); + const [activeForm, setActiveForm] = useState(null); const dispatch = useDispatch(); - // Параметры из пути /articles/:id - const params = useParams(); + // Сессия и статья + const { data: article, waiting: articleWaiting } = useSelector( + state => state.article, + shallowEqual + ); + + const { exists } = useSelector(state => state.session); + const rootComments = useSelector(state => + state.comments.items[articleId] || [] + ); + + // Загрузка данных useInit(() => { - //store.actions.article.load(params.id); - dispatch(articleActions.load(params.id)); - }, [params.id]); + dispatch(articleActions.load(articleId)); + dispatch(commentsActions.load(articleId)); // Убрали проверку sessionWaiting + }, [articleId]); + console.log('useSelector Article', useSelector(state => state)); - const select = useSelector( - state => ({ - article: state.article.data, - waiting: state.article.waiting, - }), - shallowequal, - ); // Нужно указать функцию для сравнения свойства объекта, так как хуком вернули объект + const handleAddComment = async (parentId, parentType, text) => { + try { + await dispatch(commentsActions.add(parentId, parentType, text)); + setActiveForm(null); + } catch (e) { + console.error('Ошибка добавления комментария:', e); + } + }; const { t } = useTranslate(); const callbacks = { - // Добавление в корзину addToBasket: useCallback(_id => store.actions.basket.addToBasket(_id), [store]), }; @@ -49,14 +61,21 @@ function Article() { - + - - - + + + diff --git a/src/components/article-comment-list/index.js b/src/components/article-comment-list/index.js index 7dbc77eb5..3bd15b710 100644 --- a/src/components/article-comment-list/index.js +++ b/src/components/article-comment-list/index.js @@ -1,48 +1,103 @@ -import { memo, useCallback } from 'react'; +import { memo, useCallback, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; -import useSelector from '../../hooks/use-selector'; +import { useDispatch, useSelector } from 'react-redux'; import Button from '../button'; import NewComment from '../new-comment'; import Comment from '../comment'; - +import commentsActions from '../../store-redux/comments/actions'; +import useInit from '../../hooks/use-init'; import { cn as bem } from '@bem-react/classname'; import './style.css'; -function ArticleCommentList(props) { +function ArticleCommentList({ articleId }) { const cn = bem('ArticleCommentList'); const navigate = useNavigate(); const location = useLocation(); + const dispatch = useDispatch(); + const [activeForm, setActiveForm] = useState(null); + + // Получаем данные из хранилища + const { exists } = useSelector(state => state.session); + const rootComments = useSelector(state => state.comments.items[articleId] || []); + // console.log('Current comments state:', { + // items: state.comments.items, + // counts: state.comments.counts, + // loading: state.comments.loadingParents + // }); + const isLoading = useSelector(state => state.comments.loadingParents.includes(articleId)); + const commentsCount = useSelector(state => state.comments.counts[articleId] || 0); - const select = useSelector(state => ({ - user: state.session.user, - exists: state.session.exists, - })); + // Загрузка комментариев + useInit(() => { + dispatch(commentsActions.load(articleId)); + }, [articleId]); const callbacks = { // Переход к авторизации onSignIn: useCallback(() => { navigate('/login', { state: { back: location.pathname } }); }, [location.pathname]), + + // Отправка комментария (добавляем тип родителя) + onSubmit: async (parentId, parentType, text) => { + try { + await dispatch(commentsActions.add(parentId, parentType, text)); + setActiveForm(null); + } catch (e) { + console.error('Ошибка при отправке комментария:', e); + } + }, + + // Открытие формы ответа + onReply: useCallback((commentId) => setActiveForm(commentId), []), }; return (
-

Комментарии (0)

- - - - - - - - { - select.exists ? - : -

); } - export default memo(ArticleCommentList); diff --git a/src/components/comment/index.js b/src/components/comment/index.js index 57e6877e9..af0833b0a 100644 --- a/src/components/comment/index.js +++ b/src/components/comment/index.js @@ -5,31 +5,102 @@ import { cn as bem } from '@bem-react/classname'; import PropTypes from 'prop-types'; import './style.css'; -function Comment({children}) { +function Comment({ + comment, + onReply, + isFormOpen, + onSubmit, + onCancel, + childComments = [] // Добавляем проп для вложенных комментариев +}) { const cn = bem('Comment'); + // Форматирование даты + const formattedDate = new Date(comment.dateCreate).toLocaleString('ru-RU', { + day: 'numeric', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + return (
-
-
-

User №1

- +
+
+

{comment.author.profile.name}

+
-

- Текст комментрия о том какой товар. Комментатор может оставить большой комментрий и он весь показывается. Текст комментрия о том какой товар. -

-
-
- {children} - -
+ + {/* Вложенные комментарии */} + {childComments.length > 0 && ( +
+ {childComments.map(child => ( + onSubmit(child._id, 'comment', text)} + onCancel={onCancel} + /> + ))} +
+ )} + + {/* Форма ответа */} + {isFormOpen && ( +
+ onSubmit(comment._id, 'comment', text)} + onCancel={onCancel} + /> +
+ )}
); } Comment.propTypes = { - children: PropTypes.node, + comment: PropTypes.shape({ + _id: PropTypes.string.isRequired, + text: PropTypes.string.isRequired, + dateCreate: PropTypes.string.isRequired, + author: PropTypes.shape({ + profile: PropTypes.shape({ + name: PropTypes.string.isRequired + }).isRequired + }).isRequired, + isDeleted: PropTypes.bool + }).isRequired, + onReply: PropTypes.func, + isFormOpen: PropTypes.bool, + onSubmit: PropTypes.func, + onCancel: PropTypes.func, + childComments: PropTypes.arrayOf(PropTypes.object) }; export default memo(Comment); diff --git a/src/components/new-comment/index.js b/src/components/new-comment/index.js index 4080d52b0..95ffb16ca 100644 --- a/src/components/new-comment/index.js +++ b/src/components/new-comment/index.js @@ -1,22 +1,82 @@ -import { memo } from 'react'; -import Button from '../button'; +import { memo, useState, useEffect, useRef } from 'react'; import { cn as bem } from '@bem-react/classname'; +import PropTypes from 'prop-types'; +import Button from '../button'; import './style.css'; -function NewComment() { +function NewComment({ onSubmit, onCancel, autoFocus = false }) { const cn = bem('NewComment'); + const [text, setText] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const textareaRef = useRef(null); + + // Автофокус при монтировании + useEffect(() => { + if (autoFocus && textareaRef.current) { + textareaRef.current.focus(); + } + }, [autoFocus]); + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!text.trim() || isSubmitting) return; + + setIsSubmitting(true); + try { + await onSubmit(text); + setText(''); + } catch (error) { + console.error('Ошибка при отправке комментария:', error); + } finally { + setIsSubmitting(false); + } + }; + + const handleCancel = () => { + setText(''); + onCancel?.(); + }; return ( -
-

Новый комментарий

- -
-