diff --git a/.eslintrc.js b/.eslintrc.js index 03cb26e2..82a43dba 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -46,7 +46,15 @@ module.exports = { 'error', { markupOnly: true, - ignoreAttribute: ['data-testid', 'to', 'target'], + ignoreAttribute: [ + 'data-testid', + 'to', + 'target', + 'justify', + 'align', + 'direction', + 'gap', + ], }, ], 'max-len': ['error', { ignoreComments: true, code: 125 }], diff --git a/src/entities/Article/ui/AticleDetails/ArticleDetails.module.scss b/src/entities/Article/ui/AticleDetails/ArticleDetails.module.scss index ead4c9ad..086c6a24 100644 --- a/src/entities/Article/ui/AticleDetails/ArticleDetails.module.scss +++ b/src/entities/Article/ui/AticleDetails/ArticleDetails.module.scss @@ -1,34 +1,3 @@ .ArticleDetails { min-height: 100%; } - -.avatar { - margin: 0 auto; -} - -.title { - margin-top: 20px; -} - -.skeleton { - margin-top: 15px; -} - -.articleInfo { - display: flex; - align-items: center; -} - -.icon { - margin-right: 8px; -} - -.avatarWrapper { - display: flex; - width: 100%; - justify-content: center; -} - -.block { - margin-top: 16px; -} diff --git a/src/entities/Article/ui/AticleDetails/ArticleDetails.tsx b/src/entities/Article/ui/AticleDetails/ArticleDetails.tsx index ca0e8a3f..c5f1b69d 100644 --- a/src/entities/Article/ui/AticleDetails/ArticleDetails.tsx +++ b/src/entities/Article/ui/AticleDetails/ArticleDetails.tsx @@ -14,6 +14,7 @@ import EyeIcon from 'shared/assets/icons/eye.svg'; import CalendarIcon from 'shared/assets/icons/calendar.svg'; import { Icon } from 'shared/ui/Icon/Icon'; import { useInitialEffect } from 'shared/lib/hooks/useInitialEffect/useInitialEffect'; +import { HStack, VStack } from 'shared/ui/Stack'; import { ArticleBlock, ArticleBlockType } from '../../model/types/article'; import { fetchArticleById } from '../../model/services/fetchArticleById/fetchArticleById'; import cls from './ArticleDetails.module.scss'; @@ -35,7 +36,7 @@ interface ArticleDetailsProps { const reducers: ReducersList = { articleDetails: articleDetailsReducer, }; -/* eslint-disable indent */ + const renderBlock = (block: ArticleBlock) => { switch (block.type) { case ArticleBlockType.CODE: @@ -66,7 +67,6 @@ const renderBlock = (block: ArticleBlock) => { return null; } }; -/* eslint-enable indent */ export const ArticleDetails = memo(({ className, id }: ArticleDetailsProps) => { const { t } = useTranslation('article-details'); @@ -83,50 +83,45 @@ export const ArticleDetails = memo(({ className, id }: ArticleDetailsProps) => { if (isLoading) { content = ( - <> - - - - - - + + + + + + + ); } else if (error) { content = ( - + + + ); } else { content = ( <> -
- + + + + -
- -
- - -
-
- - -
+ + + + + + + + + {article?.blocks.map(renderBlock)} ); @@ -134,9 +129,13 @@ export const ArticleDetails = memo(({ className, id }: ArticleDetailsProps) => { return ( -
+ {content} -
+
); }); diff --git a/src/entities/Comment/ui/CommentCard/CommentCard.module.scss b/src/entities/Comment/ui/CommentCard/CommentCard.module.scss index 8c3792f8..72d55960 100644 --- a/src/entities/Comment/ui/CommentCard/CommentCard.module.scss +++ b/src/entities/Comment/ui/CommentCard/CommentCard.module.scss @@ -2,16 +2,3 @@ padding: 10px; border: 1px solid var(--primary-color); } - -.header { - display: flex; - align-items: center; -} - -.text { - margin-top: 10px; -} - -.username { - margin-left: 10px; -} diff --git a/src/entities/Comment/ui/CommentCard/CommentCard.tsx b/src/entities/Comment/ui/CommentCard/CommentCard.tsx index 9d8f075c..9515ed82 100644 --- a/src/entities/Comment/ui/CommentCard/CommentCard.tsx +++ b/src/entities/Comment/ui/CommentCard/CommentCard.tsx @@ -4,6 +4,7 @@ import { Avatar } from 'shared/ui/Avatar/Avatar'; import { Text } from 'shared/ui/Text/Text'; import { AppLink } from 'shared/ui/AppLink/AppLink'; import { RoutePath } from 'shared/config/routeConfig/routeConfig'; +import { HStack, VStack } from 'shared/ui/Stack'; import cls from './CommentCard.module.scss'; import { Comment } from '../../model/types/comment'; @@ -17,17 +18,20 @@ export const CommentCard = memo(({ className, comment }: CommentCardProps) => { return null; } return ( -
- - {comment.user.avatar ? ( - - ) : null} - + + + + {comment.user.avatar ? ( + + ) : null} + + - -
+ + ); }); diff --git a/src/entities/Comment/ui/CommentList/CommentList.module.scss b/src/entities/Comment/ui/CommentList/CommentList.module.scss deleted file mode 100644 index c1772621..00000000 --- a/src/entities/Comment/ui/CommentList/CommentList.module.scss +++ /dev/null @@ -1,16 +0,0 @@ -.comment { - margin-top: 20px; -} - -.header { - display: flex; - align-items: center; -} - -.text { - margin-top: 10px; -} - -.username { - margin-left: 10px; -} diff --git a/src/entities/Comment/ui/CommentList/CommentList.tsx b/src/entities/Comment/ui/CommentList/CommentList.tsx index 1fb3cb1b..19b0c880 100644 --- a/src/entities/Comment/ui/CommentList/CommentList.tsx +++ b/src/entities/Comment/ui/CommentList/CommentList.tsx @@ -3,7 +3,7 @@ import { classNames } from 'shared/lib/classNames/classNames'; import { useTranslation } from 'react-i18next'; import { Text } from 'shared/ui/Text/Text'; import { Skeleton } from 'shared/ui/Skeleton/Skeleton'; -import cls from './CommentList.module.scss'; +import { HStack, VStack } from 'shared/ui/Stack'; import { Comment } from '../../model/types/comment'; import { CommentCard } from '../CommentCard/CommentCard'; @@ -18,33 +18,25 @@ export const CommentList = memo( const { t } = useTranslation('comments'); if (isLoading) { return ( -
-
+ + - -
- -
+ + + + ); } return ( -
+ {comments.length ? ( comments.map((comment) => ( - + )) ) : ( )} -
+ ); }, ); diff --git a/src/entities/Profile/ui/ProfileCard/ProfileCard.module.scss b/src/entities/Profile/ui/ProfileCard/ProfileCard.module.scss index e1f07a00..03d39140 100644 --- a/src/entities/Profile/ui/ProfileCard/ProfileCard.module.scss +++ b/src/entities/Profile/ui/ProfileCard/ProfileCard.module.scss @@ -3,20 +3,7 @@ border: 2px solid var(--inverted-bg-color); } -.input { - margin-top: 10px; -} - .loading, .error { - display: flex; - align-items: center; - justify-content: center; height: 300px; } - -.avatarWrapper { - width: 100%; - display: flex; - justify-content: center; -} diff --git a/src/entities/Profile/ui/ProfileCard/ProfileCard.tsx b/src/entities/Profile/ui/ProfileCard/ProfileCard.tsx index 3a8ba13e..63280fb8 100644 --- a/src/entities/Profile/ui/ProfileCard/ProfileCard.tsx +++ b/src/entities/Profile/ui/ProfileCard/ProfileCard.tsx @@ -6,6 +6,7 @@ import { Loader } from 'shared/ui/Loader/Loader'; import { Avatar } from 'shared/ui/Avatar/Avatar'; import { Currency, CurrencySelect } from 'entities/Currency'; import { Country, CountrySelect } from 'entities/Country'; +import { HStack, VStack } from 'shared/ui/Stack'; import cls from './ProfileCard.module.scss'; import { Profile } from '../../model/types/profile'; @@ -49,20 +50,24 @@ export const ProfileCard = (props: ProfileCardProps) => { if (isLoading) { return ( -
-
+ ); } if (error) { return ( -
{ title={t('There was an error')} align={TextAlign.CENTER} /> -
+ ); } const onKeyPress = (event: React.KeyboardEvent) => { @@ -84,69 +89,63 @@ export const ProfileCard = (props: ProfileCardProps) => { }; return ( -
-
- {data?.avatar && ( -
- -
- )} - - - - - - - - -
-
+ + {data?.avatar && ( + + + + )} + + + + + + + + + ); }; diff --git a/src/features/AddCommentForm/ui/AddCommentForm/AddCommentForm.module.scss b/src/features/AddCommentForm/ui/AddCommentForm/AddCommentForm.module.scss index dc3a9b5c..a95f9918 100644 --- a/src/features/AddCommentForm/ui/AddCommentForm/AddCommentForm.module.scss +++ b/src/features/AddCommentForm/ui/AddCommentForm/AddCommentForm.module.scss @@ -1,11 +1,4 @@ .AddCommentForm { border: 1px solid var(--primary-color); padding: 16px; - display: flex; - align-items: center; - justify-content: space-between; -} - -.input { - flex-grow: 1; } diff --git a/src/features/AddCommentForm/ui/AddCommentForm/AddCommentForm.tsx b/src/features/AddCommentForm/ui/AddCommentForm/AddCommentForm.tsx index 39b1e96a..0a6c718a 100644 --- a/src/features/AddCommentForm/ui/AddCommentForm/AddCommentForm.tsx +++ b/src/features/AddCommentForm/ui/AddCommentForm/AddCommentForm.tsx @@ -6,6 +6,7 @@ import { Button } from 'shared/ui/Button/Button'; import { useSelector } from 'react-redux'; import { useAppDispatch } from 'shared/lib/hooks/useAppDispatch/useAppDispatch'; import { DynamicModuleLoader } from 'shared/lib/components/DynamicModuleLoader/DynamicModuleLoader'; +import { HStack } from 'shared/ui/Stack'; import { addCommentFormActions, addCommentFormReducer, @@ -50,17 +51,18 @@ const AddCommentForm = memo( return ( -
-
+
); }, diff --git a/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.module.scss b/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.module.scss index 6acb60a2..cb35cea8 100644 --- a/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.module.scss +++ b/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.module.scss @@ -2,13 +2,7 @@ width: 100%; } -.commentTitle { - margin-top: 24px; -} - .recommendations { - padding: 10px; - margin-top: 24px; flex-wrap: nowrap; overflow-y: hidden; overflow-x: auto; diff --git a/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.tsx b/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.tsx index 997523fa..ba2c6118 100644 --- a/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.tsx +++ b/src/pages/ArticleDetailsPage/ui/ArticleDetailsPage/ArticleDetailsPage.tsx @@ -21,6 +21,7 @@ import { AddCommentForm } from 'features/AddCommentForm'; import { Button, ButtonVariant } from 'shared/ui/Button/Button'; import { RoutePath } from 'shared/config/routeConfig/routeConfig'; import { Page } from 'widgets/Page'; +import { HStack, VStack } from 'shared/ui/Stack'; import { articleDetailsPageReducer } from '../../model/slices'; import { fetchArticleRecommendations } from '../../model/services/fetchArticleRecommendations/fetchArticleRecommendations'; import { getArticleRecommendationsIsLoading } from '../../model/selectors/recommendations'; @@ -81,18 +82,14 @@ const ArticleDetailsPage = ({ className }: ArticleDetailsPageProps) => { let commentBlock; if (!errorArticle) { commentBlock = ( - <> - + + - + ); } @@ -101,23 +98,26 @@ const ArticleDetailsPage = ({ className }: ArticleDetailsPageProps) => { - - - - - {commentBlock} + + + + + + + + {commentBlock} + ); diff --git a/src/pages/ProfilePage/ui/ProfilePage.tsx b/src/pages/ProfilePage/ui/ProfilePage.tsx index 875d11c8..4028d7d4 100644 --- a/src/pages/ProfilePage/ui/ProfilePage.tsx +++ b/src/pages/ProfilePage/ui/ProfilePage.tsx @@ -25,6 +25,7 @@ import { useTranslation } from 'react-i18next'; import { useInitialEffect } from 'shared/lib/hooks/useInitialEffect/useInitialEffect'; import { useParams } from 'react-router-dom'; import { Page } from 'widgets/Page'; +import { VStack } from 'shared/ui/Stack'; import { ProfilePageHeader } from './ProfilePageHeader/ProfilePageHeader'; const redusers: ReducersList = { @@ -111,7 +112,6 @@ const ProfilePage = ({ className }: ProfilePageProps) => { }, [dispatch], ); - const onChangeCountry = useCallback( (country: Country) => { dispatch(profileActions.updateProfile({ country })); @@ -122,29 +122,31 @@ const ProfilePage = ({ className }: ProfilePageProps) => { return ( - - {validateErrors?.length && - validateErrors.map((error) => ( - - ))} - + + + {validateErrors?.length && + validateErrors.map((error) => ( + + ))} + + ); diff --git a/src/pages/ProfilePage/ui/ProfilePageHeader/ProfilePageHeader.module.scss b/src/pages/ProfilePage/ui/ProfilePageHeader/ProfilePageHeader.module.scss deleted file mode 100644 index a559bef0..00000000 --- a/src/pages/ProfilePage/ui/ProfilePageHeader/ProfilePageHeader.module.scss +++ /dev/null @@ -1,14 +0,0 @@ -.ProfilePageHeader { - display: flex; - margin-bottom: 12px; - - .editBtn, - .saveBtn { - margin-left: auto; - margin-right: 12px; - } -} - -.btnsWrapper { - margin-left: auto; -} diff --git a/src/pages/ProfilePage/ui/ProfilePageHeader/ProfilePageHeader.tsx b/src/pages/ProfilePage/ui/ProfilePageHeader/ProfilePageHeader.tsx index fc0f9235..8e4f52a3 100644 --- a/src/pages/ProfilePage/ui/ProfilePageHeader/ProfilePageHeader.tsx +++ b/src/pages/ProfilePage/ui/ProfilePageHeader/ProfilePageHeader.tsx @@ -12,7 +12,7 @@ import { import { useSelector } from 'react-redux'; import { useCallback } from 'react'; import { getUserAuthData } from 'entities/User'; -import cls from './ProfilePageHeader.module.scss'; +import { HStack, VStack } from 'shared/ui/Stack'; interface ProfilePageHeaderProps { className?: string; @@ -39,22 +39,24 @@ export const ProfilePageHeader = ({ className }: ProfilePageHeaderProps) => { }, [dispatch]); return ( -
+ {canEdit && ( -
+
{readOnly ? ( ) : ( - <> + - + )}
)} -
+
); }; diff --git a/src/shared/ui/Stack/Flex/Flex.module.scss b/src/shared/ui/Stack/Flex/Flex.module.scss new file mode 100644 index 00000000..b5eacb0c --- /dev/null +++ b/src/shared/ui/Stack/Flex/Flex.module.scss @@ -0,0 +1,63 @@ +.Flex { + display: flex; +} + +.justifyStart { + justify-content: flex-start; +} + +.justifyCenter { + justify-content: center; +} + +.justifyEnd { + justify-content: flex-end; +} + +.justifyBetween { + justify-content: space-between; +} + +.alignStart { + align-items: flex-start; +} + +.alignCenter { + align-items: center; +} + +.alignEnd { + align-items: flex-end; +} + +.directionRow { + flex-direction: row; +} + +.directionColumn { + flex-direction: column; +} + +.gap4 { + gap: 4px; +} + +.gap8 { + gap: 8px; +} + +.gap10 { + gap: 10px; +} + +.gap12 { + gap: 12px; +} + +.gap16 { + gap: 16px; +} + +.max { + width: 100%; +} diff --git a/src/shared/ui/Stack/Flex/Flex.stories.tsx b/src/shared/ui/Stack/Flex/Flex.stories.tsx new file mode 100644 index 00000000..7a374e7d --- /dev/null +++ b/src/shared/ui/Stack/Flex/Flex.stories.tsx @@ -0,0 +1,92 @@ +/* eslint-disable i18next/no-literal-string */ +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { Flex } from './Flex'; + +export default { + title: 'shared/Flex', + component: Flex, + argTypes: { + backgroundColor: { control: 'color' }, + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ; + +export const Row = Template.bind({}); +Row.args = { + children: ( + <> +
flex
+
flex
+
flex
+
flex
+ + ), +}; + +export const RowGap4 = Template.bind({}); +RowGap4.args = { + gap: '4', + children: ( + <> +
flex
+
flex
+
flex
+
flex
+ + ), +}; + +export const RowGap16 = Template.bind({}); +RowGap16.args = { + gap: '16', + children: ( + <> +
flex
+
flex
+
flex
+
flex
+ + ), +}; + +export const Column = Template.bind({}); +Column.args = { + direction: 'column', + children: ( + <> +
flex
+
flex
+
flex
+
flex
+ + ), +}; + +export const ColumnGap4 = Template.bind({}); +ColumnGap4.args = { + direction: 'column', + gap: '4', + children: ( + <> +
flex
+
flex
+
flex
+
flex
+ + ), +}; + +export const ColumnGap16 = Template.bind({}); +ColumnGap16.args = { + direction: 'column', + gap: '16', + children: ( + <> +
flex
+
flex
+
flex
+
flex
+ + ), +}; diff --git a/src/shared/ui/Stack/Flex/Flex.tsx b/src/shared/ui/Stack/Flex/Flex.tsx new file mode 100644 index 00000000..cab93d49 --- /dev/null +++ b/src/shared/ui/Stack/Flex/Flex.tsx @@ -0,0 +1,76 @@ +import { classNames } from 'shared/lib/classNames/classNames'; +import { ReactNode } from 'react'; +import cls from './Flex.module.scss'; + +export type FlexJustify = 'start' | 'center' | 'end' | 'between'; +export type FlexAlign = 'start' | 'center' | 'end'; +export type FlexDirection = 'row' | 'column'; +export type FlexGap = '4' | '8' | '10' | '12' | '16'; + +const justifyClasses: Record = { + start: cls.justifyStart, + between: cls.justifyBetween, + center: cls.justifyCenter, + end: cls.justifyCenter, +}; + +const alignClasses: Record = { + start: cls.alignStart, + center: cls.alignCenter, + end: cls.alignCenter, +}; + +const directionClasses: Record = { + row: cls.directionRow, + column: cls.directionColumn, +}; + +const gapClasses: Record = { + 4: cls.gap4, + 8: cls.gap8, + 10: cls.gap10, + 12: cls.gap12, + 16: cls.gap16, +}; + +export interface FlexProps { + className?: string; + children: ReactNode; + justify?: FlexJustify; + align?: FlexAlign; + direction: FlexDirection; + gap?: FlexGap; + component?: keyof JSX.IntrinsicElements; + max?: boolean; +} + +export const Flex = ({ + className, + children, + justify = 'start', + align = 'center', + direction = 'row', + gap = '8', + component = 'div', + max, +}: FlexProps) => { + const classes = [ + className, + justifyClasses[justify], + alignClasses[align], + directionClasses[direction], + gap && gapClasses[gap], + ]; + + const mods = { + [cls.max]: max, + }; + + const ComponentWrapper = component; + + return ( + + {children} + + ); +}; diff --git a/src/shared/ui/Stack/HStack/HStack.tsx b/src/shared/ui/Stack/HStack/HStack.tsx new file mode 100644 index 00000000..a6b21ce8 --- /dev/null +++ b/src/shared/ui/Stack/HStack/HStack.tsx @@ -0,0 +1,7 @@ +import { Flex, FlexProps } from '../Flex/Flex'; + +type HStackProps = Omit; + +export const HStack = (props: HStackProps) => ( + +); diff --git a/src/shared/ui/Stack/VStack/VStack.tsx b/src/shared/ui/Stack/VStack/VStack.tsx new file mode 100644 index 00000000..fa6488f5 --- /dev/null +++ b/src/shared/ui/Stack/VStack/VStack.tsx @@ -0,0 +1,7 @@ +import { Flex, FlexProps } from '../Flex/Flex'; + +type VStackProps = Omit; + +export const VStack = ({ align = 'start', ...props }: VStackProps) => ( + +); diff --git a/src/shared/ui/Stack/index.ts b/src/shared/ui/Stack/index.ts new file mode 100644 index 00000000..b20bde75 --- /dev/null +++ b/src/shared/ui/Stack/index.ts @@ -0,0 +1,2 @@ +export { HStack } from './HStack/HStack'; +export { VStack } from './VStack/VStack'; diff --git a/src/widgets/Sidebar/ui/Sidebar/Sidebar.module.scss b/src/widgets/Sidebar/ui/Sidebar/Sidebar.module.scss index 67d53d37..b56fd28e 100644 --- a/src/widgets/Sidebar/ui/Sidebar/Sidebar.module.scss +++ b/src/widgets/Sidebar/ui/Sidebar/Sidebar.module.scss @@ -5,6 +5,7 @@ position: relative; transition: width 0.2s; flex-shrink: 0; + padding-left: 30px; } .switchers { @@ -26,15 +27,9 @@ z-index: 1000; } -.items { - margin-top: 20px; - margin-left: 30px; - display: flex; - flex-direction: column; -} - .collapsed { width: var(--sidebar-width-collapsed); + padding-left: 0; .switchers { flex-direction: column; diff --git a/src/widgets/Sidebar/ui/Sidebar/Sidebar.tsx b/src/widgets/Sidebar/ui/Sidebar/Sidebar.tsx index 6a7e4b7b..7d7fbb01 100644 --- a/src/widgets/Sidebar/ui/Sidebar/Sidebar.tsx +++ b/src/widgets/Sidebar/ui/Sidebar/Sidebar.tsx @@ -4,6 +4,7 @@ import { ThemeSwitcher } from 'shared/ui/ThemeSwitcher/ThemeSwitcher'; import { LangSwitcher } from 'shared/ui/LangSwitcher/LangSwitcher'; import { Button, ButtonSize, ButtonVariant } from 'shared/ui/Button/Button'; import { useSelector } from 'react-redux'; +import { VStack } from 'shared/ui/Stack'; import { getSiderbarItems } from '../../model/selecotrs/getSidebarItems'; import cls from './Sidebar.module.scss'; import { SidebarItem } from '../SidebarItem/SidebarItem'; @@ -42,7 +43,9 @@ export const Sidebar = memo(({ className }: SidebarProps) => { className, ])} > -
{itemList}
+ + {itemList} +