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
10 changes: 6 additions & 4 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, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import useStore from '../../hooks/use-store';
import useTranslate from '../../hooks/use-translate';
Expand All @@ -14,19 +14,21 @@ import { useDispatch, useSelector } from 'react-redux';
import shallowequal from 'shallowequal';
import articleActions from '../../store-redux/article/actions';
import HeadLayout from '../../components/head-layout';
import CommentsList from '../../components/comments-list'

function Article() {
const store = useStore();

const dispatch = useDispatch();
// Параметры из пути /articles/:id

const { t, lang } = useTranslate();

const params = useParams();

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

const select = useSelector(
state => ({
Expand All @@ -36,7 +38,6 @@ function Article() {
shallowequal,
); // Нужно указать функцию для сравнения свойства объекта, так как хуком вернули объект

const { t } = useTranslate();

const callbacks = {
// Добавление в корзину
Expand All @@ -56,6 +57,7 @@ function Article() {
<Spinner active={select.waiting}>
<ArticleCard article={select.article} onAdd={callbacks.addToBasket} t={t} />
</Spinner>
<CommentsList />
</PageLayout>
</>
);
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 @@ -13,16 +13,16 @@ 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
8 changes: 4 additions & 4 deletions src/components/article-card/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,22 @@ function ArticleCard(props) {
<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.madeIn')}:</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.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.edition')}:</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.price')}:</div>
<div className={cn('value')}>{numberFormat(article.price)} ₽</div>
</div>
<Button style="primary" onClick={() => onAdd(article._id)} title={t('article.add')} />
Expand Down
42 changes: 42 additions & 0 deletions src/components/comment-form/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { memo } from 'react';
import PropTypes from 'prop-types';
import { cn as bem } from '@bem-react/classname';
import Button from '../button';
import './style.css';

function CommentForm({ title, onSubmit, submitTitle, cancelTitle, onCancel, onChange, value }) {
const cn = bem('CommentForm');

return (
<form className={cn()} onSubmit={onSubmit}>
<h3 className={cn('title')}>{title}</h3>
<textarea
className={cn('text')}
name='text' id='text'
value={value}
onChange={e => onChange(e.target.value)}></textarea>
<div className={cn('actions')}>
<Button style="primary" type="submit" title={submitTitle} />
{value && <Button
onClick={onCancel}
type="button"
style={'outline'}
title={cancelTitle}>
</Button>}

</div>
</form>
);
}

CommentForm.propTypes = {
onSubmit: PropTypes.func,
onCancel: PropTypes.func,
onChange: PropTypes.func,
title: PropTypes.string,
submitTitle: PropTypes.string,
cancelTitle: PropTypes.string,
value: PropTypes.string
};

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;
width: 100%;
}

.CommentForm-title {
font-size: 16px;
}

.CommentForm-text {
padding: 8px 12px;
font-size: 14px;
width: 100%;
height: 88px;
resize: none;
border-radius: 4px;
border: 1px solid var(--filter-border);

&:focus{
outline: none;
}
}

.CommentForm-actions {
display: flex;
gap: 16px;
}
40 changes: 40 additions & 0 deletions src/components/comment/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { memo } from 'react';
import PropTypes from 'prop-types';
import { cn as bem } from '@bem-react/classname';
import './style.css';
import dateFormat from '../../utils/date-format'

function Comment({ comment, onAnswer = () => {}, t}) {

const cn = bem('Comment');
return (
<div className={cn()}>
<div className={cn('header')}>
<h3 className={cn('title')}>{comment.author}</h3>
<time className={cn('date')} dateTime={comment.dateCreate}>{comment.dateCreate && dateFormat(comment.dateCreate)}</time>
</div>
<div className={cn('text')}>
<p>{comment.text}</p>
</div>
<button
className={cn('action')}
onClick={onAnswer}
>
{t('comments.answer')}
</button>
</div>
);
}

Comment.propTypes = {
comment: PropTypes.shape({
id: PropTypes.string,
text: PropTypes.string,
dateCreate: PropTypes.string,
author: PropTypes.string,
}),
t: PropTypes.func,
onAnswer: PropTypes.func,
};

export default memo(Comment);
39 changes: 39 additions & 0 deletions src/components/comment/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.Comment {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 6px;
/* padding-block: 24px; */
font-size: 12px;
}

.Comment-header {
display: flex;
align-items: center;
gap: 12px;
}

.Comment-title {
font-size: 12px;
}

.Comment-date {
color: var(--date);
}

.Comment-text {
font-size: 14px;
line-height: 143%;
}

.Comment-text > p {
margin: 0;
}

.Comment-action {
padding: 0;
font-family: var(--second-font-family);
font-weight: 700;
color: var(--primary);
}

128 changes: 128 additions & 0 deletions src/components/comments-list/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { memo, useCallback, useMemo, useState } from 'react';
import { cn as bem } from '@bem-react/classname';
import './style.css';
import Comment from '../comment'
import useInit from '../../hooks/use-init';
import { useParams } from 'react-router-dom';
import { useDispatch, useSelector as useSelectorRedux } from 'react-redux';
import listToTree from '../../utils/list-to-tree'
import treeToList from '../../utils/tree-to-list'
import commentsActions from '../../store-redux/comments/actions'
import shallowequal from 'shallowequal';
import useSelector from '../../hooks/use-selector'
import LoginMessage from '../login-message'
import CommentForm from '../comment-form'
import useTranslate from '../../hooks/use-translate'


function CommentsList() {
const dispatch = useDispatch();
// Параметры из пути /articles/:id
const params = useParams();
const [newComment, setNewComment] = useState({text: '', parentId: params.id, parentType: 'article'})
const [currentComment, setCurrentComment] = useState(params.id)
const { t } =useTranslate()

useInit(() => {
dispatch(commentsActions.load(params.id))
}, [params.id]);

const select = useSelector(state => ({
exist: state.session.exists
}))

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

const options = {
comments: useMemo(
() => [
...treeToList(listToTree(selectRedux.comments.items, '_id', '_type'), (comment, level) => ({
id: comment._id,
level: level * 40,
text: comment.text,
dateCreate: comment.dateCreate,
author: comment.author?.profile.name,
parent: comment.parent?._type,
parentId: comment.parent?._id
}))
],
[selectRedux.comments]
),
}

const callbacks = {
onSubmit: useCallback((e) => {
e.preventDefault()
dispatch(commentsActions.post(newComment))
setNewComment({ text: '', parentId: params.id, parentType: 'article' });
setCurrentComment(params.id)
dispatch(commentsActions.load(params.id))
}, [newComment, params.id, dispatch]),
onChange: useCallback((value) => {
setNewComment(prev => ({...prev, text: value}))
}),
onCancel: useCallback(() => {
setNewComment({text: '', parentId: params.id, parentType: 'article'})
setCurrentComment(params.id)
}),
onAnswer: useCallback((commentId) => {
setNewComment(prev => ({...prev, parentId: commentId, parentType: 'comment'}))
setCurrentComment(commentId)
})
}

const cn = bem('CommentsList');
return (
<div className={cn()}>
<h2>{t('comments.title')} ({selectRedux.comments.count})</h2>
{<ul className={cn('list')}>
{options.comments.map(comment => (
<li
className={cn('item')}
key={comment.id}
style={{marginLeft: 1 * comment.level}}
>
<Comment
comment={comment}
onAnswer={() => callbacks.onAnswer(comment.id)}
t={t}
/>
{currentComment === comment.id && (select.exist
? <CommentForm
title={t('comments.formAnswer')}
submitTitle={t('comments.submit')}
cancelTitle={t('comments.cancel')}
value={newComment.text}
onSubmit={callbacks.onSubmit}
onChange={callbacks.onChange}
onCancel={callbacks.onCancel}
/>
: <LoginMessage />
)}
</li>
)
)}
</ul>}
{currentComment === params.id && (select.exist
? <CommentForm
title={t('comments.formComment')}
submitTitle={t('comments.submit')}
cancelTitle={t('comments.cancel')}
value={newComment.text}
onSubmit={callbacks.onSubmit}
onChange={callbacks.onChange}
onCancel={callbacks.onCancel}
/>
: <LoginMessage />
)}
</div>
);
}

export default memo(CommentsList);
16 changes: 16 additions & 0 deletions src/components/comments-list/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.CommentsList {
display: flex;
flex-direction: column;
gap: 24px;
padding-block: 26px;
}

.CommentsList-list {
display: flex;
flex-direction: column;
gap: 16px;

margin: 0;
padding: 0;
list-style: none;
}
Loading