diff --git a/extractedTranslations/en/translation.json b/extractedTranslations/en/translation.json index 60d401a..0439777 100644 --- a/extractedTranslations/en/translation.json +++ b/extractedTranslations/en/translation.json @@ -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 ": "", diff --git a/src/entities/Article/model/services/fetchArticleById/fetchArticleById.ts b/src/entities/Article/model/services/fetchArticleById/fetchArticleById.ts index cb7f42c..15e2565 100644 --- a/src/entities/Article/model/services/fetchArticleById/fetchArticleById.ts +++ b/src/entities/Article/model/services/fetchArticleById/fetchArticleById.ts @@ -10,7 +10,11 @@ ThunkConfig const { extra, rejectWithValue } = thunkAPI; try { - const response = await extra.api.get
(`/articles/${articleId}`); + const response = await extra.api.get
(`/articles/${articleId}`, { + params: { + _expand: 'user', + }, + }); if (!response.data) { throw new Error(); diff --git a/src/pages/ArticleEditPage/index.ts b/src/pages/ArticleEditPage/index.ts new file mode 100644 index 0000000..002af34 --- /dev/null +++ b/src/pages/ArticleEditPage/index.ts @@ -0,0 +1 @@ +export { ArticleEditPageAsync as ArticleEditPage } from './ui/ArticleEditPage.async'; diff --git a/src/pages/ArticleEditPage/ui/ArticleEditPage.async.tsx b/src/pages/ArticleEditPage/ui/ArticleEditPage.async.tsx new file mode 100644 index 0000000..3cee215 --- /dev/null +++ b/src/pages/ArticleEditPage/ui/ArticleEditPage.async.tsx @@ -0,0 +1,7 @@ +import { lazy } from 'react'; + +export const ArticleEditPageAsync = lazy(() => new Promise((resolve) => { + // @ts-ignore + // ТАК В РЕАЛЬНЫХ ПРОЕКТАХ НЕ ДЕЛАТЬ!!!!! ДЕЛАЕМ ДЛЯ КУРСА! + setTimeout(() => resolve(import('./ArticleEditPage')), 1500); +})); diff --git a/src/pages/ArticleEditPage/ui/ArticleEditPage.module.scss b/src/pages/ArticleEditPage/ui/ArticleEditPage.module.scss new file mode 100644 index 0000000..66f4eca --- /dev/null +++ b/src/pages/ArticleEditPage/ui/ArticleEditPage.module.scss @@ -0,0 +1,3 @@ +.ArticleEditPage { + +} diff --git a/src/pages/ArticleEditPage/ui/ArticleEditPage.stories.tsx b/src/pages/ArticleEditPage/ui/ArticleEditPage.stories.tsx new file mode 100644 index 0000000..d7c1ece --- /dev/null +++ b/src/pages/ArticleEditPage/ui/ArticleEditPage.stories.tsx @@ -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; + +const Template: ComponentStory = (args) => ; + +export const Normal = Template.bind({}); +Normal.args = {}; diff --git a/src/pages/ArticleEditPage/ui/ArticleEditPage.tsx b/src/pages/ArticleEditPage/ui/ArticleEditPage.tsx new file mode 100644 index 0000000..6302f94 --- /dev/null +++ b/src/pages/ArticleEditPage/ui/ArticleEditPage.tsx @@ -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 ( + + {isEdit ? 'Edit article' : 'Create new article'} + + ); +}); + +export default ArticleEditPage; diff --git a/src/pages/ArticlesDetailsPage/index.ts b/src/pages/ArticlesDetailsPage/index.ts index cc14eb3..4119a4f 100644 --- a/src/pages/ArticlesDetailsPage/index.ts +++ b/src/pages/ArticlesDetailsPage/index.ts @@ -1,4 +1,4 @@ -import { ArticlesDetailsPageAsync } from './ui/ArticlesDetailsPage.async'; +import { ArticlesDetailsPageAsync } from './ui/ArticlesDetailsPage/ArticlesDetailsPage.async'; export { ArticlesDetailsPageAsync as ArticlesDetailsPage, diff --git a/src/pages/ArticlesDetailsPage/model/selectors/article.ts b/src/pages/ArticlesDetailsPage/model/selectors/article.ts new file mode 100644 index 0000000..4aec603 --- /dev/null +++ b/src/pages/ArticlesDetailsPage/model/selectors/article.ts @@ -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; +}); diff --git a/src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPage.async.tsx b/src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPage/ArticlesDetailsPage.async.tsx similarity index 100% rename from src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPage.async.tsx rename to src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPage/ArticlesDetailsPage.async.tsx diff --git a/src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPage.module.scss b/src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPage/ArticlesDetailsPage.module.scss similarity index 100% rename from src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPage.module.scss rename to src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPage/ArticlesDetailsPage.module.scss diff --git a/src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPage.stories.tsx b/src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPage/ArticlesDetailsPage.stories.tsx similarity index 98% rename from src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPage.stories.tsx rename to src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPage/ArticlesDetailsPage.stories.tsx index fe70772..9b1130e 100644 --- a/src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPage.stories.tsx +++ b/src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPage/ArticlesDetailsPage.stories.tsx @@ -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', diff --git a/src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPage.tsx b/src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPage/ArticlesDetailsPage.tsx similarity index 74% rename from src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPage.tsx rename to src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPage/ArticlesDetailsPage.tsx index f47518d..75d54c2 100644 --- a/src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPage.tsx +++ b/src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPage/ArticlesDetailsPage.tsx @@ -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, @@ -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]); @@ -62,9 +57,7 @@ const ArticlesDetailsPage = (props: any) => { return ( - + ; + +const Template: ComponentStory = (args) => ; + +export const Normal = Template.bind({}); +Normal.args = {}; diff --git a/src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPageHeader/ArticlesDetailsPageHeader.tsx b/src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPageHeader/ArticlesDetailsPageHeader.tsx new file mode 100644 index 0000000..0401eca --- /dev/null +++ b/src/pages/ArticlesDetailsPage/ui/ArticlesDetailsPageHeader/ArticlesDetailsPageHeader.tsx @@ -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 ( +
+ + {canEdit && ( + + )} + +
+ ); +}); diff --git a/src/shared/config/routeConfig/routeConfig.tsx b/src/shared/config/routeConfig/routeConfig.tsx index 4f79561..fd870b2 100644 --- a/src/shared/config/routeConfig/routeConfig.tsx +++ b/src/shared/config/routeConfig/routeConfig.tsx @@ -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; @@ -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', } @@ -25,6 +28,8 @@ export const RoutePath: Record = { [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]: '*', }; @@ -52,6 +57,16 @@ export const routeConfig: Record = { element: , authOnly: true, }, + [AppRoutes.ARTICLE_CREATE]: { + path: `${RoutePath.article_create}`, + element: , + authOnly: true, + }, + [AppRoutes.ARTICLE_EDIT]: { + path: `${RoutePath.article_edit}`, + element: , + authOnly: true, + }, [AppRoutes.NOT_FOUND]: { path: RoutePath.not_found, element: , diff --git a/src/shared/ui/Text/Text.module.scss b/src/shared/ui/Text/Text.module.scss index cfe805a..d1d1f1b 100644 --- a/src/shared/ui/Text/Text.module.scss +++ b/src/shared/ui/Text/Text.module.scss @@ -16,6 +16,16 @@ } } +.inverted { + .title { + color: var(--inverted-primary-color); + } + + .text { + color: var(--inverted-secondary-color); + } +} + .left { text-align: left; } diff --git a/src/shared/ui/Text/Text.tsx b/src/shared/ui/Text/Text.tsx index 3e51bfc..c3523c7 100644 --- a/src/shared/ui/Text/Text.tsx +++ b/src/shared/ui/Text/Text.tsx @@ -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 { diff --git a/src/widgets/Navbar/ui/Navbar.module.scss b/src/widgets/Navbar/ui/Navbar.module.scss index 2715c54..3d31eec 100644 --- a/src/widgets/Navbar/ui/Navbar.module.scss +++ b/src/widgets/Navbar/ui/Navbar.module.scss @@ -14,3 +14,8 @@ .mainLink { margin-right: 15px; } + +.appName { + width: var(--sidebar-width); + margin-left: 15px; +} diff --git a/src/widgets/Navbar/ui/Navbar.tsx b/src/widgets/Navbar/ui/Navbar.tsx index 4cf7ef5..62421de 100644 --- a/src/widgets/Navbar/ui/Navbar.tsx +++ b/src/widgets/Navbar/ui/Navbar.tsx @@ -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'; @@ -32,6 +35,18 @@ export const Navbar = memo(({ className }: NavbarProps) => { if (authData) { return (
+ + + {t('Create new article')} +