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
72 changes: 45 additions & 27 deletions src/api/index.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,64 @@
class APIService {
/**
* @param services {Services} Менеджер сервисов
* @param config {Object}
*/
constructor(services, config = {}) {
this.services = services;
this.config = config;
this.defaultHeaders = {
'Content-Type': 'application/json',
'Accept-Language': 'ru',
};
}

/**
* HTTP запрос
* @param url
* @param method
* @param headers
* @param options
* @returns {Promise<{}>}
*/
async request({ url, method = 'GET', headers = {}, ...options }) {
if (!url.match(/^(http|\/\/)/)) url = this.config.baseUrl + url;
const res = await fetch(url, {
method,
headers: { ...this.defaultHeaders, ...headers },
...options,
});
return { data: await res.json(), status: res.status, headers: res.headers };
}

/**
* Установка или сброс заголовка
* @param name {String} Название заголовка
* @param value {String|null} Значение заголовка
*/
setHeader(name, value = null) {
if (value) {
this.defaultHeaders[name] = value;
} else if (this.defaultHeaders[name]) {
delete this.defaultHeaders[name];
}
}

async request({ url, method = 'GET', headers = {}, ...options }) {
if (!url.match(/^(http|\/\/)/)) url = this.config.baseUrl + url;

try {
const res = await fetch(url, {
method,
headers: { ...this.defaultHeaders, ...headers },
...options,
});

const data = await res.json();

if (data?.translations) {
this.services.i18n.addTranslations(data.translations);
}

return {
data,
status: res.status,
headers: res.headers
};
} catch (e) {
console.error('API request failed:', e);
throw e;
}
}

async fetchItemsDetails(items) {
const updates = {};
await Promise.all(
items.map(async item => {
try {
const res = await this.request({
url: `/api/v1/articles/${item._id}?fields=title,price,description`
});
updates[item._id] = res.data.result;
} catch (e) {
updates[item._id] = item;
}
})
);
return updates;
}
}

export default APIService;
26 changes: 16 additions & 10 deletions src/app/article/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,33 @@ import TopHead from '../../containers/top-head';
import { useDispatch, useSelector } from 'react-redux';
import shallowequal from 'shallowequal';
import articleActions from '../../store-redux/article/actions';
import CommentsContainer from '../../containers/comments-container';
import HeadLayout from '../../components/head-layout';

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]);

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

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

const { t } = useTranslate();
);

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

Expand All @@ -48,13 +48,19 @@ function Article() {
<HeadLayout>
<TopHead />
</HeadLayout>
<Head title={select.article.title}>
<Head title={select.article?.title || t('article.title')}>
<LocaleSelect />
</Head>
<PageLayout>
<Navigation />
<Spinner active={select.waiting}>
<ArticleCard article={select.article} onAdd={callbacks.addToBasket} t={t} />
<ArticleCard
article={select.article}
onAdd={callbacks.addToBasket}
t={t}
key={`article-${params.id}-${lang}`}
/>
<CommentsContainer />
</Spinner>
</PageLayout>
</>
Expand Down
84 changes: 51 additions & 33 deletions src/app/basket/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { memo, useCallback } from 'react';
import { useDispatch, useStore as useStoreRedux } from 'react-redux';
import { memo, useCallback, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
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 @@ -13,39 +13,46 @@ import modalsActions from '../../store-redux/modals/actions';
function Basket() {
const store = useStore();
const dispatch = useDispatch();
const navigate = useNavigate();
const { t, lang } = useTranslate();

const select = useSelector(state => ({
list: state.basket.list,
amount: state.basket.amount,
sum: state.basket.sum,
const { list: basketItems, sum, amount } = useSelector(state => state.basket);

const [itemsCache, setItemsCache] = useState({});

const updateItemsCache = useCallback(async () => {
const newItems = basketItems.filter(item => !itemsCache[item._id]);
if (newItems.length === 0) return;

const updates = await store.services.api.fetchItemsDetails(newItems);
setItemsCache(prev => ({ ...prev, ...updates }));
}, [basketItems, itemsCache, store.services.api]);

useEffect(() => {
updateItemsCache();
}, [basketItems, lang, updateItemsCache]);

const resolvedItems = basketItems.map(item => ({
...item,
...(itemsCache[item._id] || {}),
}));

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

const { t } = useTranslate();

const renders = {
itemBasket: useCallback(
item => (
<ItemBasket
item={item}
link={`/articles/${item._id}`}
onRemove={callbacks.removeFromBasket}
onLink={callbacks.closeModal}
labelUnit={t('basket.unit')}
labelDelete={t('basket.delete')}
/>
),
[callbacks.removeFromBasket, t],
),
closeModal: useCallback(() => dispatch(modalsActions.close()), [dispatch]),

navigateToArticle: useCallback((id) => {
dispatch(modalsActions.close());
navigate(`/articles/${id}`);
}, [dispatch, navigate]),
};

return (
Expand All @@ -54,8 +61,19 @@ function Basket() {
labelClose={t('basket.close')}
onClose={callbacks.closeModal}
>
<List list={select.list} renderItem={renders.itemBasket} />
<BasketTotal sum={select.sum} t={t} />
<List
list={resolvedItems}
renderItem={item => (
<ItemBasket
item={item}
onRemove={callbacks.removeFromBasket}
onNavigate={() => callbacks.navigateToArticle(item._id)}
labelUnit={t('basket.unit')}
labelDelete={t('basket.delete')}
/>
)}
/>
<BasketTotal sum={sum} t={t} />
</ModalLayout>
);
}
Expand Down
2 changes: 0 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 Down
5 changes: 3 additions & 2 deletions src/app/profile/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import HeadLayout from '../../components/head-layout';
function Profile() {
const store = useStore();


useInit(() => {
store.actions.profile.load();
}, []);
Expand All @@ -24,7 +25,7 @@ function Profile() {
waiting: state.profile.waiting,
}));

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

return (
<>
Expand All @@ -37,7 +38,7 @@ function Profile() {
<PageLayout>
<Navigation />
<Spinner active={select.waiting}>
<ProfileCard data={select.profile} />
<ProfileCard data={select.profile} t={t} lang={lang}/>
</Spinner>
</PageLayout>
</>
Expand Down
22 changes: 13 additions & 9 deletions src/components/article-card/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,37 @@ import numberFormat from '../../utils/number-format';
import Button from '../button';
import './style.css';

function ArticleCard(props) {
const { article, onAdd = () => {}, t = text => text } = props;
function ArticleCard({ article, onAdd = () => {}, t }) {
const cn = bem('ArticleCard');

return (
<div className={cn()}>
<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')} />
<Button
style="primary"
onClick={() => onAdd(article._id)}
title={t('article.add')}
/>
</div>
);
}
Expand All @@ -44,9 +48,9 @@ ArticleCard.propTypes = {
category: PropTypes.object,
edition: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
price: PropTypes.number,
}).isRequired,
}),
onAdd: PropTypes.func,
t: PropTypes.func,
t: PropTypes.func.isRequired,
};

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

function AuthPrompt({ onLoginRedirect, t }) {
return (
<div className="auth-prompt">
<Link to="/login" state={{ back: onLoginRedirect }} style={{ textDecoration: 'none' }}>
<Button title={t('comments.signInToComment')} style="textRevers" />
</Link>
<span className="auth-prompt-text">{t('comments.toComment')}</span>
</div>
);
}

AuthPrompt.propTypes = {
onLoginRedirect: PropTypes.string.isRequired,
t: PropTypes.func.isRequired
};

export default AuthPrompt;
10 changes: 10 additions & 0 deletions src/components/auth-prompt/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.auth-prompt {
margin-top: 10px;
display: flex;
align-items: center;
font-family: 'Golos Text', sans-serif;
font-weight: 400;
font-size: 16px;
line-height: 22px;
letter-spacing: 0%;
}
Loading