Skip to content

Commit

Permalink
feat: add create new and edit article pages
Browse files Browse the repository at this point in the history
  • Loading branch information
TomatoVan committed Feb 6, 2024
1 parent ea28d42 commit 3279048
Show file tree
Hide file tree
Showing 21 changed files with 209 additions and 25 deletions.
2 changes: 2 additions & 0 deletions extractedTranslations/en/translation.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
{
"": "",
"All": "",
"Articles app": "",
"Articles not found": "",
"Back to list": "",
"Cancel": "",
"Choose country": "",
"Choose currency": "",
"Comments": "",
"Create new article": "",
"Economics": "",
"Edit": "",
"Enter ": "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ ThunkConfig<string>
const { extra, rejectWithValue } = thunkAPI;

try {
const response = await extra.api.get<Article>(`/articles/${articleId}`);
const response = await extra.api.get<Article>(`/articles/${articleId}`, {
params: {
_expand: 'user',
},
});

if (!response.data) {
throw new Error();
Expand Down
1 change: 1 addition & 0 deletions src/pages/ArticleEditPage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ArticleEditPageAsync as ArticleEditPage } from './ui/ArticleEditPage.async';
7 changes: 7 additions & 0 deletions src/pages/ArticleEditPage/ui/ArticleEditPage.async.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { lazy } from 'react';

export const ArticleEditPageAsync = lazy(() => new Promise((resolve) => {
// @ts-ignore
// ТАК В РЕАЛЬНЫХ ПРОЕКТАХ НЕ ДЕЛАТЬ!!!!! ДЕЛАЕМ ДЛЯ КУРСА!
setTimeout(() => resolve(import('./ArticleEditPage')), 1500);
}));
3 changes: 3 additions & 0 deletions src/pages/ArticleEditPage/ui/ArticleEditPage.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.ArticleEditPage {

}
17 changes: 17 additions & 0 deletions src/pages/ArticleEditPage/ui/ArticleEditPage.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';

import { ArticleEditPage } from './ArticleEditPage';

export default {
title: 'shared/ArticleEditPage',
component: ArticleEditPage,
argTypes: {
backgroundColor: { control: 'color' },
},
} as ComponentMeta<typeof ArticleEditPage>;

const Template: ComponentStory<typeof ArticleEditPage> = (args) => <ArticleEditPage {...args} />;

export const Normal = Template.bind({});
Normal.args = {};
23 changes: 23 additions & 0 deletions src/pages/ArticleEditPage/ui/ArticleEditPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { classNames } from 'shared/lib/classNames/classNames';
import { useTranslation } from 'react-i18next';
import { memo } from 'react';
import { Page } from 'widgets/Page/Page';
import { useParams } from 'react-router-dom';
import cls from './ArticleEditPage.module.scss';

interface ArticleEditPageProps {
className?: string;
}
const ArticleEditPage = memo((props: ArticleEditPageProps) => {
const { className } = props;
const { t } = useTranslation();
const { id } = useParams<{ id: string }>();
const isEdit = Boolean(id);
return (
<Page className={classNames(cls.ArticleEditPage, {}, [className])}>
{isEdit ? 'Edit article' : 'Create new article'}
</Page>
);
});

export default ArticleEditPage;
2 changes: 1 addition & 1 deletion src/pages/ArticlesDetailsPage/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ArticlesDetailsPageAsync } from './ui/ArticlesDetailsPage.async';
import { ArticlesDetailsPageAsync } from './ui/ArticlesDetailsPage/ArticlesDetailsPage.async';

export {
ArticlesDetailsPageAsync as ArticlesDetailsPage,
Expand Down
10 changes: 10 additions & 0 deletions src/pages/ArticlesDetailsPage/model/selectors/article.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createSelector } from '@reduxjs/toolkit';
import { getArticlesDetailsData } from '../../../../entities/Article';
import { getUserAuthData } from '../../../../entities/User';

export const getCanEditArticle = createSelector(getArticlesDetailsData, getUserAuthData, (article, user) => {
if (!article || !user) {
return false;
}
return article.user.id === user.id;
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react';
import { ArticlesDetailsPage } from 'pages/ArticlesDetailsPage';
import { Article } from 'entities/Article';
import { StoreDecorator } from 'shared/config/storybook/StoreDecorator/StoreDecorator';
import { ArticleBlockType, ArticleType } from '../../../entities/Article/model/types/article';
import { ArticleBlockType, ArticleType } from '../../../../entities/Article/model/types/article';

export default {
title: 'pages/ArticlesDetailsPage',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@ import { Button, ButtonTheme } from 'shared/ui/Button/Button';
import { RoutePath } from 'shared/config/routeConfig/routeConfig';
import { Page } from 'widgets/Page/Page';

import { articleDetailsPageReducer } from '../model/slices';
import { fetchArticleRecommendations } from '../model/services/fetchArticleRecommendations/fetchArticleRecommendations';
import { getArticleRecommendationsIsLoading } from '../model/selectors/recommendations';
import { getArticleCommentsIsLoading } from '../model/selectors/comments';
import { getArticleComments } from '../model/slices/ArticleDetailsCommentsSlice';
import { ArticleDetails, ArticleList } from '../../../entities/Article';
import { Text, TextSize } from '../../../shared/ui/Text/Text';
import { CommentList } from '../../../entities/Comment';
import { ArticlesDetailsPageHeader } from 'pages/ArticlesDetailsPage/ui/ArticlesDetailsPageHeader/ArticlesDetailsPageHeader';
import { articleDetailsPageReducer } from '../../model/slices';
import { fetchArticleRecommendations } from '../../model/services/fetchArticleRecommendations/fetchArticleRecommendations';
import { getArticleRecommendationsIsLoading } from '../../model/selectors/recommendations';
import { getArticleCommentsIsLoading } from '../../model/selectors/comments';
import { getArticleComments } from '../../model/slices/ArticleDetailsCommentsSlice';
import { ArticleDetails, ArticleList } from '../../../../entities/Article';
import { Text, TextSize } from '../../../../shared/ui/Text/Text';
import { CommentList } from '../../../../entities/Comment';
import cls from './ArticlesDetailsPage.module.scss';
import { getArticleRecommendations } from '../../ArticlesDetailsPage/model/slices/ArticleDetailsPageRecommendationsSlice';
import { getArticleRecommendations } from '../../model/slices/ArticleDetailsPageRecommendationsSlice';

const reducers: ReducersList = {
articleDetailsPage: articleDetailsPageReducer,
Expand All @@ -40,12 +41,6 @@ const ArticlesDetailsPage = (props: any) => {
const recommendations = useSelector(getArticleRecommendations.selectAll);
const recommendationsIsLoading = useSelector(getArticleRecommendationsIsLoading);

const navigate = useNavigate();

const onBackToList = useCallback(() => {
navigate(RoutePath.articles);
}, [navigate]);

const onSendComment = useCallback((text: string) => {
dispatch(addCommentForArticle(text));
}, [dispatch]);
Expand All @@ -62,9 +57,7 @@ const ArticlesDetailsPage = (props: any) => {
return (
<DynamicModuleLoader reducers={reducers}>
<Page>
<Button theme={ButtonTheme.OUTLINE} onClick={onBackToList}>
{t('Back to list')}
</Button>
<ArticlesDetailsPageHeader />
<ArticleDetails id={id} />
<Text
size={TextSize.L}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.ArticlesDetailsPageHeader {
display: flex;
align-items: center;
}

.editBtn {
margin-left: auto;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';

import { ArticlesDetailsPageHeader } from './ArticlesDetailsPageHeader';

export default {
title: 'shared/ArticlesDetailsPageHeader',
component: ArticlesDetailsPageHeader,
argTypes: {
backgroundColor: { control: 'color' },
},
} as ComponentMeta<typeof ArticlesDetailsPageHeader>;

const Template: ComponentStory<typeof ArticlesDetailsPageHeader> = (args) => <ArticlesDetailsPageHeader {...args} />;

export const Normal = Template.bind({});
Normal.args = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { classNames } from 'shared/lib/classNames/classNames';
import { useTranslation } from 'react-i18next';
import { memo, useCallback } from 'react';
import { Button, ButtonTheme } from 'shared/ui/Button/Button';
import { useNavigate } from 'react-router-dom';
import { RoutePath } from 'shared/config/routeConfig/routeConfig';
import { useSelector } from 'react-redux';
import { getCanEditArticle } from 'pages/ArticlesDetailsPage/model/selectors/article';
import { getUserAuthData } from '../../../../entities/User';
import { getArticlesDetailsData } from '../../../../entities/Article';
import cls from './ArticlesDetailsPageHeader.module.scss';

interface ArticlesDetailsPageHeaderProps {
className?: string;
}

export const ArticlesDetailsPageHeader = memo((props: ArticlesDetailsPageHeaderProps) => {
const { className } = props;
const { t } = useTranslation();

const navigate = useNavigate();
const article = useSelector(getArticlesDetailsData);

const onBackToList = useCallback(() => {
navigate(RoutePath.articles);
}, [navigate]);

const onEditArticle = useCallback(() => {
navigate(`${RoutePath.article_details}${article?.id}/edit`);
}, [article?.id, navigate]);

const canEdit = useSelector(getCanEditArticle);

return (
<div className={classNames(cls.ArticlesDetailsPageHeader, {}, [className])}>
<Button
theme={ButtonTheme.OUTLINE}
onClick={onBackToList}
>
{t('Back to list')}
</Button>
{canEdit && (
<Button
className={cls.editBtn}
theme={ButtonTheme.OUTLINE}
onClick={onEditArticle}
>
{t('Edit')}
</Button>
)}

</div>
);
});
15 changes: 15 additions & 0 deletions src/shared/config/routeConfig/routeConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { NotFoundPage } from 'pages/NotFoundPage';
import { ProfilePage } from 'pages/ProfilePage';
import { ArticlesPage } from 'pages/ArticlesPage';
import { ArticlesDetailsPage } from 'pages/ArticlesDetailsPage';
import { ArticleEditPage } from 'pages/ArticleEditPage';

export type AppRoutesProps = RouteProps & {
authOnly?: boolean;
Expand All @@ -16,6 +17,8 @@ export enum AppRoutes {
PROFILE = 'profile',
ARTICLES = 'articles',
ARTICLE_DETAILS = 'article_details',
ARTICLE_CREATE = 'article_create',
ARTICLE_EDIT = 'article_edit',
NOT_FOUND = 'not_found',
}

Expand All @@ -25,6 +28,8 @@ export const RoutePath: Record<AppRoutes, string> = {
[AppRoutes.PROFILE]: '/profile/', // + :id
[AppRoutes.ARTICLES]: '/articles',
[AppRoutes.ARTICLE_DETAILS]: '/articles/', // + :id
[AppRoutes.ARTICLE_CREATE]: '/articles/new',
[AppRoutes.ARTICLE_EDIT]: '/articles/:id/edit',
[AppRoutes.NOT_FOUND]: '*',
};

Expand Down Expand Up @@ -52,6 +57,16 @@ export const routeConfig: Record<AppRoutes, AppRoutesProps> = {
element: <ArticlesDetailsPage />,
authOnly: true,
},
[AppRoutes.ARTICLE_CREATE]: {
path: `${RoutePath.article_create}`,
element: <ArticleEditPage />,
authOnly: true,
},
[AppRoutes.ARTICLE_EDIT]: {
path: `${RoutePath.article_edit}`,
element: <ArticleEditPage />,
authOnly: true,
},
[AppRoutes.NOT_FOUND]: {
path: RoutePath.not_found,
element: <NotFoundPage />,
Expand Down
10 changes: 10 additions & 0 deletions src/shared/ui/Text/Text.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@
}
}

.inverted {
.title {
color: var(--inverted-primary-color);
}

.text {
color: var(--inverted-secondary-color);
}
}

.left {
text-align: left;
}
Expand Down
8 changes: 4 additions & 4 deletions src/shared/ui/Text/Text.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { classNames, Mods } from 'shared/lib/classNames/classNames';
import { classNames } from 'shared/lib/classNames/classNames';
import { memo } from 'react';
import cls from './Text.module.scss';

export enum TextTheme {
PRIMARY='primary',
ERROR='error',

PRIMARY = 'primary',
INVERTED = 'inverted',
ERROR = 'error',
}

export enum TextAlign {
Expand Down
5 changes: 5 additions & 0 deletions src/widgets/Navbar/ui/Navbar.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@
.mainLink {
margin-right: 15px;
}

.appName {
width: var(--sidebar-width);
margin-left: 15px;
}
15 changes: 15 additions & 0 deletions src/widgets/Navbar/ui/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { useTranslation } from 'react-i18next';
import { Button, ButtonTheme } from 'shared/ui/Button/Button';
import { LoginModal } from 'features/AuthByUsername';
import { useDispatch, useSelector } from 'react-redux';
import { Text, TextTheme } from 'shared/ui/Text/Text';
import { AppLink, AppLinkTheme } from 'shared/ui/AppLink/AppLink';
import { RoutePath } from 'shared/config/routeConfig/routeConfig';
import { getUserAuthData, userActions } from '../../../entities/User';
import cls from './Navbar.module.scss';

Expand Down Expand Up @@ -32,6 +35,18 @@ export const Navbar = memo(({ className }: NavbarProps) => {
if (authData) {
return (
<header className={classNames(cls.Navbar, {}, [className])}>
<Text
className={cls.appName}
text={t('Articles app')}
theme={TextTheme.INVERTED}
/>
<AppLink
theme={AppLinkTheme.SECONDARY}
to={RoutePath.article_create}
className={cls.createBtn}
>
{t('Create new article')}
</AppLink>
<Button
theme={ButtonTheme.CLEAR_INVERTED}
className={cls.links}
Expand Down

0 comments on commit 3279048

Please sign in to comment.