From 9a4d05c2fa1c73f2ca3deab7415ba5c15bfc5cf6 Mon Sep 17 00:00:00 2001 From: SempaiDarcy Date: Fri, 18 Apr 2025 01:35:13 +0400 Subject: [PATCH 01/28] feat: add actions and reducer for comment loading, creation and handling errors --- src/store-redux/comments/actions.js | 77 +++++++++++++++++++++++++++++ src/store-redux/comments/reducer.js | 43 ++++++++++++++++ src/store-redux/exports.js | 1 + 3 files changed, 121 insertions(+) create mode 100644 src/store-redux/comments/actions.js create mode 100644 src/store-redux/comments/reducer.js diff --git a/src/store-redux/comments/actions.js b/src/store-redux/comments/actions.js new file mode 100644 index 000000000..dae03af8b --- /dev/null +++ b/src/store-redux/comments/actions.js @@ -0,0 +1,77 @@ +import listToTree from '../../utils/list-to-tree'; + +export default { + /** + * Загрузка комментариев по ID + */ + load: articleId => { + return async (dispatch, getState, services) => { + dispatch({ type: 'comments/load-start' }); + + try { + const res = await services.api.request({ + url: `/api/v1/comments?fields=${encodeURIComponent( + 'items(_id,text,dateCreate,author(profile(name)),parent(_id,_type),isDeleted),count', + )}&limit=*&search[parent]=${articleId}`, + }); + + // фильтрация валидных комментариев + const validItems = res.data.result.items.filter(item => item._id && !item.isDeleted); + + const tree = listToTree(validItems); + + dispatch({ type: 'comments/load-success', payload: { items: tree } }); + dispatch({ type: 'comments/set-article-id', payload: { articleId } }); + dispatch({ type: 'comments/reset-form-target' }); + + return Promise.resolve(); + } catch (e) { + dispatch({ type: 'comments/load-error' }); + return Promise.reject(e); + } + }; + }, + + /** + * Создание комментария или ответа + */ + create: (text, parent) => { + return async (dispatch, getState, services) => { + dispatch({ type: 'comments/create-start' }); + + try { + // Создаем комментарий + await services.api.request({ + url: '/api/v1/comments', + method: 'POST', + body: JSON.stringify({ text, parent }), + }); + + dispatch({ type: 'comments/create-success' }); + + // Перезагружаем комментарии с сервера + const articleId = parent._type === 'article' ? parent._id : getState().comments.articleId; + const resLoad = await services.api.request({ + url: `/api/v1/comments?fields=${encodeURIComponent( + 'items(_id,text,dateCreate,author(profile(name)),parent(_id,_type),isDeleted),count', + )}&limit=*&search[parent]=${articleId}`, + }); + + const validItems = resLoad.data.result.items.filter(item => item._id && !item.isDeleted); + const tree = listToTree(validItems); + + dispatch({ type: 'comments/load-success', payload: { items: [...tree] } }); + dispatch({ type: 'comments/set-article-id', payload: { articleId } }); + dispatch({ type: 'comments/reset-form-target' }); + } catch (e) { + dispatch({ + type: 'comments/create-error', + payload: e.message || 'Ошибка создания', + }); + } + }; + }, + + setFormTarget: id => ({ type: 'comments/set-form-target', payload: id }), + resetFormTarget: () => ({ type: 'comments/reset-form-target' }), +}; diff --git a/src/store-redux/comments/reducer.js b/src/store-redux/comments/reducer.js new file mode 100644 index 000000000..409b2fe0e --- /dev/null +++ b/src/store-redux/comments/reducer.js @@ -0,0 +1,43 @@ +const initialState = { + items: [], // дерево комментариев + waiting: false, // загрузка + creating: false, // отправка + error: null, // ошибка + articleId: null, // текущая статья + activeFormTargetId: null, // id комментария, к которому пишем ответ +}; + +function reducer(state = initialState, action) { + switch (action.type) { + case 'comments/load-start': + return { ...state, waiting: true }; + + case 'comments/load-success': + return { ...state, waiting: false, items: action.payload.items }; + + case 'comments/load-error': + return { ...state, waiting: false, error: 'Ошибка загрузки комментариев' }; + + case 'comments/create-start': + return { ...state, creating: true }; + + case 'comments/create-success': + return { ...state, creating: false }; + + case 'comments/create-error': + return { ...state, creating: false, error: action.payload || 'Ошибка создания комментария' }; + + case 'comments/set-form-target': + return { ...state, activeFormTargetId: action.payload }; + + case 'comments/reset-form-target': + return { ...state, activeFormTargetId: null }; + + case 'comments/set-article-id': + return { ...state, articleId: action.payload.articleId }; + + 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 cb766f5cc9c0ed9bc26421c2229e961992bfc7dc Mon Sep 17 00:00:00 2001 From: SempaiDarcy Date: Fri, 18 Apr 2025 01:42:17 +0400 Subject: [PATCH 02/28] feat: refactor list-to-tree function for better readability and performance --- src/utils/list-to-tree/index.js | 41 +++++++++++++++------------------ 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/utils/list-to-tree/index.js b/src/utils/list-to-tree/index.js index fb4d20a66..62d4e0b32 100644 --- a/src/utils/list-to-tree/index.js +++ b/src/utils/list-to-tree/index.js @@ -5,31 +5,26 @@ * @returns {Array} Корневые узлы */ export default function listToTree(list, key = '_id') { - let trees = {}; - let roots = {}; + const map = {}; + const roots = []; + + // Индексируем комментарии по id for (const item of list) { - // Добавление элемента в индекс узлов и создание свойства children - if (!trees[item[key]]) { - trees[item[key]] = item; - trees[item[key]].children = []; - // Ещё никто не ссылался, поэтому пока считаем корнем - roots[item[key]] = trees[item[key]]; - } else { - trees[item[key]] = Object.assign(trees[item[key]], item); - } + if (!item[key]) continue; // защита от пустых _id + map[item[key]] = { ...item, children: [] }; + } - // Если элемент имеет родителя, то добавляем его в подчиненные родителя - if (item.parent?.[key]) { - // Если родителя ещё нет в индексе, то индекс создаётся, ведь _id родителя известен - if (!trees[item.parent[key]]) { - trees[item.parent[key]] = { children: [] }; - roots[item.parent[key]] = trees[item.parent[key]]; - } - // Добавления в подчиненные родителя - trees[item.parent[key]].children.push(trees[item[key]]); - // Так как элемент добавлен к родителю, то он уже не является корневым - if (roots[item[key]]) delete roots[item[key]]; + // Формируем дерево + for (const item of list) { + const id = item[key]; + const parentId = item.parent?.[key]; + + if (parentId && map[parentId]) { + map[parentId].children.push(map[id]); + } else { + roots.push(map[id]); } } - return Object.values(roots); + + return roots; } From e1b3aed213e58dd34a64ce96446846cf28068454 Mon Sep 17 00:00:00 2001 From: SempaiDarcy Date: Fri, 18 Apr 2025 01:42:58 +0400 Subject: [PATCH 03/28] feat: refactor article reducer to handle loading state more effectively --- src/store-redux/article/reducer.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/store-redux/article/reducer.js b/src/store-redux/article/reducer.js index 1df8020ce..b79ee6636 100644 --- a/src/store-redux/article/reducer.js +++ b/src/store-redux/article/reducer.js @@ -1,6 +1,5 @@ -// Начальное состояние export const initialState = { - data: {}, + data: {}, // данные о товаре waiting: false, // признак ожидания загрузки }; @@ -8,16 +7,15 @@ export const initialState = { function reducer(state = initialState, action) { switch (action.type) { case 'article/load-start': - return { ...state, data: {}, waiting: true }; + return { ...state, waiting: true, data: {} }; case 'article/load-success': - return { ...state, data: action.payload.data, waiting: false }; + return { ...state, waiting: false, data: action.payload.data }; case 'article/load-error': - return { ...state, data: {}, waiting: false }; //@todo текст ошибки сохранять? + return { ...state, waiting: false }; // можно добавить error, если нужно default: - // Нет изменений return state; } } From 1e2f2359b8fe561e03a2fa61d012bf8fe408e00b Mon Sep 17 00:00:00 2001 From: SempaiDarcy Date: Fri, 18 Apr 2025 01:44:01 +0400 Subject: [PATCH 04/28] feat: add CommentForm component with styles --- src/components/comment-form/index.js | 47 +++++++++++++++++++++++++++ src/components/comment-form/style.css | 24 ++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/components/comment-form/index.js create mode 100644 src/components/comment-form/style.css diff --git a/src/components/comment-form/index.js b/src/components/comment-form/index.js new file mode 100644 index 000000000..4b2b8fe13 --- /dev/null +++ b/src/components/comment-form/index.js @@ -0,0 +1,47 @@ +import { memo, useState } from 'react'; +import PropTypes from 'prop-types'; +import './style.css'; +import Button from '../button'; + +function CommentForm({ onSubmit, onCancel, isReply }) { + const [text, setText] = useState(''); + + const handleSubmit = async e => { + e.preventDefault(); + const trimmed = text.trim(); + if (!trimmed) return; + + await onSubmit(trimmed); + setText(''); + if (onCancel) onCancel(); + }; + + return ( +
+ {isReply ? 'Новый ответ' : 'Новый комментарий'} +