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:
zyachel 2023-06-03 22:12:54 +05:30
parent 23eeae3558
commit 0aea2f47da
9 changed files with 94 additions and 69 deletions

View file

@ -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'],

View file

@ -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>
)} )}

View file

@ -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;
};

View file

@ -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>
. .

View file

@ -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>

View 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 } };
};

View file

@ -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,
}, },
}; };
} }

View file

@ -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 } };
} }
}; };

View file

@ -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 } };
} }
}; };