Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lecture-5 #392

Open
wants to merge 2 commits into
base: lecture-5
Choose a base branch
from
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
1 change: 1 addition & 0 deletions src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ class APIService {
}
}


export default APIService;
39 changes: 30 additions & 9 deletions src/app/article/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { memo, useCallback, useMemo } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import useStore from '../../hooks/use-store';
import useTranslate from '../../hooks/use-translate';
Expand All @@ -11,37 +11,50 @@ 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 useStoreSelector from '../../hooks/use-selector';
import shallowequal from 'shallowequal';
import articleActions from '../../store-redux/article/actions';
import { fetchComments, addComment } from '../../store-redux/comments/actions';
import ArticleComments from '../../components/article-comment';
import { organizeComments } from '../../utils/organizeComments';

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

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

const params = useParams();
const [replyTo, setReplyTo] = useState(null);

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

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

);
const storeSelect = useStoreSelector(
state => ({
exists: state.session.exists,
token: state.session.token
}),
)
const { t } = useTranslate();

const callbacks = {
// Добавление в корзину
addToBasket: useCallback(_id => store.actions.basket.addToBasket(_id), [store]),
handleAddComment: useCallback((text, parentId) => {
const parent = parentId ? { _id: parentId, _type: 'comment' } : { _id: params.id, _type: 'article' };
dispatch(addComment({ text, parent, article: params.id, token: storeSelect.token }));
setReplyTo(null);
}, [dispatch, params.id]),
setReplyTo,
};

const organizedComments = organizeComments(select.comments);
return (
<PageLayout>
<TopHead />
Expand All @@ -51,6 +64,14 @@ function Article() {
<Navigation />
<Spinner active={select.waiting}>
<ArticleCard article={select.article} onAdd={callbacks.addToBasket} t={t} />
<ArticleComments
comments={organizedComments}
replyTo={replyTo}
setReplyTo={callbacks.setReplyTo}
handleAddComment={callbacks.handleAddComment}
exists={storeSelect.exists}
link="/login"
/>
</Spinner>
</PageLayout>
);
Expand Down
77 changes: 77 additions & 0 deletions src/components/article-comment/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React, { useRef, useEffect } from 'react';
import CommentForm from '../comment-form/index';
import { cn as bem } from '@bem-react/classname';
import './style.css';
import { Link } from 'react-router-dom';

const ArticleComments = ({ comments, replyTo, setReplyTo, handleAddComment, exists, link,user }) => {
const cn = bem('ArticleComments');
const maxDepth = 3;
const replyFormRef = useRef(null);
useEffect(() => {
if (replyFormRef.current) {
replyFormRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [replyTo]);
const renderComment = (comment) => (
<li key={comment._id} className={cn('item', { child: comment.parent && comment.depth <= maxDepth })}>
<div className={cn('content')}>
<p className={cn('author')}>
<span className={comment.author?.profile?.name === user.profile?.name ? cn('author-name') : ''}>{comment.author?.profile?.name}</span> <span className={cn('date')}>
{new Date(comment.dateCreate).toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}{' '}
в{' '}
{new Date(comment.dateCreate).toLocaleTimeString('ru-RU', {
hour: '2-digit',
minute: '2-digit',
})}
</span>
</p>
<p className={cn('text')}>{comment.text}</p>
<button
className={cn('reply-button')}
onClick={() => setReplyTo(comment._id)}
>
Ответить
</button>
</div>
{comment.children && (
<ul className={cn('list')}>
{comment.children.map(renderComment)}
{replyTo === comment._id && (
exists ? (
<div ref={replyFormRef}>
<CommentForm onSubmit={(text) => handleAddComment(text, comment._id)} onCancel={() => setReplyTo(null)} /> </div>
) : (
<div className={cn('login')} ref={replyFormRef}>
<Link className={cn('link-login')} to={link}>Войдите</Link>, чтобы иметь возможность комментировать.
<button className={cn('button-cancel')} onClick={() => setReplyTo(null)}>Отмена</button>
</div>
)
)}
</ul>
)}
</li>
);

return (
<div className={cn()}>
<h3>Комментарии {`(${comments.length})`}</h3>
<ul className={cn('list')}>{comments.map(renderComment)}</ul>
{exists ? (
<div className={cn('new-comment-form')}>
<CommentForm onSubmit={(text) => handleAddComment(text)}/>
</div>
) : (
<div className={cn('link')}>
<Link className={cn('link-login')} to={link}>Войдите</Link>, чтобы иметь возможность комментировать.
</div>
)}
</div>
);
};

export default ArticleComments;
61 changes: 61 additions & 0 deletions src/components/article-comment/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
.ArticleComments {
margin: 0 40px;
}
.ArticleComments-content {
margin: 30px 0;
}
.ArticleComments-content > * {
margin: 10px 0;
}
.ArticleComments-list {
list-style-type: none;
}

.ArticleComments-item {
margin-bottom: 20px;
}

.ArticleComments-item_child {
margin-left: 30px;
}


.ArticleComments-author {
font-weight: bold;
}

.ArticleComments-date {
color: #666666;
font-size: 12px;
margin-left: 10px;
}

.ArticleComments-reply-button {
color: #0087E9;
border: none;
background-color:white;
cursor: pointer;
padding: 0;
}

.ArticleComments-new-comment-form {
margin: 0 40px 0;
}
.ArticleComments-link {
padding-bottom: 92px;
}
.ArticleComments-link-login {
color: #0087E9;
}
.ArticleComments-button-cancel {
font-size: 16px;
font-weight: 400;
color: gray;
border: none;
background: white;
text-decoration: underline;
cursor: pointer;
}
.ArticleComments-login {
margin: 30px 0px;
}
27 changes: 27 additions & 0 deletions src/components/comment-form/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { useState } from 'react';
import 'style.css'; // Import the CSS for styling

const CommentForm = ({ onSubmit }) => {
const [text, setText] = useState('');

const handleSubmit = (e) => {
e.preventDefault();
onSubmit(text);
setText('');
};

return (
<form className="CommentForm" onSubmit={handleSubmit}>
<h4 className="CommentForm-header">Новый комментарий</h4>
<textarea
className="CommentForm-textarea"
value={text}
onChange={(e) => setText(e.target.value)}
required
/>
<button className="CommentForm-button" type="submit">Отправить</button>
</form>
);
};

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

.CommentForm-textarea {
resize: none;
height: 80px;
padding: 10px;
border-radius: 5px;
border: 1px solid #ccc;
margin: 0 0 10px;
}

.CommentForm-header{
margin: 10px 0;
}
.CommentForm-button {
width: fit-content;
}

54 changes: 54 additions & 0 deletions src/containers/article-comments/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { memo, useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import shallowequal from 'shallowequal';
import { fetchComments, addComment } from '../../store-redux/comments/actions';
import ArticleComments from '../../components/article-comment';
import listToTree from '../../utils/list-to-tree';
import useStoreSelector from '../../hooks/use-selector';

function ArticleCommentsContainer() {
const dispatch = useDispatch();
const params = useParams();
const [replyTo, setReplyTo] = useState(null);

useEffect(() => {
dispatch(fetchComments(params.id));
}, [params.id, dispatch]);

const select = useSelector(
state => ({
comments: state.comments.comments,
}),
shallowequal,
);

const storeSelect = useStoreSelector(
state => ({
exists: state.session.exists,
token: state.session.token,
user: state.session.user,
}),
);

const handleAddComment = useCallback((text, parentId) => {
const parent = parentId ? { _id: parentId, _type: 'comment' } : { _id: params.id, _type: 'article' };
dispatch(addComment({ text, parent, article: params.id, token: storeSelect.token }));
setReplyTo(null);
}, [dispatch, params.id, storeSelect.token]);

const organizedComments = listToTree(select.comments);
return (
<ArticleComments
comments={organizedComments}
replyTo={replyTo}
setReplyTo={setReplyTo}
handleAddComment={handleAddComment}
exists={storeSelect.exists}
link="/login"
user={storeSelect.user}
/>
);
}

export default memo(ArticleCommentsContainer);
38 changes: 38 additions & 0 deletions src/store-redux/comments/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Services from '../../services';
const services = new Services({ api: { baseUrl: 'api/v1/' } });

export const fetchCommentsStart = () => ({ type: 'comments/fetchCommentsStart' });
export const fetchCommentsSuccess = (comments) => ({ type: 'comments/fetchCommentsSuccess', payload: comments });
export const fetchCommentsFailure = () => ({ type: 'comments/fetchCommentsFailure' });

export const fetchComments = (articleId) => async (dispatch) => {
dispatch(fetchCommentsStart());
try {
const response = await services.api.request({
url: `/comments?fields=items(_id,text,dateCreate,author(profile(name)),parent(_id,_type),isDeleted),count&limit=*&search[parent]=${articleId}`,
method: 'GET'
});
dispatch(fetchCommentsSuccess(response.data.result.items));
} catch (error) {
dispatch(fetchCommentsFailure());
}
};

export const addCommentSuccess = (comment) => ({ type: 'comments/addCommentSuccess', payload: comment });

export const addComment = ({ text, parent, article, token }) => async (dispatch) => {
try {
const response = await services.api.request({
url: '/comments',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Token': token,
},
body: JSON.stringify({ text, parent, article }),
});
dispatch(addCommentSuccess(response.data));
} catch (error) {
console.error(error);
}
};
20 changes: 20 additions & 0 deletions src/store-redux/comments/reducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const initialState = {
comments: [],
loading: false,
error: null
};

const commentsReducer = (state = initialState, action) => {
switch (action.type) {
case 'comments/fetchCommentsStart':
return { ...state, loading: true, error: null };
case 'comments/fetchCommentsSuccess':
return { ...state, loading: false, comments: action.payload };
case 'comments/fetchCommentsFailure':
return { ...state, loading: false, error: action.payload };
default:
return state;
}
};

export default commentsReducer;
1 change: 1 addition & 0 deletions src/store-redux/exports.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as article } from './article/reducer';
export { default as modals } from './modals/reducer';
export {default as comments} from './comments/reducer';
Loading
Loading