Skip to content

Commit

Permalink
feat: redesign article details
Browse files Browse the repository at this point in the history
  • Loading branch information
sashtje committed Oct 14, 2023
1 parent d545516 commit cdbf1a2
Show file tree
Hide file tree
Showing 35 changed files with 459 additions and 135 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions cypress/e2e/articles/article-details.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ describe('article details testing', () => {
});

it('should see a content of the article', () => {
cy.getByTestId('ArticleDetails.title').should('exist');
cy.getByTestId('ArticleDetailsDeprecated.title').should('exist');
});

it('should see a list of recommendations', () => {
cy.getByTestId('ArticleRecommendationsList').should('exist');
});

it('leaves a comment', () => {
cy.getByTestId('ArticleDetails.title');
cy.getByTestId('ArticleDetailsDeprecated.title');
cy.getByTestId('AddCommentForm').scrollIntoView();

cy.addComment('text');
Expand All @@ -33,7 +33,7 @@ describe('article details testing', () => {

it('leaves a rate', () => {
// cy.intercept('GET', '**/articles/*', { fixture: 'article-details.json' });
cy.getByTestId('ArticleDetails.title');
cy.getByTestId('ArticleDetailsDeprecated.title');
cy.getByTestId('RatingCard').scrollIntoView();

cy.setRate(5, 'feedback');
Expand Down
2 changes: 1 addition & 1 deletion json-server/db.json
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,7 @@
"isAppRedesigned": true
},
"jsonSettings": {
"theme": "app_dark_theme",
"theme": "app_light_theme",
"isFirstVisit": true,
"settingsPageHasBeenOpen": false,
"isArticlesPageWasOpened": true
Expand Down
9 changes: 9 additions & 0 deletions src/app/styles/reset.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,12 @@ select {
a {
text-decoration: none;
}

h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: normal;
}
2 changes: 1 addition & 1 deletion src/entities/Article/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Article Entity for Blog
#### Public api

- Components
- `ArticleDetails` - Component with article information
- `ArticleDetailsDeprecated` - Component with article information
- `ArticleList` - Component with list of articles
- `ArticleViewSelector` - Component - switcher for displaying a list of articles (tile, list)
- `ArticleSortSelector` - Component with a choice of sorting for a list of articles
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { memo } from 'react';

import { classNames } from '@/shared/lib/classNames';
import { Code } from '@/shared/ui/deprecated/Code';
import { Code as CodeDeprecated } from '@/shared/ui/deprecated/Code';
import { Code } from '@/shared/ui/redesigned/Code';
import { ToggleFeatures } from '@/shared/lib/features';

import { ArticleCodeBlock } from '../../model/types/article';
import cls from './ArticleCodeBlockComponent.module.scss';
Expand All @@ -15,9 +17,19 @@ export const ArticleCodeBlockComponent = memo((props: ArticleCodeBlockComponentP
const { className, block } = props;

return (
<div className={classNames(cls.articleCodeBlockComponent, {}, [className])}>
<Code text={block.code} />
</div>
<ToggleFeatures
feature="isAppRedesigned"
on={
<div className={classNames(cls.articleCodeBlockComponent, {}, [className])}>
<Code text={block.code} />
</div>
}
off={
<div className={classNames(cls.articleCodeBlockComponent, {}, [className])}>
<CodeDeprecated text={block.code} />
</div>
}
/>
);
});

Expand Down
118 changes: 9 additions & 109 deletions src/entities/Article/ui/ArticleDetails/ArticleDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,123 +1,23 @@
import { memo, useCallback, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { memo } from 'react';

import { classNames } from '@/shared/lib/classNames';
import {
DynamicModuleLoader,
ReducersList,
} from '@/shared/lib/components/DynamicModuleLoader/DynamicModuleLoader';
import { useAppDispatch } from '@/shared/lib/hooks/useAppDispatch/useAppDispatch';
import { Text, TextAlign, TextSize } from '@/shared/ui/deprecated/Text';
import { Skeleton } from '@/shared/ui/deprecated/Skeleton';
import { Avatar } from '@/shared/ui/deprecated/Avatar';
import { Icon } from '@/shared/ui/deprecated/Icon';
import EyeIcon from '@/shared/assets/icons/eye-20-20.svg';
import CalendarIcon from '@/shared/assets/icons/date-20-20.svg';
import { HStack, VStack } from '@/shared/ui/redesigned/Stack';

import { ArticleBlockType } from '../../model/consts/consts';
import { ArticleImageBlockComponent } from '../ArticleImageBlockComponent/ArticleImageBlockComponent';
import { ArticleCodeBlockComponent } from '../ArticleCodeBlockComponent/ArticleCodeBlockComponent';
import { ArticleTextBlockComponent } from '../ArticleTextBlockComponent/ArticleTextBlockComponent';
import { ArticleBlock } from '../../model/types/article';
import { fetchArticleById } from '../../model/services/fetchArticleById/fetchArticleById';
import {
getArticleDetails,
getArticleDetailsError,
getArticleDetailsIsLoading,
} from '../../model/selectors/getArticleDetails/getArticleDetails';
import cls from './ArticleDetails.module.scss';
import { articleDetailsReducer } from '../../model/slice/articleDetailsSlice';
import { ToggleFeatures } from '@/shared/lib/features';
import { ArticleDetailsRedesigned } from '@/entities/Article/ui/ArticleDetails/ArticleDetailsRedesigned/ArticleDetailsRedesigned';
import { ArticleDetailsDeprecated } from '@/entities/Article/ui/ArticleDetails/ArticleDetailsDeprecated/ArticleDetailsDeprecated';

interface ArticleDetailsProps {
className?: string;
id: string;
}

const reducers: ReducersList = {
articleDetails: articleDetailsReducer,
};

export const ArticleDetails = memo((props: ArticleDetailsProps) => {
const { className, id } = props;

const { t } = useTranslation('articles');
const dispatch = useAppDispatch();
const article = useSelector(getArticleDetails);
const isLoading = useSelector(getArticleDetailsIsLoading);
const error = useSelector(getArticleDetailsError);

const renderBlock = useCallback((block: ArticleBlock) => {
switch (block.type) {
case ArticleBlockType.TEXT:
return <ArticleTextBlockComponent key={block.id} block={block} className={cls.block} />;
case ArticleBlockType.CODE:
return <ArticleCodeBlockComponent key={block.id} block={block} className={cls.block} />;
case ArticleBlockType.IMAGE:
return <ArticleImageBlockComponent key={block.id} block={block} className={cls.block} />;
default:
return null;
}
}, []);

useEffect(() => {
if (__PROJECT__ !== 'storybook') {
dispatch(fetchArticleById(id));
}
}, [dispatch, id]);

let content;

if (isLoading) {
content = (
<>
<Skeleton className={cls.avatar} width={200} height={200} borderRadius="50%" />
<Skeleton className={cls.title} width={300} height={32} />
<Skeleton className={cls.skeleton} width={600} height={24} />
<Skeleton className={cls.skeleton} width="100%" height={200} />
<Skeleton className={cls.skeleton} width="100%" height={200} />
</>
);
} else if (error) {
content = <Text title={t('Произошла ошибка при загрузке статьи')} align={TextAlign.CENTER} />;
} else {
content = (
<>
<HStack max className={cls.avatarWrapper}>
<Avatar size={200} src={article?.img} className={cls.avatar} />
</HStack>

<VStack gap="4" max data-testid="ArticleDetails.title">
<Text
className={cls.title}
title={article?.title}
text={article?.subtitle}
size={TextSize.L}
/>

<HStack gap="8" className={cls.articleInfo}>
<Icon className={cls.icon} Svg={EyeIcon} />
<Text text={String(article?.views)} />
</HStack>

<HStack gap="8" className={cls.articleInfo}>
<Icon className={cls.icon} Svg={CalendarIcon} />
<Text text={article?.createdAt} />
</HStack>
</VStack>

{article?.blocks.map(renderBlock)}
</>
);
}

return (
<DynamicModuleLoader reducers={reducers} removeAfterUnmount>
<VStack gap="16" max className={classNames(cls.articleDetails, {}, [className])}>
{content}
</VStack>
</DynamicModuleLoader>
<ToggleFeatures
feature="isAppRedesigned"
on={<ArticleDetailsRedesigned id={id} className={className} />}
off={<ArticleDetailsDeprecated id={id} className={className} />}
/>
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react';

import { StoreDecorator } from '@/shared/config/storybook/StoreDecorator/StoreDecorator';

import { ArticleDetails } from './ArticleDetails';
import { article } from '../../mocks/data';
import { ArticleDetailsDeprecated } from './ArticleDetailsDeprecated';
import { article } from '../../../mocks/data';

export default {
title: 'entities/Article/ArticleDetails',
component: ArticleDetails,
title: 'entities/Article/ArticleDetailsDeprecated',
component: ArticleDetailsDeprecated,
argTypes: {
backgroundColor: { control: 'color' },
},
} as ComponentMeta<typeof ArticleDetails>;
} as ComponentMeta<typeof ArticleDetailsDeprecated>;

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

export const Normal = Template.bind({});
Normal.args = {};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { memo, useCallback, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';

import { classNames } from '@/shared/lib/classNames';
import {
DynamicModuleLoader,
ReducersList,
} from '@/shared/lib/components/DynamicModuleLoader/DynamicModuleLoader';
import { useAppDispatch } from '@/shared/lib/hooks/useAppDispatch/useAppDispatch';
import { Text, TextAlign, TextSize } from '@/shared/ui/deprecated/Text';
import { Skeleton } from '@/shared/ui/deprecated/Skeleton';
import { Avatar } from '@/shared/ui/deprecated/Avatar';
import { Icon } from '@/shared/ui/deprecated/Icon';
import EyeIcon from '@/shared/assets/icons/eye-20-20.svg';
import CalendarIcon from '@/shared/assets/icons/date-20-20.svg';
import { HStack, VStack } from '@/shared/ui/redesigned/Stack';

import { ArticleBlockType } from '../../../model/consts/consts';
import { ArticleImageBlockComponent } from '../../ArticleImageBlockComponent/ArticleImageBlockComponent';
import { ArticleCodeBlockComponent } from '../../ArticleCodeBlockComponent/ArticleCodeBlockComponent';
import { ArticleTextBlockComponent } from '../../ArticleTextBlockComponent/ArticleTextBlockComponent';
import { ArticleBlock } from '../../../model/types/article';
import { fetchArticleById } from '../../../model/services/fetchArticleById/fetchArticleById';
import {
getArticleDetails,
getArticleDetailsError,
getArticleDetailsIsLoading,
} from '../../../model/selectors/getArticleDetails/getArticleDetails';
import cls from './ArticleDetailsDeprecated.module.scss';
import { articleDetailsReducer } from '../../../model/slice/articleDetailsSlice';

interface ArticleDetailsProps {
className?: string;
id: string;
}

const reducers: ReducersList = {
articleDetails: articleDetailsReducer,
};

export const ArticleDetailsDeprecated = memo((props: ArticleDetailsProps) => {
const { className, id } = props;

const { t } = useTranslation('articles');
const dispatch = useAppDispatch();
const article = useSelector(getArticleDetails);
const isLoading = useSelector(getArticleDetailsIsLoading);
const error = useSelector(getArticleDetailsError);

const renderBlock = useCallback((block: ArticleBlock) => {
switch (block.type) {
case ArticleBlockType.TEXT:
return <ArticleTextBlockComponent key={block.id} block={block} className={cls.block} />;
case ArticleBlockType.CODE:
return <ArticleCodeBlockComponent key={block.id} block={block} className={cls.block} />;
case ArticleBlockType.IMAGE:
return <ArticleImageBlockComponent key={block.id} block={block} className={cls.block} />;
default:
return null;
}
}, []);

useEffect(() => {
if (__PROJECT__ !== 'storybook') {
dispatch(fetchArticleById(id));
}
}, [dispatch, id]);

let content;

if (isLoading) {
content = (
<>
<Skeleton className={cls.avatar} width={200} height={200} borderRadius="50%" />
<Skeleton className={cls.title} width={300} height={32} />
<Skeleton className={cls.skeleton} width={600} height={24} />
<Skeleton className={cls.skeleton} width="100%" height={200} />
<Skeleton className={cls.skeleton} width="100%" height={200} />
</>
);
} else if (error) {
content = <Text title={t('Произошла ошибка при загрузке статьи')} align={TextAlign.CENTER} />;
} else {
content = (
<>
<HStack max className={cls.avatarWrapper}>
<Avatar size={200} src={article?.img} className={cls.avatar} />
</HStack>

<VStack gap="4" max data-testid="ArticleDetails.title">
<Text
className={cls.title}
title={article?.title}
text={article?.subtitle}
size={TextSize.L}
/>

<HStack gap="8" className={cls.articleInfo}>
<Icon className={cls.icon} Svg={EyeIcon} />
<Text text={String(article?.views)} />
</HStack>

<HStack gap="8" className={cls.articleInfo}>
<Icon className={cls.icon} Svg={CalendarIcon} />
<Text text={article?.createdAt} />
</HStack>
</VStack>

{article?.blocks.map(renderBlock)}
</>
);
}

return (
<DynamicModuleLoader reducers={reducers} removeAfterUnmount>
<VStack gap="16" max className={classNames(cls.articleDetails, {}, [className])}>
{content}
</VStack>
</DynamicModuleLoader>
);
});

ArticleDetailsDeprecated.displayName = 'ArticleDetailsDeprecated';
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.articleDetails {
min-height: 100%;
}

.avatar {
margin: 0 auto;
}

.img {
max-height: 420px;
max-width: 100%;
margin: 0 auto;
}
Loading

0 comments on commit cdbf1a2

Please sign in to comment.