feat: major rewrite
the application is now rewritten in next.js. this commit also adds the ability to see trailers, did you know, more like this, etc. on title page. BREAKING CHANGE: the whole application is rewritten from scratch.
This commit is contained in:
48
src/components/Error/ErrorInfo.tsx
Normal file
48
src/components/Error/ErrorInfo.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import Link from 'next/link';
|
||||
import Layout from '../../layouts/Layout';
|
||||
import Meta from '../Meta/Meta';
|
||||
|
||||
import styles from '../../styles/modules/components/error/error-info.module.scss';
|
||||
|
||||
// for details regarding the svg, go to sadgnu.svg file
|
||||
// description copied verbatim from https://www.gnu.org/graphics/sventsitsky-sadgnu.html
|
||||
// 404 idea from ninamori.org 404 page.
|
||||
|
||||
const ErrorInfo = ({ message = 'Not found, sorry.', statusCode = 404 }) => {
|
||||
return (
|
||||
<>
|
||||
<Meta
|
||||
title={`${message} (${statusCode})`}
|
||||
description='you encountered an error page!'
|
||||
/>
|
||||
<Layout className={styles.error}>
|
||||
<svg
|
||||
className={styles.gnu}
|
||||
focusable='false'
|
||||
role='img'
|
||||
aria-labelledby='gnu-title gnu-desc'
|
||||
>
|
||||
<title id='gnu-title'>GNU and Tux</title>
|
||||
<desc id='gnu-desc'>
|
||||
A pencil drawing of a big gnu and a small penguin, both very sad.
|
||||
GNU is despondently sitting on a bench, and Tux stands beside him,
|
||||
looking down and patting him on the back.
|
||||
</desc>
|
||||
<use href='/svg/sadgnu.svg#sad-gnu'></use>
|
||||
</svg>
|
||||
<h1 className={`heading heading__primary ${styles.heading}`}>
|
||||
<span>{message}</span>
|
||||
<span> ({statusCode})</span>
|
||||
</h1>
|
||||
<p className={styles.back}>
|
||||
Go back to{' '}
|
||||
<Link href='/about'>
|
||||
<a className='link'>the homepage</a>
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default ErrorInfo;
|
40
src/components/Meta/Meta.tsx
Normal file
40
src/components/Meta/Meta.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import Head from 'next/head';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description?: string;
|
||||
imgUrl?: string;
|
||||
};
|
||||
|
||||
const Meta = ({
|
||||
title,
|
||||
description = 'libremdb, a free & open source IMDb front-end.',
|
||||
imgUrl = 'icon.svg',
|
||||
}: Props) => {
|
||||
return (
|
||||
<Head>
|
||||
<meta charSet='UTF-8' />
|
||||
<meta httpEquiv='X-UA-Compatible' content='IE=edge' />
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
||||
|
||||
<title key='title'>{`${title} | libremdb`}</title>
|
||||
<meta key='desc' name='description' content={description} />
|
||||
<link rel='icon' href='/favicon.ico' sizes='any' />
|
||||
<link rel='icon' href='/icon.svg' type='image/svg+xml' />
|
||||
<link rel='apple-touch-icon' href='/apple-touch-icon.png' />
|
||||
<link rel='manifest' href='/site.webmanifest' />
|
||||
<meta name='theme-color' content='#ffe5ef' />
|
||||
|
||||
<meta property='og:title' content={title} />
|
||||
<meta property='og:description' content={description} />
|
||||
<meta property='og:site_name' content='libremdb' />
|
||||
<meta property='og:locale' content='en_US' />
|
||||
<meta property='og:type' content='video.movie' />
|
||||
<meta
|
||||
property='og:image'
|
||||
content={`${process.env.NEXT_PUBLIC_URL}/${imgUrl}`}
|
||||
/>
|
||||
</Head>
|
||||
);
|
||||
};
|
||||
export default Meta;
|
35
src/components/buttons/ThemeToggler.tsx
Normal file
35
src/components/buttons/ThemeToggler.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { useContext } from 'react';
|
||||
import { themeContext } from '../../context/theme-context';
|
||||
|
||||
import styles from '../../styles/modules/components/buttons/themeToggler.module.scss';
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
};
|
||||
|
||||
const ThemeToggler = (props: Props) => {
|
||||
const { theme, setTheme } = useContext(themeContext);
|
||||
const clickHandler = () => {
|
||||
const themeToSet = theme === 'light' ? 'dark' : 'light';
|
||||
setTheme(themeToSet);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`${styles.button} ${props.className}`}
|
||||
aria-label='Change theme'
|
||||
onClick={clickHandler}
|
||||
>
|
||||
<svg
|
||||
className={`icon ${styles.icon}`}
|
||||
focusable='false'
|
||||
aria-hidden='true'
|
||||
role='img'
|
||||
>
|
||||
<use href='/svg/sprite.svg#icon-theme-switcher'></use>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeToggler;
|
6
src/components/loaders/ProgressBar.tsx
Normal file
6
src/components/loaders/ProgressBar.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import styles from '../../styles/modules/components/loaders/progress-bar.module.scss';
|
||||
|
||||
const ProgressBar = () => {
|
||||
return <span className={styles.progress} role='progressbar'></span>;
|
||||
};
|
||||
export default ProgressBar;
|
153
src/components/title/Basic.tsx
Normal file
153
src/components/title/Basic.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import { Fragment } from 'react';
|
||||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { formatNumber, formatTime, modifyIMDbImg } from '../../utils/helpers';
|
||||
import { Basic } from '../../interfaces/shared/title';
|
||||
import styles from '../../styles/modules/components/title/basic.module.scss';
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
data: Basic;
|
||||
};
|
||||
|
||||
const Basic = ({ data, className }: Props) => {
|
||||
const titleType = data.type.id;
|
||||
const releaseTime =
|
||||
titleType === 'tvSeries'
|
||||
? `${data.releaseYear?.start}-${data.releaseYear?.end || 'present'}`
|
||||
: data.releaseYear?.start;
|
||||
|
||||
return (
|
||||
<section
|
||||
// role is valid but not known to jsx-a11y
|
||||
// aria-description={`basic info for '${data.title}'`}
|
||||
// style={{ backgroundImage: data.poster && `url(${data.poster?.url})` }}
|
||||
className={`${styles.container} ${className}`}
|
||||
>
|
||||
<div
|
||||
className={styles.imageContainer}
|
||||
style={{
|
||||
backgroundImage:
|
||||
data.poster && `url(${modifyIMDbImg(data.poster.url, 300)})`,
|
||||
}}
|
||||
>
|
||||
{data.poster ? (
|
||||
<Image
|
||||
className={styles.image}
|
||||
src={modifyIMDbImg(data.poster.url)}
|
||||
alt={data.poster.caption}
|
||||
priority
|
||||
fill
|
||||
sizes='300px'
|
||||
/>
|
||||
) : (
|
||||
<svg className={styles.image__NA}>
|
||||
<use href='/svg/sprite.svg#icon-image-slash' />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<h1 className={`${styles.title} heading heading__primary`}>
|
||||
{data.title}
|
||||
</h1>
|
||||
<ul className={styles.meta} aria-label='quick facts'>
|
||||
{data.status.id !== 'released' && (
|
||||
<li className={styles.meta__text}>{data.status.text}</li>
|
||||
)}
|
||||
<li className={styles.meta__text}>{data.type.name}</li>
|
||||
{data.releaseYear && (
|
||||
<li className={styles.meta__text}>{releaseTime}</li>
|
||||
)}
|
||||
{data.ceritficate && (
|
||||
<li className={styles.meta__text}>{data.ceritficate}</li>
|
||||
)}
|
||||
{data.runtime && (
|
||||
<li className={styles.meta__text}>{formatTime(data.runtime)}</li>
|
||||
)}
|
||||
</ul>
|
||||
<div className={styles.ratings}>
|
||||
{data.ratings.avg && (
|
||||
<>
|
||||
<p className={styles.rating}>
|
||||
<span className={styles.rating__num}>{data.ratings.avg}</span>
|
||||
<svg className={styles.rating__icon}>
|
||||
<use href='/svg/sprite.svg#icon-rating'></use>
|
||||
</svg>
|
||||
<span className={styles.rating__text}> Avg. rating</span>
|
||||
</p>
|
||||
<p className={styles.rating}>
|
||||
<span className={styles.rating__num}>
|
||||
{formatNumber(data.ratings.numVotes)}
|
||||
</span>
|
||||
<svg className={styles.rating__icon}>
|
||||
<use href='/svg/sprite.svg#icon-like-dislike'></use>
|
||||
</svg>
|
||||
<span className={styles.rating__text}> No. of votes</span>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{data.ranking && (
|
||||
<p className={styles.rating}>
|
||||
<span className={styles.rating__num}>
|
||||
{formatNumber(data.ranking.position)}
|
||||
</span>
|
||||
<svg className={styles.rating__icon}>
|
||||
<use href='/svg/sprite.svg#icon-graph-rising'></use>
|
||||
</svg>
|
||||
<span className={styles.rating__text}>
|
||||
{' '}
|
||||
Popularity (
|
||||
<span className={styles.rating__sub}>
|
||||
{data.ranking.direction === 'UP'
|
||||
? `\u2191${formatNumber(data.ranking.change)}`
|
||||
: data.ranking.direction === 'DOWN'
|
||||
? `\u2193${formatNumber(data.ranking.change)}`
|
||||
: ''}
|
||||
</span>
|
||||
)
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!!data.genres.length && (
|
||||
<p className={styles.genres}>
|
||||
<span className={styles.genres__heading}>Genres: </span>
|
||||
{data.genres.map((genre, i) => (
|
||||
<Fragment key={genre.id}>
|
||||
{i > 0 && ', '}
|
||||
<Link href={`/search/title?genres=${genre.id}`}>
|
||||
<a className={styles.link}>{genre.text}</a>
|
||||
</Link>
|
||||
</Fragment>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
{
|
||||
<p className={styles.overview}>
|
||||
<span className={styles.overview__heading}>Plot: </span>
|
||||
<span className={styles.overview__text}>{data.plot || '-'}</span>
|
||||
</p>
|
||||
}
|
||||
{data.primaryCrew.map(crewType => (
|
||||
<p className={styles.crewType} key={crewType.type.id}>
|
||||
<span className={styles.crewType__heading}>
|
||||
{`${crewType.type.category}: `}
|
||||
</span>
|
||||
{crewType.crew.map((crew, i) => (
|
||||
<Fragment key={crew.id}>
|
||||
{i > 0 && ', '}
|
||||
<Link href={`/name/${crew.id}`}>
|
||||
<a className={styles.link}>{crew.name}</a>
|
||||
</Link>
|
||||
</Fragment>
|
||||
))}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Basic;
|
56
src/components/title/Cast.tsx
Normal file
56
src/components/title/Cast.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
import { Cast } from '../../interfaces/shared/title';
|
||||
import { modifyIMDbImg } from '../../utils/helpers';
|
||||
|
||||
import styles from '../../styles/modules/components/title/cast.module.scss';
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
cast: Cast;
|
||||
};
|
||||
|
||||
const Cast = ({ className, cast }: Props) => {
|
||||
if (!cast.length) return <></>;
|
||||
|
||||
return (
|
||||
<section className={`${className} ${styles.container}`}>
|
||||
<h2 className='heading heading__secondary'>Cast</h2>
|
||||
<ul className={styles.cast}>
|
||||
{cast.map(member => (
|
||||
<li key={member.id} className={styles.member}>
|
||||
<div className={styles.member__imgContainer}>
|
||||
{member.image ? (
|
||||
<Image
|
||||
src={modifyIMDbImg(member.image, 400)}
|
||||
alt=''
|
||||
fill
|
||||
className={styles.member__img}
|
||||
sizes='200px'
|
||||
/>
|
||||
) : (
|
||||
<svg className={styles.member__imgNA}>
|
||||
<use href='/svg/sprite.svg#icon-image-slash' />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.member__textContainer}>
|
||||
<p>
|
||||
<Link href={`/name/${member.id}`}>
|
||||
<a className={styles.member__name}>{member.name}</a>
|
||||
</Link>
|
||||
</p>
|
||||
<p className={styles.member__role}>
|
||||
{member.characters?.join(', ')}
|
||||
{member.attributes && (
|
||||
<span> ({member.attributes.join(', ')})</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
export default Cast;
|
106
src/components/title/DidYouKnow.tsx
Normal file
106
src/components/title/DidYouKnow.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
import Link from 'next/link';
|
||||
import { Fragment } from 'react';
|
||||
import { DidYouKnow } from '../../interfaces/shared/title';
|
||||
import styles from '../../styles/modules/components/title/did-you-know.module.scss';
|
||||
|
||||
type Props = {
|
||||
data: DidYouKnow;
|
||||
};
|
||||
|
||||
const DidYouKnow = ({ data }: Props) => {
|
||||
if (!Object.keys(data).length) return <></>;
|
||||
return (
|
||||
<section className={styles.didYouKnow}>
|
||||
<h2 className='heading heading__secondary'>Did you know</h2>
|
||||
<div className={styles.container}>
|
||||
{data.trivia && (
|
||||
<div className={styles.item}>
|
||||
<h3 className='heading heading__tertiary'>Trivia</h3>
|
||||
<div
|
||||
className={styles.item__desc}
|
||||
dangerouslySetInnerHTML={{ __html: data.trivia.html }}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
{data.goofs && (
|
||||
<div className={styles.item}>
|
||||
<h3 className='heading heading__tertiary'>Goofs</h3>
|
||||
<div
|
||||
className={styles.item__desc}
|
||||
dangerouslySetInnerHTML={{ __html: data.goofs.html }}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
{data.quotes?.lines.length && (
|
||||
// html spec says not to use blockquote & cite for conversations, even though it seems a perfect choice here.
|
||||
// see 'note' part https://html.spec.whatwg.org/multipage/grouping-content.html#the-blockquote-element
|
||||
<div className={styles.item}>
|
||||
<h3 className='heading heading__tertiary'>Quotes</h3>
|
||||
{data.quotes.lines.map((line, i) => (
|
||||
<div className={styles.quotes} key={i}>
|
||||
<p className={styles.quote}>
|
||||
{line.name && (
|
||||
<Link href={`/name/${line.id}`}>
|
||||
<a className={'link'}>{line.name}</a>
|
||||
</Link>
|
||||
)}
|
||||
{line.stageDirection && <i> [{line.stageDirection}] </i>}
|
||||
{line.text && <span>: {line.text}</span>}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{data.crazyCredits && (
|
||||
<div className={styles.item}>
|
||||
<h3 className='heading heading__tertiary'>Crazy credits</h3>
|
||||
<div
|
||||
className={styles.item__desc}
|
||||
dangerouslySetInnerHTML={{ __html: data.crazyCredits.html }}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
{data.alternativeVersions && (
|
||||
<div className={styles.item}>
|
||||
<h3 className='heading heading__tertiary'>Alternate versions</h3>
|
||||
<div
|
||||
className={styles.item__desc}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: data.alternativeVersions.html,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
{data.connections && (
|
||||
<div className={styles.item}>
|
||||
<h3 className='heading heading__tertiary'>Connections</h3>
|
||||
<p className={styles.item__desc}>
|
||||
<span>{data.connections.startText} </span>
|
||||
<Link href={`/title/${data.connections.title.id}`}>
|
||||
<a className={'link'}>{data.connections.title.text}</a>
|
||||
</Link>
|
||||
<span> ({data.connections.title.year})</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{data.soundTrack && (
|
||||
<div className={styles.item}>
|
||||
<h3 className='heading heading__tertiary'>Soundtracks</h3>
|
||||
<div className={styles.list}>
|
||||
<p>{data.soundTrack.title}</p>
|
||||
{data.soundTrack.htmls &&
|
||||
data.soundTrack.htmls.map(html => (
|
||||
<div
|
||||
key={html}
|
||||
className={styles.item__desc}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
export default DidYouKnow;
|
333
src/components/title/Info.tsx
Normal file
333
src/components/title/Info.tsx
Normal file
@ -0,0 +1,333 @@
|
||||
import Link from 'next/link';
|
||||
import { NextRouter } from 'next/router';
|
||||
import { Info } from '../../interfaces/shared/title';
|
||||
import { formatMoney, formatTime } from '../../utils/helpers';
|
||||
|
||||
import styles from '../../styles/modules/components/title/info.module.scss';
|
||||
|
||||
type Props = {
|
||||
info: Info;
|
||||
className: string;
|
||||
router: NextRouter;
|
||||
};
|
||||
|
||||
const Info = ({ info, className, router }: Props) => {
|
||||
const { titleId } = router.query;
|
||||
const { boxOffice, details, meta, keywords, technicalSpecs, accolades } =
|
||||
info;
|
||||
|
||||
return (
|
||||
<div className={`${className} ${styles.info}`}>
|
||||
{meta.infoEpisode && (
|
||||
<section className={styles.episodeInfo}>
|
||||
<h2 className='heading heading__secondary'>Episode info</h2>
|
||||
<div className={styles.episodeInfo__container}>
|
||||
{meta.infoEpisode.numSeason && (
|
||||
<p className={styles.series}>
|
||||
<span>Season: </span>
|
||||
<span>{meta.infoEpisode.numSeason}</span>
|
||||
</p>
|
||||
)}
|
||||
{meta.infoEpisode.numEpisode && (
|
||||
<p>
|
||||
<span>Episode: </span>
|
||||
<span>{meta.infoEpisode.numEpisode}</span>
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
<span>Series: </span>
|
||||
<span>
|
||||
<Link href={`/title/${meta.infoEpisode.series.id}`}>
|
||||
<a className={'link'}>{meta.infoEpisode.series.title}</a>
|
||||
</Link>
|
||||
<span>
|
||||
{' '}
|
||||
({meta.infoEpisode.series.startYear}-
|
||||
{meta.infoEpisode.series.endYear || 'present'})
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
{meta.infoEpisode.prevId && (
|
||||
<p>
|
||||
<Link href={`/title/${meta.infoEpisode.prevId}`}>
|
||||
<a className='link'>Go to previous episode</a>
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
{meta.infoEpisode.nextId && (
|
||||
<p>
|
||||
<Link href={`/title/${meta.infoEpisode.nextId}`}>
|
||||
<a className='link'>Go to next episode</a>
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
{meta.infoSeries && (
|
||||
<section className={styles.seriesInfo}>
|
||||
<h2 className='heading heading__secondary'>Series info</h2>
|
||||
<div className={styles.seriesInfo__container}>
|
||||
<p>
|
||||
<span>Total Seasons: </span>
|
||||
<span>{meta.infoSeries.seasons.length}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span>Total Years: </span>
|
||||
<span>{meta.infoSeries.years.length}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span>Total Episodes: </span>
|
||||
<span>{meta.infoSeries.totalEpisodes}</span>
|
||||
</p>
|
||||
<p>
|
||||
<Link href={`/title/${titleId}/episodes`}>
|
||||
<a className='link'>See all Episodes</a>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
<section className={styles.accolades}>
|
||||
<h2 className='heading heading__secondary'>Accolades</h2>
|
||||
<div className={styles.accolades__container}>
|
||||
{accolades.topRating && (
|
||||
<p>
|
||||
<Link href={`/chart/top`}>
|
||||
<a className='link'>Top rated (#{accolades.topRating})</a>
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
{accolades.awards && (
|
||||
<p>
|
||||
<span>
|
||||
Won {accolades.awards.wins} {accolades.awards.name}
|
||||
</span>
|
||||
<span> (out of {accolades.awards.nominations} nominations)</span>
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
{accolades.wins} wins and {accolades.nominations} nominations in
|
||||
total
|
||||
</p>
|
||||
<p>
|
||||
<Link href={`/title/${titleId}/awards`}>
|
||||
<a className='link'>View all awards</a>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
{!!keywords.total && (
|
||||
<section className={styles.keywords}>
|
||||
<h2 className='heading heading__secondary'>Keywords</h2>
|
||||
<ul className={styles.keywords__container}>
|
||||
{keywords.list.map(word => (
|
||||
<li className={styles.keywords__item} key={word}>
|
||||
<Link
|
||||
href={`/search/keyword/?keywords=${word.replaceAll(
|
||||
' ',
|
||||
'-'
|
||||
)}`}
|
||||
>
|
||||
<a className='link'>{word}</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
{!!Object.keys(details).length && (
|
||||
<section className={styles.details}>
|
||||
<h2 className='heading heading__secondary'>Details</h2>
|
||||
<div className={styles.details__container}>
|
||||
{details.releaseDate && (
|
||||
<p>
|
||||
<span>Release date: </span>
|
||||
<time dateTime={details.releaseDate.date}>
|
||||
{details.releaseDate.date}
|
||||
</time>
|
||||
<span> ({details.releaseDate.country.text})</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{details.countriesOfOrigin && (
|
||||
<p>
|
||||
<span>Countries of origin: </span>
|
||||
{details.countriesOfOrigin.map((country, i) => (
|
||||
<span key={country.id}>
|
||||
{!!i && ', '}
|
||||
<Link
|
||||
href={`/search/title/?country_of_origin=${country.id}`}
|
||||
>
|
||||
<a className='link'>{country.text}</a>
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
{details.officialSites && (
|
||||
<p>
|
||||
<span>Official sites: </span>
|
||||
{details.officialSites.sites.map((site, i) => (
|
||||
<span key={site.url}>
|
||||
{!!i && ', '}
|
||||
<a href={site.url} className='link'>
|
||||
{site.name}
|
||||
</a>
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
{details.languages?.length && (
|
||||
<p>
|
||||
<span>Languages: </span>
|
||||
{details.languages.map((lang, i) => (
|
||||
<span key={lang.id}>
|
||||
{!!i && ', '}
|
||||
<Link href={`/search/title/?primary_language=${lang.id}`}>
|
||||
<a className='link'>{lang.text}</a>
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
{details.alsoKnownAs && (
|
||||
<p>
|
||||
<span>Also known as: </span>
|
||||
<span>{details.alsoKnownAs}</span>
|
||||
</p>
|
||||
)}
|
||||
{details.filmingLocations?.total && (
|
||||
<p>
|
||||
<span>Filming locations: </span>
|
||||
{details.filmingLocations.locations.map((loc, i) => (
|
||||
<span key={loc}>
|
||||
{!!i && ', '}
|
||||
<Link href={`/search/title/?locations=${loc}`}>
|
||||
<a className='link'>{loc}</a>
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
{!!details.production?.total && (
|
||||
<p>
|
||||
<span>Production companies: </span>
|
||||
{details.production.companies.map((co, i) => (
|
||||
<span key={co.id}>
|
||||
{!!i && ', '}
|
||||
<Link href={`/company/${co.id}`}>
|
||||
<a className='link'>{co.name}</a>
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
{!!Object.keys(boxOffice).length && (
|
||||
<section className={styles.boxoffice}>
|
||||
<h2 className='heading heading__secondary'>Box office</h2>
|
||||
<div className={styles.boxoffice__container}>
|
||||
{boxOffice.budget && (
|
||||
<p>
|
||||
<span>Budget: </span>
|
||||
<span>
|
||||
{formatMoney(
|
||||
boxOffice.budget.amount,
|
||||
boxOffice.budget.currency
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
{boxOffice.grossUs && (
|
||||
<p>
|
||||
<span>Gross US & Canada: </span>
|
||||
<span>
|
||||
{formatMoney(
|
||||
boxOffice.grossUs.amount,
|
||||
boxOffice.grossUs.currency
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
{boxOffice.openingGrossUs && (
|
||||
<p>
|
||||
<span>Opening weekend US & Canada: </span>
|
||||
<span>
|
||||
{formatMoney(
|
||||
boxOffice.openingGrossUs.amount,
|
||||
boxOffice.openingGrossUs.currency
|
||||
)}
|
||||
<span> ({boxOffice.openingGrossUs.date})</span>
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
{boxOffice.gross && (
|
||||
<p>
|
||||
<span>Gross worldwide: </span>
|
||||
<span>
|
||||
{formatMoney(
|
||||
boxOffice.gross.amount,
|
||||
boxOffice.gross.currency
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
{!!Object.keys(technicalSpecs).length && (
|
||||
<section className={styles.technical}>
|
||||
<h2 className='heading heading__secondary'>Technical specs</h2>
|
||||
<div className={styles.technical__container}>
|
||||
{technicalSpecs.runtime && (
|
||||
<p>
|
||||
<span>Runtime: </span>
|
||||
<span>{formatTime(technicalSpecs.runtime)}</span>
|
||||
</p>
|
||||
)}
|
||||
{!!technicalSpecs.colorations?.length && (
|
||||
<p>
|
||||
<span> Color: </span>
|
||||
<span>
|
||||
{technicalSpecs.colorations.map((color, i) => (
|
||||
<span key={color.id}>
|
||||
{!!i && ', '}
|
||||
<Link href={`/search/title/?colors=${color.id}`}>
|
||||
<a className='link'>{color.name}</a>
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
{!!technicalSpecs.soundMixes?.length && (
|
||||
<p>
|
||||
<span>Sound mix: </span>
|
||||
<span>
|
||||
{technicalSpecs.soundMixes?.map((sound, i) => (
|
||||
<span key={sound.id}>
|
||||
{!!i && ', '}
|
||||
<Link href={`/search/title/?sound_mixes=${sound.id}`}>
|
||||
<a className='link'>{sound.name}</a>
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
{!!technicalSpecs.aspectRatios?.length && (
|
||||
<p>
|
||||
<span>Aspect ratio: </span>
|
||||
<span>{technicalSpecs.aspectRatios.join(', ')}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Info;
|
90
src/components/title/Media.tsx
Normal file
90
src/components/title/Media.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
import { NextRouter } from 'next/router';
|
||||
import { Media } from '../../interfaces/shared/title';
|
||||
import { modifyIMDbImg } from '../../utils/helpers';
|
||||
|
||||
import styles from '../../styles/modules/components/title/media.module.scss';
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
media: Media;
|
||||
router: NextRouter;
|
||||
};
|
||||
|
||||
const Media = ({ className, media, router }: Props) => {
|
||||
return (
|
||||
<div className={`${className} ${styles.media}`}>
|
||||
{(media.trailer || !!media.videos.total) && (
|
||||
<section className={styles.videos}>
|
||||
<h2 className='heading heading__secondary'>Videos</h2>
|
||||
|
||||
<div className={styles.videos__container}>
|
||||
{media.trailer && (
|
||||
<div key={router.asPath} className={styles.trailer}>
|
||||
<video
|
||||
aria-label='trailer video'
|
||||
// it's a relatively new tag. hence jsx-all1 complains
|
||||
aria-description={media.trailer.caption}
|
||||
controls
|
||||
playsInline
|
||||
poster={modifyIMDbImg(media.trailer.thumbnail)}
|
||||
className={styles.trailer__video}
|
||||
>
|
||||
{media.trailer.urls.map(source => (
|
||||
<source
|
||||
key={source.url}
|
||||
type={source.mimeType}
|
||||
src={source.url}
|
||||
data-res={source.resolution}
|
||||
/>
|
||||
))}
|
||||
</video>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!!media.videos.total &&
|
||||
media.videos.videos.map(video => (
|
||||
<Link href={`/video/${video.id}`} key={video.id}>
|
||||
<a className={styles.video}>
|
||||
<Image
|
||||
className={styles.video__img}
|
||||
src={modifyIMDbImg(video.thumbnail)}
|
||||
alt=''
|
||||
fill
|
||||
sizes='400px'
|
||||
/>
|
||||
<p className={styles.video__caption}>
|
||||
{video.caption} ({video.runtime}s)
|
||||
</p>
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
{!!media.images.total && (
|
||||
<section className={styles.images}>
|
||||
<h2 className='heading heading__secondary'>Images</h2>
|
||||
<div className={styles.images__container}>
|
||||
{media.images.images.map(image => (
|
||||
<figure key={image.id} className={styles.image}>
|
||||
<Image
|
||||
className={styles.image__img}
|
||||
src={modifyIMDbImg(image.url)}
|
||||
alt=''
|
||||
fill
|
||||
sizes='400px'
|
||||
/>
|
||||
<figcaption className={styles.image__caption}>
|
||||
{image.caption.plainText}
|
||||
</figcaption>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Media;
|
64
src/components/title/MoreLikeThis.tsx
Normal file
64
src/components/title/MoreLikeThis.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
import { MoreLikeThis } from '../../interfaces/shared/title';
|
||||
import { formatNumber, modifyIMDbImg } from '../../utils/helpers';
|
||||
import styles from '../../styles/modules/components/title/more-like-this.module.scss';
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
data: MoreLikeThis;
|
||||
};
|
||||
|
||||
const MoreLikeThis = ({ className, data }: Props) => {
|
||||
if (!data.length) return <></>;
|
||||
|
||||
return (
|
||||
<section className={`${className} ${styles.morelikethis}`}>
|
||||
<h2 className='heading heading__secondary'>More like this</h2>
|
||||
<ul className={styles.container}>
|
||||
{data.map(title => (
|
||||
<li key={title.id}>
|
||||
<Link href={`/title/${title.id}`}>
|
||||
<a className={styles.item}>
|
||||
<div className={styles.item__imgContainer}>
|
||||
{title.poster ? (
|
||||
<Image
|
||||
src={modifyIMDbImg(title.poster.url, 400)}
|
||||
alt=''
|
||||
fill
|
||||
className={styles.item__img}
|
||||
sizes='200px'
|
||||
/>
|
||||
) : (
|
||||
<svg className={styles.item__imgNA}>
|
||||
<use href='/svg/sprite.svg#icon-image-slash' />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.item__textContainer}>
|
||||
<h3 className={`heading ${styles.item__heading}`}>
|
||||
{title.title}
|
||||
</h3>
|
||||
{title.ratings.avg && (
|
||||
<p className={styles.item__rating}>
|
||||
<span className={styles.item__ratingNum}>
|
||||
{title.ratings.avg}
|
||||
</span>
|
||||
<svg className={styles.item__ratingIcon}>
|
||||
<use href='/svg/sprite.svg#icon-rating'></use>
|
||||
</svg>
|
||||
<span>
|
||||
({formatNumber(title.ratings.numVotes)} votes)
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
export default MoreLikeThis;
|
82
src/components/title/Reviews.tsx
Normal file
82
src/components/title/Reviews.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import { NextRouter } from 'next/router';
|
||||
import Link from 'next/link';
|
||||
import { Reviews } from '../../interfaces/shared/title';
|
||||
import { formatNumber } from '../../utils/helpers';
|
||||
import styles from '../../styles/modules/components/title/reviews.module.scss';
|
||||
|
||||
type Props = {
|
||||
reviews: Reviews;
|
||||
router: NextRouter;
|
||||
};
|
||||
|
||||
const Reviews = ({ reviews, router }: Props) => {
|
||||
const { titleId } = router.query;
|
||||
|
||||
return (
|
||||
<section className={styles.reviews}>
|
||||
<h2 className='heading heading__secondary'>Reviews</h2>
|
||||
|
||||
{reviews.featuredReview && (
|
||||
<article className={styles.reviews__reviewContainer}>
|
||||
<details className={styles.review}>
|
||||
<summary className={styles.review__summary}>
|
||||
<strong>{reviews.featuredReview.review.summary}</strong>
|
||||
</summary>
|
||||
<div
|
||||
className={styles.review__text}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: reviews.featuredReview.review.html,
|
||||
}}
|
||||
></div>
|
||||
</details>
|
||||
<footer className={styles.review__metadata}>
|
||||
<p>
|
||||
{reviews.featuredReview.rating && (
|
||||
<span>Rated {reviews.featuredReview.rating}/10</span>
|
||||
)}
|
||||
<span>
|
||||
{' '}
|
||||
by{' '}
|
||||
<Link href={`/user/${reviews.featuredReview.reviewer.id}`}>
|
||||
<a className='link'>{reviews.featuredReview.reviewer.name}</a>
|
||||
</Link>
|
||||
</span>
|
||||
<span> on {reviews.featuredReview.date}.</span>
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
{formatNumber(reviews.featuredReview.votes.up)} upvotes
|
||||
</span>
|
||||
<span>
|
||||
, {formatNumber(reviews.featuredReview.votes.down)} downvotes
|
||||
</span>
|
||||
</p>
|
||||
</footer>
|
||||
</article>
|
||||
)}
|
||||
|
||||
<div className={styles.reviews__stats}>
|
||||
<p>
|
||||
<Link href={`/title/${titleId}/reviews`}>
|
||||
<a className='link'>
|
||||
{formatNumber(reviews.numUserReviews)} User reviews
|
||||
</a>
|
||||
</Link>
|
||||
</p>
|
||||
<p>
|
||||
<Link href={`/title/${titleId}/externalreviews`}>
|
||||
<a className='link'>
|
||||
{formatNumber(reviews.numCriticReviews)} Critic reviews
|
||||
</a>
|
||||
</Link>
|
||||
</p>
|
||||
<p>
|
||||
<Link href={`/title/${titleId}/criticreviews`}>
|
||||
<a className='link'> {reviews.metacriticScore} Metascore</a>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
export default Reviews;
|
Reference in New Issue
Block a user