Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api-examples/comments.http
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Комментрии к товару

GET http://query.rest/api/v1/comments?fields=items(_id,text,dateCreate,author(profile(name)),parent(_id,_type),isDeleted),count&limit=*&search[parent]=670260bb7dd498df5525e5ed
GET http://query.rest/api/v1/comments?fields=items(_id,text,dateCreate,author(profile(name)),parent(_id,_type),isDeleted),count&limit=*&search[parent]=67fb67e98702ec3fc67fef7e

# Остальные методы в http://query.rest/api/v1/docs/ (http://localhost:8010/api/v1/docs/)

Expand Down
41 changes: 36 additions & 5 deletions src/app/article/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { memo, useCallback } from 'react';
import { memo, useCallback, useEffect, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import useStore from '../../hooks/use-store';
import useTranslate from '../../hooks/use-translate';
Expand All @@ -10,10 +10,15 @@ 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 commentActions from '../../store-redux/comment/actions';
import HeadLayout from '../../components/head-layout';
import Comment from '../../components/comment';
import listToTree from '../../utils/list-to-tree';
import treeToList from '../../utils/tree-to-list';
import useSelector from '../../hooks/use-selector';

function Article() {
const store = useStore();
Expand All @@ -23,20 +28,45 @@ function Article() {

const params = useParams();

const { t, lang } = useTranslate();

useInit(() => {
//store.actions.article.load(params.id);
dispatch(articleActions.load(params.id));
}, [params.id]);
dispatch(commentActions.load(params.id));
}, [params.id, lang]);

const select = useSelector(
const select = useSelectorRedux(
state => ({
article: state.article.data,
waiting: state.article.waiting,
}),
shallowequal,
); // Нужно указать функцию для сравнения свойства объекта, так как хуком вернули объект

const { t } = useTranslate();
const selectComment = useSelectorRedux(
state => ({
comment: state.comment.data,
postRes: state.comment.postRes,
waitingComment: state.comment.waiting,
}),
shallowequal,
);

const selectUser = useSelector(state => ({
user: state.session.user,
}));

const commentList = useMemo(() => {
if (!selectComment.waitingComment) {
return [
...treeToList(listToTree(selectComment.comment.items, '_id', 'article'), (item, level) => ({
...item,
level: level,
})),
];
} else return [];
}, [selectComment.comment, selectComment.postRes]);

const callbacks = {
// Добавление в корзину
Expand All @@ -55,6 +85,7 @@ function Article() {
<Navigation />
<Spinner active={select.waiting}>
<ArticleCard article={select.article} onAdd={callbacks.addToBasket} t={t} />
<Comment count={selectComment.comment.count} comments={commentList} />
</Spinner>
</PageLayout>
</>
Expand Down
4 changes: 2 additions & 2 deletions src/app/login/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ import HeadLayout from '../../components/head-layout';
import Form from '../../components/form';

function Login() {
const { t } = useTranslate();
const { t, lang } = useTranslate();
const location = useLocation();
const navigate = useNavigate();
const store = useStore();

useInit(() => {
store.actions.session.resetErrors();
});
}, [lang]);

const select = useSelector(state => ({
waiting: state.session.waiting,
Expand Down
4 changes: 2 additions & 2 deletions src/app/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ import HeadLayout from '../../components/head-layout';
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 (
<>
Expand Down
6 changes: 3 additions & 3 deletions src/app/profile/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ import HeadLayout from '../../components/head-layout';
function Profile() {
const store = useStore();

const { t, lang } = useTranslate();

useInit(() => {
store.actions.profile.load();
}, []);
}, [lang]);

const select = useSelector(state => ({
profile: state.profile.data,
waiting: state.profile.waiting,
}));

const { t } = useTranslate();

return (
<>
<HeadLayout>
Expand Down
9 changes: 5 additions & 4 deletions src/components/article-card/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,29 @@ import './style.css';

function ArticleCard(props) {
const { article, onAdd = () => {}, t = text => text } = props;

const cn = bem('ArticleCard');
return (
<div className={cn()}>
<div className={cn('description')}>{article.description}</div>
<div className={cn('prop-wrapper')}>
<div className={cn('prop')}>
<div className={cn('label')}>Страна производитель:</div>
<div className={cn('label')}>{t("article-card.origin-country")}:</div>
<div className={cn('value')}>
{article.madeIn?.title} ({article.madeIn?.code})
</div>
</div>
<div className={cn('prop')}>
<div className={cn('label')}>Категория:</div>
<div className={cn('label')}>{t("article-card.category")}:</div>
<div className={cn('value')}>{article.category?.title}</div>
</div>
<div className={cn('prop')}>
<div className={cn('label')}>Год выпуска:</div>
<div className={cn('label')}>{t("article-card.release-year")}:</div>
<div className={cn('value')}>{article.edition}</div>
</div>
</div>
<div className={cn('prop', { size: 'big' })}>
<div className={cn('label')}>Цена:</div>
<div className={cn('label')}>{t("article-card.price")}:</div>
<div className={cn('value')}>{numberFormat(article.price)} ₽</div>
</div>
<Button style="primary" onClick={() => onAdd(article._id)} title={t('article.add')} />
Expand Down
92 changes: 92 additions & 0 deletions src/components/comment-form/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { memo, useEffect } from 'react';
import PropTypes from 'prop-types';
import { cn as bem } from '@bem-react/classname';
import './style.css';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { useState } from 'react';
import Button from '../button';
import useSelector from '../../hooks/use-selector';
import { useSelector as useSelectorRedux } from 'react-redux';
import { useDispatch } from 'react-redux';
import commentActions from '../../store-redux/comment/actions';
import commentFormAction from '../../store-redux/comment-form/actions';
import useTranslate from '../../hooks/use-translate';
import { useCallback } from 'react';


function CommentForm() {
const cn = bem('CommentForm');
const [textValue, setTextValue] = useState('');

const dispatch = useDispatch();
const params = useParams();
const { t } = useTranslate();
const navigate = useNavigate();

const select = useSelector(state => ({
token: state.session.token,
exists: state.session.exists,
}));

const selectPlace = useSelectorRedux(state => state.commentForm.place);
const selectData = useSelectorRedux(state => state.comment.data)

const isCommonPlace = selectPlace === 'common';

const onSignIn = useCallback(() => {
navigate('/login', { state: { back: location.pathname } });
}, [location.pathname]);

const handlePostComment = () => {
if (textValue.trim().length !== 0) {
if (isCommonPlace) {
dispatch(commentActions.post(select.token, textValue, params.id));

} else {
dispatch(commentActions.reply(select.token, textValue, selectPlace));
}
}
setTextValue('');
};

return (
<div className={cn()} style={{ marginLeft: `${!isCommonPlace ? '40px' : '0'}` }}>
{select.exists ? (
<>
<span className={cn('title')}>
{isCommonPlace ? t('comment-form.new-comment') : t('comment-form.new-reply')}
</span>
<textarea
className={cn('textarea')}
value={textValue}
onChange={e => setTextValue(e.target.value)}
rows={5}
></textarea>
<div className={cn('buttons')}>
<Button onClick={handlePostComment} title={t('comment-form.send')} style="primary" />
{!isCommonPlace && (
<Button
title={t('comment-form.cancel')}
style="outline"
onClick={() => dispatch(commentFormAction.reset())}
/>
)}
</div>
</>
) : (
<div className={cn('unauthorized')}>
<button className={cn('link-login')} onClick={onSignIn}>
{t('comment.login')}
</button>
{t('comment.login-able-to-comment')}
</div>
)}
</div>
);
}

CommentForm.propTypes = {
children: PropTypes.node,
};

export default memo(CommentForm);
29 changes: 29 additions & 0 deletions src/components/comment-form/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.CommentForm {
display: flex;
flex-direction: column;
gap: 16px;
}

.CommentForm-buttons {
display: flex;
gap: 16px;
}

.CommentForm-textarea {
font-family: var(--font-family);
padding: 8px 12px;
color: var(--main-text);
&::placeholder {
color: var(--main-text);
}
&:focus {
outline: none;
}
}

.CommentForm-link-login {
padding: 0;
color: var(--primary);
font-family: var(--font-family);
font-weight: 400;
}
84 changes: 84 additions & 0 deletions src/components/comment-item/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { memo, useEffect, useRef, useState } from 'react';
import { cn as bem } from '@bem-react/classname';
import formatISODateToCustomString from '../../utils/format-iso-date';

import './style.css';
import CommentList from '../comment-list';
import CommentForm from '../comment-form';
import { useDispatch } from 'react-redux';
import commentFormActions from '../../store-redux/comment-form/actions';
import { useSelector as useSelectorRedux } from 'react-redux';
import useSelector from '../../hooks/use-selector';
import shallowequal from 'shallowequal';
import useTranslate from '../../hooks/use-translate';

function CommentItem({ item }) {
const cn = bem('CommentItem');
const dispatch = useDispatch();
const { t } = useTranslate();
const ref = useRef(null);
const [shouldScroll, setShouldScroll] = useState(false);

useEffect(() => {
if (shouldScroll) {
ref.current?.scrollIntoView({ behavior: 'smooth' });
setShouldScroll(false);
}
}, [shouldScroll]);

const select = useSelector(state => ({
user: state.session.user,
exists: state.session.exists,
}));

const selectCommentForm = useSelectorRedux(
state => ({
place: state.commentForm.place,
}),
shallowequal,
);

const scrollToTarget = () => {
setShouldScroll(true);
};

return (
<>
<div className={cn()}>
<div className={cn('head')}>
<span
className={`${cn('name')} ${item.author._id === select.user._id ? ' auth-user-name' : ''}`}
>
{item.author?.profile?.name || select.user.profile.name}
</span>
<span className={cn('date')}>{formatISODateToCustomString(item.dateCreate)}</span>
</div>
<p className={cn('text')}>{item.text}</p>
<button
className={cn('button')}
onClick={() => {
dispatch(commentFormActions.change(item._id));
scrollToTarget();
}}
>
{t('comment-item.reply')}
</button>
</div>
{item.children.length !== 0 && (
<CommentList
comments={item.children.map(el => ({ ...el, level: item.level + 1 }))}
curLevel={item.level + 1}
/>
)}
{selectCommentForm.place === item._id && (
<div className={cn('form')} ref={ref}>
<CommentForm />
</div>
)}
</>
);
}

Comment.propTypes = {};

export default memo(CommentItem);
Loading