diff --git a/package-lock.json b/package-lock.json index 59bb3583a..2e2b2a549 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "babel-loader": "^9.1.3", "cross-env": "^7.0.3", "css-loader": "^7.1.2", + "file-loader": "^6.2.0", "html-webpack-plugin": "^5.6.0", "jest": "^29.7.0", "mini-css-extract-plugin": "^2.9.1", @@ -3988,6 +3989,16 @@ "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "dev": true }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -5006,6 +5017,16 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -5327,6 +5348,27 @@ "bser": "2.1.1" } }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -8148,6 +8190,21 @@ "node": ">=6.11.5" } }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -13919,6 +13976,12 @@ "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "dev": true }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, "binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -14639,6 +14702,12 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, "encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -14889,6 +14958,16 @@ "bser": "2.1.1" } }, + "file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + } + }, "fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -16910,6 +16989,17 @@ "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", "dev": true }, + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", diff --git a/package.json b/package.json index 5da2f1714..707304945 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "babel-loader": "^9.1.3", "cross-env": "^7.0.3", "css-loader": "^7.1.2", + "file-loader": "^6.2.0", "html-webpack-plugin": "^5.6.0", "jest": "^29.7.0", "mini-css-extract-plugin": "^2.9.1", diff --git a/src/api/index.js b/src/api/index.js index a5f073295..fbdad2cb5 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -1,8 +1,4 @@ class APIService { - /** - * @param services {Services} Менеджер сервисов - * @param config {Object} - */ constructor(services, config = {}) { this.services = services; this.config = config; @@ -11,35 +7,25 @@ class APIService { }; } - /** - * HTTP запрос - * @param url - * @param method - * @param headers - * @param options - * @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 }; + setHeader(name, value = null) { + if (value != null) this.defaultHeaders[name] = value; + else delete this.defaultHeaders[name]; } - /** - * Установка или сброс заголовка - * @param name {String} Название заголовка - * @param value {String|null} Значение заголовка - */ - setHeader(name, value = null) { - if (value) { - this.defaultHeaders[name] = value; - } else if (this.defaultHeaders[name]) { - delete this.defaultHeaders[name]; - } + async request({ url, method='GET', headers={}, ...opts }) { + if (!/^(http|\/\/)/.test(url)) url = this.config.baseUrl + url; + + const langHeader = this.services.i18n.lang; + const finalHeaders = { + ...this.defaultHeaders, + 'Accept-Language': langHeader, + ...headers, + }; + + const res = await fetch(url, { method, headers: finalHeaders, ...opts }); + const data = await res.json(); + + return { data, status: res.status, headers: res.headers }; } } diff --git a/src/app/article/index.js b/src/app/article/index.js index 54f037b64..7ff8bdf73 100644 --- a/src/app/article/index.js +++ b/src/app/article/index.js @@ -1,64 +1,71 @@ -import { memo, useCallback } from 'react'; -import { useParams } from 'react-router-dom'; -import useStore from '../../hooks/use-store'; -import useTranslate from '../../hooks/use-translate'; -import useInit from '../../hooks/use-init'; -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 { useDispatch, useSelector } from 'react-redux'; -import shallowequal from 'shallowequal'; -import articleActions from '../../store-redux/article/actions'; -import HeadLayout from '../../components/head-layout'; +import React, { memo, useCallback } from 'react' +import { useParams } from 'react-router-dom' +import useStore from '../../hooks/use-store' +import useTranslate from '../../hooks/use-translate' +import { useArticle } from '../../hooks/use-article' +import { useComments } from '../../hooks/use-comments' +import PageLayout from '../../components/page-layout' +import Head from '../../components/head' +import Spinner from '../../components/spinner' +import ArticleCard from '../../components/article-card' +import LocaleSelect from '../../containers/locale-select' +import TopHead from '../../containers/top-head' +import Navigation from '../../containers/navigation' +import HeadLayout from '../../components/head-layout' +import CommentsSection from '../../components/comments-section' function Article() { - const store = useStore(); + const store = useStore() + const { t, lang } = useTranslate() + const { id } = useParams() + const { article, waiting: artLoading } = useArticle(id) + const { + comments, waiting: comLoading, + replyTo, text, setText, + onReplyClick, onSubmit + } = useComments(id) - const dispatch = useDispatch(); - // Параметры из пути /articles/:id - - const params = useParams(); - - useInit(() => { - //store.actions.article.load(params.id); - dispatch(articleActions.load(params.id)); - }, [params.id]); - - const select = useSelector( - state => ({ - article: state.article.data, - waiting: state.article.waiting, - }), - shallowequal, - ); // Нужно указать функцию для сравнения свойства объекта, так как хуком вернули объект - - const { t } = useTranslate(); - - const callbacks = { - // Добавление в корзину - addToBasket: useCallback(_id => store.actions.basket.addToBasket(_id), [store]), - }; + const isAuth = Boolean(localStorage.getItem('token')) + const addToBasket = useCallback( + id => store.actions.basket.addToBasket(id), + [store] + ) return ( <> - + - - + + - - + + + +
+

{t('comments.heading')} ({comments.length})

+ + + +
- ); + ) } -export default memo(Article); +export default memo(Article) diff --git a/src/app/basket/index.js b/src/app/basket/index.js index 52a7badfd..865f3b8e1 100644 --- a/src/app/basket/index.js +++ b/src/app/basket/index.js @@ -2,7 +2,7 @@ import { memo, useCallback } from 'react'; import { useDispatch, useStore as useStoreRedux } from 'react-redux'; import useStore from '../../hooks/use-store'; import useSelector from '../../hooks/use-selector'; -import useInit from '../../hooks/use-init'; + import useTranslate from '../../hooks/use-translate'; import ItemBasket from '../../components/item-basket'; import List from '../../components/list'; @@ -21,11 +21,10 @@ function Basket() { })); const callbacks = { - // Удаление из корзины + removeFromBasket: useCallback(_id => store.actions.basket.removeFromBasket(_id), [store]), - // Закрытие любой модалки + closeModal: useCallback(() => { - //store.actions.modals.close(); dispatch(modalsActions.close()); }, [store]), }; diff --git a/src/app/index.js b/src/app/index.js index c54c2afc5..3014dc1e6 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -1,6 +1,4 @@ -import { useCallback, useContext, useEffect, useState } from 'react'; import { Routes, Route } from 'react-router-dom'; -import useSelector from '../hooks/use-selector'; import useStore from '../hooks/use-store'; import useInit from '../hooks/use-init'; import Main from './main'; @@ -21,6 +19,7 @@ function App() { await store.actions.session.remind(); }); + const activeModal = useSelectorRedux(state => state.modals.name); return ( diff --git a/src/app/profile/index.js b/src/app/profile/index.js index 166d127fb..4b6ea76c6 100644 --- a/src/app/profile/index.js +++ b/src/app/profile/index.js @@ -24,7 +24,7 @@ function Profile() { waiting: state.profile.waiting, })); - const { t } = useTranslate(); +const { t, lang } = useTranslate(); return ( <> @@ -37,7 +37,7 @@ function Profile() { - + diff --git a/src/assets/icon/cart_empty.png b/src/assets/icon/cart_empty.png new file mode 100644 index 000000000..ab1c4b449 Binary files /dev/null and b/src/assets/icon/cart_empty.png differ diff --git a/src/components/article-card/index.js b/src/components/article-card/index.js index d636d9f88..68875cfef 100644 --- a/src/components/article-card/index.js +++ b/src/components/article-card/index.js @@ -13,22 +13,22 @@ function ArticleCard(props) {
{article.description}
-
Страна производитель:
+
{t('article.country.dev')}:
{article.madeIn?.title} ({article.madeIn?.code})
-
Категория:
+
{t('article.product.cat')}:
{article.category?.title}
-
Год выпуска:
+
{t('article.country.dev')}:
{article.edition}
-
Цена:
+
{t('article.price')}:
{numberFormat(article.price)} ₽
+ + {comment.replies.length > 0 && ( +
+ {comment.replies.map(child => ( + + ))} +
+ )} + + {/* Форма ответа */} + {isAuthorized && replyTo === comment.id && ( +
+

Новый ответ

+