From 89f828645e7abb128ead72b7098d3504651986ed Mon Sep 17 00:00:00 2001 From: Elena Date: Tue, 8 Oct 2024 19:50:47 +0400 Subject: [PATCH 01/18] =?UTF-8?q?feat:=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=20redux=20=D0=B4=D0=BB=D1=8F=20=D0=B7=D0=B0=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=81=D0=B0=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=D1=80=D0=B8=D0=B5=D0=B2,=20=D1=81=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D1=8B=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D1=8B=20Comments=20=D0=B8=20Comment,=20=D0=B8?= =?UTF-8?q?=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=B2=20Article-card=20=D0=B8=20=D0=B2=20Articl?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/article/index.js | 18 ++++++++---- src/components/article-card/index.js | 5 +++- src/components/comment/index.js | 28 +++++++++++++++++++ src/components/comment/style.css | 25 +++++++++++++++++ src/components/comments/index.js | 42 ++++++++++++++++++++++++++++ src/components/comments/style.css | 18 ++++++++++++ src/components/page-layout/style.css | 12 ++++++++ src/i18n/translations/en.json | 3 +- src/i18n/translations/ru.json | 3 +- src/store-redux/comments/actions.js | 24 ++++++++++++++++ src/store-redux/comments/reducer.js | 25 +++++++++++++++++ src/store-redux/exports.js | 1 + 12 files changed, 195 insertions(+), 9 deletions(-) create mode 100644 src/components/comment/index.js create mode 100644 src/components/comment/style.css create mode 100644 src/components/comments/index.js create mode 100644 src/components/comments/style.css create mode 100644 src/store-redux/comments/actions.js create mode 100644 src/store-redux/comments/reducer.js diff --git a/src/app/article/index.js b/src/app/article/index.js index 56859bc76..c5b399990 100644 --- a/src/app/article/index.js +++ b/src/app/article/index.js @@ -13,6 +13,8 @@ import TopHead from '../../containers/top-head'; import { useDispatch, useSelector } from 'react-redux'; import shallowequal from 'shallowequal'; import articleActions from '../../store-redux/article/actions'; +import commentsActions from '../../store-redux/comments/actions'; +import comments from "../../components/comments"; function Article() { const store = useStore(); @@ -22,15 +24,19 @@ function Article() { const params = useParams(); - useInit(() => { - //store.actions.article.load(params.id); - dispatch(articleActions.load(params.id)); - }, [params.id]); + useInit( + async () => { + await Promise.all([dispatch(articleActions.load(params.id)), dispatch(commentsActions.load(params.id))]); + }, + [params.id], + true, + ); const select = useSelector( state => ({ article: state.article.data, - waiting: state.article.waiting, + comments: state.comments.data, + waiting: state.article.waiting || state.comments.waiting, }), shallowequal, ); // Нужно указать функцию для сравнения свойства объекта, так как хуком вернули объект @@ -50,7 +56,7 @@ function Article() { - + ); diff --git a/src/components/article-card/index.js b/src/components/article-card/index.js index e68016256..608fb0a17 100644 --- a/src/components/article-card/index.js +++ b/src/components/article-card/index.js @@ -3,9 +3,10 @@ import PropTypes from 'prop-types'; import { cn as bem } from '@bem-react/classname'; import numberFormat from '../../utils/number-format'; import './style.css'; +import Comments from '../../components/comments'; function ArticleCard(props) { - const { article, onAdd = () => {}, t = text => text } = props; + const { article, onAdd = () => {}, t = text => text, comments } = props; const cn = bem('ArticleCard'); return (
@@ -29,6 +30,7 @@ function ArticleCard(props) {
{numberFormat(article.price)} ₽
+ ); } @@ -44,6 +46,7 @@ ArticleCard.propTypes = { }).isRequired, onAdd: PropTypes.func, t: PropTypes.func, + comments: PropTypes.array }; export default memo(ArticleCard); diff --git a/src/components/comment/index.js b/src/components/comment/index.js new file mode 100644 index 000000000..0fd1bebc7 --- /dev/null +++ b/src/components/comment/index.js @@ -0,0 +1,28 @@ +import { memo } from 'react'; +import PropTypes from 'prop-types'; +import { cn as bem } from '@bem-react/classname'; +import useTranslate from "../../hooks/use-translate"; +import './style.css'; + +function Comment({ name, dateCreate, key, text }) { + const cn = bem('Comment'); + const { t } = useTranslate(); + + return ( +
+
+ {name} + {dateCreate} +
+
{text}
+
Ответить
+
+ ); +} + +Comment.propTypes = { + // sum: PropTypes.number, + // t: PropTypes.func, +}; + +export default memo(Comment); diff --git a/src/components/comment/style.css b/src/components/comment/style.css new file mode 100644 index 000000000..f6443eea0 --- /dev/null +++ b/src/components/comment/style.css @@ -0,0 +1,25 @@ +.Comment { + display: flex; + flex-direction: column; + align-items: flex-start; + font-size: 12px; + padding-top: 25px; +} + +.Comment-author { + font-weight: bold; + padding-right: 10px; +} + +.Comment-dateCreate { + color: #666666; +} + +.Comment-text { + padding: 10px 0; +} + +.Comment-action { + color: #0087E9; +} + diff --git a/src/components/comments/index.js b/src/components/comments/index.js new file mode 100644 index 000000000..80dabb7d4 --- /dev/null +++ b/src/components/comments/index.js @@ -0,0 +1,42 @@ +import { memo } from 'react'; +import PropTypes from 'prop-types'; +import { cn as bem } from '@bem-react/classname'; +import useTranslate from '../../hooks/use-translate'; +import './style.css'; +import { Link } from 'react-router-dom'; +import Comment from '../comment'; + +function Comments(props) { + const cn = bem('Comments'); + const { t } = useTranslate(); + + return ( +
+
+ {t('comments.title')} + {` (${props.comments.count || 0})`} +
+
+ {props.comments.items && + props.comments.items.map(item => ( + + ))} +
+

+ Войдите, чтобы иметь возможность ответить. Отмена +

+
+ ); +} + +Comments.propTypes = { + // sum: PropTypes.number, + // t: PropTypes.func, +}; + +export default memo(Comments); diff --git a/src/components/comments/style.css b/src/components/comments/style.css new file mode 100644 index 000000000..8eb79e4e9 --- /dev/null +++ b/src/components/comments/style.css @@ -0,0 +1,18 @@ +.Comments { + padding-top: 50px; +} + +.Comments-title { + /*text-align: right;*/ + font-size: 24px; + line-height: 28px; + white-space: nowrap; +} + +.Comments-body { + display: flex; + flex-direction: column; + align-items: flex-start; +} + + diff --git a/src/components/page-layout/style.css b/src/components/page-layout/style.css index 18bcd9d78..890eae2f7 100644 --- a/src/components/page-layout/style.css +++ b/src/components/page-layout/style.css @@ -5,6 +5,18 @@ body { font-family: sans-serif; } +button { + min-width: 76px; + background-color: #EFEFEF; + border: 1px solid #767676; + border-radius: 2px; + color: black; + padding: 3px 8px 2px 9px; + box-sizing: border-box; + font-size: 13px; + cursor: pointer; +} + .PageLayout { max-width: 1024px; min-height: 100vh; diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json index 171a28882..93a158978 100644 --- a/src/i18n/translations/en.json +++ b/src/i18n/translations/en.json @@ -20,5 +20,6 @@ "auth.password": "Password", "auth.signIn": "Sign in", "session.signIn": "Sign In", - "session.signOut": "Sign Out" + "session.signOut": "Sign Out", + "comments.title": "Comments" } diff --git a/src/i18n/translations/ru.json b/src/i18n/translations/ru.json index b2a32bfe1..2e997ad86 100644 --- a/src/i18n/translations/ru.json +++ b/src/i18n/translations/ru.json @@ -22,5 +22,6 @@ "auth.password": "Пароль", "auth.signIn": "Войти", "session.signIn": "Вход", - "session.signOut": "Выход" + "session.signOut": "Выход", + "comments.title": "Комментарии" } diff --git a/src/store-redux/comments/actions.js b/src/store-redux/comments/actions.js new file mode 100644 index 000000000..9dc2c4894 --- /dev/null +++ b/src/store-redux/comments/actions.js @@ -0,0 +1,24 @@ +export default { + /** + * Загрузка комментариев для товара + * @param id + * @return {Function} + */ + load: id => { + return async (dispatch, getState, services) => { + // Сброс текущего стека комментариев и установка признака ожидания загрузки + dispatch({ type: 'comments/load-start' }); + + try { + const res = await services.api.request({ + url: `/api/v1/comments?fields=items(_id,text,dateCreate,author(profile(name)),parent(_id,_type),isDeleted),count&limit=*&search[parent]=${id}`, + }); + // Комментарии загружены успешно + dispatch({ type: 'comments/load-success', payload: { data: res.data.result } }); + } catch (e) { + //Ошибка загрузки + dispatch({ type: 'comments/load-error' }); + } + }; + }, +}; diff --git a/src/store-redux/comments/reducer.js b/src/store-redux/comments/reducer.js new file mode 100644 index 000000000..fd4cc9621 --- /dev/null +++ b/src/store-redux/comments/reducer.js @@ -0,0 +1,25 @@ +// Начальное состояние +export const initialState = { + data: {}, + waiting: false, // признак ожидания загрузки +}; + +// Обработчик действий +function reducer(state = initialState, action) { + switch (action.type) { + case 'comments/load-start': + return { ...state, data: {}, waiting: true }; + + case 'comments/load-success': + return { ...state, data: action.payload.data, waiting: false }; + + case 'comments/load-error': + return { ...state, data: {}, waiting: false }; //@todo текст ошибки сохранять? + + default: + // Нет изменений + return state; + } +} + +export default reducer; diff --git a/src/store-redux/exports.js b/src/store-redux/exports.js index 1a0a3d742..c7f1c588b 100644 --- a/src/store-redux/exports.js +++ b/src/store-redux/exports.js @@ -1,2 +1,3 @@ export { default as article } from './article/reducer'; export { default as modals } from './modals/reducer'; +export { default as comments } from './comments/reducer'; From 49d62e656dd3575d366f6906a06fd590be8f5858 Mon Sep 17 00:00:00 2001 From: Elena Date: Wed, 9 Oct 2024 14:20:10 +0400 Subject: [PATCH 02/18] =?UTF-8?q?feat:=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B0=D1=80=D0=B8=D0=B8=20=D1=80=D0=B5=D0=BD=D0=B4?= =?UTF-8?q?=D0=B5=D1=80=D1=8F=D1=82=D1=81=D1=8F=20=D1=81=D0=BE=D0=B3=D0=BB?= =?UTF-8?q?=D0=B0=D1=81=D0=BD=D0=BE=20=D0=B8=D0=B5=D1=80=D0=B0=D1=80=D1=85?= =?UTF-8?q?=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/article/index.js | 52 ++++++++++++++++++++++++++++--- src/components/comments/index.js | 24 ++++++++------ src/components/comments/style.css | 9 ++++++ 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/src/app/article/index.js b/src/app/article/index.js index c5b399990..0646ed711 100644 --- a/src/app/article/index.js +++ b/src/app/article/index.js @@ -14,7 +14,8 @@ import { useDispatch, useSelector } from 'react-redux'; import shallowequal from 'shallowequal'; import articleActions from '../../store-redux/article/actions'; import commentsActions from '../../store-redux/comments/actions'; -import comments from "../../components/comments"; +import treeToList from '../../utils/tree-to-list'; +import listToTree from '../../utils/list-to-tree'; function Article() { const store = useStore(); @@ -26,7 +27,10 @@ function Article() { useInit( async () => { - await Promise.all([dispatch(articleActions.load(params.id)), dispatch(commentsActions.load(params.id))]); + await Promise.all([ + dispatch(articleActions.load(params.id)), + dispatch(commentsActions.load(params.id)), + ]); }, [params.id], true, @@ -36,11 +40,46 @@ function Article() { state => ({ article: state.article.data, comments: state.comments.data, - waiting: state.article.waiting || state.comments.waiting, + waiting: state.article.waiting && state.comments.waiting, }), shallowequal, ); // Нужно указать функцию для сравнения свойства объекта, так как хуком вернули объект + // Формируем массив комментариев для рендера + const transformedComments = useMemo(() => { + // если загрузились комментарии из апи + if (select.comments && select.comments?.items?.length > 0) { + // сортируем их по родителям + const tree = listToTree(select.comments.items); + + // убираем объект верхнего уровня из результата (иначе он тоже пушит в массив для рендера объект со всеми свойствами + // равными undefined и еще ломает иерархию вложенности) + const flatItems = tree.flatMap(item => { + return [...item.children]; + }); + // формируем массив для рендера комментариев согласно их иерархии + const items = treeToList(flatItems, (item, level) => { + return { + _id: item._id, + text: item.text, + dateCreate: item.dateCreate, + type: item?.parent?._type, + level: level, + author: item?.author?.profile?.name || 'Unknown', + isDeleted: item.isDeleted, + }; + }); + + return { + ...select.comments, + items: items, + }; + } + + return select.comments; + + }, [select.comments]); + const { t } = useTranslate(); const callbacks = { @@ -56,7 +95,12 @@ function Article() { - + ); diff --git a/src/components/comments/index.js b/src/components/comments/index.js index 80dabb7d4..75970ab62 100644 --- a/src/components/comments/index.js +++ b/src/components/comments/index.js @@ -10,6 +10,20 @@ function Comments(props) { const cn = bem('Comments'); const { t } = useTranslate(); + const renderComments = (items) => { + return items.map(item => { + return
+ +
+ }) + } + return (
@@ -17,15 +31,7 @@ function Comments(props) { {` (${props.comments.count || 0})`}
- {props.comments.items && - props.comments.items.map(item => ( - - ))} + {props.comments.items && renderComments(props.comments.items)}

Войдите, чтобы иметь возможность ответить. Отмена diff --git a/src/components/comments/style.css b/src/components/comments/style.css index 8eb79e4e9..76c78190d 100644 --- a/src/components/comments/style.css +++ b/src/components/comments/style.css @@ -15,4 +15,13 @@ align-items: flex-start; } +.Comments-item { + +} + +.Comments-nestedItem { + + +} + From 932990fdf56caf4cc429505c2a45bb2e4838ea57 Mon Sep 17 00:00:00 2001 From: Elena Date: Wed, 9 Oct 2024 14:39:34 +0400 Subject: [PATCH 03/18] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=83=D1=82=D0=B8=D0=BB=D0=B8=D1=82?= =?UTF-8?q?=D0=B0=20=D0=B4=D0=BB=D1=8F=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B4?= =?UTF-8?q?=D0=B0=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/comment/index.js | 3 ++- src/utils/date-format.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 src/utils/date-format.js diff --git a/src/components/comment/index.js b/src/components/comment/index.js index 0fd1bebc7..90358ecb5 100644 --- a/src/components/comment/index.js +++ b/src/components/comment/index.js @@ -2,6 +2,7 @@ import { memo } from 'react'; import PropTypes from 'prop-types'; import { cn as bem } from '@bem-react/classname'; import useTranslate from "../../hooks/use-translate"; +import dateFormat from "../../utils/date-format"; import './style.css'; function Comment({ name, dateCreate, key, text }) { @@ -12,7 +13,7 @@ function Comment({ name, dateCreate, key, text }) {

{name} - {dateCreate} + {dateFormat(dateCreate).fullDate} в {dateFormat(dateCreate).time}
{text}
Ответить
diff --git a/src/utils/date-format.js b/src/utils/date-format.js new file mode 100644 index 000000000..6eee9e474 --- /dev/null +++ b/src/utils/date-format.js @@ -0,0 +1,31 @@ +/** + * Получение данных даты из формата ISO + * @param value {String} + * @returns {Object} + */ + +const months = [ + 'января', + 'февраля', + 'марта', + 'апреля', + 'мая', + 'июня', + 'июля', + 'августа', + 'сентября', + 'октября', + 'ноября', + 'декабря', +]; + +export default function dateFormat(value) { + const date = new Date(value); + const fullDate = `${date.getDate().toString().padStart(2, '0')} ${months[date.getMonth()]} ${date.getFullYear()} `; + const time = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`; + + return { + fullDate, + time, + }; +} From 5d893281fcafadee5b0b4e6ed7668af7882cb43a Mon Sep 17 00:00:00 2001 From: Elena Date: Wed, 9 Oct 2024 18:27:56 +0400 Subject: [PATCH 04/18] =?UTF-8?q?feat:=20=D1=80=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=BE=20=D0=BE=D0=BA=D0=BD=D0=BE=20?= =?UTF-8?q?=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D0=B0=20=D0=BD=D0=B0=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=80=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/article/index.js | 6 ++++ src/components/article-card/index.js | 16 +++++++-- src/components/comment/index.js | 32 ++++++++++++++--- src/components/comment/style.css | 28 ++++++++++++++- src/components/comments/index.js | 50 +++++++++++++++++++-------- src/components/comments/style.css | 23 ++++++++---- src/components/input/style.css | 4 +++ src/components/reply-window/index.js | 32 +++++++++++++++++ src/components/reply-window/style.css | 14 ++++++++ src/store-redux/comment/actions.js | 31 +++++++++++++++++ src/store-redux/comment/reducer.js | 31 +++++++++++++++++ src/store-redux/exports.js | 1 + 12 files changed, 238 insertions(+), 30 deletions(-) create mode 100644 src/components/reply-window/index.js create mode 100644 src/components/reply-window/style.css create mode 100644 src/store-redux/comment/actions.js create mode 100644 src/store-redux/comment/reducer.js diff --git a/src/app/article/index.js b/src/app/article/index.js index 0646ed711..7be1b48ce 100644 --- a/src/app/article/index.js +++ b/src/app/article/index.js @@ -14,6 +14,7 @@ import { useDispatch, useSelector } from 'react-redux'; import shallowequal from 'shallowequal'; import articleActions from '../../store-redux/article/actions'; import commentsActions from '../../store-redux/comments/actions'; +//import commentActions from '../../store-redux/comment/actions'; import treeToList from '../../utils/tree-to-list'; import listToTree from '../../utils/list-to-tree'; @@ -41,6 +42,7 @@ function Article() { article: state.article.data, comments: state.comments.data, waiting: state.article.waiting && state.comments.waiting, + session: store.getState().session.exists }), shallowequal, ); // Нужно указать функцию для сравнения свойства объекта, так как хуком вернули объект @@ -85,6 +87,8 @@ function Article() { const callbacks = { // Добавление в корзину addToBasket: useCallback(_id => store.actions.basket.addToBasket(_id), [store]), + // отправка коммента в ответ на коммент + //replyComment: useCallback(() => dispatch(commentActions.reply(params)), []) }; return ( @@ -98,8 +102,10 @@ function Article() { diff --git a/src/components/article-card/index.js b/src/components/article-card/index.js index 608fb0a17..c7d6fe4d3 100644 --- a/src/components/article-card/index.js +++ b/src/components/article-card/index.js @@ -6,7 +6,14 @@ import './style.css'; import Comments from '../../components/comments'; function ArticleCard(props) { - const { article, onAdd = () => {}, t = text => text, comments } = props; + const { + article, + onAdd = () => {}, + t = text => text, + comments, + session, + } = props; + const cn = bem('ArticleCard'); return (
@@ -30,7 +37,10 @@ function ArticleCard(props) {
{numberFormat(article.price)} ₽
- +
); } @@ -46,7 +56,7 @@ ArticleCard.propTypes = { }).isRequired, onAdd: PropTypes.func, t: PropTypes.func, - comments: PropTypes.array + comments: PropTypes.array, }; export default memo(ArticleCard); diff --git a/src/components/comment/index.js b/src/components/comment/index.js index 90358ecb5..11e5e42b2 100644 --- a/src/components/comment/index.js +++ b/src/components/comment/index.js @@ -1,11 +1,13 @@ import { memo } from 'react'; import PropTypes from 'prop-types'; import { cn as bem } from '@bem-react/classname'; -import useTranslate from "../../hooks/use-translate"; -import dateFormat from "../../utils/date-format"; +import useTranslate from '../../hooks/use-translate'; +import dateFormat from '../../utils/date-format'; import './style.css'; +import { Link } from 'react-router-dom'; +import ReplyWindow from '../reply-window'; -function Comment({ name, dateCreate, key, text }) { +function Comment({ id, name, dateCreate, text, session, isVisible, onToggleReply }) { const cn = bem('Comment'); const { t } = useTranslate(); @@ -13,10 +15,30 @@ function Comment({ name, dateCreate, key, text }) {
{name} - {dateFormat(dateCreate).fullDate} в {dateFormat(dateCreate).time} + + {dateFormat(dateCreate).fullDate} в {dateFormat(dateCreate).time} +
{text}
-
Ответить
+
onToggleReply(id)}> + Ответить +
+ {isVisible && + (session ? ( +
+ +
+ ) : ( +
+

+ Войдите, чтобы иметь возможность ответить.{' '} + Отмена +

+
+ ))}
); } diff --git a/src/components/comment/style.css b/src/components/comment/style.css index f6443eea0..32748bedd 100644 --- a/src/components/comment/style.css +++ b/src/components/comment/style.css @@ -3,7 +3,9 @@ flex-direction: column; align-items: flex-start; font-size: 12px; - padding-top: 25px; + padding-bottom: 30px; + width: 100%; + flex: 1; } .Comment-author { @@ -23,3 +25,27 @@ color: #0087E9; } +.Comment-reply { + padding-top: 30px; + width: 100%; + box-sizing: border-box; +} + +.Comment-link { + margin: 0; + padding: 0; + font-size: 16px; +} + +.Comment-link a { + margin: 0; + padding: 0; + color: #0087E9; +} + +.Comment-link span { + color: #666666; + text-decoration: underline; +} + + diff --git a/src/components/comments/index.js b/src/components/comments/index.js index 75970ab62..ebbfcc61f 100644 --- a/src/components/comments/index.js +++ b/src/components/comments/index.js @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import { memo, useState } from 'react'; import PropTypes from 'prop-types'; import { cn as bem } from '@bem-react/classname'; import useTranslate from '../../hooks/use-translate'; @@ -10,19 +10,35 @@ function Comments(props) { const cn = bem('Comments'); const { t } = useTranslate(); - const renderComments = (items) => { + const [visibleCommentId, setVisibleCommentId] = useState(null); // Хранит id видимого комментария + + const toggleReplyWindow = (id) => { + setVisibleCommentId((prevId) => (prevId === id ? null : id)); // Переключает видимость окна ответа + }; + + const renderComments = items => { return items.map(item => { - return
- -
- }) - } + style={{ paddingLeft: `${item.level * 30}px` }} + > + toggleReplyWindow(item.dateCreate)} + /> +
+ ); + }); + }; return (
@@ -33,9 +49,13 @@ function Comments(props) {
{props.comments.items && renderComments(props.comments.items)}
-

- Войдите, чтобы иметь возможность ответить. Отмена -

+ {visibleCommentId === null && ( +
+

+ Войдите, чтобы иметь возможность комментировать +

+
+ )}
); } diff --git a/src/components/comments/style.css b/src/components/comments/style.css index 76c78190d..144c27ae3 100644 --- a/src/components/comments/style.css +++ b/src/components/comments/style.css @@ -1,27 +1,38 @@ .Comments { padding-top: 50px; + display: flex; + flex-direction: column; +} + +.Comments-item { + flex: 1; + width: 100%; + box-sizing: border-box; } .Comments-title { - /*text-align: right;*/ font-size: 24px; line-height: 28px; white-space: nowrap; + padding-bottom: 25px; } .Comments-body { display: flex; flex-direction: column; align-items: flex-start; + width: 100%; } -.Comments-item { - +.Comments-link { + margin: 0; + padding: 0; } -.Comments-nestedItem { - - +.Comments-link a { + margin: 0; + padding: 0; + color: #0087E9; } diff --git a/src/components/input/style.css b/src/components/input/style.css index badf5a782..4aa603fd5 100644 --- a/src/components/input/style.css +++ b/src/components/input/style.css @@ -6,3 +6,7 @@ .Input_theme_big { width: 300px; } + +.Input_theme_fullWidth { + width: 100%; +} diff --git a/src/components/reply-window/index.js b/src/components/reply-window/index.js new file mode 100644 index 000000000..615c0f4b3 --- /dev/null +++ b/src/components/reply-window/index.js @@ -0,0 +1,32 @@ +import { memo } from 'react'; +import PropTypes from 'prop-types'; +import { cn as bem } from '@bem-react/classname'; +import './style.css'; +import Input from '../input'; + +function ReplyWindow({ name, theme, onToggleReply }) { + const cn = bem('ReplyWindow'); + + return ( +
+
{}} className={cn('form')}> + Новый ответ + {}} + /> +
+ + +
+
+
+ ); +} + +ReplyWindow.propTypes = { + data: PropTypes.object.isRequired, +}; + +export default memo(ReplyWindow); diff --git a/src/components/reply-window/style.css b/src/components/reply-window/style.css new file mode 100644 index 000000000..aecfbd105 --- /dev/null +++ b/src/components/reply-window/style.css @@ -0,0 +1,14 @@ +.ReplyWindow { + width: 100% +} + +.ReplyWindow-form { + display: flex; + flex-direction: column; + width: 100%; +} + +.ReplyWindow-controls { + display: flex; + justify-content: flex-start; +} diff --git a/src/store-redux/comment/actions.js b/src/store-redux/comment/actions.js new file mode 100644 index 000000000..8057d6939 --- /dev/null +++ b/src/store-redux/comment/actions.js @@ -0,0 +1,31 @@ +export default { + /** + * Ответ на комментарий + * @param data + * @return {Function} + */ + reply: (data) => { + return async (dispatch, getState, services) => { + // Сброс текущего стека комментариев и установка признака ожидания загрузки + dispatch({ type: 'comment/reply-start' }); + + try { + const response = await services.api.request({ + url: `/api/v1/comments`, + method: 'POST', + body: JSON.stringify({ + text: data.text, + parent: { + _id: data.parent._id, + _type: data.parent._type, + }, + }), + }); + + dispatch({ type: 'comment/reply-success' }); + } catch (error) { + dispatch({ type: 'comment/reply-error', payload: { error: error.message } }); + } + }; + }, +}; diff --git a/src/store-redux/comment/reducer.js b/src/store-redux/comment/reducer.js new file mode 100644 index 000000000..7c7d670e9 --- /dev/null +++ b/src/store-redux/comment/reducer.js @@ -0,0 +1,31 @@ +// Начальное состояние +export const initialState = { + data: { + text: '', + parent: { + _id: '', + _type: '', + }, + }, + waiting: false, // признак ожидания загрузки +}; + +// Обработчик действий +function reducer(state = initialState, action) { + switch (action.type) { + case 'comment/reply-start': + return { ...state, data: initialState.data, waiting: true }; + + case 'comment/reply-success': + return { ...state, waiting: false }; + + case 'comment/reply-error': + return { ...state, data: {}, waiting: false }; //@todo текст ошибки сохранять? + + default: + // Нет изменений + return state; + } +} + +export default reducer; diff --git a/src/store-redux/exports.js b/src/store-redux/exports.js index c7f1c588b..b15b51e29 100644 --- a/src/store-redux/exports.js +++ b/src/store-redux/exports.js @@ -1,3 +1,4 @@ export { default as article } from './article/reducer'; export { default as modals } from './modals/reducer'; export { default as comments } from './comments/reducer'; +export { default as comment } from './comment/reducer'; From 1a026ad3f2e01930202ac180776b9cf04f00feda Mon Sep 17 00:00:00 2001 From: Elena Date: Wed, 9 Oct 2024 19:42:16 +0400 Subject: [PATCH 05/18] =?UTF-8?q?feat:=20=D1=80=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BA=D0=B0=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=80?= =?UTF-8?q?=D0=B8=D0=B5=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/article/index.js | 3 --- src/components/article-card/index.js | 2 +- src/components/comment/index.js | 17 +++++++----- src/components/reply-window/index.js | 40 +++++++++++++++++++++------- 4 files changed, 42 insertions(+), 20 deletions(-) diff --git a/src/app/article/index.js b/src/app/article/index.js index 7be1b48ce..9fa8969aa 100644 --- a/src/app/article/index.js +++ b/src/app/article/index.js @@ -14,7 +14,6 @@ import { useDispatch, useSelector } from 'react-redux'; import shallowequal from 'shallowequal'; import articleActions from '../../store-redux/article/actions'; import commentsActions from '../../store-redux/comments/actions'; -//import commentActions from '../../store-redux/comment/actions'; import treeToList from '../../utils/tree-to-list'; import listToTree from '../../utils/list-to-tree'; @@ -87,8 +86,6 @@ function Article() { const callbacks = { // Добавление в корзину addToBasket: useCallback(_id => store.actions.basket.addToBasket(_id), [store]), - // отправка коммента в ответ на коммент - //replyComment: useCallback(() => dispatch(commentActions.reply(params)), []) }; return ( diff --git a/src/components/article-card/index.js b/src/components/article-card/index.js index c7d6fe4d3..d36fbfd7c 100644 --- a/src/components/article-card/index.js +++ b/src/components/article-card/index.js @@ -56,7 +56,7 @@ ArticleCard.propTypes = { }).isRequired, onAdd: PropTypes.func, t: PropTypes.func, - comments: PropTypes.array, + comments: PropTypes.object, }; export default memo(ArticleCard); diff --git a/src/components/comment/index.js b/src/components/comment/index.js index 11e5e42b2..8e9f9e2a0 100644 --- a/src/components/comment/index.js +++ b/src/components/comment/index.js @@ -1,16 +1,24 @@ import { memo } from 'react'; -import PropTypes from 'prop-types'; import { cn as bem } from '@bem-react/classname'; import useTranslate from '../../hooks/use-translate'; import dateFormat from '../../utils/date-format'; import './style.css'; import { Link } from 'react-router-dom'; import ReplyWindow from '../reply-window'; +import {useDispatch} from "react-redux"; +import commentActions from '../../store-redux/comment/actions'; function Comment({ id, name, dateCreate, text, session, isVisible, onToggleReply }) { const cn = bem('Comment'); const { t } = useTranslate(); + const dispatch = useDispatch(); + + // Функция для отправки комментария + const handleReplySubmit = (data) => { + dispatch(commentActions.reply(data)); // Отправляем комментарий через Redux action + }; + return (
@@ -27,8 +35,10 @@ function Comment({ id, name, dateCreate, text, session, isVisible, onToggleReply (session ? (
) : ( @@ -43,9 +53,4 @@ function Comment({ id, name, dateCreate, text, session, isVisible, onToggleReply ); } -Comment.propTypes = { - // sum: PropTypes.number, - // t: PropTypes.func, -}; - export default memo(Comment); diff --git a/src/components/reply-window/index.js b/src/components/reply-window/index.js index 615c0f4b3..a92954760 100644 --- a/src/components/reply-window/index.js +++ b/src/components/reply-window/index.js @@ -1,23 +1,47 @@ -import { memo } from 'react'; -import PropTypes from 'prop-types'; +import { memo, useState } from 'react'; import { cn as bem } from '@bem-react/classname'; import './style.css'; import Input from '../input'; +import commentsActions from "../../store-redux/comments/actions"; +import {useDispatch} from "react-redux"; +import {useParams} from "react-router-dom"; -function ReplyWindow({ name, theme, onToggleReply }) { + +function ReplyWindow({ id, name, theme, onToggleReply, onSubmitReply }) { const cn = bem('ReplyWindow'); + const dispatch = useDispatch(); + const params = useParams(); + + const [comment, setComment] = useState(''); + + // Обработчик изменения инпута + const handleInputChange = (value) => { + setComment(value); // сохраняем введённое значение в стейте + }; + + // Обработчик отправки формы + const handleSubmit = async (event) => { + event.preventDefault(); + if (comment.trim()) { + onSubmitReply({ text: comment, parent: { _id: id, _type: 'comment' } }); // вызываем экшен для отправки комментария + setComment(''); + onToggleReply(); + dispatch(commentsActions.load(params.id)) + } + }; return (
-
{}} className={cn('form')}> + Новый ответ {}} + onChange={handleInputChange} + value={comment} />
- +
@@ -25,8 +49,4 @@ function ReplyWindow({ name, theme, onToggleReply }) { ); } -ReplyWindow.propTypes = { - data: PropTypes.object.isRequired, -}; - export default memo(ReplyWindow); From 1cc3996bc63811f2d3630422dee200d345f452b9 Mon Sep 17 00:00:00 2001 From: Elena Date: Thu, 10 Oct 2024 12:51:17 +0400 Subject: [PATCH 06/18] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=20Textarea=20fix:=20=D0=B2=20=D0=B1=D0=BB=D0=BE?= =?UTF-8?q?=D0=BA=D0=B0=D1=85=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=80?= =?UTF-8?q?=D0=B8=D0=B5=D0=B2=20input=20=D0=B7=D0=B0=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BD=D0=B0=20textarea?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/comment/index.js | 15 ++----- src/components/comment/style.css | 4 +- src/components/comments/index.js | 40 +++++++++++------ src/components/input/style.css | 4 -- src/components/new-comment-window/index.js | 50 +++++++++++++++++++++ src/components/new-comment-window/style.css | 22 +++++++++ src/components/reply-window/index.js | 26 +++++------ src/components/reply-window/style.css | 14 ++++-- src/components/textarea/index.js | 46 +++++++++++++++++++ src/components/textarea/style.css | 11 +++++ 10 files changed, 181 insertions(+), 51 deletions(-) create mode 100644 src/components/new-comment-window/index.js create mode 100644 src/components/new-comment-window/style.css create mode 100644 src/components/textarea/index.js create mode 100644 src/components/textarea/style.css diff --git a/src/components/comment/index.js b/src/components/comment/index.js index 8e9f9e2a0..f6228759e 100644 --- a/src/components/comment/index.js +++ b/src/components/comment/index.js @@ -5,20 +5,11 @@ import dateFormat from '../../utils/date-format'; import './style.css'; import { Link } from 'react-router-dom'; import ReplyWindow from '../reply-window'; -import {useDispatch} from "react-redux"; -import commentActions from '../../store-redux/comment/actions'; -function Comment({ id, name, dateCreate, text, session, isVisible, onToggleReply }) { +function Comment({ id, name, dateCreate, text, session, isVisible, onToggleReply, onSubmit }) { const cn = bem('Comment'); const { t } = useTranslate(); - const dispatch = useDispatch(); - - // Функция для отправки комментария - const handleReplySubmit = (data) => { - dispatch(commentActions.reply(data)); // Отправляем комментарий через Redux action - }; - return (
@@ -37,8 +28,8 @@ function Comment({ id, name, dateCreate, text, session, isVisible, onToggleReply
) : ( diff --git a/src/components/comment/style.css b/src/components/comment/style.css index 32748bedd..41574b6de 100644 --- a/src/components/comment/style.css +++ b/src/components/comment/style.css @@ -4,8 +4,6 @@ align-items: flex-start; font-size: 12px; padding-bottom: 30px; - width: 100%; - flex: 1; } .Comment-author { @@ -23,6 +21,7 @@ .Comment-action { color: #0087E9; + cursor: pointer; } .Comment-reply { @@ -46,6 +45,7 @@ .Comment-link span { color: #666666; text-decoration: underline; + cursor: pointer; } diff --git a/src/components/comments/index.js b/src/components/comments/index.js index ebbfcc61f..6c4fc09ad 100644 --- a/src/components/comments/index.js +++ b/src/components/comments/index.js @@ -5,15 +5,24 @@ import useTranslate from '../../hooks/use-translate'; import './style.css'; import { Link } from 'react-router-dom'; import Comment from '../comment'; +import NewCommentWindow from '../new-comment-window'; +import { useDispatch } from 'react-redux'; +import commentActions from '../../store-redux/comment/actions'; function Comments(props) { const cn = bem('Comments'); const { t } = useTranslate(); + const dispatch = useDispatch(); + + // Функция для отправки комментария + const handleReplySubmit = data => { + dispatch(commentActions.reply(data)); // Отправляем комментарий через Redux action + }; const [visibleCommentId, setVisibleCommentId] = useState(null); // Хранит id видимого комментария - const toggleReplyWindow = (id) => { - setVisibleCommentId((prevId) => (prevId === id ? null : id)); // Переключает видимость окна ответа + const toggleReplyWindow = id => { + setVisibleCommentId(prevId => (prevId === id ? null : id)); // Переключает видимость окна ответа }; const renderComments = items => { @@ -34,6 +43,7 @@ function Comments(props) { session={props.session} isVisible={visibleCommentId === item.dateCreate} // Передает состояние видимости onToggleReply={() => toggleReplyWindow(item.dateCreate)} + onSubmit={handleReplySubmit} />
); @@ -49,20 +59,22 @@ function Comments(props) {
{props.comments.items && renderComments(props.comments.items)}
- {visibleCommentId === null && ( -
-

- Войдите, чтобы иметь возможность комментировать -

-
- )} + {visibleCommentId === null && + (props.session ? ( + + ) : ( +
+

+ Войдите, чтобы иметь возможность комментировать +

+
+ ))}
); } -Comments.propTypes = { - // sum: PropTypes.number, - // t: PropTypes.func, -}; - export default memo(Comments); diff --git a/src/components/input/style.css b/src/components/input/style.css index 4aa603fd5..badf5a782 100644 --- a/src/components/input/style.css +++ b/src/components/input/style.css @@ -6,7 +6,3 @@ .Input_theme_big { width: 300px; } - -.Input_theme_fullWidth { - width: 100%; -} diff --git a/src/components/new-comment-window/index.js b/src/components/new-comment-window/index.js new file mode 100644 index 000000000..cfaab871b --- /dev/null +++ b/src/components/new-comment-window/index.js @@ -0,0 +1,50 @@ +import { memo, useState } from 'react'; +import { cn as bem } from '@bem-react/classname'; +import './style.css'; +import commentsActions from "../../store-redux/comments/actions"; +import {useDispatch} from "react-redux"; +import {useParams} from "react-router-dom"; +import Textarea from "../textarea"; + + +function NewCommentWindow({ name, theme, onSubmitReply }) { + const cn = bem('NewCommentWindow'); + const dispatch = useDispatch(); + const params = useParams(); + + const [comment, setComment] = useState(''); + + // Обработчик изменения поля ввода + const handleTextareaChange = (value) => { + setComment(value); // сохраняем введённое значение в стейте + }; + + // Обработчик отправки формы + const handleSubmit = async (event) => { + event.preventDefault(); + if (comment.trim()) { + onSubmitReply({ text: comment, parent: { _id: params.id, _type: 'article' } }); // вызываем экшен для отправки комментария + setComment(''); + dispatch(commentsActions.load(params.id)) + } + }; + + return ( +
+
+ Новый комментарий +