diff --git a/package-lock.json b/package-lock.json index d7db33104..de8bc0474 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@bem-react/classname": "^1.6.0", + "@redux-devtools/extension": "^3.3.0", "lodash.debounce": "^4.0.8", "lodash.throttle": "^4.1.1", "prop-types": "^15.8.1", @@ -1823,12 +1824,11 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", - "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", - "dev": true, + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", + "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" @@ -2694,6 +2694,18 @@ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "dev": true }, + "node_modules/@redux-devtools/extension": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@redux-devtools/extension/-/extension-3.3.0.tgz", + "integrity": "sha512-X34S/rC8S/M1BIrkYD1mJ5f8vlH0BDqxXrs96cvxSBo4FhMdbhU+GUGsmNYov1xjSyLMHgo8NYrUG8bNX7525g==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "immutable": "^4.3.4" + }, + "peerDependencies": { + "redux": "^3.1.0 || ^4.0.0 || ^5.0.0" + } + }, "node_modules/@remix-run/router": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.1.tgz", @@ -5644,6 +5656,11 @@ "postcss": "^8.1.0" } }, + "node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==" + }, "node_modules/import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -8903,10 +8920,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "dev": true + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regenerator-transform": { "version": "0.15.2", @@ -11760,12 +11776,11 @@ "dev": true }, "@babel/runtime": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", - "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", - "dev": true, + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", + "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==", "requires": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" } }, "@babel/template": { @@ -12414,6 +12429,15 @@ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "dev": true }, + "@redux-devtools/extension": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@redux-devtools/extension/-/extension-3.3.0.tgz", + "integrity": "sha512-X34S/rC8S/M1BIrkYD1mJ5f8vlH0BDqxXrs96cvxSBo4FhMdbhU+GUGsmNYov1xjSyLMHgo8NYrUG8bNX7525g==", + "requires": { + "@babel/runtime": "^7.23.2", + "immutable": "^4.3.4" + } + }, "@remix-run/router": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.1.tgz", @@ -14658,6 +14682,11 @@ "dev": true, "requires": {} }, + "immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==" + }, "import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -17015,10 +17044,9 @@ } }, "regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "dev": true + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "regenerator-transform": { "version": "0.15.2", diff --git a/package.json b/package.json index e356433ef..ad2d3c989 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "homepage": "https://github.com/ylabio/react-webinar-3#readme", "dependencies": { "@bem-react/classname": "^1.6.0", + "@redux-devtools/extension": "^3.3.0", "lodash.debounce": "^4.0.8", "lodash.throttle": "^4.1.1", "prop-types": "^15.8.1", @@ -41,9 +42,9 @@ "html-webpack-plugin": "^5.6.0", "jest": "^29.7.0", "mini-css-extract-plugin": "^2.9.1", + "prettier": "3.3.3", "webpack": "^5.94.0", "webpack-cli": "^5.1.4", - "webpack-dev-server": "^5.1.0", - "prettier": "3.3.3" + "webpack-dev-server": "^5.1.0" } } diff --git a/src/api/index.js b/src/api/index.js index a5f073295..5c3d28c19 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -4,11 +4,18 @@ class APIService { * @param config {Object} */ constructor(services, config = {}) { + const storageLang = window.localStorage.getItem('lang') !== 'undefined' ? window.localStorage.getItem('lang') : undefined + this.services = services; this.config = config; this.defaultHeaders = { 'Content-Type': 'application/json', + 'Accept-Language': storageLang || services.i18n.getLang() || 'ru', }; + + this.services.i18n.subscribe(lang => { + this.setHeader('Accept-Language', lang) + }) } /** diff --git a/src/app/article/index.js b/src/app/article/index.js index 56859bc76..741e85e7e 100644 --- a/src/app/article/index.js +++ b/src/app/article/index.js @@ -1,4 +1,4 @@ -import { memo, useCallback, useMemo } from 'react'; +import React, { memo, useCallback } from 'react'; import { useParams } from 'react-router-dom'; import useStore from '../../hooks/use-store'; import useTranslate from '../../hooks/use-translate'; @@ -10,47 +10,71 @@ import Spinner from '../../components/spinner'; import ArticleCard from '../../components/article-card'; import LocaleSelect from '../../containers/locale-select'; import TopHead from '../../containers/top-head'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch, useSelector as useSelectorRedux } from 'react-redux'; import shallowequal from 'shallowequal'; import articleActions from '../../store-redux/article/actions'; +import CommentsList from "../../components/comments-list"; +import commentsActions from "../../store-redux/comments/actions"; +import useSelector from "../../hooks/use-selector"; function Article() { const store = useStore(); const dispatch = useDispatch(); - // Параметры из пути /articles/:id const params = useParams(); + const { t, lang } = useTranslate(); + useInit(() => { //store.actions.article.load(params.id); dispatch(articleActions.load(params.id)); - }, [params.id]); + dispatch(commentsActions.load(params.id)) + }, [params.id, lang]); - const select = useSelector( + const selectRedux = useSelectorRedux( state => ({ article: state.article.data, + comments: state.comments.data, + count: state.comments.count, waiting: state.article.waiting, + commentsWaiting: state.comments.waiting, }), shallowequal, - ); // Нужно указать функцию для сравнения свойства объекта, так как хуком вернули объект + ); - const { t } = useTranslate(); + const select = useSelector(state => ({ + exists: state.session.exists, + currentUser: state.session.user.profile?.name + })) const callbacks = { // Добавление в корзину addToBasket: useCallback(_id => store.actions.basket.addToBasket(_id), [store]), + addComment: useCallback((id, text, _type) => dispatch(commentsActions.send(id, text, _type)), [dispatch, commentsActions.send]), }; return ( - + - - + + + + + ); diff --git a/src/app/login/index.js b/src/app/login/index.js index 9cecc31a4..1a492f1d0 100644 --- a/src/app/login/index.js +++ b/src/app/login/index.js @@ -1,4 +1,4 @@ -import { memo, useCallback, useState } from 'react'; +import { memo, useCallback, useEffect, useState } from 'react'; import useTranslate from '../../hooks/use-translate'; import Head from '../../components/head'; import LocaleSelect from '../../containers/locale-select'; @@ -18,6 +18,7 @@ function Login() { const location = useLocation(); const navigate = useNavigate(); const store = useStore(); + const back = location.state?.back || '/'; useInit(() => { store.actions.session.resetErrors(); @@ -26,6 +27,7 @@ function Login() { const select = useSelector(state => ({ waiting: state.session.waiting, errors: state.session.errors, + exists: state.session.exists })); const [data, setData] = useState({ @@ -49,13 +51,27 @@ function Login() { location.state?.back && location.state?.back !== location.pathname ? location.state?.back : '/'; - navigate(back); + + const state = {} + + if (location.state?.commentId) { + state.commentId = location.state?.commentId; + state.username = location.state?.username ? location.state.username : null + } + + navigate(back, { state }); }); }, [data, location.state], ), }; + useEffect(() => { + if (select.exists) { + navigate(back); + } + }, [select.exists, navigate, back]) + return ( diff --git a/src/app/main/index.js b/src/app/main/index.js index 21a36457a..cdd351cd5 100644 --- a/src/app/main/index.js +++ b/src/app/main/index.js @@ -13,16 +13,16 @@ import TopHead from '../../containers/top-head'; 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 9c6149159..ea35a2940 100644 --- a/src/app/profile/index.js +++ b/src/app/profile/index.js @@ -1,5 +1,4 @@ -import { memo, useCallback, useMemo } from 'react'; -import { useParams } from 'react-router-dom'; +import { memo } from 'react'; import useStore from '../../hooks/use-store'; import useSelector from '../../hooks/use-selector'; import useTranslate from '../../hooks/use-translate'; @@ -8,7 +7,6 @@ import PageLayout from '../../components/page-layout'; import Head from '../../components/head'; import Navigation from '../../containers/navigation'; import Spinner from '../../components/spinner'; -import ArticleCard from '../../components/article-card'; import LocaleSelect from '../../containers/locale-select'; import TopHead from '../../containers/top-head'; import ProfileCard from '../../components/profile-card'; @@ -16,6 +14,8 @@ import ProfileCard from '../../components/profile-card'; function Profile() { const store = useStore(); + const { t } = useTranslate(); + useInit(() => { store.actions.profile.load(); }, []); @@ -25,8 +25,6 @@ function Profile() { waiting: state.profile.waiting, })); - const { t } = useTranslate(); - return ( @@ -35,7 +33,7 @@ function Profile() { - + ); diff --git a/src/components/article-card/index.js b/src/components/article-card/index.js index e68016256..8a2faf902 100644 --- a/src/components/article-card/index.js +++ b/src/components/article-card/index.js @@ -11,21 +11,21 @@ function ArticleCard(props) {
{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')}

+