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
5 changes: 3 additions & 2 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 } from 'react';
import { useParams } from 'react-router-dom';
import useStore from '../../hooks/use-store';
import useSelector from '../../hooks/use-selector';
Expand All @@ -9,6 +9,7 @@ import Head from '../../components/head';
import Navigation from '../../containers/navigation';
import Spinner from '../../components/spinner';
import ArticleCard from '../../components/article-card';
import AuthContainer from '../../containers/auth';
import LocaleSelect from '../../containers/locale-select';

/**
Expand All @@ -32,12 +33,12 @@ function Article() {
const { t } = useTranslate();

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

return (
<>
<AuthContainer />
<Head title={select.article.title}>
<LocaleSelect />
</Head>
Expand Down
3 changes: 0 additions & 3 deletions src/app/basket/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { memo, useCallback } from 'react';
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 All @@ -21,9 +20,7 @@ function Basket() {
}));

const callbacks = {
// Удаление из корзины
removeFromBasket: useCallback(_id => store.actions.basket.removeFromBasket(_id), [store]),
// Закрытие любой модалки
closeModal: useCallback(() => store.actions.modals.close(), [store]),
};

Expand Down
23 changes: 21 additions & 2 deletions src/app/index.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,39 @@
import { useCallback, useContext, useEffect, useState } from 'react';
import { memo, useEffect } from 'react';
import { Routes, Route } from 'react-router-dom';
import useSelector from '../hooks/use-selector';
import ProtectedRoute from './protected-route'
import Main from './main';
import Basket from './basket';
import Article from './article';
import Login from './login';
import Profile from './profile';
import useStore from '../hooks/use-store';

/**
* Приложение
* Маршрутизация по страницам и модалкам
*/
function App() {
const activeModal = useSelector(state => state.modals.name);
const store = useStore();

// Инициализация авторизации при загрузке
useEffect(() => {
const token = localStorage.getItem('token');
if (token && !store.state.login?.user) {
store.actions.login.autoLogin();
}
}, [store]);

return (
<>
<Routes>
<Route path={'/login'} element={<Login />} />
<Route path="/profile" element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
} />
<Route path={''} element={<Main />} />
<Route path={'/articles/:id'} element={<Article />} />
</Routes>
Expand All @@ -24,4 +43,4 @@ function App() {
);
}

export default App;
export default memo(App);
70 changes: 70 additions & 0 deletions src/app/login/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { memo, useCallback, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import useStore from '../../hooks/use-store';
import useTranslate from '../../hooks/use-translate';
import useSelector from '../../hooks/use-selector';
import PageLayout from '../../components/page-layout';
import Head from '../../components/head';
import LocaleSelect from '../../containers/locale-select';
import AuthContainer from '../../containers/auth';
import Navigation from '../../containers/navigation';
import Form from '../../components/login-form';

/**
* Страница авторизации пользователя
*/
function Login() {
const store = useStore();
const { t } = useTranslate();
const navigate = useNavigate();
const [login, setLogin] = useState('');
const [password, setPassword] = useState('');
const { issues = null } = useSelector(state => state.login || {});

const handleLogin = useCallback(async (credentials) => {
try {
const success = await store.actions.login.login(credentials);
if (success) {
navigate('/');
}
} catch (e) {
console.error('Ошибка:', e);
}
}, [store, navigate]);

const handleSubmit = useCallback((e) => {
e.preventDefault();
handleLogin({
login: login.trim(),
password: password.trim()
});
}, [login, password, handleLogin]);

return (
<>
<AuthContainer />
<Head title={t('title')}>
<LocaleSelect />
</Head>
<PageLayout>
<Navigation />
<Form
onSubmit={handleSubmit}
title={t('login.title')}
loginBtn={t('login.btn')}
loginLabel={t('login.label')}
pswLabel={t('psw.label')}
issues={issues}
loginValue={login}
passwordValue={password}
onLoginChange={setLogin}
onPasswordChange={setPassword}
placeholderLog={t('placeholder.log')}
placeholderPsw={t('placeholder.psw')}
/>
</PageLayout>
</>
);
}

export default memo(Login);
26 changes: 14 additions & 12 deletions src/app/main/index.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,36 @@
import { memo } from 'react';
import { memo, useEffect } from 'react';
import useStore from '../../hooks/use-store';
import useTranslate from '../../hooks/use-translate';
import useDynamicTitle from '../../hooks/use-title';
import useInit from '../../hooks/use-init';
import Navigation from '../../containers/navigation';
import PageLayout from '../../components/page-layout';
import Head from '../../components/head';
import CatalogFilter from '../../containers/catalog-filter';
import CatalogList from '../../containers/catalog-list';
import LocaleSelect from '../../containers/locale-select';
import AuthContainer from '../../containers/auth';

/**
* Главная страница - первичная загрузка каталога
*/
function Main() {
const store = useStore();
const { getTitle } = useDynamicTitle();

useInit(
() => {
store.actions.catalog.initParams();
},
[],
true,
);
useInit(() => {
store.actions.catalog.initParams();
}, [], true);

useEffect(() => {
document.title = getTitle();
}, [getTitle]);

const { t } = useTranslate();

return (
<>
<Head title={t('title')}>
<LocaleSelect />
<AuthContainer />
<Head title={getTitle()}>
<LocaleSelect />
</Head>
<PageLayout>
<Navigation />
Expand Down
40 changes: 40 additions & 0 deletions src/app/profile/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { memo } from 'react';
import useTranslate from '../../hooks/use-translate';
import useSelector from '../../hooks/use-selector';
import PageLayout from '../../components/page-layout';
import Head from '../../components/head';
import LocaleSelect from '../../containers/locale-select';
import AuthContainer from '../../containers/auth';
import User from '../../components/user-info';
import Navigation from '../../containers/navigation';

/**
* Страница профиля пользователя
*/
function Profile() {
const { t } = useTranslate();
const { user } = useSelector(state => state.login || {});

return (
<>
<AuthContainer />
<Head title={t('title')}>
<LocaleSelect />
</Head>
<PageLayout>
<Navigation />
<User
title={t('profile.title')}
userLabel={t('user.name')}
userPhone={t('user.phone')}
userEmail={t('user.email')}
userName={user?.profile.name}
email={user?.email}
phone={user?.profile.phone}
/>
</PageLayout>
</>
);
}

export default memo(Profile);
25 changes: 25 additions & 0 deletions src/app/protected-route/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useEffect } from 'react';
import useStore from '../../hooks/use-store';
import useSelector from '../../hooks/use-selector';
import { useNavigate } from 'react-router-dom';

/**
* Маршрут, защищающий данные от неавторизованных пользователей
*/
function ProtectedRoute({ children }) {
const store = useStore();
const navigate = useNavigate();
const { token, user } = useSelector(state => state.login || {});

useEffect(() => {
if (!token) {
navigate('/');
} else if (!user) {
store.actions.login.autoLogin();
}
}, [token, user, store, navigate]);

return token ? children : null;
}

export default ProtectedRoute;
29 changes: 17 additions & 12 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.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.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 All @@ -38,15 +38,20 @@ function ArticleCard(props) {

ArticleCard.propTypes = {
article: PropTypes.shape({
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
description: PropTypes.string,
madeIn: PropTypes.object,
category: PropTypes.object,
edition: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
price: PropTypes.number,
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
description: PropTypes.string.isRequired,
madeIn: PropTypes.shape({
title: PropTypes.string,
code: PropTypes.string
}),
category: PropTypes.shape({
title: PropTypes.string.isRequired
}).isRequired,
edition: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
price: PropTypes.number.isRequired,
}).isRequired,
onAdd: PropTypes.func,
t: PropTypes.func,
onAdd: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};

export default memo(ArticleCard);
39 changes: 39 additions & 0 deletions src/components/auth-view/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { memo } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import Button from '../../components/button';
import './style.css';

function AuthView(props) {
return (
<div className="Authorization">
{props.isAuth ? (
<div className='Authorization-container'>
<Link to="/profile" className="profile">
{props.username || 'Пользователь'}
</Link>
<Button
style="text"
onClick={props.onLogout}
className="login"
title={props.logoutTitle}
/>
</div>
) : (
<div className='Authorization-container'>
<Link to="/login" className="login">
{props.loginTitle}
</Link>
</div>
)}
</div>
);
}

AuthView.propTypes = {
isAuth: PropTypes.bool.isRequired,
username: PropTypes.string,
onLogout: PropTypes.func.isRequired
};

export default memo(AuthView);
Loading
Loading