{article.description}
-
Страна производитель:
+
{t('article.country')}:
{article.madeIn?.title} ({article.madeIn?.code})
-
Категория:
+
{t('article.category')}:
{article.category?.title}
-
Год выпуска:
+
{t('article.year')}:
{article.edition}
-
Цена:
+
{t('article.price')}:
{numberFormat(article.price)} ₽
diff --git a/src/components/comment-field/index.js b/src/components/comment-field/index.js
new file mode 100644
index 000000000..9fd281583
--- /dev/null
+++ b/src/components/comment-field/index.js
@@ -0,0 +1,103 @@
+import React, {useEffect, useRef, useState} from 'react';
+import PropTypes from "prop-types";
+import {cn as bem} from "@bem-react/classname";
+import { useLocation, useNavigate } from "react-router-dom";
+import './style.css';
+
+const CommentField = (
+ {
+ isOpen = false,
+ placeholder = 'Текст',
+ addComment,
+ id,
+ isAuth = false,
+ username = '',
+ className = '',
+ onClose = () => {},
+ t = text => text
+ }
+) => {
+
+ const cn = bem('CommentField');
+ const [data, setData] = useState('');
+ const navigate = useNavigate();
+ const location = useLocation()
+ const textareaRef = useRef(null)
+ const notAuthRef = useRef(null)
+
+ const onSendHandler = () => {
+ const sendData = data.trim()
+ if (sendData) {
+ addComment(id, data, username ? 'comment' : 'article')
+ if (!username) {
+ setData('');
+ }
+ } else {
+ textareaRef.current.focus()
+ }
+ }
+
+ const onLoginHandler = () => {
+ navigate('/login', { state: { back: location, commentId: id, username } })
+ }
+
+ useEffect(() => {
+ if (username && isOpen && textareaRef.current) {
+ textareaRef.current.setSelectionRange(data.length, data.length)
+ textareaRef.current.scrollIntoView({ behavior: "smooth", block: "center" })
+ }
+
+ return () => setData('')
+ }, [username, isOpen, id]);
+
+ useEffect(() => {
+ if (notAuthRef.current && isOpen && username) {
+ notAuthRef.current.scrollIntoView({ behavior: "smooth", block: "center" })
+ }
+ }, [isOpen, id, username]);
+
+ if (!isOpen) return null;
+
+ return (
+
+ {
+ isAuth ? (
+ <>
+
{username ? t('comments.newReply') : t('comments.newComment')}
+
+ );
+};
+
+CommentField.propTypes = {
+ isOpen: PropTypes.bool,
+ placeholder: PropTypes.string,
+ addComment: PropTypes.func.isRequired,
+ id: PropTypes.string.isRequired,
+ isAuth: PropTypes.bool,
+ username: PropTypes.string,
+ className: PropTypes.string,
+ onClose: PropTypes.func,
+ t: PropTypes.func
+};
+
+export default CommentField;
diff --git a/src/components/comment-field/style.css b/src/components/comment-field/style.css
new file mode 100644
index 000000000..8ea7fa3b1
--- /dev/null
+++ b/src/components/comment-field/style.css
@@ -0,0 +1,47 @@
+.CommentField-title {
+ font-size: 12px;
+ margin: 0 0 10px;
+}
+
+.CommentField-field {
+ width: 100%;
+ padding: 4px 5px;
+ box-sizing: border-box;
+ min-height: 80px;
+ margin-bottom: 5px;
+
+ font-family: inherit;
+ font-size: 14px;
+}
+
+.CommentField-controls {
+ display: flex;
+ gap: 10px;
+ font-size: 13px;
+}
+
+.CommentField-controls button {
+ font-size: inherit;
+}
+
+.CommentField-notAuth {
+ font-size: 16px;
+}
+
+.CommentField-notAuth button {
+ border: none;
+ background-color: transparent;
+ font-size: inherit;
+ padding: 0;
+ cursor: pointer;
+ text-decoration: underline;
+}
+
+.CommentField-loginButton {
+ color: #0087e9;
+}
+
+.CommentField-cancelButton {
+ margin-left: 5px;
+ color: #666;
+}
diff --git a/src/components/comment-item/index.js b/src/components/comment-item/index.js
new file mode 100644
index 000000000..e248dbd48
--- /dev/null
+++ b/src/components/comment-item/index.js
@@ -0,0 +1,54 @@
+import React from 'react';
+import PropTypes from "prop-types";
+import { cn as bem } from "@bem-react/classname";
+import { formatDate } from "../../utils/format-date";
+import './style.css';
+
+const CommentItem = ({
+ comment,
+ commentToggle,
+ lang = 'ru',
+ currentUser = '',
+ openAnswerField = (_, __) => {},
+ replyButtonText = 'Ответить',
+}) => {
+ const cn = bem('CommentItem');
+
+ const onOpenModal = () => {
+ commentToggle(comment._id)
+ openAnswerField(comment._id, comment.author.profile.name)
+ }
+
+ return (
+
+
+
{comment.author.profile.name}
+
{formatDate(comment.dateCreate, lang)}
+
+
{comment.text}
+
+
+ );
+};
+
+
+CommentItem.propTypes = {
+ comment: PropTypes.shape({
+ _id: PropTypes.string,
+ text: PropTypes.string,
+ padding: PropTypes.number,
+ dateCreate: PropTypes.string,
+ author: PropTypes.shape({
+ profile: PropTypes.shape({
+ name: PropTypes.string,
+ })
+ })
+ }).isRequired,
+ commentToggle: PropTypes.func.isRequired,
+ lang: PropTypes.string,
+ isCurrentUser: PropTypes.string,
+ openAnswerField: PropTypes.func,
+ replyButtonText: PropTypes.string
+};
+
+export default CommentItem;
diff --git a/src/components/comment-item/style.css b/src/components/comment-item/style.css
new file mode 100644
index 000000000..6b1ee70d6
--- /dev/null
+++ b/src/components/comment-item/style.css
@@ -0,0 +1,40 @@
+.CommentItem-head {
+ margin-bottom: 10px;
+
+ display: flex;
+ gap: 10px;
+
+ font-size: 12px;
+}
+
+.CommentItem-username {
+ font-weight: 700;
+}
+
+.CommentItem-username_current {
+ color: #666;
+}
+
+.CommentItem-date {
+ color: #666;
+}
+
+.CommentItem-text {
+ font-size: 14px;
+
+ margin-bottom: 10px;
+ word-wrap: break-word;
+}
+
+.CommentItem-button {
+ border: none;
+ background-color: transparent;
+ font-size: 12px;
+ color: #0087e9;
+ padding: 0;
+ cursor: pointer;
+}
+
+.CommentItem-field {
+ margin-top: 30px;
+}
diff --git a/src/components/comments-list/index.js b/src/components/comments-list/index.js
new file mode 100644
index 000000000..cab057f7f
--- /dev/null
+++ b/src/components/comments-list/index.js
@@ -0,0 +1,132 @@
+import React, {memo, useEffect, useMemo, useState} from 'react';
+import {useLocation} from "react-router-dom";
+import PropTypes from "prop-types";
+import treeToList from "../../utils/tree-to-list";
+import listToTree from "../../utils/list-to-tree";
+import CommentItem from "../comment-item";
+import CommentField from "../comment-field";
+import './style.css';
+
+const CommentsList = ({
+ comments = [],
+ count = 0,
+ addComment,
+ isAuth = false,
+ params,
+ t = text => text,
+ lang = 'ru',
+ currentUser = ''
+}) => {
+ const location = useLocation()
+ const [commentOpen, setCommentOpen] = useState('')
+ const [commentsList, setCommentsList] = useState([]);
+
+ useEffect(() => {
+ setCommentsList(comments);
+ }, [comments]);
+
+ const formatComments = useMemo(() => {
+ return [...treeToList(listToTree(commentsList), (item, level) => {
+
+ let padding = 30 * (level - 1)
+
+ if (padding > 270) {
+ padding = 270
+ }
+
+ return {
+ ...item,
+ padding,
+ }
+ }).slice(1)]
+ }, [comments, commentsList, addComment])
+
+ const onCloseField = () => {
+ setCommentOpen('')
+ setCommentsList((prevState) => {
+ return [...prevState.filter((item) => item._id !== 'answer')]
+ })
+ }
+
+ const openAnswerField = (_id, username) => {
+ setCommentsList((prevState) => ([
+ ...prevState.filter((item) => item._id !== 'answer'),
+ {
+ _id: 'answer',
+ username,
+ parent: { _id}
+ }
+ ]))
+ }
+
+ useEffect(() => {
+ if (location.state?.commentId && location.state?.username) {
+ setCommentOpen(location.state?.commentId)
+ openAnswerField(location.state?.commentId, location.state?.username)
+ }
+ }, [location]);
+
+ const onAddHandler = (id, username, type) => {
+ addComment(id, username, type)
+ setCommentOpen('')
+ }
+
+ return (
+
+
{t('article.comments')} ({count})
+
+ {
+ formatComments.length !== 0 &&
+ formatComments.map((comment) => (
+
+ {
+ comment._id !== 'answer' ? (
+
+ ) : (
+
+ )
+ }
+
+ ))
+ }
+
+
+
+ );
+};
+
+CommentsList.propTypes = {
+ comments: PropTypes.arrayOf(
+ PropTypes.shape({
+ _id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ }),
+ ),
+ count: PropTypes.number,
+ addComment: PropTypes.func.isRequired,
+ isAuth: PropTypes.bool,
+ params: PropTypes.shape({
+ id: PropTypes.string,
+ }).isRequired,
+ t: PropTypes.func,
+ lang: PropTypes.string,
+ currentUser: PropTypes.string,
+};
+
+export default memo(CommentsList);
diff --git a/src/components/comments-list/style.css b/src/components/comments-list/style.css
new file mode 100644
index 000000000..7364addea
--- /dev/null
+++ b/src/components/comments-list/style.css
@@ -0,0 +1,15 @@
+.CommentsList {
+ padding: 50px 40px
+}
+
+.CommentsList-title {
+ font-weight: 400;
+ font-size: 24px;
+ margin: 0 0 25px;
+}
+
+.CommentsList-inner {
+ display: flex;
+ flex-direction: column;
+ gap: 30px;
+}
diff --git a/src/components/profile-card/index.js b/src/components/profile-card/index.js
index 13d3b6cf7..1a5304d66 100644
--- a/src/components/profile-card/index.js
+++ b/src/components/profile-card/index.js
@@ -3,18 +3,18 @@ import PropTypes from 'prop-types';
import { cn as bem } from '@bem-react/classname';
import './style.css';
-function ProfileCard({ data }) {
+function ProfileCard({ data, t }) {
const cn = bem('ProfileCard');
return (
-
Профиль
+
{t('profile.title')}
-
Имя:
+
{t('profile.name')}:
{data?.profile?.name}
-
Телефон:
+
{t('profile.phone')}:
{data?.profile?.phone}
diff --git a/src/config.js b/src/config.js
index 67c72f734..b2bc483f5 100644
--- a/src/config.js
+++ b/src/config.js
@@ -18,6 +18,9 @@ const config = {
api: {
baseUrl: '',
},
+ i18n: {
+ defaultLanguage: 'ru',
+ }
};
export default config;
diff --git a/src/containers/catalog-filter/index.js b/src/containers/catalog-filter/index.js
index 18d40d4fb..d8265eab1 100644
--- a/src/containers/catalog-filter/index.js
+++ b/src/containers/catalog-filter/index.js
@@ -11,6 +11,8 @@ import listToTree from '../../utils/list-to-tree';
function CatalogFilter() {
const store = useStore();
+ const { t, lang } = useTranslate();
+
const select = useSelector(state => ({
sort: state.catalog.params.sort,
query: state.catalog.params.query,
@@ -40,28 +42,26 @@ function CatalogFilter() {
// Варианты сортировок
sort: useMemo(
() => [
- { value: 'order', title: 'По порядку' },
- { value: 'title.ru', title: 'По именованию' },
- { value: '-price', title: 'Сначала дорогие' },
- { value: 'edition', title: 'Древние' },
+ { value: 'order', title: t('filter.sort.order') },
+ { value: 'title.ru', title: t('filter.sort.title') },
+ { value: '-price', title: t('filter.sort.price') },
+ { value: 'edition', title: t('filter.sort.edition') },
],
- [],
+ [lang],
),
// Категории для фильтра
categories: useMemo(
() => [
- { value: '', title: 'Все' },
+ { value: '', title: t('filter.allCategories') },
...treeToList(listToTree(select.categories), (item, level) => ({
value: item._id,
title: '- '.repeat(level) + item.title,
})),
],
- [select.categories],
+ [select.categories, lang],
),
};
- const { t } = useTranslate();
-
return (
diff --git a/src/hooks/use-translate.js b/src/hooks/use-translate.js
index bdcbf1071..a0596e7a2 100644
--- a/src/hooks/use-translate.js
+++ b/src/hooks/use-translate.js
@@ -1,9 +1,34 @@
-import { useCallback, useContext } from 'react';
-import { I18nContext } from '../i18n/context';
+import useServices from "./use-services";
+import {useEffect, useMemo, useState} from "react";
/**
* Хук возвращает функцию для локализации текстов, код языка и функцию его смены
*/
export default function useTranslate() {
- return useContext(I18nContext);
+ const { i18n } = useServices()
+ const [data, setData] = useState(i18n.getLang());
+
+ useEffect(() => {
+ const listener = () => {
+ setData(i18n.getLang())
+ }
+
+ i18n.subscribe(listener);
+
+ return () => i18n.unsubscribe(listener);
+ }, [i18n]);
+
+ return useMemo(
+ () => ({
+ // Код локали
+ lang: data,
+ // Функция для смены локали
+ setLang: (lang) => {
+ i18n.setLang(lang);
+ },
+ // Функция для локализации текстов с замыканием на код языка
+ t: (text, number) => i18n.t(text, number),
+ }),
+ [i18n.getLang()],
+ );
}
diff --git a/src/i18n/context.js b/src/i18n/context.js
deleted file mode 100644
index 3733d9bac..000000000
--- a/src/i18n/context.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import { createContext, useMemo, useState } from 'react';
-import translate from './translate';
-
-/**
- * @type {React.Context<{}>}
- */
-export const I18nContext = createContext({});
-
-/**
- * Обертка над провайдером контекста, чтобы управлять изменениями в контексте
- * @param children
- * @return {JSX.Element}
- */
-export function I18nProvider({ children }) {
- const [lang, setLang] = useState('ru');
-
- const i18n = useMemo(
- () => ({
- // Код локали
- lang,
- // Функция для смены локали
- setLang,
- // Функция для локализации текстов с замыканием на код языка
- t: (text, number) => translate(lang, text, number),
- }),
- [lang],
- );
-
- return {children};
-}
diff --git a/src/i18n/index.js b/src/i18n/index.js
new file mode 100644
index 000000000..2496308b4
--- /dev/null
+++ b/src/i18n/index.js
@@ -0,0 +1,42 @@
+import translate from './translate';
+
+class I18nService {
+ constructor(services, config = {}) {
+ const storageLang = window.localStorage.getItem('lang') !== 'undefined' ? window.localStorage.getItem('lang') : undefined
+
+ this.services = services;
+ this.config = config;
+ this.lang = storageLang || config.defaultLanguage || 'ru';
+ this.subscribers = []
+
+ this.notifySubscribers(this.lang)
+ }
+
+ getLang() {
+ return this.lang
+ }
+
+ setLang(lang) {
+ this.lang = lang
+ window.localStorage.setItem('lang', lang)
+ this.notifySubscribers(lang)
+ }
+
+ t(text, number) {
+ return translate(this.lang, text, number)
+ }
+
+ subscribe(listener) {
+ this.subscribers.push(listener);
+ }
+
+ unsubscribe(listener) {
+ this.subscribers = this.subscribers.filter(item => item !== listener);
+ }
+
+ notifySubscribers(lang) {
+ this.subscribers.forEach(subscriber => subscriber(lang))
+ }
+}
+
+export default I18nService
diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json
index 171a28882..8c1dc2ad4 100644
--- a/src/i18n/translations/en.json
+++ b/src/i18n/translations/en.json
@@ -14,11 +14,34 @@
"other": "articles"
},
"article.add": "Add",
+ "article.country": "Country",
+ "article.category": "Category",
+ "article.year": "Year",
+ "article.price": "Price",
+ "article.comments": "Comments",
"filter.reset": "Reset",
"auth.title": "Sign In",
"auth.login": "Login",
"auth.password": "Password",
"auth.signIn": "Sign in",
"session.signIn": "Sign In",
- "session.signOut": "Sign Out"
+ "session.signOut": "Sign Out",
+ "filter.sort.order": "By Order",
+ "filter.sort.title": "By Title",
+ "filter.sort.price": "Price: High to Low",
+ "filter.sort.edition": "Edition",
+ "filter.allCategories": "All",
+ "filter.search": "Search",
+ "profile.title": "Profile",
+ "profile.name": "Name",
+ "profile.phone": "Phone",
+ "comments.reply": "Reply",
+ "comments.placeholder": "Text",
+ "comments.newReply": "New reply",
+ "comments.newComment": "New comment",
+ "comments.cancel": "Cancel",
+ "comments.send": "Send",
+ "comments.signIn": "Sing in",
+ "comments.able.comment": " in to be able to comment",
+ "comments.able.reply": " in to be able to reply."
}
diff --git a/src/i18n/translations/ru.json b/src/i18n/translations/ru.json
index b2a32bfe1..231752afe 100644
--- a/src/i18n/translations/ru.json
+++ b/src/i18n/translations/ru.json
@@ -16,11 +16,34 @@
"other": "товара"
},
"article.add": "Добавить",
+ "article.country": "Страна производитель",
+ "article.category": "Категория",
+ "article.year": "Год выпуска",
+ "article.price": "Цена",
+ "article.comments": "Комментарии",
"filter.reset": "Сбросить",
"auth.title": "Вход",
"auth.login": "Логин",
"auth.password": "Пароль",
"auth.signIn": "Войти",
"session.signIn": "Вход",
- "session.signOut": "Выход"
+ "session.signOut": "Выход",
+ "filter.sort.order": "По порядку",
+ "filter.sort.title": "По именованию",
+ "filter.sort.price": "Сначала дорогие",
+ "filter.sort.edition": "Древние",
+ "filter.allCategories": "Все",
+ "filter.search": "Поиск",
+ "profile.title": "Профиль",
+ "profile.name": "Имя",
+ "profile.phone": "Телефон",
+ "comments.reply": "Ответить",
+ "comments.placeholder": "Текст",
+ "comments.newReply": "Новый ответ",
+ "comments.newComment": "Новый комментарий",
+ "comments.cancel": "Отмена",
+ "comments.send": "Отправить",
+ "comments.signIn": "Войдите",
+ "comments.able.comment": ", чтобы иметь возможность комментировать",
+ "comments.able.reply": ", чтобы иметь возможность ответить."
}
diff --git a/src/index.js b/src/index.js
index 83da7d13c..cf15e0c2a 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,7 +2,6 @@ import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { ServicesContext } from './context';
-import { I18nProvider } from './i18n/context';
import App from './app';
import Services from './services';
import config from './config';
@@ -15,11 +14,9 @@ const root = createRoot(document.getElementById('root'));
root.render(
-
-
,
);
diff --git a/src/services.js b/src/services.js
index a32b14d57..5f81434dd 100644
--- a/src/services.js
+++ b/src/services.js
@@ -1,6 +1,7 @@
import APIService from './api';
import Store from './store';
import createStoreRedux from './store-redux';
+import I18nService from "./i18n";
class Services {
constructor(config) {
@@ -38,6 +39,13 @@ class Services {
}
return this._redux;
}
+
+ get i18n() {
+ if (!this._i18n) {
+ this._i18n = new I18nService(this, this.config.i18n);
+ }
+ return this._i18n
+ }
}
export default Services;
diff --git a/src/store-redux/comments/actions.js b/src/store-redux/comments/actions.js
new file mode 100644
index 000000000..31d5d140c
--- /dev/null
+++ b/src/store-redux/comments/actions.js
@@ -0,0 +1,58 @@
+import simplifyErrors from "../../utils/simplify-errors";
+
+export default {
+ load: id => {
+ return async (dispatch, getState, services) => {
+ dispatch({type: 'comments/load-start'})
+
+ try {
+ const res = await services.api.request({
+ url: `/api/v1/comments?${new URLSearchParams({
+ 'search[parent]': id,
+ fields: 'items(_id,text,dateCreate,parent,author(profile(name))),count',
+ limit: '*',
+ })}`
+ })
+
+ if (!res.data.error) {
+ dispatch({type: 'comments/load-success', payload: { data: res.data.result.items, count: res.data.result.count }});
+ } else {
+ dispatch({ type: 'comments/load-error', payload: { errors: simplifyErrors(res.data.error.data.issues) } });
+ }
+
+ } catch (e) {
+ console.log(e)
+ }
+ }
+ },
+
+ send: (_id, text, _type = 'article') => {
+ return async (dispatch, getState, services) => {
+ try {
+ const res = await services.api.request({
+ method: 'POST',
+ url: `/api/v1/comments?${new URLSearchParams({
+ fields: '_id,text,dateCreate,parent(_id),author(profile(name))',
+ })}`,
+ body: JSON.stringify({
+ text,
+ parent: {
+ _id,
+ _type,
+ }
+ })
+ })
+
+ if (!res.data.error) {
+ const currentState = getState().comments.data
+ dispatch({type: 'comments/load-success', payload: { data: [...currentState, res.data.result], count: getState().comments.count + 1 } });
+ } else {
+ dispatch({ type: 'comments/load-error', payload: { errors: simplifyErrors(res.data.error.data.issues) } });
+ }
+
+ } catch (e) {
+ console.log(e)
+ }
+ }
+ }
+}
diff --git a/src/store-redux/comments/reducer.js b/src/store-redux/comments/reducer.js
new file mode 100644
index 000000000..32cd3c2d3
--- /dev/null
+++ b/src/store-redux/comments/reducer.js
@@ -0,0 +1,24 @@
+export const initialState = {
+ data: [],
+ count: 0,
+ waiting: false,
+ errors: null
+}
+
+function reducer(state = initialState, action) {
+ switch (action.type) {
+ case 'comments/load-start':
+ return { ...state, data: [], count: 0, waiting: true };
+
+ case 'comments/load-success':
+ return { ...state, data: action.payload.data, count: action.payload.count, waiting: false };
+
+ case 'comments/load-error':
+ return { ...state, data: [], count: 0, waiting: false };
+
+ 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';
diff --git a/src/store-redux/index.js b/src/store-redux/index.js
index b885ee585..d6c545a2a 100644
--- a/src/store-redux/index.js
+++ b/src/store-redux/index.js
@@ -2,11 +2,12 @@ import { applyMiddleware, combineReducers, createStore } from 'redux';
import * as reducers from './exports';
import { thunk, withExtraArgument } from 'redux-thunk';
+import { composeWithDevTools } from "@redux-devtools/extension";
export default function createStoreRedux(services, config = {}) {
return createStore(
combineReducers(reducers),
undefined,
- applyMiddleware(withExtraArgument(services)),
+ composeWithDevTools(applyMiddleware(withExtraArgument(services))),
);
}
diff --git a/src/store/session/index.js b/src/store/session/index.js
index 32fecf0c4..7b57822e7 100644
--- a/src/store/session/index.js
+++ b/src/store/session/index.js
@@ -141,7 +141,7 @@ class SessionState extends StoreModule {
* Сброс ошибок авторизации
*/
resetErrors() {
- this.setState({ ...this.initState(), errors: null });
+ this.setState({ ...this.getState(), errors: null });
}
}
diff --git a/src/utils/format-date.js b/src/utils/format-date.js
new file mode 100644
index 000000000..2039da471
--- /dev/null
+++ b/src/utils/format-date.js
@@ -0,0 +1,10 @@
+export const formatDate = (dateString, lang = 'ru') => {
+ const date = new Date(dateString);
+
+ const formattedData = new Intl.DateTimeFormat(lang, {
+ dateStyle: 'long',
+ timeStyle: 'short',
+ }).format(date);
+
+ return lang === 'ru' ? formattedData.replace('г. ', '') : formattedData;
+}
diff --git a/src/utils/list-to-tree/index.js b/src/utils/list-to-tree/index.js
index fb4d20a66..233f70b56 100644
--- a/src/utils/list-to-tree/index.js
+++ b/src/utils/list-to-tree/index.js
@@ -5,9 +5,10 @@
* @returns {Array} Корневые узлы
*/
export default function listToTree(list, key = '_id') {
+ const newList = JSON.parse(JSON.stringify(list))
let trees = {};
let roots = {};
- for (const item of list) {
+ for (const item of newList) {
// Добавление элемента в индекс узлов и создание свойства children
if (!trees[item[key]]) {
trees[item[key]] = item;
diff --git a/src/utils/tree-to-list/index.js b/src/utils/tree-to-list/index.js
index 3a79c0b06..38879530e 100644
--- a/src/utils/tree-to-list/index.js
+++ b/src/utils/tree-to-list/index.js
@@ -7,7 +7,8 @@
* @returns {Array} Корневые узлы
*/
export default function treeToList(tree, callback, level = 0, result = []) {
- for (const item of tree) {
+ const newTree = JSON.parse(JSON.stringify(tree))
+ for (const item of newTree) {
result.push(callback ? callback(item, level) : item);
if (item.children?.length) treeToList(item.children, callback, level + 1, result);
}