Skip to content

Commit

Permalink
feat(list): add list route
Browse files Browse the repository at this point in the history
adds ability to see titles, names, and images lists

closes #6
  • Loading branch information
zyachel committed Oct 28, 2023
1 parent 60fb23f commit 97f1432
Show file tree
Hide file tree
Showing 20 changed files with 818 additions and 0 deletions.
23 changes: 23 additions & 0 deletions src/components/list/Data.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { DataKind, Data as TData } from 'src/interfaces/shared/list';
import type { ToArray } from 'src/interfaces/shared';
import Images from './Images';
import Names from './Names';
import Titles from './Titles';

type Props = {
data: ToArray<TData<DataKind>>;
};

const Data = ({ data }: Props) => {
if (isDataImages(data)) return <Images images={data} />;
if (isDataNames(data)) return <Names names={data} />;

return <Titles titles={data} />;
};
export default Data;

const isDataImages = (data: unknown): data is TData<'images'>[] =>
Array.isArray(data) && typeof data[0] === 'string';

const isDataNames = (data: unknown): data is TData<'names'>[] =>
Array.isArray(data) && data[0] && typeof data[0] === 'object' && 'about' in data[0];
22 changes: 22 additions & 0 deletions src/components/list/Images.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Image from 'next/future/image';
import { modifyIMDbImg } from 'src/utils/helpers';
import type { Data } from 'src/interfaces/shared/list';
import styles from 'src/styles/modules/components/list/images.module.scss';

type Props = {
images: Data<'images'>[];
};

const Images = ({ images }: Props) => {
return (
<section className={styles.container}>
{images.map(image => (
<figure className={styles.imgContainer} key={image}>
<Image src={modifyIMDbImg(image, 400)} alt='' fill className={styles.img} sizes='200px'/>
</figure>
))}
</section>
);
};

export default Images;
35 changes: 35 additions & 0 deletions src/components/list/Meta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Link from 'next/link';
import { formatDate } from 'src/utils/helpers';
import List from 'src/interfaces/shared/list';
import styles from 'src/styles/modules/components/list/meta.module.scss';

type Props = {
title: string;
meta: List['meta'];
description: List['description'];
};
const Meta = ({ title, meta, description }: Props) => {
const by = meta.by.link ? (
<Link href={meta.by.link}>
<a className='link'>{meta.by.name}</a>
</Link>
) : (
meta.by.name
);

return (
<header className={styles.container}>
<h1 className='heading heading__secondary'>{title}</h1>
<ul className={styles.list}>
<li>by {by}</li>
<li>{meta.created}</li>
{meta.updated && <li>{meta.updated}</li>}
<li>
{meta.num} {meta.type}
</li>
</ul>
{description && <p>{description}</p>}
</header>
);
};
export default Meta;
57 changes: 57 additions & 0 deletions src/components/list/Names.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Image from 'next/future/image';
import { getProxiedIMDbImgUrl, modifyIMDbImg } from 'src/utils/helpers';
import { Card } from 'src/components/card';
import type { Data } from 'src/interfaces/shared/list';
import styles from 'src/styles/modules/components/list/names.module.scss';
import OptionalLink from './OptionalLink';

type Props = {
names: Data<'names'>[];
};

const Names = ({ names }: Props) => {
return (
<ul className={styles.names}>
{names.map(name => (
<Name {...name} key={name.name} />
))}
</ul>
);
};
export default Names;

const Name = ({ about, image, job, knownFor, knownForLink, name, url }: Props['names'][number]) => {
// const style: CSSProperties = {
// backgroundImage: image ? `url(${getProxiedIMDbImgUrl(modifyIMDbImg(image, 300))})` : undefined,
// };

return (
<Card hoverable className={styles.name}>
<div className={styles.imgContainer}>
{image ? (
<Image src={modifyIMDbImg(image, 400)} alt='' fill className={styles.img} sizes='200px' />
) : (
<svg className={styles.imgNA}>
<use href='/svg/sprite.svg#icon-image-slash' />
</svg>
)}
</div>
<div className={styles.info}>
<h2 className={`heading ${styles.heading}`}>
<OptionalLink href={url} className={`heading ${styles.heading}`}>
{name}
</OptionalLink>
</h2>
<ul className={styles.basicInfo} aria-label='quick facts'>
{job && <li>{job}</li>}
{knownFor && (
<li>
<OptionalLink href={knownForLink}>{knownFor}</OptionalLink>
</li>
)}
</ul>
<p>{about}</p>
</div>
</Card>
);
};
20 changes: 20 additions & 0 deletions src/components/list/OptionalLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { ReactNode, ComponentPropsWithoutRef } from 'react';
import Link from 'next/link';

const OptionalLink = ({
href,
children,
...rest
}: { href?: string | null; children: ReactNode } & Omit<ComponentPropsWithoutRef<'a'>, 'href'>) => (
<>
{href ? (
<Link href={href}>
<a {...rest}>{children}</a>
</Link>
) : (
children
)}
</>
);

export default OptionalLink;
33 changes: 33 additions & 0 deletions src/components/list/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import OptionalLink from './OptionalLink';
import type List from 'src/interfaces/shared/list';
import styles from 'src/styles/modules/components/list/pagination.module.scss';

type Props = {
pagination: List['pagination'];
};
const Pagination = ({ pagination }: Props) => {
const prevLink = pagination.prev && pagination.prev !== '#' ? pagination.prev : null;
const nextLink = pagination.next && pagination.next !== '#' ? pagination.next : null;

if (!prevLink && !nextLink) return null;

return (
<nav aria-label='pagination'>
<ul className={styles.nav}>
<li aria-hidden={!prevLink}>
<OptionalLink href={prevLink} className='link'>
Prev
</OptionalLink>
</li>
<li>{pagination.range} shown</li>
<li aria-hidden={!nextLink}>
<OptionalLink href={nextLink} className='link'>
Next
</OptionalLink>
</li>
</ul>
</nav>
);
};

export default Pagination;
79 changes: 79 additions & 0 deletions src/components/list/Titles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import Image from 'next/future/image';
import { getProxiedIMDbImgUrl, modifyIMDbImg } from 'src/utils/helpers';
import { Card } from 'src/components/card';
import type { Data } from 'src/interfaces/shared/list';
import styles from 'src/styles/modules/components/list/titles.module.scss';
import { CSSProperties } from 'react';
import OptionalLink from './OptionalLink';

type Props = {
titles: Data<'titles'>[];
};

const Titles = ({ titles }: Props) => {
return (
<ul className={styles.titles}>
{titles.map(title => (
<Title {...title} key={title.name} />
))}
</ul>
);
};
export default Titles;

const Title = (props: Props['titles'][number]) => {
const style: CSSProperties = {
backgroundImage: props.image
? `url(${getProxiedIMDbImgUrl(modifyIMDbImg(props.image, 300))})`
: undefined,
};

return (
<Card hoverable className={styles.title}>
<div className={styles.imgContainer}>
{props.image ? (
<Image src={modifyIMDbImg(props.image, 400)} alt='' fill className={styles.img} />
) : (
<svg className={styles.imgNA}>
<use href='/svg/sprite.svg#icon-image-slash' />
</svg>
)}
</div>
<div className={styles.info}>
<h2 className={`heading heading__tertiary ${styles.heading}`}>
<OptionalLink href={props.url} className={`heading ${styles.heading}`}>
{props.name} {props.year}
</OptionalLink>
</h2>
<ul className={styles.basicInfo} aria-label='quick facts'>
{props.certificate && <li>{props.certificate}</li>}
{props.runtime && <li>{props.runtime}</li>}
{props.genre && <li>{props.genre}</li>}
</ul>
<ul className={styles.ratings}>
{Boolean(props.rating) && <li className={styles.rating}>
<span className={styles.rating__num}>{props.rating}</span>
<svg className={styles.rating__icon}>
<use href='/svg/sprite.svg#icon-rating'></use>
</svg>
<span className={styles.rating__text}> Avg. rating</span>
</li>}
{Boolean(props.metascore) && <li className={styles.rating}>
<span className={styles.rating__num}>{props.metascore}</span>
<span className={styles.rating__text}>Metascore</span>
</li>}
</ul>
<p className={styles.plot}>
<span>Plot:</span> {props.plot}
</p>
<ul className={styles.otherInfo}>
{props.otherInfo.map(([infoHeading, info]) => (
<li key={infoHeading}>
<span>{infoHeading}:</span> {info}
</li>
))}
</ul>
</div>
</Card>
);
};
3 changes: 3 additions & 0 deletions src/components/list/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as Data } from './Data';
export { default as Meta } from './Meta';
export { default as Pagination } from './Pagination';
3 changes: 3 additions & 0 deletions src/interfaces/shared/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import type Name from './name';

export type Media = Name['media']; // exactly the same in title and name

// forcefully makes array of individual elements of T, where t is any conditional type.
export type ToArray<T> = T extends any ? T[] : never;
39 changes: 39 additions & 0 deletions src/interfaces/shared/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import list from 'src/utils/fetchers/list';

// for full title
type List = Awaited<ReturnType<typeof list>>;
export type { List as default };

type DataTitle = {
image: string | null;
name: string;
url: string | null;
year: string;
certificate: string;
runtime: string;
genre: string;
plot: string;
rating: string;
metascore: string;
otherInfo: string[][];
};

type DataName = {
image: string | null;
name: string;
url: string | null;
job: string | null;
knownFor: string | null;
knownForLink: string | null;
about: string;
};

type DataImage = string;

export type DataKind = 'images' | 'titles' | 'names';

export type Data<T extends DataKind> = T extends 'images'
? DataImage
: T extends 'names'
? DataName
: DataTitle;
54 changes: 54 additions & 0 deletions src/pages/list/[listId]/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
import Meta from 'src/components/meta/Meta';
import Layout from 'src/components/layout';
import ErrorInfo from 'src/components/error/ErrorInfo';
import { Data, Meta as ListMeta, Pagination } from 'src/components/list';
import { AppError } from 'src/interfaces/shared/error';
import TList from 'src/interfaces/shared/list';
import getOrSetApiCache from 'src/utils/getOrSetApiCache';
import list from 'src/utils/fetchers/list';
import { listKey } from 'src/utils/constants/keys';
import styles from 'src/styles/modules/pages/list/list.module.scss';

type Props = InferGetServerSidePropsType<typeof getServerSideProps>;

const List = ({ data, error, originalPath }: Props) => {
if (error) return <ErrorInfo {...error} originalPath={originalPath} />;

const description = data.description || `List created by ${data.meta.by.name} (${data.meta.num} ${data.meta.type}).`

return (
<>
<Meta title={data.title} description={description} />
<Layout className={styles.list} originalPath={originalPath}>
<ListMeta title={data.title} description={data.description} meta={data.meta} />
{/* @ts-expect-error don't have time to fix it. just a type fluff. */}
<Data data={data.data} />
<Pagination pagination={data.pagination} />
</Layout>
</>
);
};

type TData = ({ data: TList; error: null } | { error: AppError; data: null }) & {
originalPath: string;
};
type Params = { listId: string };

export const getServerSideProps: GetServerSideProps<TData, Params> = async ctx => {
const listId = ctx.params!.listId;
const pageNum = (ctx.query.page as string | undefined) ?? '1';
const originalPath = ctx.resolvedUrl;
try {
const data = await getOrSetApiCache(listKey(listId, pageNum), list, listId, pageNum);

return { props: { data, error: null, originalPath } };
} catch (error: any) {
const { message = 'Internal server error', statusCode = 500 } = error;
ctx.res.statusCode = statusCode;
ctx.res.statusMessage = message;
return { props: { error: { message, statusCode }, data: null, originalPath } };
}
};

export default List;

0 comments on commit 97f1432

Please sign in to comment.