Skip to content

fix: update next template with getStaticPaths #292

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

Merged
merged 8 commits into from
Jun 23, 2021
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
45 changes: 45 additions & 0 deletions templates/next/components/common/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Link from "next/link";
import { PagedCollection } from "../../types/Collection";

interface Props {
collection: PagedCollection<unknown>;
}

const Pagination = ({ collection }: Props) => {
const view = collection && collection['{{{hydraPrefix}}}view'];
if (!view) return;

const {
'{{{hydraPrefix}}}first': first,
'{{{hydraPrefix}}}previous': previous,
'{{{hydraPrefix}}}next': next,
'{{{hydraPrefix}}}last': last
} = view;

return (
<nav aria-label="Page navigation">
<Link href={first ? first : '#'}>
<a className={`btn btn-primary${previous ? '' : ' disabled'}`}>
<span aria-hidden="true">&lArr;</span> First
</a>
</Link>
<Link href={previous ? previous : '#'}>
<a className={`btn btn-primary${previous ? '' : ' disabled'}`}>
<span aria-hidden="true">&larr;</span> Previous
</a>
</Link>
<Link href={next ? next : '#'}>
<a className={`btn btn-primary${next ? '' : ' disabled'}`}>
Next <span aria-hidden="true">&rarr;</span>
</a>
</Link>
<Link href={last ? last : '#'}>
<a className={`btn btn-primary${next ? '' : ' disabled'}`}>
Last <span aria-hidden="true">&rArr;</span>
</a>
</Link>
</nav>
);
};

export default Pagination;
64 changes: 54 additions & 10 deletions templates/next/pages/foos/[id]/edit.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,74 @@
import { NextComponentType, NextPageContext } from 'next';
import { Form } from '../../../components/{{{lc}}}/Form';
import { {{{ucf}}} } from '../../../types/{{{ucf}}}';
import { fetch } from '../../../utils/dataAccess';
import { GetStaticPaths, GetStaticProps, NextComponentType, NextPageContext } from "next";
import { Form } from "../../../components/{{{lc}}}/Form";
import { {{{ucf}}} } from "../../../types/{{{ucf}}}";
import { fetch } from "../../../utils/dataAccess";
import Head from "next/head";
import DefaultErrorPage from "next/error";

interface Props {
{{{lc}}}: {{{ucf}}};
};

const Page: NextComponentType<NextPageContext, Props, Props> = ({ {{{lc}}} }) => {
if (!{{{lc}}}) {
return <DefaultErrorPage statusCode={404} />;
}

return (
<div>
<div>
<Head>
<title>{ {{{lc}}} && `Edit {{{ucf}}} ${ {{~lc}}['@id']}`}</title>
<title>{ {{{lc}}} && `Edit {{{ucf}}} ${ {{~lc}}['@id'] }` }</title>
</Head>
</div>
<Form {{{lc}}}={ {{{lc}}} }/>
<Form {{{lc}}}={ {{{lc}}} } />
</div>
);
};

Page.getInitialProps = async ({ asPath }: NextPageContext) => {
const {{{lc}}} = await fetch(asPath.replace( '/edit', ''));
export const getStaticProps: GetStaticProps = async ({ params }) => {
return {
props: {
{{{lc}}}: await fetch(`/{{{name}}}/${params.id}`),
},
revalidate: 1,
};
}

return { {{{lc}}} };
};
export const getStaticPaths: GetStaticPaths = async () => {
try {
const response = await fetch("/{{{name}}}");
} catch (e) {
console.error(e);

return {
paths: [],
fallback: true,
};
}

const view = response.data['{{{hydraPrefix}}}view'];
const paths = response.data["{{{hydraPrefix}}}member"].map(({{{lc}}}) => `${ {{~lc}}['@id'] }/edit`);

if (view) {
try {
const {
'{{{hydraPrefix}}}last': last
} = view;
for (let page = 2; page <= parseInt(last.replace(/^\/{{{name}}}\?page=(\d+)/, '$1')); page++) {
paths.concat(
await fetch(`/{{{name}}}?page=${page}`).data["{{{hydraPrefix}}}member"].map(({{{lc}}}) => `${ {{~lc}}['@id'] }/edit`)
);
}
} catch (e) {
console.error(e);
}
}

return {
paths,
fallback: true,
};
}

export default Page;
79 changes: 65 additions & 14 deletions templates/next/pages/foos/[id]/index.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,81 @@
import { NextComponentType, NextPageContext } from 'next';
import { Show } from '../../../components/{{{lc}}}/Show';
import { {{{ucf}}} } from '../../../types/{{{ucf}}}';
import { fetch } from '../../../utils/dataAccess';
import { GetStaticPaths, GetStaticProps, NextComponentType, NextPageContext } from "next";
import { Show } from "../../../components/{{{lc}}}/Show";
import { {{{ucf}}} } from "../../../types/{{{ucf}}}";
import { fetch } from "../../../utils/dataAccess";
import Head from "next/head";
import DefaultErrorPage from "next/error";
import { useMercure } from "../../../utils/mercure";

interface Props {
{{{lc}}}: {{{ucf}}};
hubURL: null | string;
};

const Page: NextComponentType<NextPageContext, Props, Props> = ({ {{{lc}}} }) => {
const Page: NextComponentType<NextPageContext, Props, Props> = (props) => {
const {{{lc}}} = props.hubURL === null ? props.{{{lc}}} : useMercure(props.{{{lc}}}, props.hubURL);

if (!{{{lc}}}) {
return <DefaultErrorPage statusCode={404} />;
}

return (
<div>
<div>
<Head>
<title>{`Show {{{ucf}}} ${ {{~lc}}['@id']}`}</title>
</Head>
</div>
<Show {{{lc}}}={ {{{lc}}} }/>
<Head>
<title>{`Show {{{ucf}}} ${ {{~lc}}['@id'] }`}</title>
</Head>
</div>
<Show {{{lc}}}={ {{{lc}}} } />
</div>
);
};

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

return { {{{lc}}} };
};
return {
props: {
{{{lc}}}: response.data,
hubURL: response.hubURL,
},
revalidate: 1,
};
}

export const getStaticPaths: GetStaticPaths = async () => {
try {
const response = await fetch("/{{{name}}}");
} catch (e) {
console.error(e);

return {
paths: [],
fallback: true,
};
}

const view = response.data['{{{hydraPrefix}}}view'];
const paths = response.data["{{{hydraPrefix}}}member"].map(({{{lc}}}) => `${ {{~lc}}['@id'] }`);

if (view) {
try {
const {
'{{{hydraPrefix}}}last': last
} = view;
for (let page = 2; page <= parseInt(last.replace(/^\/{{{name}}}\?page=(\d+)/, '$1')); page++) {
paths.concat(
await fetch(`/{{{name}}}?page=${page}`).data["{{{hydraPrefix}}}member"].map(({{{lc}}}) => `${ {{~lc}}['@id'] }`)
);
}
} catch (e) {
console.error(e);
}
}

return {
paths,
fallback: true,
};
}

export default Page;
3 changes: 1 addition & 2 deletions templates/next/pages/foos/create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const Page: NextComponentType<NextPageContext> = () => (
</div>
<Form />
</div>
)

);

export default Page;
37 changes: 25 additions & 12 deletions templates/next/pages/foos/index.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,42 @@
import { NextComponentType, NextPageContext } from 'next';
import { List } from '../../components/{{{lc}}}/List';
import { PagedCollection } from '../../types/Collection';
import { {{{ucf}}} } from '../../types/{{{ucf}}}';
import { fetch } from '../../utils/dataAccess';
import { GetServerSideProps, NextComponentType, NextPageContext } from "next";
import { List } from "../../components/{{{lc}}}/List";
import { PagedCollection } from "../../types/Collection";
import { {{{ucf}}} } from "../../types/{{{ucf}}}";
import { fetch } from "../../utils/dataAccess";
import Head from "next/head";
import Pagination from "../../components/common/Pagination";
import { useMercure } from "../../utils/mercure";

interface Props {
collection: PagedCollection<{{{ucf}}}>;
hubURL: string;
}

const Page: NextComponentType<NextPageContext, Props, Props> = ({collection}) => (
<div>
const Page: NextComponentType<NextPageContext, Props, Props> = (props) => {
const collection = useMercure(props.collection, props.hubURL);

return (
<div>
<div>
<Head>
<title>{{{ucf}}} List</title>
</Head>
</div>
<List {{{name}}}={collection['{{{hydraPrefix}}}member']}/>
<List {{{name}}}={collection["{{{hydraPrefix}}}member"]} />
<Pagination collection={collection} />
</div>
);
);
}

Page.getInitialProps = async () => {
export const getServerSideProps: GetServerSideProps = async () => {
const collection = await fetch('/{{{name}}}');

return {collection};
};
return {
props: {
collection: response.data,
hubURL: response.hubURL,
},
}
}

export default Page;
26 changes: 21 additions & 5 deletions templates/next/utils/dataAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ interface Violation {
propertyPath: string;
}

const extractHubURL = (response: Response): null | URL => {
const linkHeader = response.headers.get('Link');
if (!linkHeader) return null;

const matches = linkHeader.match(
/<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/
);

return matches && matches[1] ? new URL(matches[1], ENTRYPOINT) : null;
};

export const fetch = async (id: string, init: RequestInit = {}) => {
if (typeof init.headers === "undefined") init.headers = {};
if (!init.headers.hasOwnProperty("Accept"))
Expand All @@ -26,10 +37,15 @@ export const fetch = async (id: string, init: RequestInit = {}) => {
if (resp.status === 204) return;

const json = await resp.json();
if (resp.ok) return normalize(json);
if (resp.ok) {
return {
hubURL: extractHubURL(resp)?.toString(), // URL cannot be serialized as JSON, must be sent as string
data: normalize(json),
};
}

const defaultErrorMsg = json["hydra:title"];
const status = json["hydra:description"] || resp.statusText;
const defaultErrorMsg = json["{{{hydraPrefix}}}title"];
const status = json["{{{hydraPrefix}}}description"] || resp.statusText;
if (!json.violations) throw Error(defaultErrorMsg);
const fields = {};
json.violations.map(
Expand All @@ -40,12 +56,12 @@ export const fetch = async (id: string, init: RequestInit = {}) => {
throw { defaultErrorMsg, status, fields };
};

export const normalize = (data: any) => {
export const normalize = (data: unknown) => {
if (has(data, "{{{hydraPrefix}}}member")) {
// Normalize items in collections
data["{{{hydraPrefix}}}member"] = data[
"{{{hydraPrefix}}}member"
].map((item: any) => normalize(item));
].map((item: unknown) => normalize(item));

return data;
}
Expand Down
54 changes: 54 additions & 0 deletions templates/next/utils/mercure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import has from "lodash/has";
import { useEffect, useState } from "react";
import { PagedCollection } from "../types/Collection";
import { normalize } from "./dataAccess";

const mercureSubscribe = (hubURL: string, data: unknown | PagedCollection<unknown>, setData: (data: unknown) => void) => {
const url = new URL(hubURL, window.origin);
url.searchParams.append("topic", (new URL(data["@id"], window.origin)).toString());
const eventSource = new EventSource(url.toString());
eventSource.addEventListener("message", (event) => setData(normalize(JSON.parse(event.data))));

return eventSource;
}

export const useMercure = (deps: unknown | PagedCollection<unknown>, hubURL: string) => {
const [data, setData] = useState(deps);

useEffect(() => {
setData(deps);
}, [deps]);

if (!data) {
return data;
}

if (!has(data, "{{{hydraPrefix}}}member") && !has(data, "@id")) {
console.error("Object sent is not in JSON-LD format.");

return data;
}

useEffect(() => {
if (has(data, "{{{hydraPrefix}}}member") && Array.isArray(data["{{{hydraPrefix}}}member"]) && data["{{{hydraPrefix}}}member"].length !== 0) {
// It's a PagedCollection
data["{{{hydraPrefix}}}member"].forEach((obj, pos) => mercureSubscribe(hubURL, obj, (datum) => {
data["{{{hydraPrefix}}}member"][pos] = datum;
setData(data);
}));

return () => data;
}

// It's a single object
const eventSource = mercureSubscribe(hubURL, data, setData);

return () => {
eventSource.removeEventListener("message", (event) => setData(normalize(JSON.parse(event.data))));

return data;
};
}, [data]);

return data;
}