@prezly/theme-kit-nextjs 1.6.3 → 1.6.4-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/build/components-nextjs/PageSeo/PageSeo.d.ts +12 -0
- package/build/components-nextjs/PageSeo/PageSeo.js +89 -0
- package/build/components-nextjs/PageSeo/PageSeo.js.map +1 -0
- package/build/components-nextjs/PageSeo/index.d.ts +2 -0
- package/build/components-nextjs/PageSeo/index.js +17 -0
- package/build/components-nextjs/PageSeo/index.js.map +1 -0
- package/build/components-nextjs/PageSeo/lib/getAbsoluteUrl.d.ts +1 -0
- package/build/components-nextjs/PageSeo/lib/getAbsoluteUrl.js +11 -0
- package/build/components-nextjs/PageSeo/lib/getAbsoluteUrl.js.map +1 -0
- package/build/components-nextjs/PageSeo/lib/types.d.ts +4 -0
- package/build/components-nextjs/PageSeo/lib/types.js +3 -0
- package/build/components-nextjs/PageSeo/lib/types.js.map +1 -0
- package/build/components-nextjs/Sitemap/SitemapBuilder.d.ts +11 -0
- package/build/components-nextjs/Sitemap/SitemapBuilder.js +51 -0
- package/build/components-nextjs/Sitemap/SitemapBuilder.js.map +1 -0
- package/build/components-nextjs/Sitemap/createPaths.d.ts +2 -0
- package/build/components-nextjs/Sitemap/createPaths.js +50 -0
- package/build/components-nextjs/Sitemap/createPaths.js.map +1 -0
- package/build/components-nextjs/Sitemap/getInitialProps.d.ts +2 -0
- package/build/components-nextjs/Sitemap/getInitialProps.js +78 -0
- package/build/components-nextjs/Sitemap/getInitialProps.js.map +1 -0
- package/build/components-nextjs/Sitemap/index.d.ts +4 -0
- package/build/components-nextjs/Sitemap/index.js +21 -0
- package/build/components-nextjs/Sitemap/index.js.map +1 -0
- package/build/components-nextjs/Sitemap/types.d.ts +5 -0
- package/build/components-nextjs/Sitemap/types.js +3 -0
- package/build/components-nextjs/Sitemap/types.js.map +1 -0
- package/build/components-nextjs/StorySeo/StorySeo.d.ts +11 -0
- package/build/components-nextjs/StorySeo/StorySeo.js +47 -0
- package/build/components-nextjs/StorySeo/StorySeo.js.map +1 -0
- package/build/components-nextjs/StorySeo/index.d.ts +1 -0
- package/build/components-nextjs/StorySeo/index.js +6 -0
- package/build/components-nextjs/StorySeo/index.js.map +1 -0
- package/build/components-nextjs/index.d.ts +3 -0
- package/build/components-nextjs/index.js +19 -0
- package/build/components-nextjs/index.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/build/infinite-loading/apiHandlers/fetchGalleries.d.ts +2 -0
- package/build/infinite-loading/apiHandlers/fetchGalleries.js +73 -0
- package/build/infinite-loading/apiHandlers/fetchGalleries.js.map +1 -0
- package/build/infinite-loading/apiHandlers/fetchStories.d.ts +2 -0
- package/build/infinite-loading/apiHandlers/fetchStories.js +75 -0
- package/build/infinite-loading/apiHandlers/fetchStories.js.map +1 -0
- package/build/infinite-loading/apiHandlers/index.d.ts +2 -0
- package/build/infinite-loading/apiHandlers/index.js +8 -0
- package/build/infinite-loading/apiHandlers/index.js.map +1 -0
- package/build/infinite-loading/index.d.ts +5 -0
- package/build/infinite-loading/index.js +22 -0
- package/build/infinite-loading/index.js.map +1 -0
- package/build/infinite-loading/types.d.ts +5 -0
- package/build/infinite-loading/types.js +3 -0
- package/build/infinite-loading/types.js.map +1 -0
- package/build/infinite-loading/useInfiniteGalleriesLoading.d.ts +13 -0
- package/build/infinite-loading/useInfiniteGalleriesLoading.js +98 -0
- package/build/infinite-loading/useInfiniteGalleriesLoading.js.map +1 -0
- package/build/infinite-loading/useInfiniteLoading.d.ts +16 -0
- package/build/infinite-loading/useInfiniteLoading.js +115 -0
- package/build/infinite-loading/useInfiniteLoading.js.map +1 -0
- package/build/infinite-loading/useInfiniteStoriesLoading.d.ts +13 -0
- package/build/infinite-loading/useInfiniteStoriesLoading.js +116 -0
- package/build/infinite-loading/useInfiniteStoriesLoading.js.map +1 -0
- package/package.json +6 -3
- package/src/components-nextjs/PageSeo/PageSeo.tsx +123 -0
- package/src/components-nextjs/PageSeo/index.ts +2 -0
- package/src/components-nextjs/PageSeo/lib/getAbsoluteUrl.ts +7 -0
- package/src/components-nextjs/PageSeo/lib/types.ts +4 -0
- package/src/components-nextjs/Sitemap/SitemapBuilder.ts +56 -0
- package/src/components-nextjs/Sitemap/createPaths.ts +21 -0
- package/src/components-nextjs/Sitemap/getInitialProps.ts +37 -0
- package/src/components-nextjs/Sitemap/index.ts +5 -0
- package/src/components-nextjs/Sitemap/types.ts +5 -0
- package/src/components-nextjs/StorySeo/StorySeo.tsx +68 -0
- package/src/components-nextjs/StorySeo/index.ts +1 -0
- package/src/components-nextjs/index.ts +3 -0
- package/src/index.ts +2 -0
- package/src/infinite-loading/apiHandlers/fetchGalleries.ts +24 -0
- package/src/infinite-loading/apiHandlers/fetchStories.ts +26 -0
- package/src/infinite-loading/apiHandlers/index.ts +2 -0
- package/src/infinite-loading/index.ts +5 -0
- package/src/infinite-loading/types.ts +5 -0
- package/src/infinite-loading/useInfiniteGalleriesLoading.ts +53 -0
- package/src/infinite-loading/useInfiniteLoading.ts +66 -0
- package/src/infinite-loading/useInfiniteStoriesLoading.ts +81 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prezly/theme-kit-nextjs",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.4-0",
|
|
4
4
|
"description": "Data layer and utility library for developing Prezly themes with NextJS",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -48,8 +48,11 @@
|
|
|
48
48
|
"react-dom": "^17.x"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@prezly/sdk": "^6.
|
|
52
|
-
"
|
|
51
|
+
"@prezly/sdk": "^6.21.0",
|
|
52
|
+
"@react-hookz/web": "^13.2.1",
|
|
53
|
+
"next-seo": "^5.4.0",
|
|
54
|
+
"parse-data-url": "^4.0.1",
|
|
55
|
+
"string-strip-html": "^9.1.9"
|
|
53
56
|
},
|
|
54
57
|
"devDependencies": {
|
|
55
58
|
"@prezly/eslint-config": "^4.0.0",
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { NextSeo } from 'next-seo';
|
|
2
|
+
import type { NextSeoProps } from 'next-seo';
|
|
3
|
+
import { useRouter } from 'next/router';
|
|
4
|
+
import { useMemo } from 'react';
|
|
5
|
+
import { stripHtml } from 'string-strip-html';
|
|
6
|
+
|
|
7
|
+
import { getNewsroomLogoUrl, getUsedLanguages } from '../../data-fetching';
|
|
8
|
+
import { LocaleObject } from '../../intl';
|
|
9
|
+
import {
|
|
10
|
+
useCompanyInformation,
|
|
11
|
+
useCurrentLocale,
|
|
12
|
+
useCurrentStory,
|
|
13
|
+
useGetLinkLocaleSlug,
|
|
14
|
+
useGetTranslationUrl,
|
|
15
|
+
useLanguages,
|
|
16
|
+
useNewsroom,
|
|
17
|
+
} from '../../newsroom-context';
|
|
18
|
+
|
|
19
|
+
import { getAbsoluteUrl } from './lib/getAbsoluteUrl';
|
|
20
|
+
import type { AlternateLanguageLink } from './lib/types';
|
|
21
|
+
|
|
22
|
+
type Props = NextSeoProps & {
|
|
23
|
+
imageUrl?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* This component provides base page attributes for every Newsroom page.
|
|
28
|
+
* It depends on information from NewsroomContext, so the component should be placed inside the `NewsroomContextProvider`.
|
|
29
|
+
* You can pass additional props expected by NextSeo component to override any info that is provided by the NewsroomContext.
|
|
30
|
+
*/
|
|
31
|
+
export function PageSeo({
|
|
32
|
+
title,
|
|
33
|
+
description,
|
|
34
|
+
imageUrl,
|
|
35
|
+
canonical,
|
|
36
|
+
openGraph,
|
|
37
|
+
twitter,
|
|
38
|
+
...nextSeoProps
|
|
39
|
+
}: Props) {
|
|
40
|
+
const companyInformation = useCompanyInformation();
|
|
41
|
+
const newsroom = useNewsroom();
|
|
42
|
+
const currentLocale = useCurrentLocale();
|
|
43
|
+
const languages = useLanguages();
|
|
44
|
+
const getTranslationUrl = useGetTranslationUrl();
|
|
45
|
+
const getLinkLocaleSlug = useGetLinkLocaleSlug();
|
|
46
|
+
const currentStory = useCurrentStory();
|
|
47
|
+
const { asPath } = useRouter();
|
|
48
|
+
|
|
49
|
+
const pageTitle = title || companyInformation.name;
|
|
50
|
+
const pageDescription = description || stripHtml(companyInformation.about).result;
|
|
51
|
+
const canonicalUrl =
|
|
52
|
+
canonical || getAbsoluteUrl(asPath, newsroom.url, getLinkLocaleSlug(currentLocale));
|
|
53
|
+
const siteName = companyInformation.name;
|
|
54
|
+
const logoUrl = imageUrl || getNewsroomLogoUrl(newsroom);
|
|
55
|
+
|
|
56
|
+
const alternateLanguageLinks: AlternateLanguageLink[] = useMemo(() => {
|
|
57
|
+
if (!languages.length) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const alternateLanguages = getUsedLanguages(languages).filter(
|
|
62
|
+
(language) => language.code !== currentLocale.toUnderscoreCode(),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return alternateLanguages
|
|
66
|
+
.map((language) => {
|
|
67
|
+
const locale = LocaleObject.fromAnyCode(language.code);
|
|
68
|
+
|
|
69
|
+
const translationLink = getTranslationUrl(locale, true);
|
|
70
|
+
|
|
71
|
+
if (!translationLink) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
hrefLang: locale.toHyphenCode(),
|
|
77
|
+
href: getAbsoluteUrl(
|
|
78
|
+
translationLink,
|
|
79
|
+
newsroom.url,
|
|
80
|
+
currentStory && translationLink !== '/' ? false : getLinkLocaleSlug(locale),
|
|
81
|
+
),
|
|
82
|
+
};
|
|
83
|
+
})
|
|
84
|
+
.filter<AlternateLanguageLink>(Boolean as any);
|
|
85
|
+
}, [
|
|
86
|
+
currentLocale,
|
|
87
|
+
getLinkLocaleSlug,
|
|
88
|
+
getTranslationUrl,
|
|
89
|
+
languages,
|
|
90
|
+
newsroom.url,
|
|
91
|
+
currentStory,
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<NextSeo
|
|
96
|
+
title={pageTitle}
|
|
97
|
+
description={pageDescription}
|
|
98
|
+
canonical={canonicalUrl}
|
|
99
|
+
openGraph={{
|
|
100
|
+
url: canonicalUrl,
|
|
101
|
+
title: pageTitle,
|
|
102
|
+
description: pageDescription,
|
|
103
|
+
locale: currentLocale.toUnderscoreCode(),
|
|
104
|
+
images: [
|
|
105
|
+
{
|
|
106
|
+
url: logoUrl,
|
|
107
|
+
alt: pageTitle,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
site_name: siteName,
|
|
111
|
+
...openGraph,
|
|
112
|
+
}}
|
|
113
|
+
twitter={{
|
|
114
|
+
site: siteName,
|
|
115
|
+
cardType: 'summary',
|
|
116
|
+
...twitter,
|
|
117
|
+
}}
|
|
118
|
+
additionalMetaTags={[{ name: 'twitter:image', content: logoUrl }]}
|
|
119
|
+
languageAlternates={alternateLanguageLinks}
|
|
120
|
+
{...nextSeoProps}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Returns absolute URL based on the path and without any query parameters
|
|
2
|
+
export function getAbsoluteUrl(path: string, origin: string, localeCode?: string | false): string {
|
|
3
|
+
const url = new URL(`${localeCode ? `/${localeCode}` : ''}${path || '/'}`, origin);
|
|
4
|
+
url.search = '';
|
|
5
|
+
|
|
6
|
+
return url.toString();
|
|
7
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { SitemapUrl } from './types';
|
|
2
|
+
|
|
3
|
+
export class SitemapBuilder {
|
|
4
|
+
private baseUrl: string;
|
|
5
|
+
|
|
6
|
+
private urls: SitemapUrl[] = [];
|
|
7
|
+
|
|
8
|
+
constructor(baseUrl: string) {
|
|
9
|
+
this.baseUrl = baseUrl;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public addUrl(location: string) {
|
|
13
|
+
this.urls.push({
|
|
14
|
+
location: this.baseUrl + location,
|
|
15
|
+
changeFrequency: SitemapBuilder.guessFrequency(location),
|
|
16
|
+
priority: SitemapBuilder.guessPriority(location),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private static guessFrequency(location: string) {
|
|
21
|
+
if (location === '/') return 'daily';
|
|
22
|
+
|
|
23
|
+
return 'weekly';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private static guessPriority(location: string) {
|
|
27
|
+
if (location === '/') return '0.9';
|
|
28
|
+
|
|
29
|
+
if (location.startsWith('/category/')) {
|
|
30
|
+
return '0.8';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return '0.7';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static serializeLocation(url: SitemapUrl) {
|
|
37
|
+
return [
|
|
38
|
+
'<url>',
|
|
39
|
+
url.location && `\t<loc>${url.location}</loc>`,
|
|
40
|
+
url.changeFrequency && `\t<changefreq>${url.changeFrequency}</changefreq>`,
|
|
41
|
+
url.priority && `\t<priority>${url.priority}</priority>`,
|
|
42
|
+
'</url>',
|
|
43
|
+
]
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
.join('\n');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
serialize() {
|
|
49
|
+
return [
|
|
50
|
+
'<?xml version="1.0" encoding="UTF-8" ?>',
|
|
51
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">',
|
|
52
|
+
this.urls.map(SitemapBuilder.serializeLocation).join('\n'),
|
|
53
|
+
'</urlset>',
|
|
54
|
+
].join('\n');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Category, Story } from '@prezly/sdk';
|
|
2
|
+
|
|
3
|
+
export function createPaths(stories: Story[], categories: Category[]) {
|
|
4
|
+
const storiesUrls = stories.map(({ slug }) => `/${slug}`);
|
|
5
|
+
const categoriesUrls = categories
|
|
6
|
+
.map((category) => {
|
|
7
|
+
const translations = Object.values(category.i18n);
|
|
8
|
+
const allSlugs = translations
|
|
9
|
+
.map(({ slug }) => slug || '')
|
|
10
|
+
.filter(Boolean)
|
|
11
|
+
.reduce<string[]>(
|
|
12
|
+
(slugs, slug) => (slugs.includes(slug) ? [...slugs] : [...slugs, slug]),
|
|
13
|
+
[],
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
return allSlugs.map((slug) => `/category/${slug}`);
|
|
17
|
+
})
|
|
18
|
+
.flat();
|
|
19
|
+
|
|
20
|
+
return [...storiesUrls, ...categoriesUrls];
|
|
21
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { NextPageContext } from 'next';
|
|
2
|
+
|
|
3
|
+
import { getPrezlyApi } from '../../data-fetching';
|
|
4
|
+
|
|
5
|
+
import { createPaths } from './createPaths';
|
|
6
|
+
import { SitemapBuilder } from './SitemapBuilder';
|
|
7
|
+
|
|
8
|
+
export async function getInitialProps(ctx: NextPageContext, additionalPaths?: string[]) {
|
|
9
|
+
const { res, req } = ctx;
|
|
10
|
+
|
|
11
|
+
if (!req || !res) {
|
|
12
|
+
// client side
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const baseUrl = req.headers.host || '/';
|
|
17
|
+
|
|
18
|
+
const api = getPrezlyApi(req);
|
|
19
|
+
const stories = await api.getAllStories();
|
|
20
|
+
const categories = await api.getCategories();
|
|
21
|
+
|
|
22
|
+
const paths = createPaths(stories, categories);
|
|
23
|
+
const sitemapBuilder = new SitemapBuilder(baseUrl);
|
|
24
|
+
|
|
25
|
+
sitemapBuilder.addUrl('/');
|
|
26
|
+
paths.forEach((path) => sitemapBuilder.addUrl(path));
|
|
27
|
+
|
|
28
|
+
if (additionalPaths) {
|
|
29
|
+
additionalPaths.forEach((path) => sitemapBuilder.addUrl(path));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
res.setHeader('Content-Type', 'text/xml');
|
|
33
|
+
res.write(sitemapBuilder.serialize());
|
|
34
|
+
res.end();
|
|
35
|
+
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ExtendedStory } from '@prezly/sdk';
|
|
2
|
+
import { ArticleJsonLd, NextSeo } from 'next-seo';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
story: ExtendedStory;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* This components provides additional page attributes for the Story page.
|
|
10
|
+
* It works in conjunction with `PageSeo` component, overidding some of its attributes.
|
|
11
|
+
*/
|
|
12
|
+
export function StorySeo({ story }: Props) {
|
|
13
|
+
const { title, subtitle, published_at, updated_at, author, oembed, newsroom, summary } = story;
|
|
14
|
+
|
|
15
|
+
const authorName = author?.display_name || author?.email || 'Unknown';
|
|
16
|
+
const description = subtitle || summary;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
<NextSeo
|
|
21
|
+
title={title}
|
|
22
|
+
description={description}
|
|
23
|
+
canonical={oembed.url}
|
|
24
|
+
openGraph={{
|
|
25
|
+
title,
|
|
26
|
+
description,
|
|
27
|
+
url: oembed.url,
|
|
28
|
+
site_name: newsroom.display_name,
|
|
29
|
+
type: 'article',
|
|
30
|
+
article: {
|
|
31
|
+
publishedTime: published_at || undefined,
|
|
32
|
+
modifiedTime: updated_at,
|
|
33
|
+
authors: [authorName],
|
|
34
|
+
},
|
|
35
|
+
...(oembed.thumbnail_url && {
|
|
36
|
+
images: [
|
|
37
|
+
{
|
|
38
|
+
url: oembed.thumbnail_url,
|
|
39
|
+
alt: oembed.title,
|
|
40
|
+
width: oembed.thumbnail_width,
|
|
41
|
+
height: oembed.thumbnail_height,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
}),
|
|
45
|
+
}}
|
|
46
|
+
additionalMetaTags={
|
|
47
|
+
oembed.thumbnail_url
|
|
48
|
+
? [
|
|
49
|
+
{ name: 'twitter:card', content: 'summary_large_image' },
|
|
50
|
+
{ name: 'twitter:image', content: oembed.thumbnail_url },
|
|
51
|
+
]
|
|
52
|
+
: undefined
|
|
53
|
+
}
|
|
54
|
+
/>
|
|
55
|
+
<ArticleJsonLd
|
|
56
|
+
url={oembed.url}
|
|
57
|
+
title={title}
|
|
58
|
+
images={oembed.thumbnail_url ? [oembed.thumbnail_url] : []}
|
|
59
|
+
datePublished={published_at || ''}
|
|
60
|
+
dateModified={updated_at}
|
|
61
|
+
authorName={[authorName]}
|
|
62
|
+
publisherName={newsroom.display_name}
|
|
63
|
+
publisherLogo={newsroom.thumbnail_url}
|
|
64
|
+
description={description}
|
|
65
|
+
/>
|
|
66
|
+
</>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { StorySeo } from './StorySeo';
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
2
|
+
|
|
3
|
+
import { getPrezlyApi } from '../../data-fetching';
|
|
4
|
+
|
|
5
|
+
export async function fetchGalleries(req: NextApiRequest, res: NextApiResponse) {
|
|
6
|
+
if (req.method !== 'POST') {
|
|
7
|
+
res.status(405);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { page, pageSize } = req.body;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const api = getPrezlyApi(req);
|
|
15
|
+
|
|
16
|
+
const { galleries } = await api.getGalleries({ page, pageSize });
|
|
17
|
+
|
|
18
|
+
res.status(200).json({ galleries });
|
|
19
|
+
} catch (error) {
|
|
20
|
+
res.status(500).send({
|
|
21
|
+
message: (error as Error).message,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
2
|
+
|
|
3
|
+
import { getPrezlyApi } from '../../data-fetching';
|
|
4
|
+
|
|
5
|
+
export async function fetchStories(req: NextApiRequest, res: NextApiResponse) {
|
|
6
|
+
if (req.method !== 'POST') {
|
|
7
|
+
res.status(405);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { page, pageSize, category, include, localeCode } = req.body;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const api = getPrezlyApi(req);
|
|
15
|
+
|
|
16
|
+
const { stories } = await (category
|
|
17
|
+
? api.getStoriesFromCategory(category, { page, pageSize, include, localeCode })
|
|
18
|
+
: api.getStories({ page, pageSize, include, localeCode }));
|
|
19
|
+
|
|
20
|
+
res.status(200).json({ stories });
|
|
21
|
+
} catch (error) {
|
|
22
|
+
res.status(500).send({
|
|
23
|
+
message: (error as Error).message,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { useInfiniteLoading } from './useInfiniteLoading';
|
|
2
|
+
export { useInfiniteStoriesLoading } from './useInfiniteStoriesLoading';
|
|
3
|
+
export { useInfiniteGalleriesLoading } from './useInfiniteGalleriesLoading';
|
|
4
|
+
export * from './apiHandlers';
|
|
5
|
+
export * from './types';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { NewsroomGallery } from '@prezly/sdk';
|
|
2
|
+
|
|
3
|
+
import type { PaginationProps } from './types';
|
|
4
|
+
import { useInfiniteLoading } from './useInfiniteLoading';
|
|
5
|
+
|
|
6
|
+
async function fetchGalleries(
|
|
7
|
+
page: number,
|
|
8
|
+
pageSize: number,
|
|
9
|
+
): Promise<{ galleries: NewsroomGallery[] }> {
|
|
10
|
+
const result = await fetch('/api/fetch-galleries', {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
},
|
|
15
|
+
body: JSON.stringify({
|
|
16
|
+
page,
|
|
17
|
+
pageSize,
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (!result.ok) {
|
|
22
|
+
const { message } = await result.json();
|
|
23
|
+
throw new Error(message);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return result.json();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* NOTE: This hook depends on the presence of a Next API route (/api/fetch-galleries)!
|
|
31
|
+
* The handler for this route is exported from `apiHandlers/fetchGalleries.ts` file.
|
|
32
|
+
* See https://github.com/prezly/theme-nextjs-bea/blob/d381e9878187ca98815d240d3ffdf10a75993c14/pages/media/index.tsx#L23 for usage example
|
|
33
|
+
*/
|
|
34
|
+
export function useInfiniteGalleriesLoading(
|
|
35
|
+
initialGalleries: NewsroomGallery[],
|
|
36
|
+
pagination: PaginationProps,
|
|
37
|
+
) {
|
|
38
|
+
const { canLoadMore, data, isLoading, loadMore } = useInfiniteLoading<NewsroomGallery>({
|
|
39
|
+
fetchingFn: async (nextPage, pageSize) => {
|
|
40
|
+
const { galleries } = await fetchGalleries(nextPage, pageSize);
|
|
41
|
+
return galleries;
|
|
42
|
+
},
|
|
43
|
+
initialData: initialGalleries,
|
|
44
|
+
pagination,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
canLoadMore,
|
|
49
|
+
galleries: data,
|
|
50
|
+
isLoading,
|
|
51
|
+
loadMoreGalleries: loadMore,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useAsync } from '@react-hookz/web';
|
|
2
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import type { PaginationProps } from './types';
|
|
5
|
+
|
|
6
|
+
interface Parameters<T> {
|
|
7
|
+
fetchingFn: (page: number, pageSize: number) => Promise<T[]>;
|
|
8
|
+
initialData: T[];
|
|
9
|
+
pagination: PaginationProps;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface State<T> {
|
|
13
|
+
canLoadMore: boolean;
|
|
14
|
+
currentPage: number;
|
|
15
|
+
data: T[];
|
|
16
|
+
isLoading: boolean;
|
|
17
|
+
loadMore: () => void;
|
|
18
|
+
resetData: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useInfiniteLoading<T>({
|
|
22
|
+
fetchingFn,
|
|
23
|
+
initialData,
|
|
24
|
+
pagination,
|
|
25
|
+
}: Parameters<T>): State<T> {
|
|
26
|
+
const [data, setData] = useState<T[]>(initialData);
|
|
27
|
+
const [currentPage, setCurrentPage] = useState(pagination.currentPage);
|
|
28
|
+
|
|
29
|
+
const { itemsTotal, pageSize } = pagination;
|
|
30
|
+
const pagesTotal = Math.ceil(itemsTotal / pageSize);
|
|
31
|
+
const canLoadMore = currentPage < pagesTotal;
|
|
32
|
+
|
|
33
|
+
const [{ error, status }, { execute: loadMoreFn }] = useAsync(async () => {
|
|
34
|
+
const nextPage = currentPage + 1;
|
|
35
|
+
const newData = await fetchingFn(nextPage, pageSize);
|
|
36
|
+
setCurrentPage(nextPage);
|
|
37
|
+
setData((currentData) => [...currentData, ...newData]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const loadMore = useCallback(() => {
|
|
41
|
+
if (canLoadMore) {
|
|
42
|
+
loadMoreFn();
|
|
43
|
+
}
|
|
44
|
+
}, [canLoadMore, loadMoreFn]);
|
|
45
|
+
|
|
46
|
+
const resetData = useCallback(() => {
|
|
47
|
+
setData(initialData);
|
|
48
|
+
setCurrentPage(pagination.currentPage);
|
|
49
|
+
}, [pagination.currentPage, initialData]);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (error) {
|
|
53
|
+
// eslint-disable-next-line no-console
|
|
54
|
+
console.error(error);
|
|
55
|
+
}
|
|
56
|
+
}, [error]);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
canLoadMore,
|
|
60
|
+
currentPage,
|
|
61
|
+
data,
|
|
62
|
+
isLoading: status === 'loading',
|
|
63
|
+
loadMore,
|
|
64
|
+
resetData,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Category, ExtraStoryFields, Story } from '@prezly/sdk';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
import type { LocaleObject } from '../intl';
|
|
5
|
+
import { useCurrentLocale } from '../newsroom-context';
|
|
6
|
+
|
|
7
|
+
import type { PaginationProps } from './types';
|
|
8
|
+
import { useInfiniteLoading } from './useInfiniteLoading';
|
|
9
|
+
|
|
10
|
+
async function fetchStories<T extends Story = Story>(
|
|
11
|
+
page: number,
|
|
12
|
+
pageSize: number,
|
|
13
|
+
category?: Category,
|
|
14
|
+
locale?: LocaleObject,
|
|
15
|
+
include?: (keyof ExtraStoryFields)[],
|
|
16
|
+
): Promise<{ stories: T[] }> {
|
|
17
|
+
const result = await fetch('/api/fetch-stories', {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: {
|
|
20
|
+
'Content-Type': 'application/json',
|
|
21
|
+
},
|
|
22
|
+
body: JSON.stringify({
|
|
23
|
+
page,
|
|
24
|
+
pageSize,
|
|
25
|
+
category,
|
|
26
|
+
include,
|
|
27
|
+
...(locale && {
|
|
28
|
+
localeCode: locale.toUnderscoreCode(),
|
|
29
|
+
}),
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!result.ok) {
|
|
34
|
+
const { message } = await result.json();
|
|
35
|
+
throw new Error(message);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return result.json();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* NOTE: This hook depends on the presence of a Next API route (/api/fetch-stories)!
|
|
43
|
+
* The handler for this route is exported from `apiHandlers/fetchStories.ts` file.
|
|
44
|
+
* See https://github.com/prezly/theme-nextjs-bea/blob/d381e9878187ca98815d240d3ffdf10a75993c14/pages/index.tsx#L24 for usage example
|
|
45
|
+
*/
|
|
46
|
+
export function useInfiniteStoriesLoading<T extends Story = Story>(
|
|
47
|
+
initialStories: T[],
|
|
48
|
+
pagination: PaginationProps,
|
|
49
|
+
category?: Category,
|
|
50
|
+
include?: (keyof ExtraStoryFields)[],
|
|
51
|
+
) {
|
|
52
|
+
const currentLocale = useCurrentLocale();
|
|
53
|
+
|
|
54
|
+
const { canLoadMore, data, isLoading, loadMore, resetData } = useInfiniteLoading<T>({
|
|
55
|
+
fetchingFn: async (nextPage, pageSize) => {
|
|
56
|
+
const { stories } = await fetchStories<T>(
|
|
57
|
+
nextPage,
|
|
58
|
+
pageSize,
|
|
59
|
+
category,
|
|
60
|
+
currentLocale,
|
|
61
|
+
include,
|
|
62
|
+
);
|
|
63
|
+
return stories;
|
|
64
|
+
},
|
|
65
|
+
initialData: initialStories,
|
|
66
|
+
pagination,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (category?.id) {
|
|
71
|
+
resetData();
|
|
72
|
+
}
|
|
73
|
+
}, [category?.id, resetData]);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
canLoadMore,
|
|
77
|
+
isLoading,
|
|
78
|
+
loadMoreStories: loadMore,
|
|
79
|
+
stories: data,
|
|
80
|
+
};
|
|
81
|
+
}
|