Skip to content

Commit

Permalink
feat: add pagination
Browse files Browse the repository at this point in the history
  • Loading branch information
sashtje committed Sep 6, 2023
1 parent 5e2be76 commit 7bd02c6
Show file tree
Hide file tree
Showing 24 changed files with 717 additions and 111 deletions.
516 changes: 462 additions & 54 deletions json-server/db.json

Large diffs are not rendered by default.

17 changes: 6 additions & 11 deletions src/app/providers/router/ui/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,16 @@ import { routerConfig, AppRoutesProps } from 'shared/config/routerConfig';
import { RequireAuth } from './RequireAuth';

export const AppRouter = memo(() => {
const renderWithWrapper = useCallback(({ path, element, authOnly }: AppRoutesProps) => {
const routeElement = (
<div className="page-wrapper">
{element}
</div>
);

return (
const renderWithWrapper = useCallback(
({ path, element, authOnly }: AppRoutesProps) => (
<Route
key={path}
path={path}
element={authOnly ? <RequireAuth>{routeElement}</RequireAuth> : routeElement}
element={authOnly ? <RequireAuth>{element}</RequireAuth> : element}
/>
);
}, []);
),
[],
);

return (
<Suspense fallback={<PageLoader />}>
Expand Down
5 changes: 3 additions & 2 deletions src/app/providers/router/ui/RequireAuth.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { ReactNode } from 'react';
import { useSelector } from 'react-redux';
import { Navigate, useLocation } from 'react-router-dom';

import { getUserAuthData } from 'entities/User';
import { AppRoutes, RoutePath } from 'shared/config/routerConfig/routerConfig';

export function RequireAuth({ children }: {children: JSX.Element}) {
export function RequireAuth({ children }: {children: ReactNode}) {
const auth = useSelector(getUserAuthData);
const location = useLocation();

if (!auth?.username) {
return <Navigate to={RoutePath[AppRoutes.MAIN]} state={{ from: location }} replace />;
}

return children;
return children as JSX.Element;
}
8 changes: 0 additions & 8 deletions src/app/styles/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,3 @@ body {
.content-page {
display: flex;
}

.page-wrapper {
flex-grow: 1;
padding: 20px 20px 20px 40px;
height: calc(100vh - var(--navbar-height));
overflow-y: auto;
width: 100%;
}
9 changes: 1 addition & 8 deletions src/entities/Article/ui/ArticleList/ArticleList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,14 @@ export const ArticleList = memo((props: ArticleListProps) => {
/>
), [view]);

if (isLoading) {
return (
<div className={classNames(cls.articleList, {}, [className, cls[view]])}>
{getSkeletons(view)}
</div>
);
}

return (
<div className={classNames(cls.articleList, {}, [className, cls[view]])}>
{
articles.length > 0
? articles.map(renderArticle)
: null
}
{isLoading && getSkeletons(view)}
</div>
);
});
Expand Down
6 changes: 4 additions & 2 deletions src/pages/AboutPage/ui/AboutPage.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { VFC } from 'react';
import { useTranslation } from 'react-i18next';

import { Page } from 'shared/ui/Page';

export const AboutPage: VFC = () => {
const { t } = useTranslation('about');

return (
<div>
<Page>
{t('О сайте')}
</div>
</Page>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useInitialEffect } from 'shared/lib/hooks/useInitialEffect/useInitialEf
import { useAppDispatch } from 'shared/lib/hooks/useAppDispatch/useAppDispatch';
import { AddCommentForm } from 'features/addCommentForm';
import { AppRoutes, RoutePath } from 'shared/config/routerConfig/routerConfig';
import { Page } from 'shared/ui/Page';

import {
fetchCommentsByArticleId,
Expand Down Expand Up @@ -58,15 +59,15 @@ export const ArticleDetailsPage = memo((props: ArticleDetailsPageProps) => {

if (!id) {
return (
<div className={classNames(cls.articleDetailsPage, {}, [className])}>
<Page className={classNames(cls.articleDetailsPage, {}, [className])}>
{t('Статья не найдена')}
</div>
</Page>
);
}

return (
<DynamicModuleLoader reducers={reducersList} removeAfterUnmount>
<div className={classNames(cls.articleDetailsPage, {}, [className])}>
<Page className={classNames(cls.articleDetailsPage, {}, [className])}>
<Button onClick={onBackToList}>{t('Назад к списку')}</Button>

<ArticleDetails id={id} />
Expand All @@ -82,7 +83,7 @@ export const ArticleDetailsPage = memo((props: ArticleDetailsPageProps) => {
isLoading={commentsIsLoading}
comments={comments}
/>
</div>
</Page>
</DynamicModuleLoader>
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ 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;
export const getArticlesPageNumber = (state: StateSchema) => state.articlesPage?.page || 1;
export const getArticlesPageLimit = (state: StateSchema) => state.articlesPage?.limit || 9;
export const getArticlesPageHasMore = (state: StateSchema) => state.articlesPage?.hasMore;
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,25 @@ import { createAsyncThunk } from '@reduxjs/toolkit';
import { ThunkConfig } from 'app/providers/StoreProvider';

import { Article } from 'entities/Article';
import { getArticlesPageLimit } from 'pages/ArticlesPage/model/selectors/articlesPageSelectors';

export const fetchArticlesList = createAsyncThunk<Article[], void, ThunkConfig<string>>(
interface FetchArticlesListProps {
page?: number;
}

export const fetchArticlesList = createAsyncThunk<Article[], FetchArticlesListProps, ThunkConfig<string>>(
'articlesPage/fetchArticlesList',
async (_, thunkAPI) => {
const { extra, rejectWithValue } = thunkAPI;
async (args, thunkAPI) => {
const { extra, rejectWithValue, getState } = thunkAPI;
const { page = 1 } = args;
const limit = getArticlesPageLimit(getState());

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { TestAsyncThunk } from 'shared/lib/tests/testAsyncThunk/TestAsyncThunk';

import { fetchArticlesList } from '../fetchArticlesList/fetchArticlesList';
import { fetchNextArticlesPage } from './fetchNextArticlesPage';

jest.mock('../fetchArticlesList/fetchArticlesList');

describe('fetchNextArticlesPage.test', () => {
test('success fetch', async () => {
const thunk = new TestAsyncThunk(fetchNextArticlesPage, {
articlesPage: {
page: 2,
ids: [],
entities: {},
limit: 5,
isLoading: false,
hasMore: true,
},
});

await thunk.callThunk();

expect(thunk.dispatch).toBeCalledTimes(4);
expect(fetchArticlesList).toBeCalledWith({
page: 3,
});
});

test('fetchArticleList not called', async () => {
const thunk = new TestAsyncThunk(fetchNextArticlesPage, {
articlesPage: {
page: 2,
ids: [],
entities: {},
limit: 5,
isLoading: false,
hasMore: false,
},
});

await thunk.callThunk();

expect(thunk.dispatch).toBeCalledTimes(2);
expect(fetchArticlesList).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createAsyncThunk } from '@reduxjs/toolkit';

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

import { articlesPageActions } from '../../slices/articlesPageSlice';
import { fetchArticlesList } from '../../services/fetchArticlesList/fetchArticlesList';
import {
getArticlesPageHasMore,
getArticlesPageIsLoading,
getArticlesPageNumber,
} from '../../selectors/articlesPageSelectors';

export const fetchNextArticlesPage = createAsyncThunk<void, void, ThunkConfig<string>>(
'articlesPage/fetchNextArticlesPage',
async (_, thunkAPI) => {
const {
getState, dispatch,
} = thunkAPI;
const hasMore = getArticlesPageHasMore(getState());
const page = getArticlesPageNumber(getState());
const isLoading = getArticlesPageIsLoading(getState());

if (hasMore && !isLoading) {
dispatch(articlesPageActions.setPage(page + 1));
dispatch(fetchArticlesList({
page: page + 1,
}));
}
},
);
12 changes: 10 additions & 2 deletions src/pages/ArticlesPage/model/slices/articlesPageSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,21 @@ const articlesPageSlice = createSlice({
ids: [],
entities: {},
view: ArticleView.SMALL,
page: 1,
hasMore: true,
}),
reducers: {
setView: (state, action: PayloadAction<ArticleView>) => {
state.view = action.payload;
localStorage.setItem(ARTICLES_VIEW_LOCALSTORAGE_KEY, action.payload);
},
setPage: (state, action: PayloadAction<number>) => {
state.page = action.payload;
},
initState: (state) => {
state.view = localStorage.getItem(ARTICLES_VIEW_LOCALSTORAGE_KEY) as ArticleView || ArticleView.SMALL;
const view = localStorage.getItem(ARTICLES_VIEW_LOCALSTORAGE_KEY) as ArticleView || ArticleView.SMALL;
state.view = view;
state.limit = view === ArticleView.BIG ? 4 : 9;
},
},
extraReducers: (builder) => {
Expand All @@ -41,7 +48,8 @@ const articlesPageSlice = createSlice({
})
.addCase(fetchArticlesList.fulfilled, (state, action: PayloadAction<Article[]>) => {
state.isLoading = false;
articlesAdapter.setAll(state, action.payload);
articlesAdapter.addMany(state, action.payload);
state.hasMore = action.payload.length > 0;
})
.addCase(fetchArticlesList.rejected, (state, action) => {
state.isLoading = false;
Expand Down
5 changes: 5 additions & 0 deletions src/pages/ArticlesPage/model/types/articlesPageSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,9 @@ export interface ArticlesPageSchema extends EntityState<Article>{
error?: string;

view: ArticleView;

// pagination
page: number;
limit?: number;
hasMore: boolean;
}
19 changes: 14 additions & 5 deletions src/pages/ArticlesPage/ui/ArticlesPage/ArticlesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ 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 { Page } from 'shared/ui/Page';

import { fetchNextArticlesPage } from '../../model/services/fetchNextArticlesPage/fetchNextArticlesPage';
import {
getArticlesPageError,
getArticlesPageIsLoading,
getArticlesPageView,
} from '../../model/selectors/articlesPageSelectors';
Expand All @@ -30,29 +31,37 @@ export const ArticlesPage = memo((props: ArticlesPageProps) => {
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]);

const onLoadNextPart = useCallback(() => {
dispatch(fetchNextArticlesPage());
}, [dispatch]);

useInitialEffect(() => {
dispatch(fetchArticlesList());
dispatch(articlesPageActions.initState());
dispatch(fetchArticlesList({
page: 1,
}));
});

return (
<DynamicModuleLoader reducers={reducers}>
<div className={classNames(cls.articlesPage, {}, [className])}>
<Page
onScrollEnd={onLoadNextPart}
className={classNames(cls.articlesPage, {}, [className])}
>
<ArticleViewSelector view={view} onViewClick={onChangeView} />

<ArticleList
isLoading={isLoading}
view={view}
articles={articles}
/>
</div>
</Page>
</DynamicModuleLoader>
);
});
Expand Down
6 changes: 4 additions & 2 deletions src/pages/MainPage/ui/MainPage.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { VFC } from 'react';
import { useTranslation } from 'react-i18next';

import { Page } from 'shared/ui/Page';

export const MainPage: VFC = () => {
const { t } = useTranslation('main');

return (
<div>
<Page>
{t('Главная страница')}
</div>
</Page>
);
};
5 changes: 3 additions & 2 deletions src/pages/NotFoundPage/ui/NotFoundPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next';

import { classNames } from 'shared/lib/classNames';
import { Page } from 'shared/ui/Page';

import cls from './NotFoundPage.module.scss';

Expand All @@ -12,8 +13,8 @@ export const NotFoundPage = ({ className }: NotFoundPageProps) => {
const { t } = useTranslation();

return (
<div className={classNames(cls.notFoundPage, {}, [className])}>
<Page className={classNames(cls.notFoundPage, {}, [className])}>
{t('Страница не найдена')}
</div>
</Page>
);
};
Loading

0 comments on commit 7bd02c6

Please sign in to comment.