Skip to content

Commit

Permalink
feat: add article view selector
Browse files Browse the repository at this point in the history
  • Loading branch information
sashtje committed Sep 6, 2023
1 parent 85a3e5a commit 5e2be76
Show file tree
Hide file tree
Showing 16 changed files with 380 additions and 9 deletions.
144 changes: 144 additions & 0 deletions json-server/db.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/app/providers/StoreProvider/config/StateSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ProfileSchema } from 'entities/Profile';
import { ArticleDetailsSchema } from 'entities/Article';
import { ArticleDetailsCommentsSchema } from 'pages/ArticleDetailsPage';
import { AddCommentFormSchema } from 'features/addCommentForm';
import { ArticlesPageSchema } from 'pages/ArticlesPage';

export interface StateSchema {
counter: CounterSchema,
Expand All @@ -22,6 +23,7 @@ export interface StateSchema {
articleDetails?: ArticleDetailsSchema,
articleDetailsComments?: ArticleDetailsCommentsSchema,
addCommentForm?: AddCommentFormSchema,
articlesPage?: ArticlesPageSchema,
}

export type StateSchemaKey = keyof StateSchema;
Expand Down
1 change: 1 addition & 0 deletions src/entities/Article/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export {
getArticleDetailsError,
} from './model/selectors/getArticleDetails/getArticleDetails';
export { ArticleList } from './ui/ArticleList/ArticleList';
export { ArticleViewSelector } from './ui/ArticleViewSelector/ArticleViewSelector';
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
.img {
width: 200px;
height: 200px;
object-fit: cover;
}

.infoWrapper {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.notSelected {
fill: var(--secondary-color);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { ComponentMeta, ComponentStory } from '@storybook/react';

import { ArticleViewSelector } from './ArticleViewSelector';

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

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

export const Normal = Template.bind({});
Normal.args = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { memo } from 'react';

import { Button, ButtonTheme } from 'shared/ui/Button';
import { Icon } from 'shared/ui/Icon';
import { classNames } from 'shared/lib/classNames';
import ViewSmallIcon from 'shared/assets/icons/view-small.svg';
import ViewBigIcon from 'shared/assets/icons/view-big.svg';

import { ArticleView } from '../../model/types/article';
import cls from './ArticleViewSelector.module.scss';

interface ArticleViewSelectorProps {
className?: string;
view: ArticleView;
onViewClick?: (view: ArticleView) => void;
}

const viewTypes = [
{
view: ArticleView.SMALL,
icon: ViewSmallIcon,
},
{
view: ArticleView.BIG,
icon: ViewBigIcon,
},
];

export const ArticleViewSelector = memo((props: ArticleViewSelectorProps) => {
const {
className,
view,
onViewClick,
} = props;

const onClick = (newView: ArticleView) => () => {
onViewClick?.(newView);
};

return (
<div className={classNames(cls.articleViewSelector, {}, [className])}>
{viewTypes.map((viewType) => (
<Button
key={viewType.view}
theme={ButtonTheme.CLEAR}
onClick={onClick(viewType.view)}
>
<Icon
className={classNames('', { [cls.notSelected]: viewType.view !== view }, [])}
Svg={viewType.icon}
/>
</Button>
))}
</div>
);
});

ArticleViewSelector.displayName = 'ArticleViewSelector';
1 change: 1 addition & 0 deletions src/pages/ArticlesPage/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { ArticlesPageAsync as ArticlesPage } from './ui/ArticlesPage/ArticlesPage.async';
export { ArticlesPageSchema } from './model/types/articlesPageSchema';
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { StateSchema } from 'app/providers/StoreProvider';
import { ArticleView } from 'entities/Article';

export const getArticlesPageIsLoading = (state: StateSchema) => state.articlesPage?.isLoading || false;
export const getArticlesPageError = (state: StateSchema) => state.articlesPage?.error;
export const getArticlesPageView = (state: StateSchema) => state.articlesPage?.view || ArticleView.SMALL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createAsyncThunk } from '@reduxjs/toolkit';

import { ThunkConfig } from 'app/providers/StoreProvider';

import { Article } from 'entities/Article';

export const fetchArticlesList = createAsyncThunk<Article[], void, ThunkConfig<string>>(
'articlesPage/fetchArticlesList',
async (_, thunkAPI) => {
const { extra, rejectWithValue } = thunkAPI;

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

if (!response.data) {
throw new Error();
}

return response.data;
} catch (e) {
return rejectWithValue('error');
}
},
);
56 changes: 56 additions & 0 deletions src/pages/ArticlesPage/model/slices/articlesPageSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { createEntityAdapter, createSlice, PayloadAction } from '@reduxjs/toolkit';

import { Article, ArticleView } from 'entities/Article';
import { StateSchema } from 'app/providers/StoreProvider';
import { ARTICLES_VIEW_LOCALSTORAGE_KEY } from 'shared/const/localStorage';

import { fetchArticlesList } from '../../model/services/fetchArticlesList/fetchArticlesList';
import { ArticlesPageSchema } from '../types/articlesPageSchema';

const articlesAdapter = createEntityAdapter<Article>({
selectId: (article) => article.id,
});

export const getArticles = articlesAdapter.getSelectors<StateSchema>(
(state) => state.articlesPage || articlesAdapter.getInitialState(),
);

const articlesPageSlice = createSlice({
name: 'articlesPageSlice',
initialState: articlesAdapter.getInitialState<ArticlesPageSchema>({
isLoading: false,
error: undefined,
ids: [],
entities: {},
view: ArticleView.SMALL,
}),
reducers: {
setView: (state, action: PayloadAction<ArticleView>) => {
state.view = action.payload;
localStorage.setItem(ARTICLES_VIEW_LOCALSTORAGE_KEY, action.payload);
},
initState: (state) => {
state.view = localStorage.getItem(ARTICLES_VIEW_LOCALSTORAGE_KEY) as ArticleView || ArticleView.SMALL;
},
},
extraReducers: (builder) => {
builder
.addCase(fetchArticlesList.pending, (state) => {
state.error = undefined;
state.isLoading = true;
})
.addCase(fetchArticlesList.fulfilled, (state, action: PayloadAction<Article[]>) => {
state.isLoading = false;
articlesAdapter.setAll(state, action.payload);
})
.addCase(fetchArticlesList.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload;
});
},
});

export const {
reducer: articlesPageReducer,
actions: articlesPageActions,
} = articlesPageSlice;
10 changes: 10 additions & 0 deletions src/pages/ArticlesPage/model/types/articlesPageSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { EntityState } from '@reduxjs/toolkit';

import { Article, ArticleView } from 'entities/Article';

export interface ArticlesPageSchema extends EntityState<Article>{
isLoading?: boolean;
error?: string;

view: ArticleView;
}
52 changes: 43 additions & 9 deletions src/pages/ArticlesPage/ui/ArticlesPage/ArticlesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,59 @@
import { memo } from 'react';
import { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';

import { classNames } from 'shared/lib/classNames';
import { ArticleList, ArticleView } from 'entities/Article';
import { ArticleList, ArticleView, ArticleViewSelector } from 'entities/Article';
import { DynamicModuleLoader, ReducersList } from 'shared/lib/components/DynamicModuleLoader/DynamicModuleLoader';
import { useInitialEffect } from 'shared/lib/hooks/useInitialEffect/useInitialEffect';
import { useAppDispatch } from 'shared/lib/hooks/useAppDispatch/useAppDispatch';

import {
getArticlesPageError,
getArticlesPageIsLoading,
getArticlesPageView,
} from '../../model/selectors/articlesPageSelectors';
import { fetchArticlesList } from '../../model/services/fetchArticlesList/fetchArticlesList';
import { articlesPageActions, articlesPageReducer, getArticles } from '../../model/slices/articlesPageSlice';
import cls from './ArticlesPage.module.scss';

interface ArticlesPageProps {
className?: string;
}

const reducers: ReducersList = {
articlesPage: articlesPageReducer,
};

export const ArticlesPage = memo((props: ArticlesPageProps) => {
const { className } = props;
const dispatch = useAppDispatch();
const articles = useSelector(getArticles.selectAll);

const isLoading = useSelector(getArticlesPageIsLoading);
const error = useSelector(getArticlesPageError);
const view = useSelector(getArticlesPageView);

const onChangeView = useCallback((view: ArticleView) => {
dispatch(articlesPageActions.setView(view));
}, [dispatch]);

useInitialEffect(() => {
dispatch(fetchArticlesList());
dispatch(articlesPageActions.initState());
});

return (
<div className={classNames(cls.articlesPage, {}, [className])}>
<ArticleList
isLoading
view={ArticleView.BIG}
articles={[]}
/>
</div>
<DynamicModuleLoader reducers={reducers}>
<div className={classNames(cls.articlesPage, {}, [className])}>
<ArticleViewSelector view={view} onViewClick={onChangeView} />

<ArticleList
isLoading={isLoading}
view={view}
articles={articles}
/>
</div>
</DynamicModuleLoader>
);
});

Expand Down
5 changes: 5 additions & 0 deletions src/shared/assets/icons/view-big.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/shared/assets/icons/view-small.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/shared/const/localStorage.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const USER_LOCALSTORAGE_KEY = 'user';
export const ARTICLES_VIEW_LOCALSTORAGE_KEY = 'articles_view';

0 comments on commit 5e2be76

Please sign in to comment.