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
13 changes: 13 additions & 0 deletions src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ class APIService {
this.defaultHeaders = {
'Content-Type': 'application/json',
};

const i18n = this.services?.i18n;
if (i18n) {
this.setHeader('X-Lang', i18n.locale);
i18n.subscribe(locale => {
return this.setHeader('X-Lang', locale);
});
}
}

/**
Expand Down Expand Up @@ -41,6 +49,11 @@ class APIService {
delete this.defaultHeaders[name];
}
}

updateLanguageHeader(locale) {
this.languageHeader = locale;
this.setHeader('Accept-Language', locale);
}
}

export default APIService;
19 changes: 17 additions & 2 deletions src/app/article/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,31 @@ import TopHead from '../../containers/top-head';
import { useDispatch, useSelector } from 'react-redux';
import shallowequal from 'shallowequal';
import articleActions from '../../store-redux/article/actions';
import commentsActions from '../../store-redux/comments/actions';
import HeadLayout from '../../components/head-layout';
import listToTree from '../../utils/list-to-tree';
import CommentList from '../../components/comment-list';

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

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

const params = useParams();

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

const select = useSelector(
state => ({
article: state.article.data,
waiting: state.article.waiting,
comments: state.comments.data,
count: state.comments.count,
}),
shallowequal,
); // Нужно указать функцию для сравнения свойства объекта, так как хуком вернули объект
Expand All @@ -42,6 +48,14 @@ function Article() {
// Добавление в корзину
addToBasket: useCallback(_id => store.actions.basket.addToBasket(_id), [store]),
};

let buildComments = [];

if (Array.isArray(select?.comments) && select.comments.length > 0) {
buildComments = listToTree(select?.comments)[0].children;
} else {
console.log('Комментарии еще не загружены или пусты');
}

return (
<>
Expand All @@ -55,6 +69,7 @@ function Article() {
<Navigation />
<Spinner active={select.waiting}>
<ArticleCard article={select.article} onAdd={callbacks.addToBasket} t={t} />
<CommentList comments={buildComments} commentCount={select?.count} />
</Spinner>
</PageLayout>
</>
Expand Down
1 change: 1 addition & 0 deletions src/app/login/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ function Login() {
e.preventDefault();
store.actions.session.signIn(data, () => {
// Возврат на страницу, с которой пришли
console.log('back', location.state?.back);
const back =
location.state?.back && location.state?.back !== location.pathname
? location.state?.back
Expand Down
4 changes: 3 additions & 1 deletion src/components/article-card/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ 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>
Expand All @@ -31,7 +33,7 @@ function ArticleCard(props) {
<div className={cn('label')}>Цена:</div>
<div className={cn('value')}>{numberFormat(article.price)} ₽</div>
</div>
<Button style="primary" onClick={() => onAdd(article._id)} title={t('article.add')} />
<Button style="primary" onClick={() => onAdd(article._id)} title={t('article.add')} />
</div>
);
}
Expand Down
184 changes: 184 additions & 0 deletions src/components/comment-card/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { memo, useCallback, useRef, useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { cn as bem } from '@bem-react/classname';
import './style.css';
import useSelector from '../../hooks/use-selector';
import dateFormat from '../../utils/date-format';
import CommentForm from '../comment-form';

function CommentCard(props) {
const {
comment,
isAuthenticated,
handleAddComment = () => {},
setAuthMessageCommentId = () => {}, authMessageCommentId,
replyToCommentId,
depth,
maxDepth,
setReplyToCommentId = () => {},
isReplyActive,
setIsReplyActive = () => {},
onChange = () => {},
errorMessage,
setErrorMessage,
handleLogin,
} = props;

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

const formRef = useRef(null);
const [currentFormRefId, setCurrentFormRefId] = useState(null);

const callbacks = {
handleReplyClick: useCallback(() => {
if (!isAuthenticated) {
setAuthMessageCommentId(comment._id);
} else {
if (replyToCommentId === comment._id) {
setReplyToCommentId(null);
setIsReplyActive(false);
setCurrentFormRefId(null);
setErrorMessage('');
} else {
setReplyToCommentId(comment._id);
setIsReplyActive(true);
setCurrentFormRefId(comment._id);
setErrorMessage('');
}
};
}, [isAuthenticated, replyToCommentId])
};

useEffect(() => {
if (isReplyActive && formRef.current) {
formRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, [replyToCommentId, currentFormRefId, isReplyActive]);

const hasChildren = comment.children && comment.children.length > 0;

const cn = bem('CommentCard');

return (
<div className={cn()}>
<div className={cn('info')}>
<div className={select?.user._id === comment.author._id ? 'CommentCard-username__auth' : cn('username')}>
{comment.author.profile?.name ? comment.author.profile?.name : select.user.profile?.name}
</div>
<div className={cn('date')}>
{dateFormat(comment.dateCreate)}
</div>
</div>
<div className={cn('comment')}>
{/* {comment._id}<br /> */}
{comment.text}
</div>
<div className={cn('answer')} onClick={callbacks.handleReplyClick}>
Ответить
</div>

{!isAuthenticated && (authMessageCommentId === comment._id) && (
<div className={cn('authcaution')}>
<span style={{ color: 'var(--primary)', cursor: 'pointer' }} onClick={handleLogin}>Войдите</span>, чтобы иметь возможность комментировать
</div>
)}

{isAuthenticated &&
(replyToCommentId === comment._id) &&
isReplyActive && (depth === maxDepth || !hasChildren) && (
<div ref={formRef}>
<CommentForm
commentTitle="Новый ответ"
type="reply"
setIsReplyActive={setIsReplyActive}
setReplyToCommentId={setReplyToCommentId}
onChange={onChange}
onSubmit={handleAddComment}
errorMessage={errorMessage}
/>
</div>
)}

{hasChildren && (depth < maxDepth) && (
<div className={cn('replies')}>
<div key={comment._id}>
{comment.children.map(child => (
<CommentCard
key={child._id}
comment={child}
handleAddComment={handleAddComment}
isAuthenticated={isAuthenticated}
setAuthMessageCommentId={setAuthMessageCommentId}
authMessageCommentId={authMessageCommentId}
replyToCommentId={replyToCommentId}
setReplyToCommentId={setReplyToCommentId}
isReplyActive={isReplyActive}
setIsReplyActive={setIsReplyActive}
onChange={onChange}
depth={depth + 1}
maxDepth={maxDepth}
errorMessage={errorMessage}
setErrorMessage={setErrorMessage}
handleLogin={handleLogin}
/>
))}
</div>
{isAuthenticated && (replyToCommentId === comment._id) && isReplyActive && (
<div ref={formRef}>
<CommentForm
className={cn('child')}
commentTitle="Новый ответ"
type="reply"
setIsReplyActive={setIsReplyActive}
setReplyToCommentId={setReplyToCommentId}
onChange={onChange}
// onSubmit={handleAddComment}
onSubmit={(newComment) => {
handleAddComment(newComment);
// setIsReplyActive(false);
// setReplyToCommentId(null);
}}
errorMessage={errorMessage}
/>
</div>
)}
</div>
)}
</div>
);
}

CommentCard.propTypes = {
comment: PropTypes.shape({
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
text: PropTypes.string,
dateCreate: PropTypes.string,
author: PropTypes.shape({
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
profile: PropTypes.shape({
name: PropTypes.string,
}),
}),
children: PropTypes.array,
parent: PropTypes.shape({
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
_type: PropTypes.string
}),
}).isRequired,
isAutheticated: PropTypes.bool,
handleAddComment: PropTypes.func,
setAuthMessageCommentId: PropTypes.func,
authMessageCommentId: PropTypes.string,
replyToCommentId: PropTypes.string,
depth: PropTypes.number,
maxDepth: PropTypes.number,
setReplyToCommentId: PropTypes.func,
isReplyActive: PropTypes.bool,
setIsReplyActive: PropTypes.func,
onChange: PropTypes.func,
handleLogin: PropTypes.func,
};

export default memo(CommentCard);
49 changes: 49 additions & 0 deletions src/components/comment-card/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
.CommentCard {

}

.CommentCard-info {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 6px;
font-size: 12px;
line-height: 18px;
}

.CommentCard-username {
font-weight: 700;
}

.CommentCard-username__auth {
color: #4B5563;
font-weight: 700;
}

.CommentCard-date {
color: var(--date-text);
}

.CommentCard-comment {
font-size: 14px;
line-height: 20px;
overflow-wrap: break-word;
}

.CommentCard-answer {
font-size: 12px;
line-height: 18px;
color: var(--primary);
font-weight: 700;
margin-bottom: 6px;
cursor: pointer;
}

.CommentCard-replies {
margin-left: 40px;
margin-bottom: 16px;
}

.CommentCard-authcaution {
margin: 24px 0 24px 0;
}
52 changes: 52 additions & 0 deletions src/components/comment-form/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { memo } from 'react';
import { cn as bem } from '@bem-react/classname';
import Button from '../button';
import './style.css';
import PropTypes from 'prop-types';

function CommentForm(props) {
const {
commentTitle, type,
setIsReplyActive = () => {},
setReplyToCommentId = () => {},
onSubmit = () => {},
onChange = () => {},
errorMessage,
} = props;

const cn = bem('CommentForm');

return (
<div className={cn()}>
<form onSubmit={e => {
e.preventDefault();
onSubmit(e);
}}>
<p>{commentTitle}</p>
<textarea name="newComment" required onChange={onChange} />
{errorMessage && <div className={cn('error')}>{errorMessage}</div>}
<div className={cn('action')}>
<Button style="primary" type="submit" title="Отправить" />

{type === "reply" &&
<Button style="outline" type="button" title="Отмена" onClick={() => {
setIsReplyActive(false),
setReplyToCommentId(null)
}
} />
}
</div>
</form>
</div>
);
}

CommentForm.propTypes = {
commentTitle: PropTypes.string,
type: PropTypes.string,
setIsReplyActive: PropTypes.func,
onSubmit: PropTypes.func,
onChange: PropTypes.func,
};

export default memo(CommentForm);
Loading