From fa509d9b2470fae7fd07b762d65010a67446bdad Mon Sep 17 00:00:00 2001 From: protonio git-H Date: Thu, 10 Oct 2024 21:17:58 +0300 Subject: [PATCH 1/5] add i18n service --- src/api/index.js | 4 ++++ src/hooks/use-translate.js | 27 +++++++++++++++++++++++-- src/i18n/index.js | 40 ++++++++++++++++++++++++++++++++++++++ src/services.js | 8 ++++++++ 4 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 src/i18n/index.js diff --git a/src/api/index.js b/src/api/index.js index a5f073295..a44483704 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -41,6 +41,10 @@ class APIService { delete this.defaultHeaders[name]; } } + + setLangHeader(lang) { + this.setHeader('X-lang', lang); + } } export default APIService; diff --git a/src/hooks/use-translate.js b/src/hooks/use-translate.js index bdcbf1071..5626745b5 100644 --- a/src/hooks/use-translate.js +++ b/src/hooks/use-translate.js @@ -1,9 +1,32 @@ -import { useCallback, useContext } from 'react'; +import { useCallback, useContext, useEffect, useState } from 'react'; import { I18nContext } from '../i18n/context'; +import useServices from './use-services'; /** * Хук возвращает функцию для локализации текстов, код языка и функцию его смены */ export default function useTranslate() { - return useContext(I18nContext); + const i18nService = useServices().i18n; + + const [currLang, setCurrLang] = useState(i18nService.lang); + + useEffect(() => { + const langChange = language => { + setCurrLang(language); + }; + const sub = i18nService.subscribe(langChange); + + return () => { + sub(); + }; + }, [i18nService, currLang]); + + const t = useCallback( + (text, plural) => { + return i18nService.translate(text, plural); + }, + [i18nService, currLang], + ); + + return { t, lang: currLang, setLang: i18nService.setLang }; } diff --git a/src/i18n/index.js b/src/i18n/index.js new file mode 100644 index 000000000..7dc87c604 --- /dev/null +++ b/src/i18n/index.js @@ -0,0 +1,40 @@ +import * as translations from './translations'; + +class I18NService { + constructor(services) { + this.services = services; + this.lang = 'ru'; + this.listeners = []; + } + + setLang = lang => { + this.lang = lang; + this.listeners.forEach(subscriber => subscriber(lang)); + this.services.api.setLangHeader(lang); + }; + + translate = (text, plural) => { + let result = + translations[this.lang] && text in translations[this.lang] + ? translations[this.lang][text] + : text; + + if (typeof plural !== 'undefined') { + const key = new Intl.PluralRules(this.lang).select(plural); + if (key in result) { + result = result[key]; + } + } + + return result; + }; + + subscribe(listener) { + this.listeners.push(listener); + return () => { + this.listeners = this.listeners.filter(item => item !== listener); + }; + } +} + +export default I18NService; diff --git a/src/services.js b/src/services.js index a32b14d57..b3aaaa8d8 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); + } + return this._i18n; + } } export default Services; From 25fdbd81b80d69c1c37c73d198a590441f99e527 Mon Sep 17 00:00:00 2001 From: protonio git-H Date: Fri, 11 Oct 2024 20:38:58 +0300 Subject: [PATCH 2/5] add main task --- src/app/article/index.js | 79 ++++++++++--- src/components/comment-area/index.js | 108 ++++++++++++++++++ src/components/comment-area/style.css | 39 +++++++ src/components/comment/index.js | 147 +++++++++++++++++++++++++ src/components/comment/style.css | 40 +++++++ src/components/comments-list/index.js | 32 ++++++ src/components/comments-list/style.css | 0 src/components/comments/index.js | 78 +++++++++++++ src/components/comments/style.css | 16 +++ src/store-redux/comments/actions.js | 54 +++++++++ src/store-redux/comments/reducer.js | 81 ++++++++++++++ src/store-redux/exports.js | 1 + src/utils/find-comment.js | 18 +++ src/utils/new-comments.js | 19 ++++ 14 files changed, 699 insertions(+), 13 deletions(-) create mode 100644 src/components/comment-area/index.js create mode 100644 src/components/comment-area/style.css create mode 100644 src/components/comment/index.js create mode 100644 src/components/comment/style.css create mode 100644 src/components/comments-list/index.js create mode 100644 src/components/comments-list/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 create mode 100644 src/utils/find-comment.js create mode 100644 src/utils/new-comments.js diff --git a/src/app/article/index.js b/src/app/article/index.js index 56859bc76..361fd2dc7 100644 --- a/src/app/article/index.js +++ b/src/app/article/index.js @@ -1,8 +1,12 @@ -import { memo, useCallback, useMemo } from 'react'; -import { useParams } from 'react-router-dom'; +import { memo, useCallback } from 'react'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; +import shallowequal from 'shallowequal'; import useStore from '../../hooks/use-store'; import useTranslate from '../../hooks/use-translate'; import useInit from '../../hooks/use-init'; +import useSelectorStore from '../../hooks/use-selector'; +import useServices from '../../hooks/use-services'; import PageLayout from '../../components/page-layout'; import Head from '../../components/head'; import Navigation from '../../containers/navigation'; @@ -10,36 +14,68 @@ 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 Comments from '../../components/comments'; import articleActions from '../../store-redux/article/actions'; +import commentsActions from '../../store-redux/comments/actions'; +import { cn as bem } from '@bem-react/classname'; function Article() { const store = useStore(); - + const services = useServices(); + const cn = bem('ArticleCard'); const dispatch = useDispatch(); - // Параметры из пути /articles/:id - const params = useParams(); + const location = useLocation(); + const navigate = useNavigate(); + const { t, lang } = useTranslate(); + // Инициализация загрузки данных useInit(() => { - //store.actions.article.load(params.id); + dispatch(commentsActions.load(params.id)); dispatch(articleActions.load(params.id)); - }, [params.id]); + }, [params.id, lang]); + // Селекторы состояния const select = useSelector( state => ({ article: state.article.data, waiting: state.article.waiting, + count: state.comments.count, + list: state.comments.list, + isWaiting: state.comments.isWaiting, }), shallowequal, - ); // Нужно указать функцию для сравнения свойства объекта, так как хуком вернули объект + ); + + const selectStore = useSelectorStore(state => ({ + exists: state.session.exists, + user: state.session.user, + })); - const { t } = useTranslate(); + const currentName = selectStore.user?.profile?.name; + // Обработчики событий const callbacks = { - // Добавление в корзину addToBasket: useCallback(_id => store.actions.basket.addToBasket(_id), [store]), + createFirstComment: useCallback( + (value, type) => dispatch(commentsActions.createComment(params.id, value, type)), + [dispatch, params.id], + ), + createAnswerComment: useCallback( + (id, value, type) => dispatch(commentsActions.createComment(id, value, type)), + [dispatch], + ), + loadComments: useCallback( + () => dispatch(commentsActions.load(params.id)), + [dispatch, params.id], + ), + loginNavigate: useCallback( + e => { + e.preventDefault(); + navigate('/login', { state: { back: location.pathname } }); + }, + [navigate, location.pathname], + ), }; return ( @@ -50,8 +86,25 @@ function Article() { - + + ); } diff --git a/src/components/comment-area/index.js b/src/components/comment-area/index.js new file mode 100644 index 000000000..217e72476 --- /dev/null +++ b/src/components/comment-area/index.js @@ -0,0 +1,108 @@ +import React, { memo, useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { cn as bem } from '@bem-react/classname'; +import './style.css'; + +const CommentArea = ({ + title, + cancel, + createFirstComment, + createAnswerComment, + itemId, + load, + isAuth, + lvl, + parent, + margin, + mainClass, + loginNavigate, +}) => { + const [area, setArea] = useState(''); + const cn = bem('CommentArea'); + + useEffect(() => { + const buttons = document.querySelectorAll('.CommentArea-cancel-btn'); + const activeClass = 'CommentArea--active'; + + buttons.forEach(button => { + button.addEventListener('click', () => { + button.closest(`.${cn()}`)?.classList.remove(activeClass); + document.querySelector('.Main').style.display = 'block'; + }); + }); + + return () => { + buttons.forEach(button => button.removeEventListener('click', () => {})); + }; + }, []); + + const onSend = e => { + e.preventDefault(); + if (area.trim()) { + if (itemId) { + createAnswerComment(itemId, area, 'comment'); + } else { + createFirstComment(area, 'article'); + } + setArea(''); + document.querySelector('.Main').style.display = 'block'; + } + }; + + return ( +
+ {isAuth ? ( + <> +
{title}
+