fix(error): fix incorrect 'view on IMDb' link on error page
the error was due to a faulty logic. 'useRouter' was being used to detect pathname, which doesn't keep original url on 404 page. this commit fixes that. this commit also makes it easy to go to IMDb by adding a clear link on error page. closes https://github.com/zyachel/libremdb/issues/50
This commit is contained in:
parent
23eeae3558
commit
0aea2f47da
9 changed files with 94 additions and 69 deletions
|
@ -2,21 +2,14 @@
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
swcMinify: true,
|
swcMinify: true,
|
||||||
async rewrites() {
|
async redirects() {
|
||||||
return {
|
return [
|
||||||
afterFiles: [
|
|
||||||
{
|
{
|
||||||
source: '/',
|
source: '/',
|
||||||
destination: '/find',
|
destination: '/find',
|
||||||
|
permanent: true,
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
fallback: [
|
|
||||||
{
|
|
||||||
source: '/:path*',
|
|
||||||
destination: '/404',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
images: {
|
images: {
|
||||||
domains: ['m.media-amazon.com'],
|
domains: ['m.media-amazon.com'],
|
||||||
|
|
|
@ -11,7 +11,8 @@ import styles from 'src/styles/modules/components/error/error-info.module.scss';
|
||||||
type Props = {
|
type Props = {
|
||||||
message: string;
|
message: string;
|
||||||
statusCode?: number;
|
statusCode?: number;
|
||||||
// props specific to error boundary.
|
originalPath?: string;
|
||||||
|
/** props specific to error boundary. */
|
||||||
misc?: {
|
misc?: {
|
||||||
subtext: string;
|
subtext: string;
|
||||||
buttonText: string;
|
buttonText: string;
|
||||||
|
@ -19,12 +20,12 @@ type Props = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const ErrorInfo = ({ message, statusCode, misc }: Props) => {
|
const ErrorInfo = ({ message, statusCode, misc, originalPath }: Props) => {
|
||||||
const title = statusCode ? `${message} (${statusCode})` : message;
|
const title = statusCode ? `${message} (${statusCode})` : message;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title={title} description='you encountered an error page!' />
|
<Meta title={title} description='you encountered an error page!' />
|
||||||
<Layout className={styles.error}>
|
<Layout className={styles.error} originalPath={originalPath}>
|
||||||
<svg
|
<svg
|
||||||
className={styles.gnu}
|
className={styles.gnu}
|
||||||
focusable='false'
|
focusable='false'
|
||||||
|
@ -52,6 +53,15 @@ const ErrorInfo = ({ message, statusCode, misc }: Props) => {
|
||||||
<Link href='/'>
|
<Link href='/'>
|
||||||
<a className='link'>the homepage</a>
|
<a className='link'>the homepage</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
, or view this route{' '}
|
||||||
|
<a
|
||||||
|
className='link'
|
||||||
|
href={`https://www.imdb.com${originalPath ?? ''}`}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
on IMDb
|
||||||
|
</a>
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
export type AppError = {
|
import { AppError as AppErrorClass } from 'src/utils/helpers';
|
||||||
message: string;
|
|
||||||
statusCode: number;
|
export type AppError = Omit<InstanceType<typeof AppErrorClass>, 'name'>;
|
||||||
stack?: any;
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,21 +1,14 @@
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import ThemeToggler from 'src/components/buttons/ThemeToggler';
|
import ThemeToggler from 'src/components/buttons/ThemeToggler';
|
||||||
import styles from 'src/styles/modules/layout/header.module.scss';
|
import styles from 'src/styles/modules/layout/header.module.scss';
|
||||||
|
|
||||||
type Props = { full?: boolean; children?: ReactNode };
|
type Props = { full?: boolean; originalPath?: string };
|
||||||
|
|
||||||
const Header = (props: Props) => {
|
|
||||||
const { asPath: path } = useRouter();
|
|
||||||
|
|
||||||
|
const Header = ({ full, originalPath }: Props) => {
|
||||||
return (
|
return (
|
||||||
<header
|
<header id='header' className={`${styles.header} ${full ? styles.header__about : ''}`}>
|
||||||
id='header'
|
|
||||||
className={`${styles.header} ${props.full ? styles.header__about : ''}`}
|
|
||||||
>
|
|
||||||
<div className={styles.topbar}>
|
<div className={styles.topbar}>
|
||||||
<Link href='/'>
|
<Link href='/find'>
|
||||||
<a aria-label='go to homepage' className={styles.logo}>
|
<a aria-label='go to homepage' className={styles.logo}>
|
||||||
<svg className={styles.logo__icon} role='img' aria-hidden>
|
<svg className={styles.logo__icon} role='img' aria-hidden>
|
||||||
<use href='/svg/sprite.svg#icon-logo'></use>
|
<use href='/svg/sprite.svg#icon-logo'></use>
|
||||||
|
@ -23,7 +16,7 @@ const Header = (props: Props) => {
|
||||||
<span className={styles.logo__text}>libremdb</span>
|
<span className={styles.logo__text}>libremdb</span>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
{props.full && (
|
{full && (
|
||||||
<nav className={styles.nav}>
|
<nav className={styles.nav}>
|
||||||
<ul className={styles.nav__list}>
|
<ul className={styles.nav__list}>
|
||||||
<li className={styles.nav__item}>
|
<li className={styles.nav__item}>
|
||||||
|
@ -45,14 +38,8 @@ const Header = (props: Props) => {
|
||||||
</nav>
|
</nav>
|
||||||
)}
|
)}
|
||||||
<div className={styles.misc}>
|
<div className={styles.misc}>
|
||||||
<a
|
<a href={`https://www.imdb.com${originalPath ?? ''}`} target='_blank' rel='noreferrer'>
|
||||||
href={`https://www.imdb.com${path}`}
|
<span className='visually-hidden'>View on IMDb (opens in new tab)</span>
|
||||||
target='_blank'
|
|
||||||
rel='noreferrer'
|
|
||||||
>
|
|
||||||
<span className='visually-hidden'>
|
|
||||||
View on IMDb (opens in new tab)
|
|
||||||
</span>
|
|
||||||
<svg className='icon' role='img' aria-hidden>
|
<svg className='icon' role='img' aria-hidden>
|
||||||
<use href='/svg/sprite.svg#icon-external-link'></use>
|
<use href='/svg/sprite.svg#icon-external-link'></use>
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -68,7 +55,7 @@ const Header = (props: Props) => {
|
||||||
<ThemeToggler className={styles.themeToggler} />
|
<ThemeToggler className={styles.themeToggler} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{props.full && (
|
{full && (
|
||||||
<div className={styles.hero}>
|
<div className={styles.hero}>
|
||||||
<h1 className={`heading heading__primary ${styles.hero__text}`}>
|
<h1 className={`heading heading__primary ${styles.hero__text}`}>
|
||||||
A free & open source IMDb front-end
|
A free & open source IMDb front-end
|
||||||
|
@ -83,10 +70,7 @@ const Header = (props: Props) => {
|
||||||
nitter
|
nitter
|
||||||
</a>
|
</a>
|
||||||
, and{' '}
|
, and{' '}
|
||||||
<a
|
<a href='https://github.com/digitalblossom/alternative-frontends' className='link'>
|
||||||
href='https://github.com/digitalblossom/alternative-frontends'
|
|
||||||
className='link'
|
|
||||||
>
|
|
||||||
many others
|
many others
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
|
|
|
@ -6,12 +6,13 @@ type Props = {
|
||||||
full?: true;
|
full?: true;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className: string;
|
className: string;
|
||||||
|
originalPath?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Layout = ({ full, children, className }: Props) => {
|
const Layout = ({ full, children, className, originalPath }: Props) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header full={full} />
|
<Header full={full} originalPath={originalPath} />
|
||||||
<main id='main' className={`main ${className}`}>
|
<main id='main' className={`main ${className}`}>
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
|
|
25
src/pages/[...error]/index.tsx
Normal file
25
src/pages/[...error]/index.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import type { GetServerSideProps, InferGetServerSidePropsType } from 'next';
|
||||||
|
import ErrorInfo from 'src/components/error/ErrorInfo';
|
||||||
|
|
||||||
|
const error = {
|
||||||
|
statusCode: 404,
|
||||||
|
message: 'Not found, sorry.',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
|
||||||
|
|
||||||
|
const Error404 = ({ originalPath }: Props) => {
|
||||||
|
return <ErrorInfo {...error} originalPath={originalPath} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Error404;
|
||||||
|
|
||||||
|
type Data = { originalPath: string };
|
||||||
|
type Params = { error: string[] };
|
||||||
|
|
||||||
|
export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx => {
|
||||||
|
ctx.res.statusCode = error.statusCode;
|
||||||
|
ctx.res.statusMessage = error.message;
|
||||||
|
|
||||||
|
return { props: { originalPath: ctx.resolvedUrl } };
|
||||||
|
};
|
|
@ -21,13 +21,16 @@ const getMetadata = (title: string | null) => ({
|
||||||
: 'Search for anything on libremdb, a free & open source IMDb front-end',
|
: 'Search for anything on libremdb, a free & open source IMDb front-end',
|
||||||
});
|
});
|
||||||
|
|
||||||
const BasicSearch = ({ data: { title, results }, error }: Props) => {
|
const BasicSearch = ({ data: { title, results }, error, originalPath }: Props) => {
|
||||||
if (error) return <ErrorInfo message={error.message} statusCode={error.statusCode} />;
|
if (error) return <ErrorInfo {...error} originalPath={originalPath} />;
|
||||||
|
|
||||||
|
let layoutClassName = styles.find;
|
||||||
|
if (!title) layoutClassName += ' ' + styles.find__home;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta {...getMetadata(title)} />
|
<Meta {...getMetadata(title)} />
|
||||||
<Layout className={`${styles.find} ${!title && styles.find__home}`}>
|
<Layout className={layoutClassName} originalPath={originalPath}>
|
||||||
{title && ( // only showing when user has searched for something
|
{title && ( // only showing when user has searched for something
|
||||||
<Results results={results} title={title} className={styles.results} />
|
<Results results={results} title={title} className={styles.results} />
|
||||||
)}
|
)}
|
||||||
|
@ -38,17 +41,21 @@ const BasicSearch = ({ data: { title, results }, error }: Props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: use generics for passing in queryParams(to components) for better type-checking.
|
// TODO: use generics for passing in queryParams(to components) for better type-checking.
|
||||||
type Data =
|
type Data = (
|
||||||
| { data: { title: string; results: Find }; error: null }
|
| { data: { title: string; results: Find }; error: null }
|
||||||
| { data: { title: null; results: null }; error: null }
|
| { data: { title: null; results: null }; error: null }
|
||||||
| { data: { title: string; results: null }; error: AppError };
|
| { data: { title: string; results: null }; error: AppError }
|
||||||
|
) & {
|
||||||
|
originalPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps<Data, FindQueryParams> = async ctx => {
|
export const getServerSideProps: GetServerSideProps<Data, FindQueryParams> = async ctx => {
|
||||||
// sample query str: find/?q=babylon&s=tt&ttype=ft&exact=true
|
// sample query str: find/?q=babylon&s=tt&ttype=ft&exact=true
|
||||||
const queryObj = ctx.query as FindQueryParams;
|
const queryObj = ctx.query as FindQueryParams;
|
||||||
const query = queryObj.q?.trim();
|
const query = queryObj.q?.trim();
|
||||||
|
const originalPath = ctx.resolvedUrl;
|
||||||
|
|
||||||
if (!query) return { props: { data: { title: null, results: null }, error: null } };
|
if (!query) return { props: { data: { title: null, results: null }, error: null, originalPath } };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const entries = Object.entries(queryObj);
|
const entries = Object.entries(queryObj);
|
||||||
|
@ -57,7 +64,7 @@ export const getServerSideProps: GetServerSideProps<Data, FindQueryParams> = asy
|
||||||
const res = await getOrSetApiCache(findKey(queryStr), basicSearch, queryStr);
|
const res = await getOrSetApiCache(findKey(queryStr), basicSearch, queryStr);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: { data: { title: query, results: res }, error: null },
|
props: { data: { title: query, results: res }, error: null, originalPath },
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const { message, statusCode } = error;
|
const { message, statusCode } = error;
|
||||||
|
@ -68,6 +75,7 @@ export const getServerSideProps: GetServerSideProps<Data, FindQueryParams> = asy
|
||||||
props: {
|
props: {
|
||||||
error: { message, statusCode },
|
error: { message, statusCode },
|
||||||
data: { title: query, results: null },
|
data: { title: query, results: null },
|
||||||
|
originalPath,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,8 @@ import styles from 'src/styles/modules/pages/name/name.module.scss';
|
||||||
|
|
||||||
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
|
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
|
||||||
|
|
||||||
const NameInfo = ({ data, error }: Props) => {
|
const NameInfo = ({ data, error, originalPath }: Props) => {
|
||||||
if (error) return <ErrorInfo message={error.message} statusCode={error.statusCode} />;
|
if (error) return <ErrorInfo {...error} originalPath={originalPath} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -24,7 +24,7 @@ const NameInfo = ({ data, error }: Props) => {
|
||||||
description={data.basic.bio.short + '...'}
|
description={data.basic.bio.short + '...'}
|
||||||
imgUrl={data.basic.poster?.url && getProxiedIMDbImgUrl(data.basic.poster.url)}
|
imgUrl={data.basic.poster?.url && getProxiedIMDbImgUrl(data.basic.poster.url)}
|
||||||
/>
|
/>
|
||||||
<Layout className={styles.name}>
|
<Layout className={styles.name} originalPath={originalPath}>
|
||||||
<Basic data={data.basic} className={styles.basic} />
|
<Basic data={data.basic} className={styles.basic} />
|
||||||
<Media className={styles.media} media={data.media} />
|
<Media className={styles.media} media={data.media} />
|
||||||
<div className={styles.textarea}>
|
<div className={styles.textarea}>
|
||||||
|
@ -41,23 +41,26 @@ const NameInfo = ({ data, error }: Props) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type Data = { data: Name; error: null } | { error: AppError; data: null };
|
type Data = ({ data: Name; error: null } | { error: AppError; data: null }) & {
|
||||||
|
originalPath: string;
|
||||||
|
};
|
||||||
type Params = { nameId: string };
|
type Params = { nameId: string };
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx => {
|
export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx => {
|
||||||
const nameId = ctx.params!.nameId;
|
const nameId = ctx.params!.nameId;
|
||||||
|
const originalPath = ctx.resolvedUrl;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await getOrSetApiCache(nameKey(nameId), name, nameId);
|
const data = await getOrSetApiCache(nameKey(nameId), name, nameId);
|
||||||
|
|
||||||
return { props: { data, error: null } };
|
return { props: { data, error: null, originalPath } };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const { message, statusCode } = error;
|
const { message, statusCode } = error;
|
||||||
|
|
||||||
ctx.res.statusCode = statusCode;
|
ctx.res.statusCode = statusCode;
|
||||||
ctx.res.statusMessage = message;
|
ctx.res.statusMessage = message;
|
||||||
|
|
||||||
return { props: { error: { message, statusCode }, data: null } };
|
return { props: { error: { message, statusCode }, data: null, originalPath } };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,8 @@ import styles from 'src/styles/modules/pages/title/title.module.scss';
|
||||||
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
|
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
|
||||||
|
|
||||||
// TO-DO: make a wrapper page component to display errors, if present in props
|
// TO-DO: make a wrapper page component to display errors, if present in props
|
||||||
const TitleInfo = ({ data, error }: Props) => {
|
const TitleInfo = ({ data, error, originalPath }: Props) => {
|
||||||
if (error) return <ErrorInfo message={error.message} statusCode={error.statusCode} />;
|
if (error) return <ErrorInfo {...error} originalPath={originalPath} />;
|
||||||
|
|
||||||
const info = {
|
const info = {
|
||||||
meta: data.meta,
|
meta: data.meta,
|
||||||
|
@ -34,7 +34,7 @@ const TitleInfo = ({ data, error }: Props) => {
|
||||||
description={data.basic.plot ?? undefined}
|
description={data.basic.plot ?? undefined}
|
||||||
imgUrl={data.basic.poster?.url && getProxiedIMDbImgUrl(data.basic.poster.url)}
|
imgUrl={data.basic.poster?.url && getProxiedIMDbImgUrl(data.basic.poster.url)}
|
||||||
/>
|
/>
|
||||||
<Layout className={styles.title}>
|
<Layout className={styles.title} originalPath={originalPath}>
|
||||||
<Basic data={data.basic} className={styles.basic} />
|
<Basic data={data.basic} className={styles.basic} />
|
||||||
<Media className={styles.media} media={data.media} />
|
<Media className={styles.media} media={data.media} />
|
||||||
<Cast className={styles.cast} cast={data.cast} />
|
<Cast className={styles.cast} cast={data.cast} />
|
||||||
|
@ -50,22 +50,25 @@ const TitleInfo = ({ data, error }: Props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// TO-DO: make a getServerSideProps wrapper for handling errors
|
// TO-DO: make a getServerSideProps wrapper for handling errors
|
||||||
type Data = { data: Title; error: null } | { error: AppError; data: null };
|
type Data = ({ data: Title; error: null } | { error: AppError; data: null }) & {
|
||||||
|
originalPath: string;
|
||||||
|
};
|
||||||
type Params = { titleId: string };
|
type Params = { titleId: string };
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx => {
|
export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx => {
|
||||||
const titleId = ctx.params!.titleId;
|
const titleId = ctx.params!.titleId;
|
||||||
|
const originalPath = ctx.resolvedUrl;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await getOrSetApiCache(titleKey(titleId), title, titleId);
|
const data = await getOrSetApiCache(titleKey(titleId), title, titleId);
|
||||||
|
|
||||||
return { props: { data, error: null } };
|
return { props: { data, error: null, originalPath } };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const { message, statusCode } = error;
|
const { message, statusCode } = error;
|
||||||
ctx.res.statusCode = statusCode;
|
ctx.res.statusCode = statusCode;
|
||||||
ctx.res.statusMessage = message;
|
ctx.res.statusMessage = message;
|
||||||
|
|
||||||
return { props: { error: { message, statusCode }, data: null } };
|
return { props: { error: { message, statusCode }, data: null, originalPath } };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue