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 = {
reactStrictMode: true,
swcMinify: true,
async rewrites() {
return {
afterFiles: [
{
source: '/',
destination: '/find',
},
],
fallback: [
{
source: '/:path*',
destination: '/404',
},
],
};
async redirects() {
return [
{
source: '/',
destination: '/find',
permanent: true,
},
];
},
images: {
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 = {
message: string;
statusCode?: number;
// props specific to error boundary.
originalPath?: string;
/** props specific to error boundary. */
misc?: {
subtext: 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;
return (
<>
<Meta title={title} description='you encountered an error page!' />
<Layout className={styles.error}>
<Layout className={styles.error} originalPath={originalPath}>
<svg
className={styles.gnu}
focusable='false'
@ -52,6 +53,15 @@ const ErrorInfo = ({ message, statusCode, misc }: Props) => {
<Link href='/'>
<a className='link'>the homepage</a>
</Link>
, or view this route{' '}
<a
className='link'
href={`https://www.imdb.com${originalPath ?? ''}`}
target='_blank'
rel='noreferrer'
>
on IMDb
</a>
.
</p>
)}

View file

@ -1,5 +1,3 @@
export type AppError = {
message: string;
statusCode: number;
stack?: any;
};
import { AppError as AppErrorClass } from 'src/utils/helpers';
export type AppError = Omit<InstanceType<typeof AppErrorClass>, 'name'>;

View file

@ -1,21 +1,14 @@
import { ReactNode } from 'react';
import { useRouter } from 'next/router';
import Link from 'next/link';
import ThemeToggler from 'src/components/buttons/ThemeToggler';
import styles from 'src/styles/modules/layout/header.module.scss';
type Props = { full?: boolean; children?: ReactNode };
const Header = (props: Props) => {
const { asPath: path } = useRouter();
type Props = { full?: boolean; originalPath?: string };
const Header = ({ full, originalPath }: Props) => {
return (
<header
id='header'
className={`${styles.header} ${props.full ? styles.header__about : ''}`}
>
<header id='header' className={`${styles.header} ${full ? styles.header__about : ''}`}>
<div className={styles.topbar}>
<Link href='/'>
<Link href='/find'>
<a aria-label='go to homepage' className={styles.logo}>
<svg className={styles.logo__icon} role='img' aria-hidden>
<use href='/svg/sprite.svg#icon-logo'></use>
@ -23,7 +16,7 @@ const Header = (props: Props) => {
<span className={styles.logo__text}>libremdb</span>
</a>
</Link>
{props.full && (
{full && (
<nav className={styles.nav}>
<ul className={styles.nav__list}>
<li className={styles.nav__item}>
@ -45,14 +38,8 @@ const Header = (props: Props) => {
</nav>
)}
<div className={styles.misc}>
<a
href={`https://www.imdb.com${path}`}
target='_blank'
rel='noreferrer'
>
<span className='visually-hidden'>
View on IMDb (opens in new tab)
</span>
<a href={`https://www.imdb.com${originalPath ?? ''}`} target='_blank' rel='noreferrer'>
<span className='visually-hidden'>View on IMDb (opens in new tab)</span>
<svg className='icon' role='img' aria-hidden>
<use href='/svg/sprite.svg#icon-external-link'></use>
</svg>
@ -68,7 +55,7 @@ const Header = (props: Props) => {
<ThemeToggler className={styles.themeToggler} />
</div>
</div>
{props.full && (
{full && (
<div className={styles.hero}>
<h1 className={`heading heading__primary ${styles.hero__text}`}>
A free & open source IMDb front-end
@ -83,10 +70,7 @@ const Header = (props: Props) => {
nitter
</a>
, and{' '}
<a
href='https://github.com/digitalblossom/alternative-frontends'
className='link'
>
<a href='https://github.com/digitalblossom/alternative-frontends' className='link'>
many others
</a>
.

View file

@ -6,12 +6,13 @@ type Props = {
full?: true;
children: ReactNode;
className: string;
originalPath?: string;
};
const Layout = ({ full, children, className }: Props) => {
const Layout = ({ full, children, className, originalPath }: Props) => {
return (
<>
<Header full={full} />
<Header full={full} originalPath={originalPath} />
<main id='main' className={`main ${className}`}>
{children}
</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',
});
const BasicSearch = ({ data: { title, results }, error }: Props) => {
if (error) return <ErrorInfo message={error.message} statusCode={error.statusCode} />;
const BasicSearch = ({ data: { title, results }, error, originalPath }: Props) => {
if (error) return <ErrorInfo {...error} originalPath={originalPath} />;
let layoutClassName = styles.find;
if (!title) layoutClassName += ' ' + styles.find__home;
return (
<>
<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
<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.
type Data =
type Data = (
| { data: { title: string; results: Find }; 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 => {
// sample query str: find/?q=babylon&s=tt&ttype=ft&exact=true
const queryObj = ctx.query as FindQueryParams;
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 {
const entries = Object.entries(queryObj);
@ -57,7 +64,7 @@ export const getServerSideProps: GetServerSideProps<Data, FindQueryParams> = asy
const res = await getOrSetApiCache(findKey(queryStr), basicSearch, queryStr);
return {
props: { data: { title: query, results: res }, error: null },
props: { data: { title: query, results: res }, error: null, originalPath },
};
} catch (error: any) {
const { message, statusCode } = error;
@ -68,6 +75,7 @@ export const getServerSideProps: GetServerSideProps<Data, FindQueryParams> = asy
props: {
error: { message, statusCode },
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>;
const NameInfo = ({ data, error }: Props) => {
if (error) return <ErrorInfo message={error.message} statusCode={error.statusCode} />;
const NameInfo = ({ data, error, originalPath }: Props) => {
if (error) return <ErrorInfo {...error} originalPath={originalPath} />;
return (
<>
@ -24,7 +24,7 @@ const NameInfo = ({ data, error }: Props) => {
description={data.basic.bio.short + '...'}
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} />
<Media className={styles.media} media={data.media} />
<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 };
export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx => {
const nameId = ctx.params!.nameId;
const originalPath = ctx.resolvedUrl;
try {
const data = await getOrSetApiCache(nameKey(nameId), name, nameId);
return { props: { data, error: null } };
return { props: { data, error: null, originalPath } };
} catch (error: any) {
const { message, statusCode } = error;
ctx.res.statusCode = statusCode;
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>;
// TO-DO: make a wrapper page component to display errors, if present in props
const TitleInfo = ({ data, error }: Props) => {
if (error) return <ErrorInfo message={error.message} statusCode={error.statusCode} />;
const TitleInfo = ({ data, error, originalPath }: Props) => {
if (error) return <ErrorInfo {...error} originalPath={originalPath} />;
const info = {
meta: data.meta,
@ -34,7 +34,7 @@ const TitleInfo = ({ data, error }: Props) => {
description={data.basic.plot ?? undefined}
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} />
<Media className={styles.media} media={data.media} />
<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
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 };
export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx => {
const titleId = ctx.params!.titleId;
const originalPath = ctx.resolvedUrl;
try {
const data = await getOrSetApiCache(titleKey(titleId), title, titleId);
return { props: { data, error: null } };
return { props: { data, error: null, originalPath } };
} catch (error: any) {
const { message, statusCode } = error;
ctx.res.statusCode = statusCode;
ctx.res.statusMessage = message;
return { props: { error: { message, statusCode }, data: null } };
return { props: { error: { message, statusCode }, data: null, originalPath } };
}
};