Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract infinite loading functions from Bea Theme #22

Merged
merged 1 commit into from
Apr 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
508 changes: 486 additions & 22 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@prezly/sdk": "^6.21.0",
"next-seo": "^5.4.0",
"parse-data-url": "^4.0.1",
"react-use": "^17.3.2",
"string-strip-html": "^9.1.9"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './adapter-nextjs';
export * from './components-nextjs';
export * from './data-fetching';
export * from './infinite-loading';
export * from './intl';
export * from './newsroom-context';
export * from './types';
24 changes: 24 additions & 0 deletions src/infinite-loading/apiHandlers/fetchGalleries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { NextApiRequest, NextApiResponse } from 'next';

import { getPrezlyApi } from '../../data-fetching';

export async function fetchGalleries(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
res.status(405);
return;
}

const { page, pageSize } = req.body;

try {
const api = getPrezlyApi(req);

const { galleries } = await api.getGalleries({ page, pageSize });

res.status(200).json({ galleries });
} catch (error) {
res.status(500).send({
message: (error as Error).message,
});
}
}
26 changes: 26 additions & 0 deletions src/infinite-loading/apiHandlers/fetchStories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { NextApiRequest, NextApiResponse } from 'next';

import { getPrezlyApi } from '../../data-fetching';

export async function fetchStories(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
res.status(405);
return;
}

const { page, pageSize, category, include, localeCode } = req.body;

try {
const api = getPrezlyApi(req);

const { stories } = await (category
? api.getStoriesFromCategory(category, { page, pageSize, include, localeCode })
: api.getStories({ page, pageSize, include, localeCode }));

res.status(200).json({ stories });
} catch (error) {
res.status(500).send({
message: (error as Error).message,
});
}
}
2 changes: 2 additions & 0 deletions src/infinite-loading/apiHandlers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { fetchGalleries } from './fetchGalleries';
export { fetchStories } from './fetchStories';
5 changes: 5 additions & 0 deletions src/infinite-loading/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { useInfiniteLoading } from './useInfiniteLoading';
export { useInfiniteStoriesLoading } from './useInfiniteStoriesLoading';
export { useInfiniteGalleriesLoading } from './useInfiniteGalleriesLoading';
export * from './apiHandlers';
export * from './types';
5 changes: 5 additions & 0 deletions src/infinite-loading/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface PaginationProps {
itemsTotal: number;
currentPage: number;
pageSize: number;
}
53 changes: 53 additions & 0 deletions src/infinite-loading/useInfiniteGalleriesLoading.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { NewsroomGallery } from '@prezly/sdk';

import type { PaginationProps } from './types';
import { useInfiniteLoading } from './useInfiniteLoading';

async function fetchGalleries(
page: number,
pageSize: number,
): Promise<{ galleries: NewsroomGallery[] }> {
const result = await fetch('/api/fetch-galleries', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
page,
pageSize,
}),
});

if (!result.ok) {
const { message } = await result.json();
throw new Error(message);
}

return result.json();
}

/**
* NOTE: This hook depends on the presence of a Next API route (/api/fetch-galleries)!
* The handler for this route is exported from `apiHandlers/fetchGalleries.ts` file.
* See https://github.com/prezly/theme-nextjs-bea/blob/d381e9878187ca98815d240d3ffdf10a75993c14/pages/media/index.tsx#L23 for usage example
*/
export function useInfiniteGalleriesLoading(
initialGalleries: NewsroomGallery[],
pagination: PaginationProps,
) {
const { canLoadMore, data, isLoading, loadMore } = useInfiniteLoading<NewsroomGallery>({
fetchingFn: async (nextPage, pageSize) => {
const { galleries } = await fetchGalleries(nextPage, pageSize);
return galleries;
},
initialData: initialGalleries,
pagination,
});

return {
canLoadMore,
galleries: data,
isLoading,
loadMoreGalleries: loadMore,
};
}
66 changes: 66 additions & 0 deletions src/infinite-loading/useInfiniteLoading.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useCallback, useEffect, useState } from 'react';
import { useAsyncFn } from 'react-use';

import type { PaginationProps } from './types';

interface Parameters<T> {
fetchingFn: (page: number, pageSize: number) => Promise<T[]>;
initialData: T[];
pagination: PaginationProps;
}

interface State<T> {
canLoadMore: boolean;
currentPage: number;
data: T[];
isLoading: boolean;
loadMore: () => void;
resetData: () => void;
}

export function useInfiniteLoading<T>({
fetchingFn,
initialData,
pagination,
}: Parameters<T>): State<T> {
const [data, setData] = useState<T[]>(initialData);
const [currentPage, setCurrentPage] = useState(pagination.currentPage);

const { itemsTotal, pageSize } = pagination;
const pagesTotal = Math.ceil(itemsTotal / pageSize);
const canLoadMore = currentPage < pagesTotal;

const [{ error, loading: isLoading }, loadMoreFn] = useAsyncFn(async () => {
const nextPage = currentPage + 1;
const newData = await fetchingFn(nextPage, pageSize);
setCurrentPage(nextPage);
setData((currentData) => [...currentData, ...newData]);
}, [currentPage, fetchingFn, setCurrentPage]);

const loadMore = useCallback(() => {
if (canLoadMore) {
loadMoreFn();
}
}, [canLoadMore, loadMoreFn]);

const resetData = useCallback(() => {
setData(initialData);
setCurrentPage(pagination.currentPage);
}, [pagination.currentPage, initialData]);

useEffect(() => {
if (error) {
// eslint-disable-next-line no-console
console.error(error);
}
}, [error]);

return {
canLoadMore,
currentPage,
data,
isLoading,
loadMore,
resetData,
};
}
81 changes: 81 additions & 0 deletions src/infinite-loading/useInfiniteStoriesLoading.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { Category, ExtraStoryFields, Story } from '@prezly/sdk';
import { useEffect } from 'react';

import type { LocaleObject } from '../intl';
import { useCurrentLocale } from '../newsroom-context';

import type { PaginationProps } from './types';
import { useInfiniteLoading } from './useInfiniteLoading';

async function fetchStories<T extends Story = Story>(
page: number,
pageSize: number,
category?: Category,
locale?: LocaleObject,
include?: (keyof ExtraStoryFields)[],
): Promise<{ stories: T[] }> {
const result = await fetch('/api/fetch-stories', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
page,
pageSize,
category,
include,
...(locale && {
localeCode: locale.toUnderscoreCode(),
}),
}),
});

if (!result.ok) {
const { message } = await result.json();
throw new Error(message);
}

return result.json();
}

/**
* NOTE: This hook depends on the presence of a Next API route (/api/fetch-stories)!
* The handler for this route is exported from `apiHandlers/fetchStories.ts` file.
* See https://github.com/prezly/theme-nextjs-bea/blob/d381e9878187ca98815d240d3ffdf10a75993c14/pages/index.tsx#L24 for usage example
*/
export function useInfiniteStoriesLoading<T extends Story = Story>(
initialStories: T[],
pagination: PaginationProps,
category?: Category,
include?: (keyof ExtraStoryFields)[],
) {
const currentLocale = useCurrentLocale();

const { canLoadMore, data, isLoading, loadMore, resetData } = useInfiniteLoading<T>({
fetchingFn: async (nextPage, pageSize) => {
const { stories } = await fetchStories<T>(
nextPage,
pageSize,
category,
currentLocale,
include,
);
return stories;
},
initialData: initialStories,
pagination,
});

useEffect(() => {
if (category?.id) {
resetData();
}
}, [category?.id, resetData]);

return {
canLoadMore,
isLoading,
loadMoreStories: loadMore,
stories: data,
};
}