Skip to content

Commit

Permalink
Layouts & scroll restoration (#385)
Browse files Browse the repository at this point in the history
Closes DG-62, DG-58

## What changed? Why?

1. Scroll restoration works on cross-page navigation
2. Layouts for the pages, with support for reuse of components on page
nav!
  • Loading branch information
dgattey committed Dec 16, 2022
2 parents 0a052d5 + 5984527 commit d2be4a1
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 110 deletions.
4 changes: 4 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ const nextConfig = {
],
},
i18n: {
// This allows for the language value to be passed in HTML
locales: ['en'],
defaultLocale: 'en',
},
experimental: {
scrollRestoration: true,
},
};

module.exports = withNextBundleAnalyzer(nextConfig);
56 changes: 56 additions & 0 deletions src/components/errors/ErrorPageContents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Typography } from '@mui/material';
import { Link } from 'components/Link';
import { useLinkWithName } from 'hooks/useLinkWithName';
import { useRouter } from 'next/router';

export type HasStatusCode = {
/**
* What kind of error we encountered
*/
statusCode?: number;
};

// Error codes to title of page
const TITLE_TEXT: Record<number | 'fallback', string> = {
404: "😢 Oops, couldn't find that!",
fallback: '😬 This is awkward...',
};

/**
* Contents of the page in a different element so fallback can work its server-rendered magic
*/
export function ErrorPageContents({ statusCode }: HasStatusCode) {
const router = useRouter();
const emailLink = useLinkWithName('Email');
const descriptions: Record<number | 'fallback', JSX.Element> = {
404: (
<>
I didn&apos;t see a page matching the url{' '}
<Typography variant="code" component="code">
{router.asPath}
</Typography>{' '}
on the site. Check out the homepage and see if you can find what you were looking for. If
not,
</>
),
fallback: (
<>
Looks like I encountered a serverside error, otherwise known as a dreaded{' '}
{statusCode ?? 500}. Sorry! Try refreshing the page or attempting your action again. If
it&apos;s still broken,
</>
),
};
return (
<>
<Typography variant="h1">
{(statusCode && TITLE_TEXT[statusCode]) || TITLE_TEXT.fallback}
</Typography>
<Typography variant="body1" sx={{ maxWidth: '35em' }}>
{(statusCode && descriptions[statusCode]) || descriptions.fallback}{' '}
{emailLink ? <Link layout="iconText" {...emailLink} href={emailLink.url} /> : 'Email Me'}{' '}
and I can help you out!
</Typography>
</>
);
}
20 changes: 4 additions & 16 deletions src/components/layouts/ErrorLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import { Meta } from 'components/Meta';
import { FetchedFallbackData } from 'api/fetchFallbackData';
import type { EndpointKey } from 'api/endpoints';
import { Link } from 'components/Link';
import { Section } from 'ui/Section';
import { Stack } from '@mui/material';
import { PageLayout } from './PageLayout';

type ErrorLayoutProps<Keys extends EndpointKey> = {
type ErrorLayoutProps = {
children: React.ReactNode;

/**
* Provides SWR with fallback version/header/footer data
*/
fallback: FetchedFallbackData<Keys>;

/**
* The numeric code for the error's status
*/
Expand All @@ -24,14 +16,10 @@ type ErrorLayoutProps<Keys extends EndpointKey> = {
* Basic page layout for error pages. Max-width'd content, left aligned,
* with a go home button at the bottom
*/
export function ErrorLayout<Keys extends EndpointKey>({
children,
fallback,
statusCode,
}: ErrorLayoutProps<Keys>) {
export function ErrorLayout({ children, statusCode }: ErrorLayoutProps) {
const pageTitle = statusCode === 404 ? 'Oops! Page not found' : `Error code ${statusCode}`;
return (
<PageLayout fallback={fallback}>
<>
<Meta title={pageTitle} description="An error occurred" />
<Stack component={Section} sx={{ gap: 3, marginTop: -6 }}>
{children}
Expand All @@ -47,6 +35,6 @@ export function ErrorLayout<Keys extends EndpointKey>({
Go back home
</Link>
</Stack>
</PageLayout>
</>
);
}
31 changes: 21 additions & 10 deletions src/pages/404.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
import { fetchFallbackData } from 'api/fetchFallbackData';
import { FetchedFallbackData, fetchFallbackData } from 'api/fetchFallbackData';
import { ErrorPageContents } from 'components/errors/ErrorPageContents';
import { ErrorLayout } from 'components/layouts/ErrorLayout';
import { PageLayout } from 'components/layouts/PageLayout';
import type { GetStaticProps } from 'next/types';
import { Contents, ErrorPageProps } from './_error';
import type { GetLayout } from 'types/Page';

export const getStaticProps: GetStaticProps = async () => fetchFallbackData(['version', 'footer']);
type PageProps = {
fallback: FetchedFallbackData<'footer' | 'version'>;
};

export const getStaticProps: GetStaticProps<PageProps> = async () =>
fetchFallbackData(['version', 'footer']);

/**
* Error page, for 404s specifically
*/
function Error404Page({ fallback }: ErrorPageProps) {
return (
<ErrorLayout fallback={fallback} statusCode={404}>
<Contents statusCode={404} />
</ErrorLayout>
);
function Page() {
return <ErrorPageContents statusCode={404} />;
}

export default Error404Page;
const getLayout: GetLayout<PageProps> = (page, pageProps) => (
<PageLayout fallback={pageProps.fallback}>
<ErrorLayout statusCode={404}>{page}</ErrorLayout>
</PageLayout>
);

Page.getLayout = getLayout;

export default Page;
25 changes: 20 additions & 5 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
import { GlobalStyleProvider } from 'ui/theme/GlobalStyle';
import { AppProps } from 'next/app';
import type { ReactElement, ReactNode } from 'react';
import type { NextPage } from 'next';

export type PageProps = Record<string, unknown>;

export type NextPageWithLayout = NextPage & {
/**
* All layouts must take a props from the page
*/
getLayout?: (page: ReactElement, pageProps: PageProps) => ReactNode;
};

type AppPropsWithLayout = AppProps<PageProps> & {
Component: NextPageWithLayout;
};

/**
* Responsible for injecting styles into the tree client-side.
* Responsible for injecting styles into the tree client-side
* and handling per page layouts
*/
function App({ Component, pageProps }: AppProps) {
function App({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page);
return (
<GlobalStyleProvider>
<Component {...pageProps} />
</GlobalStyleProvider>
<GlobalStyleProvider>{getLayout(<Component {...pageProps} />, pageProps)}</GlobalStyleProvider>
);
}

Expand Down
78 changes: 18 additions & 60 deletions src/pages/_error.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,25 @@
import { Typography } from '@mui/material';
import { FetchedFallbackData, fetchFallbackData } from 'api/fetchFallbackData';
import { ErrorPageContents } from 'components/errors/ErrorPageContents';
import { ErrorLayout } from 'components/layouts/ErrorLayout';
import { Link } from 'components/Link';
import { useLinkWithName } from 'hooks/useLinkWithName';
import { PageLayout } from 'components/layouts/PageLayout';
import type { NextPageContext } from 'next';
import NextErrorComponent from 'next/error';
import { useRouter } from 'next/router';
import type { GetLayout } from 'types/Page';

interface HasStatusCode {
export type PageProps = {
/**
* What kind of error we encountered
*/
statusCode?: number;
}
};

export type ErrorPageProps = HasStatusCode & {
type LayoutProps = PageProps & {
/**
* Provides SWR with fallback version data
*/
fallback: FetchedFallbackData<'version' | 'footer'>;
};

export type ErrorWithCode = Error & HasStatusCode;

// Error codes to title of page
const TITLE: Record<number | 'fallback', string> = {
404: "😢 Oops, couldn't find that!",
fallback: '😬 This is awkward...',
};

/**
* If this is on the server, it'll provide a response to use for a status code
*/
Expand All @@ -38,7 +29,7 @@ export const getStaticProps = async (context: NextPageContext) => {
const errorCode = err?.statusCode ?? 404;
const statusCode = res ? res.statusCode : errorCode;
const { props: fallbackProps } = await fetchFallbackData(['version', 'footer']);
const props: ErrorPageProps = {
const props: PageProps = {
...fallbackProps,
...errorProps,
statusCode,
Expand All @@ -58,51 +49,18 @@ export const getStaticProps = async (context: NextPageContext) => {
};

/**
* Contents of the page in a different element so fallback can work its server-rendered magic
* Generic error page, for 500s/etc
*/
export function Contents({ statusCode }: HasStatusCode) {
const router = useRouter();
const emailLink = useLinkWithName('Email');
const descriptions: Record<number | 'fallback', JSX.Element> = {
404: (
<>
I didn&apos;t see a page matching the url{' '}
<Typography variant="code" component="code">
{router.asPath}
</Typography>{' '}
on the site. Check out the homepage and see if you can find what you were looking for. If
not,
</>
),
fallback: (
<>
Looks like I encountered a serverside error, otherwise known as a dreaded{' '}
{statusCode ?? 500}. Sorry! Try refreshing the page or attempting your action again. If
it&apos;s still broken,
</>
),
};
return (
<>
<Typography variant="h1">{(statusCode && TITLE[statusCode]) || TITLE.fallback}</Typography>
<Typography variant="body1" sx={{ maxWidth: '35em' }}>
{(statusCode && descriptions[statusCode]) || descriptions.fallback}{' '}
{emailLink ? <Link layout="iconText" {...emailLink} href={emailLink.url} /> : 'Email Me'}{' '}
and I can help you out!
</Typography>
</>
);
function Page({ statusCode }: PageProps) {
return <ErrorPageContents statusCode={statusCode} />;
}

/**
* Generic error page, for 404s//500s/etc
*/
function ErrorPage({ statusCode, fallback }: ErrorPageProps) {
return (
<ErrorLayout fallback={fallback} statusCode={statusCode ?? 500}>
<Contents statusCode={statusCode} />
</ErrorLayout>
);
}
const getLayout: GetLayout<LayoutProps> = (page, pageProps) => (
<PageLayout fallback={pageProps.fallback}>
<ErrorLayout statusCode={pageProps.statusCode ?? 500}>{page}</ErrorLayout>
</PageLayout>
);

Page.getLayout = getLayout;

export default ErrorPage;
export default Page;
21 changes: 12 additions & 9 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { FetchedFallbackData, fetchFallbackData } from 'api/fetchFallbackData';
import { Homepage } from 'components/homepage/Homepage';
import { PageLayout } from 'components/layouts/PageLayout';
import type { GetStaticProps } from 'next/types';
import type { GetLayout } from 'types/Page';

type HomeProps = {
type PageProps = {
fallback: FetchedFallbackData<
'version' | 'footer' | 'projects' | 'intro' | 'location' | 'latest/track' | 'latest/activity'
>;
};

export const getStaticProps: GetStaticProps<HomeProps> = async () =>
export const getStaticProps: GetStaticProps<PageProps> = async () =>
fetchFallbackData([
'version',
'footer',
Expand All @@ -20,12 +21,14 @@ export const getStaticProps: GetStaticProps<HomeProps> = async () =>
'latest/activity',
]);

function Home({ fallback }: HomeProps) {
return (
<PageLayout fallback={fallback}>
<Homepage />
</PageLayout>
);
function Page() {
return <Homepage />;
}

export default Home;
const getLayout: GetLayout<PageProps> = (page, pageProps) => (
<PageLayout fallback={pageProps.fallback}>{page}</PageLayout>
);

Page.getLayout = getLayout;

export default Page;
23 changes: 13 additions & 10 deletions src/pages/privacy.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import { FetchedFallbackData, fetchFallbackData } from 'api/fetchFallbackData';
import { PageLayout } from 'components/layouts/PageLayout';
import { Privacy } from 'components/privacy/Privacy';
import { GetStaticProps } from 'next/types';
import type { GetStaticProps } from 'next/types';
import type { GetLayout } from 'types/Page';

type PrivacyProps = {
type PageProps = {
fallback: FetchedFallbackData<'footer' | 'version' | 'privacy'>;
};

export const getStaticProps: GetStaticProps<PrivacyProps> = async () =>
export const getStaticProps: GetStaticProps<PageProps> = async () =>
fetchFallbackData(['footer', 'version', 'privacy']);

function PrivacyPage({ fallback }: PrivacyProps) {
return (
<PageLayout fallback={fallback}>
<Privacy />
</PageLayout>
);
function Page() {
return <Privacy />;
}

export default PrivacyPage;
const getLayout: GetLayout<PageProps> = (page, pageProps) => (
<PageLayout fallback={pageProps.fallback}>{page}</PageLayout>
);

Page.getLayout = getLayout;

export default Page;
16 changes: 16 additions & 0 deletions src/types/Page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { AppProps } from 'next/app';
import type { ReactElement, ReactNode } from 'react';
import type { NextPage } from 'next';

export type GetLayout<PropsType> = (page: ReactElement, pageProps: PropsType) => ReactNode;

export type NextPageWithLayout<PropsType> = NextPage & {
/**
* All layouts must take a props from the page
*/
getLayout?: GetLayout<PropsType>;
};

export type AppPropsWithLayout<PropsType> = AppProps<PropsType> & {
Component: NextPageWithLayout<PropsType>;
};

1 comment on commit d2be4a1

@vercel
Copy link

@vercel vercel bot commented on d2be4a1 Dec 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

dg – ./

dg-dgattey.vercel.app
dylangattey.com
dg.vercel.app
dg-git-main-dgattey.vercel.app

Please sign in to comment.