feat(list): add list route
adds ability to see titles, names, and images lists closes https://github.com/zyachel/libremdb/issues/6
This commit is contained in:
23
src/components/list/Data.tsx
Normal file
23
src/components/list/Data.tsx
Normal file
@ -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
src/components/list/Images.tsx
Normal file
22
src/components/list/Images.tsx
Normal file
@ -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
src/components/list/Meta.tsx
Normal file
35
src/components/list/Meta.tsx
Normal file
@ -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
src/components/list/Names.tsx
Normal file
57
src/components/list/Names.tsx
Normal file
@ -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
src/components/list/OptionalLink.tsx
Normal file
20
src/components/list/OptionalLink.tsx
Normal file
@ -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
src/components/list/Pagination.tsx
Normal file
33
src/components/list/Pagination.tsx
Normal file
@ -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
src/components/list/Titles.tsx
Normal file
79
src/components/list/Titles.tsx
Normal file
@ -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
src/components/list/index.ts
Normal file
3
src/components/list/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { default as Data } from './Data';
|
||||
export { default as Meta } from './Meta';
|
||||
export { default as Pagination } from './Pagination';
|
Reference in New Issue
Block a user