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

feat(lecture-5): Сервисы #383

Open
wants to merge 7 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
3 changes: 2 additions & 1 deletion src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ class APIService {
* @returns {Promise<{}>}
*/
async request({ url, method = 'GET', headers = {}, ...options }) {
const lang = this.services?.i18n?.getLang();
if (!url.match(/^(http|\/\/)/)) url = this.config.baseUrl + url;
const res = await fetch(url, {
method,
headers: { ...this.defaultHeaders, ...headers },
headers: { ...this.defaultHeaders, ...headers, 'Accept-Language': lang || 'ru' },
...options,
});
return { data: await res.json(), status: res.status, headers: res.headers };
Expand Down
40 changes: 33 additions & 7 deletions src/app/article/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,52 @@ 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 commentsActions from '../../store-redux/comments/actions';
import CommentsSection from '../../components/comments-section';
import useSelector from '../../hooks/use-selector';

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

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

const params = useParams();
const { t, lang } = useTranslate();


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

useInit(() => {
dispatch(commentsActions.load(params.id));
}, [params.id]); // комментам язык не нужен

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

const { t } = useTranslate();
const {userId, userName, u} = useSelector(
state => ({
userName: state.session.user?.profile?.name,
userId: state.session.user?._id,
}),
);


const callbacks = {
// Добавление в корзину
addToBasket: useCallback(_id => store.actions.basket.addToBasket(_id), [store]),
addComment: useCallback((data, onSuccess) => dispatch(commentsActions.add(data, userName, onSuccess)), [userName]),
};

return (
Expand All @@ -52,6 +68,16 @@ function Article() {
<Spinner active={select.waiting}>
<ArticleCard article={select.article} onAdd={callbacks.addToBasket} t={t} />
</Spinner>
<Spinner active={select.commentsWaiting}>
<CommentsSection
comments={select.comments}
count={select.commentsCount}
articleId={params.id}
onAdd={callbacks.addComment}
userId={userId}
t={t}
/>
</Spinner>
</PageLayout>
);
}
Expand Down
3 changes: 1 addition & 2 deletions src/app/basket/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { memo, useCallback } from 'react';
import { useDispatch, useStore as useStoreRedux } from 'react-redux';
import { useDispatch} from 'react-redux';
import useStore from '../../hooks/use-store';
import useSelector from '../../hooks/use-selector';
import useInit from '../../hooks/use-init';
import useTranslate from '../../hooks/use-translate';
import ItemBasket from '../../components/item-basket';
import List from '../../components/list';
Expand Down
4 changes: 2 additions & 2 deletions src/app/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { useCallback, useContext, useEffect, useState } from 'react';
import { Routes, Route } from 'react-router-dom';
import useSelector from '../hooks/use-selector';
import useStore from '../hooks/use-store';
import useInit from '../hooks/use-init';
import Main from './main';
Expand All @@ -10,13 +8,15 @@ import Login from './login';
import Profile from './profile';
import Protected from '../containers/protected';
import { useSelector as useSelectorRedux } from 'react-redux';
import useTranslate from '../hooks/use-translate';

/**
* Приложение
* @returns {React.ReactElement}
*/
function App() {
const store = useStore();

useInit(async () => {
await store.actions.session.remind();
});
Expand Down
5 changes: 2 additions & 3 deletions src/app/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,16 @@ import TopHead from '../../containers/top-head';

function Main() {
const store = useStore();
const {lang, t} = useTranslate();

useInit(
async () => {
await Promise.all([store.actions.catalog.initParams(), store.actions.categories.load()]);
},
[],
[lang],
true,
);

const { t } = useTranslate();

return (
<PageLayout>
<TopHead />
Expand Down
9 changes: 3 additions & 6 deletions src/app/profile/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { memo, useCallback, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { memo } from 'react';
import useStore from '../../hooks/use-store';
import useSelector from '../../hooks/use-selector';
import useTranslate from '../../hooks/use-translate';
Expand All @@ -8,25 +7,23 @@ import PageLayout from '../../components/page-layout';
import Head from '../../components/head';
import Navigation from '../../containers/navigation';
import Spinner from '../../components/spinner';
import ArticleCard from '../../components/article-card';
import LocaleSelect from '../../containers/locale-select';
import TopHead from '../../containers/top-head';
import ProfileCard from '../../components/profile-card';

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 (
<PageLayout>
<TopHead />
Expand Down
44 changes: 44 additions & 0 deletions src/components/comment-form/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { memo, useRef } from 'react';
import PropTypes from 'prop-types';
import { cn as bem } from '@bem-react/classname';
import './style.css';

function CommentForm({ onAdd, onCancel, title, cancelButtonText, t}) {
const cn = bem('CommentForm');
const ref = useRef(null)

const onSend = (e) => {
e.preventDefault();
const value = ref.current?.value;
if (!value.trim()) {
console.error('Нельзя оставлять пустые комментарии');
return;
}
const onSuccess = () => {
ref.current.value = '';
onCancel()
}
onAdd(value, onSuccess);
}

return (
<form className={cn()} onSubmit={onSend}>
<h3 className={cn('title')}>{title}</h3>
<textarea ref={ref} className={cn('textarea')} />
<div className={cn('actions')}>
<button>{t('comments.send')}</button>
{cancelButtonText && <button onClick={onCancel}>{cancelButtonText}</button>}
</div>
</form>
);
}

CommentForm.propTypes = {
onAdd: PropTypes.func,
onCancel: PropTypes.func,
t: PropTypes.func,
title: PropTypes.string.isRequired,
cancelButtonText: PropTypes.string,
};

export default memo(CommentForm);
26 changes: 26 additions & 0 deletions src/components/comment-form/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.CommentForm {
margin-bottom: 30px;
width: 100%;
}

.CommentForm-title {
font-size: 12px;
margin-bottom: 10px;
margin-top: 0;
}

.CommentForm-textarea {
width: 100%;
min-height: 76px;
resize: vertical;
margin-bottom: 10px;
padding: 4px;
box-sizing: border-box;
font-family: Arial, sans-serif;
font-size: 14px;
}

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

const MIN_LEVEL_INDENT = 2;
const MAX_LEVEL_INDENT = 5;

function CommentList({comments, onAdd, openCommentId, onCancel, onOpen, t, isChildList, userId, level, children}) {
const cn = bem('CommentList');
return (
<ul className={cn({child: isChildList, indent: level <= MAX_LEVEL_INDENT && level >= MIN_LEVEL_INDENT})}>
{comments?.map(comment => (
<li key={comment._id}>
<Comment
openCommentId={openCommentId}
comment={comment}
onAdd={onAdd}
onCancel={onCancel}
onOpen={onOpen}
userId={userId}
level={level}
t={t}
/>
</li>
))}
{children}
</ul>
);
}

CommentList.propTypes = {
openCommentId: PropTypes.string,
comments: PropTypes.array,
onAdd: PropTypes.func,
onCancel: PropTypes.func,
onOpen: PropTypes.func,
isChildList: PropTypes.bool,
userId: PropTypes.string,
level: PropTypes.number,
t: PropTypes.func,
children: PropTypes.node,
};

export default memo(CommentList);
10 changes: 10 additions & 0 deletions src/components/comment-list/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.CommentList {
list-style: none;
padding: 0;
margin: 0;
}

.CommentList_indent {
list-style: none;
padding-left: 30px;
}
32 changes: 32 additions & 0 deletions src/components/comment-no-auth/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { memo } from 'react';
import PropTypes from 'prop-types';
import { cn as bem } from '@bem-react/classname';
import { Link, useLocation } from 'react-router-dom';
import './style.css';

function CommentNoAuth({ text, onCancel, buttonText, t = text => text}) {
const cn = bem('CommentNoAuth');
const {pathname} = useLocation();
return (
<div className={cn()}>
<Link className={cn('link')} state={{back: pathname}} to={`/login`}>
{t('comments.login-text')}
</Link>
<div>{text}</div>
{buttonText && (
<button className={cn('button')} onClick={onCancel}>
{buttonText}
</button>
)}
</div>
);
}

CommentNoAuth.propTypes = {
text: PropTypes.string,
t: PropTypes.func,
onCancel: PropTypes.func,
buttonText: PropTypes.string,
};

export default memo(CommentNoAuth);
23 changes: 23 additions & 0 deletions src/components/comment-no-auth/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.CommentNoAuth {
display: flex;
margin-bottom: 30px;
align-items: center;
}

.CommentNoAuth-button {
margin-left: 2px;
border: none;
background-color: transparent;
font-size: inherit;
color: gray;
text-decoration: underline;
cursor: pointer;
}

.CommentNoAuth-button:hover {
color: black;
}

.CommentNoAuth-link:hover {
color: black;
}
Loading
Loading