Skip to content

Commit 0b83722

Browse files
fix: update next template with getStaticPaths (#292)
1 parent cf96630 commit 0b83722

File tree

7 files changed

+265
-43
lines changed

7 files changed

+265
-43
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import Link from "next/link";
2+
import { PagedCollection } from "../../types/Collection";
3+
4+
interface Props {
5+
collection: PagedCollection<unknown>;
6+
}
7+
8+
const Pagination = ({ collection }: Props) => {
9+
const view = collection && collection['{{{hydraPrefix}}}view'];
10+
if (!view) return;
11+
12+
const {
13+
'{{{hydraPrefix}}}first': first,
14+
'{{{hydraPrefix}}}previous': previous,
15+
'{{{hydraPrefix}}}next': next,
16+
'{{{hydraPrefix}}}last': last
17+
} = view;
18+
19+
return (
20+
<nav aria-label="Page navigation">
21+
<Link href={first ? first : '#'}>
22+
<a className={`btn btn-primary${previous ? '' : ' disabled'}`}>
23+
<span aria-hidden="true">&lArr;</span> First
24+
</a>
25+
</Link>
26+
<Link href={previous ? previous : '#'}>
27+
<a className={`btn btn-primary${previous ? '' : ' disabled'}`}>
28+
<span aria-hidden="true">&larr;</span> Previous
29+
</a>
30+
</Link>
31+
<Link href={next ? next : '#'}>
32+
<a className={`btn btn-primary${next ? '' : ' disabled'}`}>
33+
Next <span aria-hidden="true">&rarr;</span>
34+
</a>
35+
</Link>
36+
<Link href={last ? last : '#'}>
37+
<a className={`btn btn-primary${next ? '' : ' disabled'}`}>
38+
Last <span aria-hidden="true">&rArr;</span>
39+
</a>
40+
</Link>
41+
</nav>
42+
);
43+
};
44+
45+
export default Pagination;
Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,74 @@
1-
import { NextComponentType, NextPageContext } from 'next';
2-
import { Form } from '../../../components/{{{lc}}}/Form';
3-
import { {{{ucf}}} } from '../../../types/{{{ucf}}}';
4-
import { fetch } from '../../../utils/dataAccess';
1+
import { GetStaticPaths, GetStaticProps, NextComponentType, NextPageContext } from "next";
2+
import { Form } from "../../../components/{{{lc}}}/Form";
3+
import { {{{ucf}}} } from "../../../types/{{{ucf}}}";
4+
import { fetch } from "../../../utils/dataAccess";
55
import Head from "next/head";
6+
import DefaultErrorPage from "next/error";
67

78
interface Props {
89
{{{lc}}}: {{{ucf}}};
910
};
1011

1112
const Page: NextComponentType<NextPageContext, Props, Props> = ({ {{{lc}}} }) => {
13+
if (!{{{lc}}}) {
14+
return <DefaultErrorPage statusCode={404} />;
15+
}
16+
1217
return (
1318
<div>
1419
<div>
1520
<Head>
16-
<title>{ {{{lc}}} && `Edit {{{ucf}}} ${ {{~lc}}['@id']}`}</title>
21+
<title>{ {{{lc}}} && `Edit {{{ucf}}} ${ {{~lc}}['@id'] }` }</title>
1722
</Head>
1823
</div>
19-
<Form {{{lc}}}={ {{{lc}}} }/>
24+
<Form {{{lc}}}={ {{{lc}}} } />
2025
</div>
2126
);
2227
};
2328

24-
Page.getInitialProps = async ({ asPath }: NextPageContext) => {
25-
const {{{lc}}} = await fetch(asPath.replace( '/edit', ''));
29+
export const getStaticProps: GetStaticProps = async ({ params }) => {
30+
return {
31+
props: {
32+
{{{lc}}}: await fetch(`/{{{name}}}/${params.id}`),
33+
},
34+
revalidate: 1,
35+
};
36+
}
2637

27-
return { {{{lc}}} };
28-
};
38+
export const getStaticPaths: GetStaticPaths = async () => {
39+
try {
40+
const response = await fetch("/{{{name}}}");
41+
} catch (e) {
42+
console.error(e);
43+
44+
return {
45+
paths: [],
46+
fallback: true,
47+
};
48+
}
49+
50+
const view = response.data['{{{hydraPrefix}}}view'];
51+
const paths = response.data["{{{hydraPrefix}}}member"].map(({{{lc}}}) => `${ {{~lc}}['@id'] }/edit`);
52+
53+
if (view) {
54+
try {
55+
const {
56+
'{{{hydraPrefix}}}last': last
57+
} = view;
58+
for (let page = 2; page <= parseInt(last.replace(/^\/{{{name}}}\?page=(\d+)/, '$1')); page++) {
59+
paths.concat(
60+
await fetch(`/{{{name}}}?page=${page}`).data["{{{hydraPrefix}}}member"].map(({{{lc}}}) => `${ {{~lc}}['@id'] }/edit`)
61+
);
62+
}
63+
} catch (e) {
64+
console.error(e);
65+
}
66+
}
67+
68+
return {
69+
paths,
70+
fallback: true,
71+
};
72+
}
2973

3074
export default Page;
Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,81 @@
1-
import { NextComponentType, NextPageContext } from 'next';
2-
import { Show } from '../../../components/{{{lc}}}/Show';
3-
import { {{{ucf}}} } from '../../../types/{{{ucf}}}';
4-
import { fetch } from '../../../utils/dataAccess';
1+
import { GetStaticPaths, GetStaticProps, NextComponentType, NextPageContext } from "next";
2+
import { Show } from "../../../components/{{{lc}}}/Show";
3+
import { {{{ucf}}} } from "../../../types/{{{ucf}}}";
4+
import { fetch } from "../../../utils/dataAccess";
55
import Head from "next/head";
6+
import DefaultErrorPage from "next/error";
7+
import { useMercure } from "../../../utils/mercure";
68

79
interface Props {
810
{{{lc}}}: {{{ucf}}};
11+
hubURL: null | string;
912
};
1013

11-
const Page: NextComponentType<NextPageContext, Props, Props> = ({ {{{lc}}} }) => {
14+
const Page: NextComponentType<NextPageContext, Props, Props> = (props) => {
15+
const {{{lc}}} = props.hubURL === null ? props.{{{lc}}} : useMercure(props.{{{lc}}}, props.hubURL);
16+
17+
if (!{{{lc}}}) {
18+
return <DefaultErrorPage statusCode={404} />;
19+
}
20+
1221
return (
1322
<div>
1423
<div>
15-
<Head>
16-
<title>{`Show {{{ucf}}} ${ {{~lc}}['@id']}`}</title>
17-
</Head>
18-
</div>
19-
<Show {{{lc}}}={ {{{lc}}} }/>
24+
<Head>
25+
<title>{`Show {{{ucf}}} ${ {{~lc}}['@id'] }`}</title>
26+
</Head>
27+
</div>
28+
<Show {{{lc}}}={ {{{lc}}} } />
2029
</div>
2130
);
2231
};
2332

24-
Page.getInitialProps = async ({ asPath }: NextPageContext) => {
25-
const {{{lc}}} = await fetch(asPath);
33+
export const getStaticProps: GetStaticProps = async ({ params }) => {
34+
const response = await fetch(`/{{{name}}}/${params.id}`);
2635

27-
return { {{{lc}}} };
28-
};
36+
return {
37+
props: {
38+
{{{lc}}}: response.data,
39+
hubURL: response.hubURL,
40+
},
41+
revalidate: 1,
42+
};
43+
}
44+
45+
export const getStaticPaths: GetStaticPaths = async () => {
46+
try {
47+
const response = await fetch("/{{{name}}}");
48+
} catch (e) {
49+
console.error(e);
50+
51+
return {
52+
paths: [],
53+
fallback: true,
54+
};
55+
}
56+
57+
const view = response.data['{{{hydraPrefix}}}view'];
58+
const paths = response.data["{{{hydraPrefix}}}member"].map(({{{lc}}}) => `${ {{~lc}}['@id'] }`);
59+
60+
if (view) {
61+
try {
62+
const {
63+
'{{{hydraPrefix}}}last': last
64+
} = view;
65+
for (let page = 2; page <= parseInt(last.replace(/^\/{{{name}}}\?page=(\d+)/, '$1')); page++) {
66+
paths.concat(
67+
await fetch(`/{{{name}}}?page=${page}`).data["{{{hydraPrefix}}}member"].map(({{{lc}}}) => `${ {{~lc}}['@id'] }`)
68+
);
69+
}
70+
} catch (e) {
71+
console.error(e);
72+
}
73+
}
74+
75+
return {
76+
paths,
77+
fallback: true,
78+
};
79+
}
2980

3081
export default Page;

templates/next/pages/foos/create.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ const Page: NextComponentType<NextPageContext> = () => (
1111
</div>
1212
<Form />
1313
</div>
14-
)
15-
14+
);
1615

1716
export default Page;

templates/next/pages/foos/index.tsx

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,42 @@
1-
import { NextComponentType, NextPageContext } from 'next';
2-
import { List } from '../../components/{{{lc}}}/List';
3-
import { PagedCollection } from '../../types/Collection';
4-
import { {{{ucf}}} } from '../../types/{{{ucf}}}';
5-
import { fetch } from '../../utils/dataAccess';
1+
import { GetServerSideProps, NextComponentType, NextPageContext } from "next";
2+
import { List } from "../../components/{{{lc}}}/List";
3+
import { PagedCollection } from "../../types/Collection";
4+
import { {{{ucf}}} } from "../../types/{{{ucf}}}";
5+
import { fetch } from "../../utils/dataAccess";
66
import Head from "next/head";
7+
import Pagination from "../../components/common/Pagination";
8+
import { useMercure } from "../../utils/mercure";
79

810
interface Props {
911
collection: PagedCollection<{{{ucf}}}>;
12+
hubURL: string;
1013
}
1114

12-
const Page: NextComponentType<NextPageContext, Props, Props> = ({collection}) => (
13-
<div>
15+
const Page: NextComponentType<NextPageContext, Props, Props> = (props) => {
16+
const collection = useMercure(props.collection, props.hubURL);
17+
18+
return (
19+
<div>
1420
<div>
1521
<Head>
1622
<title>{{{ucf}}} List</title>
1723
</Head>
1824
</div>
19-
<List {{{name}}}={collection['{{{hydraPrefix}}}member']}/>
25+
<List {{{name}}}={collection["{{{hydraPrefix}}}member"]} />
26+
<Pagination collection={collection} />
2027
</div>
21-
);
28+
);
29+
}
2230

23-
Page.getInitialProps = async () => {
31+
export const getServerSideProps: GetServerSideProps = async () => {
2432
const collection = await fetch('/{{{name}}}');
2533

26-
return {collection};
27-
};
34+
return {
35+
props: {
36+
collection: response.data,
37+
hubURL: response.hubURL,
38+
},
39+
}
40+
}
2841

2942
export default Page;

templates/next/utils/dataAccess.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@ interface Violation {
1111
propertyPath: string;
1212
}
1313

14+
const extractHubURL = (response: Response): null | URL => {
15+
const linkHeader = response.headers.get('Link');
16+
if (!linkHeader) return null;
17+
18+
const matches = linkHeader.match(
19+
/<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/
20+
);
21+
22+
return matches && matches[1] ? new URL(matches[1], ENTRYPOINT) : null;
23+
};
24+
1425
export const fetch = async (id: string, init: RequestInit = {}) => {
1526
if (typeof init.headers === "undefined") init.headers = {};
1627
if (!init.headers.hasOwnProperty("Accept"))
@@ -26,10 +37,15 @@ export const fetch = async (id: string, init: RequestInit = {}) => {
2637
if (resp.status === 204) return;
2738

2839
const json = await resp.json();
29-
if (resp.ok) return normalize(json);
40+
if (resp.ok) {
41+
return {
42+
hubURL: extractHubURL(resp)?.toString(), // URL cannot be serialized as JSON, must be sent as string
43+
data: normalize(json),
44+
};
45+
}
3046

31-
const defaultErrorMsg = json["hydra:title"];
32-
const status = json["hydra:description"] || resp.statusText;
47+
const defaultErrorMsg = json["{{{hydraPrefix}}}title"];
48+
const status = json["{{{hydraPrefix}}}description"] || resp.statusText;
3349
if (!json.violations) throw Error(defaultErrorMsg);
3450
const fields = {};
3551
json.violations.map(
@@ -40,12 +56,12 @@ export const fetch = async (id: string, init: RequestInit = {}) => {
4056
throw { defaultErrorMsg, status, fields };
4157
};
4258

43-
export const normalize = (data: any) => {
59+
export const normalize = (data: unknown) => {
4460
if (has(data, "{{{hydraPrefix}}}member")) {
4561
// Normalize items in collections
4662
data["{{{hydraPrefix}}}member"] = data[
4763
"{{{hydraPrefix}}}member"
48-
].map((item: any) => normalize(item));
64+
].map((item: unknown) => normalize(item));
4965

5066
return data;
5167
}

templates/next/utils/mercure.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import has from "lodash/has";
2+
import { useEffect, useState } from "react";
3+
import { PagedCollection } from "../types/Collection";
4+
import { normalize } from "./dataAccess";
5+
6+
const mercureSubscribe = (hubURL: string, data: unknown | PagedCollection<unknown>, setData: (data: unknown) => void) => {
7+
const url = new URL(hubURL, window.origin);
8+
url.searchParams.append("topic", (new URL(data["@id"], window.origin)).toString());
9+
const eventSource = new EventSource(url.toString());
10+
eventSource.addEventListener("message", (event) => setData(normalize(JSON.parse(event.data))));
11+
12+
return eventSource;
13+
}
14+
15+
export const useMercure = (deps: unknown | PagedCollection<unknown>, hubURL: string) => {
16+
const [data, setData] = useState(deps);
17+
18+
useEffect(() => {
19+
setData(deps);
20+
}, [deps]);
21+
22+
if (!data) {
23+
return data;
24+
}
25+
26+
if (!has(data, "{{{hydraPrefix}}}member") && !has(data, "@id")) {
27+
console.error("Object sent is not in JSON-LD format.");
28+
29+
return data;
30+
}
31+
32+
useEffect(() => {
33+
if (has(data, "{{{hydraPrefix}}}member") && Array.isArray(data["{{{hydraPrefix}}}member"]) && data["{{{hydraPrefix}}}member"].length !== 0) {
34+
// It's a PagedCollection
35+
data["{{{hydraPrefix}}}member"].forEach((obj, pos) => mercureSubscribe(hubURL, obj, (datum) => {
36+
data["{{{hydraPrefix}}}member"][pos] = datum;
37+
setData(data);
38+
}));
39+
40+
return () => data;
41+
}
42+
43+
// It's a single object
44+
const eventSource = mercureSubscribe(hubURL, data, setData);
45+
46+
return () => {
47+
eventSource.removeEventListener("message", (event) => setData(normalize(JSON.parse(event.data))));
48+
49+
return data;
50+
};
51+
}, [data]);
52+
53+
return data;
54+
}

0 commit comments

Comments
 (0)