refactor: make components more modular
would help in implementing name route also did some stylistic changes
This commit is contained in:
parent
8ce02d0236
commit
18ca98fd4a
43 changed files with 757 additions and 796 deletions
|
@ -4,5 +4,6 @@
|
|||
"arrowParens": "avoid",
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
|
|
29
src/components/card/Card.tsx
Normal file
29
src/components/card/Card.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import type { ReactNode, ElementType, ComponentPropsWithoutRef } from 'react';
|
||||
import styles from 'src/styles/modules/components/card/card.module.scss';
|
||||
|
||||
// ensuring that other attributes to <Card/> are correct based on the value of 'as' prop.
|
||||
// a cheap implementation of as prop found in libraries like CharkaUI or MaterialUI.
|
||||
type Props<T extends ElementType> = {
|
||||
children: ReactNode;
|
||||
as?: T | 'section';
|
||||
hoverable?: true;
|
||||
} & ComponentPropsWithoutRef<T>;
|
||||
|
||||
const Card = <T extends ElementType = 'li'>({
|
||||
children,
|
||||
as,
|
||||
hoverable,
|
||||
className,
|
||||
...rest
|
||||
}: Props<T>) => {
|
||||
const Component = as ?? 'li';
|
||||
const classNames = `${hoverable ? styles.hoverable : ''} ${styles.card} ${className}`;
|
||||
|
||||
return (
|
||||
<Component className={classNames} {...rest}>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
||||
export default Card;
|
45
src/components/card/CardBasic.tsx
Normal file
45
src/components/card/CardBasic.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { ComponentPropsWithoutRef, CSSProperties, ReactNode } from 'react';
|
||||
import Image from 'next/future/image';
|
||||
import Card from './Card';
|
||||
import { getProxiedIMDbImgUrl, modifyIMDbImg } from 'src/utils/helpers';
|
||||
import styles from 'src/styles/modules/components/card/card-basic.module.scss';
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
image?: string;
|
||||
title: string;
|
||||
} & ComponentPropsWithoutRef<'section'>;
|
||||
|
||||
const CardBasic = ({ image, children, className, title, ...rest }: Props) => {
|
||||
const style: CSSProperties = {
|
||||
backgroundImage: image && `url(${getProxiedIMDbImgUrl(modifyIMDbImg(image, 300))})`,
|
||||
};
|
||||
|
||||
return (
|
||||
<Card as='section' className={`${styles.container} ${className}`} {...rest}>
|
||||
<div className={styles.imageContainer} style={style}>
|
||||
{image ? (
|
||||
<Image
|
||||
className={styles.image}
|
||||
src={modifyIMDbImg(image)}
|
||||
alt=''
|
||||
priority
|
||||
fill
|
||||
sizes='300px'
|
||||
/>
|
||||
) : (
|
||||
<svg className={styles.imageNA}>
|
||||
<use href='/svg/sprite.svg#icon-image-slash' />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<h1 className={`${styles.title} heading heading__primary`}>{title}</h1>
|
||||
{children}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardBasic;
|
51
src/components/card/CardCast.tsx
Normal file
51
src/components/card/CardCast.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import Card from './Card';
|
||||
import styles from 'src/styles/modules/components/card/card-cast.module.scss';
|
||||
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/future/image';
|
||||
import { modifyIMDbImg } from 'src/utils/helpers';
|
||||
|
||||
type Props = {
|
||||
link: string;
|
||||
name: string;
|
||||
characters: string[] | null;
|
||||
attributes: string[] | null;
|
||||
image?: string | null;
|
||||
children?: ReactNode;
|
||||
} & ComponentPropsWithoutRef<'li'>;
|
||||
|
||||
const CardCast = ({ link, name, image, children, characters, attributes, ...rest }: Props) => {
|
||||
return (
|
||||
<Card hoverable {...rest}>
|
||||
<Link href={link}>
|
||||
<a className={styles.item}>
|
||||
<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.textContainer}>
|
||||
<p className={`heading ${styles.name}`}>{name}</p>
|
||||
<p className={styles.role}>
|
||||
{characters?.join(', ')}
|
||||
{attributes && <span> ({attributes.join(', ')})</span>}
|
||||
</p>
|
||||
{children}
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardCast;
|
42
src/components/card/CardResult.tsx
Normal file
42
src/components/card/CardResult.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/future/image';
|
||||
import Card from './Card';
|
||||
import { modifyIMDbImg } from 'src/utils/helpers';
|
||||
import styles from 'src/styles/modules/components/card/card-result.module.scss';
|
||||
|
||||
type Props = {
|
||||
link: string;
|
||||
name: string;
|
||||
image?: string;
|
||||
showImage?: true;
|
||||
children?: ReactNode;
|
||||
} & ComponentPropsWithoutRef<'li'>;
|
||||
|
||||
const CardResult = ({ link, name, image, showImage, children, ...rest }: Props) => {
|
||||
let ImageComponent = null;
|
||||
if (showImage)
|
||||
ImageComponent = 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>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card hoverable {...rest}>
|
||||
<Link href={link}>
|
||||
<a className={`${styles.item} ${!showImage && styles.sansImage}`}>
|
||||
<div className={styles.imgContainer}>{ImageComponent}</div>
|
||||
<div className={styles.info}>
|
||||
<p className={`heading ${styles.heading}`}>{name}</p>
|
||||
{children}
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardResult;
|
63
src/components/card/CardTitle.tsx
Normal file
63
src/components/card/CardTitle.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
import Card from './Card';
|
||||
import styles from 'src/styles/modules/components/card/card-title.module.scss';
|
||||
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/future/image';
|
||||
import { formatNumber, modifyIMDbImg } from 'src/utils/helpers';
|
||||
|
||||
type Props = {
|
||||
link: string;
|
||||
name: string;
|
||||
titleType: string;
|
||||
year?: { start: number; end: number | null };
|
||||
ratings?: { avg: number | null; numVotes: number };
|
||||
image?: string;
|
||||
children?: ReactNode;
|
||||
} & ComponentPropsWithoutRef<'li'>;
|
||||
|
||||
const CardTitle = ({ link, name, year, image, ratings, titleType, children, ...rest }: Props) => {
|
||||
const years = year?.end ? `${year.start}-${year.end}` : year?.start;
|
||||
|
||||
return (
|
||||
<Card hoverable {...rest}>
|
||||
<Link href={link}>
|
||||
<a className={styles.item}>
|
||||
<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.textContainer}>
|
||||
<p className={`heading ${styles.name}`}>{name}</p>
|
||||
<p>
|
||||
<span>{titleType}</span>
|
||||
<span>{years && ` (${years})`}</span>
|
||||
</p>
|
||||
{ratings?.avg && (
|
||||
<p className={styles.rating}>
|
||||
<span className={styles.ratingNum}>{ratings.avg}</span>
|
||||
<svg className={styles.ratingIcon}>
|
||||
<use href='/svg/sprite.svg#icon-rating'></use>
|
||||
</svg>
|
||||
<span> ({formatNumber(ratings.numVotes)} votes)</span>
|
||||
</p>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardTitle;
|
5
src/components/card/index.tsx
Normal file
5
src/components/card/index.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
export { default as Card } from './Card';
|
||||
export { default as CardTitle } from './CardTitle';
|
||||
export { default as CardBasic } from './CardBasic';
|
||||
export { default as CardCast } from './CardCast';
|
||||
export { default as CardResult } from './CardResult';
|
|
@ -33,15 +33,12 @@ const ErrorInfo = ({ message, statusCode, misc }: Props) => {
|
|||
>
|
||||
<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.
|
||||
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}`}>
|
||||
{title}
|
||||
</h1>
|
||||
<h1 className={`heading heading__primary ${styles.heading}`}>{title}</h1>
|
||||
{misc ? (
|
||||
<>
|
||||
<p>{misc.subtext}</p>
|
||||
|
@ -52,7 +49,7 @@ const ErrorInfo = ({ message, statusCode, misc }: Props) => {
|
|||
) : (
|
||||
<p>
|
||||
Go back to{' '}
|
||||
<Link href='/about'>
|
||||
<Link href='/'>
|
||||
<a className='link'>the homepage</a>
|
||||
</Link>
|
||||
.
|
||||
|
|
|
@ -1,22 +1,13 @@
|
|||
import { CardResult } from 'src/components/card';
|
||||
import { Companies } from 'src/interfaces/shared/search';
|
||||
import Link from 'next/link';
|
||||
|
||||
import styles from 'src/styles/modules/components/find/company.module.scss';
|
||||
type Props = { company: Companies[number] };
|
||||
|
||||
type Props = {
|
||||
company: Companies[0];
|
||||
};
|
||||
|
||||
const Company = ({ company }: Props) => {
|
||||
return (
|
||||
<li className={styles.company}>
|
||||
<Link href={`name/${company.id}`}>
|
||||
<a className={`heading ${styles.heading}`}>{company.name}</a>
|
||||
</Link>
|
||||
const Company = ({ company }: Props) => (
|
||||
<CardResult name={company.name} link={`/search/title?companies=${company.id}`}>
|
||||
{company.country && <p>{company.country}</p>}
|
||||
{!!company.type && <p>{company.type}</p>}
|
||||
</li>
|
||||
</CardResult>
|
||||
);
|
||||
};
|
||||
|
||||
export default Company;
|
||||
|
|
|
@ -1,20 +1,12 @@
|
|||
import Link from 'next/link';
|
||||
import { CardResult } from 'src/components/card';
|
||||
import { Keywords } from 'src/interfaces/shared/search';
|
||||
import styles from 'src/styles/modules/components/find/keyword.module.scss';
|
||||
|
||||
type Props = {
|
||||
keyword: Keywords[0];
|
||||
};
|
||||
type Props = { keyword: Keywords[number] };
|
||||
|
||||
const Keyword = ({ keyword }: Props) => {
|
||||
return (
|
||||
<li className={styles.keyword}>
|
||||
<Link href={`name/${keyword.id}`}>
|
||||
<a className={`heading ${styles.heading}`}>{keyword.text}</a>
|
||||
</Link>
|
||||
const Keyword = ({ keyword }: Props) => (
|
||||
<CardResult link={`/search/keyword?keywords=${keyword.text}`} name={keyword.text}>
|
||||
{keyword.numTitles && <p>{keyword.numTitles} titles</p>}
|
||||
</li>
|
||||
</CardResult>
|
||||
);
|
||||
};
|
||||
|
||||
export default Keyword;
|
||||
|
|
|
@ -1,44 +1,19 @@
|
|||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
import { CardResult } from 'src/components/card';
|
||||
import { People } from 'src/interfaces/shared/search';
|
||||
import { modifyIMDbImg } from 'src/utils/helpers';
|
||||
import styles from 'src/styles/modules/components/find/person.module.scss';
|
||||
|
||||
type Props = {
|
||||
person: People[0];
|
||||
};
|
||||
type Props = { person: People[number] };
|
||||
|
||||
const Person = ({ person }: Props) => {
|
||||
return (
|
||||
<li className={styles.person}>
|
||||
<div className={styles.imgContainer} style={{ position: 'relative' }}>
|
||||
{person.image ? (
|
||||
<Image
|
||||
src={modifyIMDbImg(person.image.url, 400)}
|
||||
alt={person.image.caption}
|
||||
fill
|
||||
className={styles.img}
|
||||
/>
|
||||
) : (
|
||||
<svg className={styles.imgNA}>
|
||||
<use href='/svg/sprite.svg#icon-image-slash' />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<Link href={`name/${person.id}`}>
|
||||
<a className={`heading ${styles.heading}`}>{person.name}</a>
|
||||
</Link>
|
||||
{person.aka && <p>{person.aka}</p>}
|
||||
{person.jobCateogry && <p>{person.jobCateogry}</p>}
|
||||
{(person.knownForTitle || person.knownInYear) && (
|
||||
<CardResult showImage name={person.name} link={`/name/${person.id}`} image={person.image?.url}>
|
||||
<p>{person.aka}</p>
|
||||
<p>{person.jobCateogry}</p>
|
||||
<ul className={styles.basicInfo} aria-label='quick facts'>
|
||||
{person.knownForTitle && <li>{person.knownForTitle}</li>}
|
||||
{person.knownInYear && <li>{person.knownInYear}</li>}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
</CardResult>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,38 +1,17 @@
|
|||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
import { CardResult } from 'src/components/card';
|
||||
import { Titles } from 'src/interfaces/shared/search';
|
||||
import { modifyIMDbImg } from 'src/utils/helpers';
|
||||
import styles from 'src/styles/modules/components/find/title.module.scss';
|
||||
|
||||
type Props = {
|
||||
title: Titles[0];
|
||||
};
|
||||
type Props = { title: Titles[number] };
|
||||
|
||||
const Title = ({ title }: Props) => {
|
||||
return (
|
||||
<li className={styles.title}>
|
||||
<div className={styles.imgContainer}>
|
||||
{title.image ? (
|
||||
<Image
|
||||
src={modifyIMDbImg(title.image.url, 400)}
|
||||
alt={title.image.caption}
|
||||
fill
|
||||
className={styles.img}
|
||||
/>
|
||||
) : (
|
||||
<svg className={styles.imgNA}>
|
||||
<use href='/svg/sprite.svg#icon-image-slash' />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<Link href={`/title/${title.id}`}>
|
||||
<a className={`heading ${styles.heading}`}>{title.name}</a>
|
||||
</Link>
|
||||
<CardResult showImage name={title.name} link={`/title/${title.id}`} image={title.image?.url}>
|
||||
<ul aria-label='quick facts' className={styles.basicInfo}>
|
||||
{title.type && <li>{title.type}</li>}
|
||||
{title.sAndE && <li>{title.sAndE}</li>}
|
||||
{title.releaseYear && <li>{title.releaseYear}</li>}
|
||||
<li>{title.type}</li>
|
||||
<li>{title.sAndE}</li>
|
||||
<li>{title.releaseYear}</li>
|
||||
</ul>
|
||||
{!!title.credits.length && (
|
||||
<p className={styles.stars}>
|
||||
|
@ -51,8 +30,7 @@ const Title = ({ title }: Props) => {
|
|||
{title.seriesReleaseYear && <li>{title.seriesReleaseYear}</li>}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
</CardResult>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -12,18 +12,16 @@ type Props = {
|
|||
title: string;
|
||||
};
|
||||
|
||||
const resultsExist = (results: Props['results']) => {
|
||||
if (
|
||||
!results ||
|
||||
(!results.people.length &&
|
||||
!results.keywords.length &&
|
||||
!results.companies.length &&
|
||||
!results.titles.length)
|
||||
)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
const resultsExist = (
|
||||
results: Props['results']
|
||||
): results is NonNullable<Props['results']> =>
|
||||
Boolean(
|
||||
results &&
|
||||
(results.people.length ||
|
||||
results.keywords.length ||
|
||||
results.companies.length ||
|
||||
results.titles.length)
|
||||
);
|
||||
|
||||
// MAIN COMPONENT
|
||||
const Results = ({ results, className, title }: Props) => {
|
||||
|
@ -34,7 +32,7 @@ const Results = ({ results, className, title }: Props) => {
|
|||
</h1>
|
||||
);
|
||||
|
||||
const { titles, people, keywords, companies, meta } = results!;
|
||||
const { titles, people, keywords, companies, meta } = results;
|
||||
const titlesSectionHeading = getResTitleTypeHeading(
|
||||
meta.type,
|
||||
meta.titleType
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
import { Media } from 'src/interfaces/shared/title';
|
||||
import { Media } from 'src/interfaces/shared';
|
||||
import { getProxiedIMDbImgUrl, modifyIMDbImg } from 'src/utils/helpers';
|
||||
import styles from 'src/styles/modules/components/title/media.module.scss';
|
||||
import styles from 'src/styles/modules/components/media/media.module.scss';
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
media: Media;
|
||||
};
|
||||
|
||||
// TODO: refactor this component.
|
||||
|
||||
const Media = ({ className, media }: Props) => {
|
||||
return (
|
||||
<div className={`${className} ${styles.media}`}>
|
||||
|
@ -21,13 +23,9 @@ const Media = ({ className, media }: Props) => {
|
|||
<div 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={getProxiedIMDbImgUrl(
|
||||
modifyIMDbImg(media.trailer.thumbnail)
|
||||
)}
|
||||
poster={getProxiedIMDbImgUrl(modifyIMDbImg(media.trailer.thumbnail))}
|
||||
className={styles.trailer__video}
|
||||
preload='none'
|
||||
>
|
||||
|
@ -76,9 +74,7 @@ const Media = ({ className, media }: Props) => {
|
|||
fill
|
||||
sizes='400px'
|
||||
/>
|
||||
<figcaption className={styles.image__caption}>
|
||||
{image.caption.plainText}
|
||||
</figcaption>
|
||||
<figcaption className={styles.image__caption}>{image.caption.plainText}</figcaption>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
|
@ -1,4 +1,5 @@
|
|||
import Head from 'next/head';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
|
@ -6,11 +7,15 @@ type Props = {
|
|||
imgUrl?: string;
|
||||
};
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_URL ?? 'https://iket.me';
|
||||
|
||||
const Meta = ({
|
||||
title,
|
||||
description = 'libremdb, a free & open source IMDb front-end.',
|
||||
imgUrl = 'icon.svg',
|
||||
}: Props) => {
|
||||
const url = new URL(imgUrl, BASE_URL);
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<meta charSet='UTF-8' />
|
||||
|
@ -30,10 +35,7 @@ const Meta = ({
|
|||
<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}`}
|
||||
/>
|
||||
<meta property='og:image' content={url.toString()} />
|
||||
</Head>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,13 +1,8 @@
|
|||
import { Fragment } from 'react';
|
||||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
import { CardBasic } from 'src/components/card';
|
||||
import { Basic } from 'src/interfaces/shared/title';
|
||||
import {
|
||||
formatNumber,
|
||||
formatTime,
|
||||
getProxiedIMDbImgUrl,
|
||||
modifyIMDbImg,
|
||||
} from 'src/utils/helpers';
|
||||
import { formatNumber, formatTime } from 'src/utils/helpers';
|
||||
import styles from 'src/styles/modules/components/title/basic.module.scss';
|
||||
|
||||
type Props = {
|
||||
|
@ -23,53 +18,19 @@ const Basic = ({ data, className }: Props) => {
|
|||
: 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})` }}
|
||||
<CardBasic
|
||||
className={`${styles.container} ${className}`}
|
||||
image={data.poster?.url}
|
||||
title={data.title}
|
||||
>
|
||||
<div
|
||||
className={styles.imageContainer}
|
||||
style={{
|
||||
backgroundImage:
|
||||
data.poster &&
|
||||
`url(${getProxiedIMDbImgUrl(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 && 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>
|
||||
)}
|
||||
{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 && (
|
||||
|
@ -82,9 +43,7 @@ const Basic = ({ data, className }: Props) => {
|
|||
<span className={styles.rating__text}> Avg. rating</span>
|
||||
</p>
|
||||
<p className={styles.rating}>
|
||||
<span className={styles.rating__num}>
|
||||
{formatNumber(data.ratings.numVotes)}
|
||||
</span>
|
||||
<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>
|
||||
|
@ -94,9 +53,7 @@ const Basic = ({ data, className }: Props) => {
|
|||
)}
|
||||
{data.ranking && (
|
||||
<p className={styles.rating}>
|
||||
<span className={styles.rating__num}>
|
||||
{formatNumber(data.ranking.position)}
|
||||
</span>
|
||||
<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>
|
||||
|
@ -129,17 +86,13 @@ const Basic = ({ data, className }: Props) => {
|
|||
))}
|
||||
</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>
|
||||
<span className={styles.crewType__heading}>{`${crewType.type.category}: `}</span>
|
||||
{crewType.crew.map((crew, i) => (
|
||||
<Fragment key={crew.id}>
|
||||
{i > 0 && ', '}
|
||||
|
@ -150,8 +103,7 @@ const Basic = ({ data, className }: Props) => {
|
|||
))}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</CardBasic>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
import { CardCast } from 'src/components/card';
|
||||
import { Cast } from 'src/interfaces/shared/title';
|
||||
import { modifyIMDbImg } from 'src/utils/helpers';
|
||||
import styles from 'src/styles/modules/components/title/cast.module.scss';
|
||||
|
||||
type Props = {
|
||||
|
@ -10,46 +8,25 @@ type Props = {
|
|||
};
|
||||
|
||||
const Cast = ({ className, cast }: Props) => {
|
||||
if (!cast.length) return <></>;
|
||||
if (!cast.length) return null;
|
||||
|
||||
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'
|
||||
<CardCast
|
||||
key={member.id}
|
||||
link={`/name/${member.id}`}
|
||||
name={member.name}
|
||||
image={member.image}
|
||||
characters={member.characters}
|
||||
attributes={member.attributes}
|
||||
/>
|
||||
) : (
|
||||
<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;
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
import { CardTitle } from 'src/components/card';
|
||||
import { MoreLikeThis } from 'src/interfaces/shared/title';
|
||||
import { formatNumber, modifyIMDbImg } from 'src/utils/helpers';
|
||||
import styles from 'src/styles/modules/components/title/more-like-this.module.scss';
|
||||
|
||||
type Props = {
|
||||
|
@ -10,52 +8,22 @@ type Props = {
|
|||
};
|
||||
|
||||
const MoreLikeThis = ({ className, data }: Props) => {
|
||||
if (!data.length) return <></>;
|
||||
if (!data.length) return null;
|
||||
|
||||
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'
|
||||
<CardTitle
|
||||
key={title.id}
|
||||
link={`/title/${title.id}`}
|
||||
name={title.title}
|
||||
titleType={title.type.text}
|
||||
image={title.poster?.url}
|
||||
year={title.releaseYear}
|
||||
ratings={title.ratings}
|
||||
/>
|
||||
) : (
|
||||
<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>
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import Basic from './Basic';
|
||||
import Cast from './Cast';
|
||||
import DidYouKnow from './DidYouKnow';
|
||||
import Info from './Info';
|
||||
import Media from './Media';
|
||||
import MoreLikeThis from './MoreLikeThis';
|
||||
import Reviews from './Reviews';
|
||||
|
||||
export { Basic, Cast, DidYouKnow, Info, Media, MoreLikeThis, Reviews };
|
||||
export { default as Basic } from './Basic';
|
||||
export { default as Cast } from './Cast';
|
||||
export { default as DidYouKnow } from './DidYouKnow';
|
||||
export { default as Info } from './Info';
|
||||
export { default as MoreLikeThis } from './MoreLikeThis';
|
||||
export { default as Reviews } from './Reviews';
|
||||
|
|
|
@ -5,9 +5,9 @@ const getInitialTheme = () => {
|
|||
// for server-side rendering, as window isn't availabe there
|
||||
if (typeof window === 'undefined') return 'light';
|
||||
|
||||
const userPrefersTheme = isLocalStorageAvailable()
|
||||
? window.localStorage.getItem('theme')
|
||||
: null;
|
||||
const userPrefersTheme = (
|
||||
isLocalStorageAvailable() ? window.localStorage.getItem('theme') : null
|
||||
) as 'light' | 'dark' | null;
|
||||
const browserPrefersDarkTheme = window.matchMedia(
|
||||
'(prefers-color-scheme: dark)'
|
||||
).matches;
|
||||
|
@ -28,7 +28,7 @@ const updateMetaTheme = () => {
|
|||
|
||||
const initialContext = {
|
||||
theme: '',
|
||||
setTheme: (theme: string) => {},
|
||||
setTheme: (theme: ReturnType<typeof getInitialTheme>) => { },
|
||||
};
|
||||
|
||||
export const themeContext = createContext(initialContext);
|
||||
|
@ -36,7 +36,7 @@ export const themeContext = createContext(initialContext);
|
|||
const ThemeProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [curTheme, setCurTheme] = useState(getInitialTheme);
|
||||
|
||||
const setTheme = (theme: string) => {
|
||||
const setTheme = (theme: typeof curTheme) => {
|
||||
setCurTheme(theme);
|
||||
if (isLocalStorageAvailable()) window.localStorage.setItem('theme', theme);
|
||||
document.documentElement.dataset.theme = theme;
|
||||
|
|
3
src/interfaces/shared/index.ts
Normal file
3
src/interfaces/shared/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import type Name from './name';
|
||||
|
||||
export type Media = Name['media']; // exactly the same in title and name
|
|
@ -1,8 +1,6 @@
|
|||
import cleanTitle from 'src/utils/cleaners/title';
|
||||
import title from 'src/utils/fetchers/title';
|
||||
|
||||
export type AxiosTitleRes = Awaited<ReturnType<typeof title>>;
|
||||
|
||||
// for full title
|
||||
type Title = ReturnType<typeof cleanTitle>;
|
||||
export type { Title as default };
|
||||
|
|
|
@ -2,35 +2,32 @@ import Link from 'next/link';
|
|||
import { useRouter } from 'next/router';
|
||||
import styles from '../styles/modules/layout/footer.module.scss';
|
||||
|
||||
const links = [
|
||||
{ path: '/about', text: 'About' },
|
||||
{ path: '/find', text: 'Find' },
|
||||
{ path: '/privacy', text: 'Privacy' },
|
||||
{ path: '/contact', text: 'Contact' },
|
||||
] as const;
|
||||
|
||||
const Footer = () => {
|
||||
const { pathname } = useRouter();
|
||||
const className = (link: string) =>
|
||||
pathname === link ? styles.nav__linkActive : styles.nav__link;
|
||||
|
||||
return (
|
||||
<footer id='footer' className={styles.footer}>
|
||||
<nav aria-label='primary navigation' className={styles.nav}>
|
||||
<ul className={styles.list}>
|
||||
<li className={styles.nav__item}>
|
||||
<Link href='/about'>
|
||||
<a className={className('/about')}>About</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.nav__item}>
|
||||
<Link href='/find'>
|
||||
<a className={className('/find')}>Search</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.nav__item}>
|
||||
<Link href='/privacy'>
|
||||
<a className={className('/privacy')}>Privacy</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.nav__item}>
|
||||
<Link href='/contact'>
|
||||
<a className={className('/contact')}>Contact</a>
|
||||
{links.map(link => (
|
||||
<li className={styles.nav__item} key={link.path}>
|
||||
<Link href={link.path}>
|
||||
<a
|
||||
className={styles.nav__link}
|
||||
aria-current={pathname === link.path ? 'page' : undefined}
|
||||
>
|
||||
{link.text}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
<li className={styles.nav__item}>
|
||||
<a href='#' className={styles.nav__link}>
|
||||
Back to top
|
||||
|
@ -39,7 +36,7 @@ const Footer = () => {
|
|||
</ul>
|
||||
</nav>
|
||||
<p className={styles.licence}>
|
||||
Licensed under
|
||||
Licensed under{' '}
|
||||
<a
|
||||
className={styles.nav__link}
|
||||
href='https://www.gnu.org/licenses/agpl-3.0-standalone.html'
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import Footer from './Footer';
|
||||
import Header from './Header';
|
||||
|
||||
type Props = {
|
||||
full?: boolean;
|
||||
children: React.ReactNode;
|
||||
full?: true;
|
||||
children: ReactNode;
|
||||
className: string;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,23 +1,20 @@
|
|||
// external
|
||||
import { GetServerSideProps, GetStaticProps, GetStaticPaths } from 'next';
|
||||
import Head from 'next/head';
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
|
||||
import Meta from 'src/components/meta/Meta';
|
||||
import Layout from 'src/layouts/Layout';
|
||||
import ErrorInfo from 'src/components/error/ErrorInfo';
|
||||
// prettier-ignore
|
||||
import { Basic, Cast, DidYouKnow, Info, Media, MoreLikeThis, Reviews } from 'src/components/title';
|
||||
import Media from 'src/components/media/Media';
|
||||
import { Basic, Cast, DidYouKnow, Info, MoreLikeThis, Reviews } from 'src/components/title';
|
||||
import Title from 'src/interfaces/shared/title';
|
||||
import { AppError } from 'src/interfaces/shared/error';
|
||||
import title from 'src/utils/fetchers/title';
|
||||
import { getProxiedIMDbImgUrl } from 'src/utils/helpers';
|
||||
import styles from 'src/styles/modules/pages/title/title.module.scss';
|
||||
|
||||
type Props = { data: Title; error: null } | { error: AppError; data: null };
|
||||
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} />;
|
||||
if (error) return <ErrorInfo message={error.message} statusCode={error.statusCode} />;
|
||||
|
||||
const info = {
|
||||
meta: data.meta,
|
||||
|
@ -31,21 +28,10 @@ const TitleInfo = ({ data, error }: Props) => {
|
|||
return (
|
||||
<>
|
||||
<Meta
|
||||
title={`${data.basic.title} (${
|
||||
data.basic.releaseYear?.start || data.basic.type.name
|
||||
})`}
|
||||
description={data.basic.plot || undefined}
|
||||
title={`${data.basic.title} (${data.basic.releaseYear?.start || data.basic.type.name})`}
|
||||
description={data.basic.plot ?? undefined}
|
||||
imgUrl={data.basic.poster?.url && getProxiedIMDbImgUrl(data.basic.poster.url)}
|
||||
/>
|
||||
<Head>
|
||||
<meta
|
||||
title='og:image'
|
||||
content={
|
||||
data.basic.poster?.url
|
||||
? getProxiedIMDbImgUrl(data.basic.poster?.url)
|
||||
: '/icon-512.png'
|
||||
}
|
||||
/>
|
||||
</Head>
|
||||
<Layout className={styles.title}>
|
||||
<Basic data={data.basic} className={styles.basic} />
|
||||
<Media className={styles.media} media={data.media} />
|
||||
|
@ -62,8 +48,11 @@ const TitleInfo = ({ data, error }: Props) => {
|
|||
};
|
||||
|
||||
// TO-DO: make a getServerSideProps wrapper for handling errors
|
||||
export const getServerSideProps: GetServerSideProps = async ctx => {
|
||||
const titleId = ctx.params!.titleId as string;
|
||||
type Data = { data: Title; error: null } | { error: AppError; data: null };
|
||||
type Params = { titleId: string };
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx => {
|
||||
const titleId = ctx.params!.titleId;
|
||||
|
||||
try {
|
||||
const data = await title(titleId);
|
||||
|
|
|
@ -21,8 +21,8 @@ $breakpoints: (
|
|||
);
|
||||
|
||||
// 1. colors
|
||||
$clr-primary: hsl(240, 31%, 25%);
|
||||
$clr-secondary: hsl(344, 79%, 40%);
|
||||
$clr-tertiary: hsl(176, 43%, 46%);
|
||||
$clr-quatenary: hsl(204, 4%, 23%);
|
||||
$clr-quintenary: hsl(0, 0%, 100%);
|
||||
// $clr-primary: hsl(240, 31%, 25%);
|
||||
// $clr-secondary: hsl(344, 79%, 40%);
|
||||
// $clr-tertiary: hsl(176, 43%, 46%);
|
||||
// $clr-quatenary: hsl(204, 4%, 23%);
|
||||
// $clr-quintenary: hsl(0, 0%, 100%);
|
||||
|
|
|
@ -22,17 +22,13 @@ $_light: (
|
|||
// 4.2 for borders, primarily
|
||||
fill-muted: hsl(0, 0%, 80%),
|
||||
// shadows on cards
|
||||
shadow: 0 0 1rem hsla(0, 0%, 0%, 0.2),
|
||||
shadow: 0 0 0.5em hsla(0, 0%, 0%, 0.2),
|
||||
// keyboard, focus hightlight
|
||||
highlight: hsl(176, 43%, 46%),
|
||||
// for gradient behind hero text on about page.
|
||||
gradient:
|
||||
(
|
||||
radial-gradient(
|
||||
at 23% 32%,
|
||||
hsla(344, 79%, 40%, 0.15) 0px,
|
||||
transparent 70%
|
||||
),
|
||||
radial-gradient(at 23% 32%, hsla(344, 79%, 40%, 0.15) 0px, transparent 70%),
|
||||
radial-gradient(at 72% 55%, hsla(344, 79%, 40%, 0.2) 0px, transparent 50%)
|
||||
),
|
||||
// changes color of native html elemnts, either 'light' or 'dark' must be set.
|
||||
|
|
97
src/styles/modules/components/card/card-basic.module.scss
Normal file
97
src/styles/modules/components/card/card-basic.module.scss
Normal file
|
@ -0,0 +1,97 @@
|
|||
@use '../../../abstracts' as helper;
|
||||
|
||||
.container {
|
||||
margin-inline: auto;
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: minmax(25rem, 30rem) 1fr;
|
||||
|
||||
@include helper.bp('bp-900') {
|
||||
grid-template-columns: none;
|
||||
grid-template-rows: 30rem min-content;
|
||||
}
|
||||
|
||||
@include helper.bp('bp-700') {
|
||||
grid-template-rows: 25rem min-content;
|
||||
}
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
display: flex; // for bringing out image__NA out of blur
|
||||
|
||||
position: relative;
|
||||
height: auto;
|
||||
width: auto;
|
||||
overflow: hidden;
|
||||
|
||||
background-size: cover;
|
||||
background-position: top;
|
||||
place-items: center;
|
||||
|
||||
@include helper.bp('bp-900') {
|
||||
padding: var(--spacer-2);
|
||||
isolation: isolate;
|
||||
|
||||
// for adding layer of color on top of background image
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
var(--clr-bg-accent) 10%,
|
||||
transparent
|
||||
);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
|
||||
@include helper.bp('bp-900') {
|
||||
z-index: 1;
|
||||
object-fit: contain;
|
||||
|
||||
outline: 3px solid var(--clr-fill);
|
||||
outline-offset: 5px;
|
||||
|
||||
max-height: 100%;
|
||||
margin: auto;
|
||||
|
||||
// overrriding nex/future/image defaults
|
||||
height: initial !important;
|
||||
width: initial !important;
|
||||
position: relative !important;
|
||||
}
|
||||
}
|
||||
|
||||
.imageNA {
|
||||
z-index: 1;
|
||||
fill: var(--clr-fill-muted);
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: var(--spacer-2) var(--spacer-4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacer-2);
|
||||
|
||||
@include helper.bp('bp-900') {
|
||||
// text-align: center;
|
||||
// align-items: center;
|
||||
}
|
||||
@include helper.bp('bp-450') {
|
||||
gap: var(--spacer-1);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
line-height: 1;
|
||||
|
||||
@include helper.bp('bp-900') {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
44
src/styles/modules/components/card/card-cast.module.scss
Normal file
44
src/styles/modules/components/card/card-cast.module.scss
Normal file
|
@ -0,0 +1,44 @@
|
|||
.item {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: minmax(auto, 65%) auto;
|
||||
|
||||
text-decoration: none;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.imgContainer {
|
||||
justify-self: stretch;
|
||||
position: relative;
|
||||
|
||||
// for icon when image is unavailable
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.img {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.imgNA {
|
||||
fill: var(--clr-fill-muted);
|
||||
height: 40%;
|
||||
}
|
||||
|
||||
.textContainer {
|
||||
display: grid;
|
||||
gap: var(--spacer-1);
|
||||
padding: var(--spacer-1);
|
||||
text-align: center;
|
||||
justify-items: center;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.role {
|
||||
font-size: .95em;
|
||||
}
|
62
src/styles/modules/components/card/card-result.module.scss
Normal file
62
src/styles/modules/components/card/card-result.module.scss
Normal file
|
@ -0,0 +1,62 @@
|
|||
@use '../../../abstracts' as helper;
|
||||
|
||||
.item {
|
||||
--width: 10rem;
|
||||
--height: var(--width);
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: var(--width) auto;
|
||||
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
|
||||
@include helper.bp('bp-450') {
|
||||
--height: 15rem;
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.sansImage {
|
||||
grid-template-columns: auto;
|
||||
padding: var(--spacer-1);
|
||||
|
||||
.imgContainer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.imgContainer {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: var(--height);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.img {
|
||||
object-fit: cover;
|
||||
object-position: center 25%; // most of the time, person's face is visible at 1/4 of height in a potrait image.
|
||||
}
|
||||
|
||||
.imgNA {
|
||||
width: 80%;
|
||||
fill: var(--clr-fill-muted);
|
||||
}
|
||||
|
||||
.info {
|
||||
display: grid;
|
||||
padding: var(--spacer-3);
|
||||
gap: var(--spacer-0);
|
||||
|
||||
@include helper.bp('bp-450') {
|
||||
padding: var(--spacer-1);
|
||||
}
|
||||
|
||||
& :empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: var(--fs-4);
|
||||
text-decoration: none;
|
||||
}
|
56
src/styles/modules/components/card/card-title.module.scss
Normal file
56
src/styles/modules/components/card/card-title.module.scss
Normal file
|
@ -0,0 +1,56 @@
|
|||
.item {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: minmax(auto, 65%) auto;
|
||||
|
||||
text-decoration: none;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.imgContainer {
|
||||
justify-self: stretch;
|
||||
position: relative;
|
||||
|
||||
// for icon when image is unavailable
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.img {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.imgNA {
|
||||
fill: var(--clr-fill-muted);
|
||||
height: 40%;
|
||||
}
|
||||
|
||||
.textContainer {
|
||||
display: grid;
|
||||
gap: var(--spacer-1);
|
||||
padding: var(--spacer-1);
|
||||
text-align: center;
|
||||
justify-items: center;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacer-0);
|
||||
line-height: 1;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ratingIcon {
|
||||
--dim: 1em;
|
||||
height: var(--dim);
|
||||
width: var(--dim);
|
||||
fill: var(--clr-fill);
|
||||
}
|
11
src/styles/modules/components/card/card.module.scss
Normal file
11
src/styles/modules/components/card/card.module.scss
Normal file
|
@ -0,0 +1,11 @@
|
|||
.card {
|
||||
overflow: hidden;
|
||||
border-radius: 5px;
|
||||
background-color: var(--clr-bg-accent);
|
||||
box-shadow: var(--clr-shadow);
|
||||
}
|
||||
|
||||
.hoverable:hover,
|
||||
.hoverable:focus-within {
|
||||
background-color: var(--clr-bg-muted);
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
.company {
|
||||
background: var(--clr-bg-accent);
|
||||
box-shadow: var(--clr-shadow);
|
||||
border-radius: 5px;
|
||||
display: grid;
|
||||
padding: var(--spacer-3);
|
||||
gap: var(--spacer-0);
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: var(--fs-4);
|
||||
text-decoration: none;
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
.keyword {
|
||||
background: var(--clr-bg-accent);
|
||||
box-shadow: var(--clr-shadow);
|
||||
border-radius: 5px;
|
||||
display: grid;
|
||||
padding: var(--spacer-3);
|
||||
gap: var(--spacer-0);
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: var(--fs-4);
|
||||
text-decoration: none;
|
||||
}
|
|
@ -1,57 +1,4 @@
|
|||
@use '../../../abstracts' as helper;
|
||||
|
||||
.person {
|
||||
--width: 10rem;
|
||||
--height: var(--width);
|
||||
|
||||
background: var(--clr-bg-accent);
|
||||
box-shadow: var(--clr-shadow);
|
||||
border-radius: 5px;
|
||||
overflow: hidden; // for background image
|
||||
display: grid;
|
||||
grid-template-columns: var(--width) auto;
|
||||
|
||||
@include helper.bp('bp-450') {
|
||||
--height: 15rem;
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.imgContainer {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: var(--height);
|
||||
}
|
||||
|
||||
.img {
|
||||
object-fit: cover;
|
||||
object-position: center 25%; // most of the time, person's face is visible at 1/4 of height in a potrait image.
|
||||
|
||||
}
|
||||
|
||||
.imgNA {
|
||||
width: 80%;
|
||||
fill: var(--clr-fill-muted);
|
||||
}
|
||||
|
||||
.info {
|
||||
display: grid;
|
||||
padding: var(--spacer-3);
|
||||
gap: var(--spacer-0);
|
||||
|
||||
@include helper.bp('bp-450') {
|
||||
padding: var(--spacer-1);
|
||||
}
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: var(--fs-4);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.basicInfo, .seriesInfo {
|
||||
.basicInfo {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
flex-wrap: wrap;
|
||||
|
@ -64,11 +11,3 @@
|
|||
font-size: var(--fs-5);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.stars {
|
||||
|
||||
span {
|
||||
font-weight: var(--fw-bold);
|
||||
}
|
||||
}
|
|
@ -10,7 +10,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.titles, .people, .companies, .keywords {
|
||||
.titles,
|
||||
.people,
|
||||
.companies,
|
||||
.keywords {
|
||||
display: grid;
|
||||
gap: var(--spacer-2);
|
||||
|
||||
|
@ -18,8 +21,5 @@
|
|||
padding: var(--spacer-2);
|
||||
display: grid;
|
||||
gap: var(--spacer-4);
|
||||
// justify-self: start;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,57 +1,5 @@
|
|||
@use '../../../abstracts' as helper;
|
||||
|
||||
.title {
|
||||
--width: 10rem;
|
||||
--height: 10rem;
|
||||
|
||||
background: var(--clr-bg-accent);
|
||||
box-shadow: var(--clr-shadow);
|
||||
border-radius: 5px;
|
||||
overflow: hidden; // for background image
|
||||
display: grid;
|
||||
grid-template-columns: var(--width) auto;
|
||||
|
||||
@include helper.bp('bp-450') {
|
||||
--height: 15rem;
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.imgContainer {
|
||||
min-height: var(--height);
|
||||
|
||||
display: grid;
|
||||
place-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.img {
|
||||
object-fit: cover;
|
||||
|
||||
@include helper.bp('bp-450') {
|
||||
object-position: center 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.imgNA {
|
||||
width: 80%;
|
||||
fill: var(--clr-fill-muted);
|
||||
}
|
||||
|
||||
.info {
|
||||
display: grid;
|
||||
gap: var(--spacer-0);
|
||||
padding: var(--spacer-3);
|
||||
|
||||
@include helper.bp('bp-450') {
|
||||
padding: var(--spacer-1);
|
||||
}
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: var(--fs-4);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.basicInfo,
|
||||
.seriesInfo {
|
||||
|
@ -59,7 +7,7 @@
|
|||
list-style: none;
|
||||
flex-wrap: wrap;
|
||||
|
||||
& * + ::before {
|
||||
& :not(:last-child)::after {
|
||||
content: '\00b7';
|
||||
padding-inline: var(--spacer-1);
|
||||
font-weight: 900;
|
||||
|
|
|
@ -1,97 +1,5 @@
|
|||
@use '../../../abstracts' as helper;
|
||||
|
||||
.container {
|
||||
margin-inline: auto;
|
||||
background: var(--clr-bg-accent);
|
||||
box-shadow: var(--clr-shadow);
|
||||
border-radius: 5px;
|
||||
overflow: hidden; // for background image
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: minmax(25rem, 30rem) 1fr;
|
||||
|
||||
@include helper.bp('bp-900') {
|
||||
grid-template-columns: none;
|
||||
grid-template-rows: 30rem min-content;
|
||||
}
|
||||
|
||||
@include helper.bp('bp-700') {
|
||||
grid-template-rows: 25rem min-content;
|
||||
}
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
display: flex; // for bringing out image__NA out of blur
|
||||
|
||||
position: relative;
|
||||
height: auto;
|
||||
width: auto;
|
||||
overflow: hidden;
|
||||
|
||||
background-size: cover;
|
||||
background-position: top;
|
||||
place-items: center;
|
||||
|
||||
@include helper.bp('bp-900') {
|
||||
padding: var(--spacer-2);
|
||||
isolation: isolate;
|
||||
|
||||
// for adding layer of color on top of background image
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
var(--clr-bg-accent) 10%,
|
||||
transparent
|
||||
);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
|
||||
@include helper.bp('bp-900') {
|
||||
z-index: 1;
|
||||
object-fit: contain;
|
||||
|
||||
outline: 3px solid var(--clr-fill);
|
||||
outline-offset: 5px;
|
||||
|
||||
max-height: 100%;
|
||||
margin: auto;
|
||||
|
||||
// overrriding nex/future/image defaults
|
||||
height: initial !important;
|
||||
width: initial !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
&__NA {
|
||||
z-index: 1;
|
||||
fill: var(--clr-fill-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: var(--spacer-2) var(--spacer-4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacer-2);
|
||||
|
||||
@include helper.bp('bp-900') {
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
@include helper.bp('bp-450') {
|
||||
gap: var(--spacer-1);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
line-height: 1;
|
||||
}
|
||||
|
|
|
@ -24,53 +24,3 @@
|
|||
--min-height: 30rem;
|
||||
}
|
||||
}
|
||||
|
||||
.member {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: minmax(auto, 70%) min-content auto;
|
||||
justify-items: center;
|
||||
text-align: center;
|
||||
font-size: var(--fs-5);
|
||||
|
||||
overflow: hidden;
|
||||
border-radius: 5px;
|
||||
box-shadow: var(--clr-shadow);
|
||||
background-color: var(--clr-bg-accent);
|
||||
|
||||
&__imgContainer {
|
||||
justify-self: stretch;
|
||||
position: relative;
|
||||
|
||||
// for icon when image is unavailable
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
&__img {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&__imgNA {
|
||||
fill: var(--clr-fill-muted);
|
||||
height: 40%;
|
||||
}
|
||||
|
||||
&__textContainer {
|
||||
display: grid;
|
||||
gap: var(--spacer-0);
|
||||
padding: var(--spacer-0);
|
||||
// place-content: center;
|
||||
text-align: center;
|
||||
justify-items: center;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
&__name {
|
||||
@include helper.prettify-link(var(--clr-link));
|
||||
}
|
||||
|
||||
&__role {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,77 +29,3 @@
|
|||
min-height: 37rem;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
overflow: hidden;
|
||||
border-radius: 5px;
|
||||
box-shadow: var(--clr-shadow);
|
||||
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: minmax(auto, 70%) auto;
|
||||
|
||||
background-color: var(--clr-bg-accent);
|
||||
text-decoration: none;
|
||||
color: currentColor;
|
||||
|
||||
&__imgContainer {
|
||||
justify-self: stretch;
|
||||
position: relative;
|
||||
|
||||
// for icon when image is unavailable
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
&__textContainer {
|
||||
display: grid;
|
||||
gap: var(--spacer-1);
|
||||
padding: var(--spacer-1);
|
||||
// place-content: center;
|
||||
text-align: center;
|
||||
justify-items: center;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
&__img {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&__imgNA {
|
||||
fill: var(--clr-fill-muted);
|
||||
height: 40%;
|
||||
// vertical-align: center;
|
||||
}
|
||||
|
||||
&__heading {
|
||||
}
|
||||
|
||||
&__genres {
|
||||
}
|
||||
|
||||
&__rating {
|
||||
// font-size: 0.9em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacer-0);
|
||||
line-height: 1;
|
||||
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__ratingNum {
|
||||
}
|
||||
|
||||
&__ratingIcon {
|
||||
--dim: 1em;
|
||||
height: var(--dim);
|
||||
width: var(--dim);
|
||||
fill: var(--clr-fill);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--clr-bg-muted);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,12 +23,12 @@
|
|||
|
||||
&__link {
|
||||
@include helper.prettify-link(var(--clr-link));
|
||||
}
|
||||
|
||||
&__linkActive {
|
||||
&[aria-current] {
|
||||
@include helper.prettify-link(var(--clr-link), $animate: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.licence {
|
||||
margin-top: var(--spacer-1);
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
import Redis from 'ioredis';
|
||||
|
||||
const redisUrl = process.env.REDIS_URL;
|
||||
const toUseRedis = process.env.USE_REDIS === 'true';
|
||||
|
||||
let redis: Redis | null;
|
||||
const stub: Pick<Redis, 'get' | 'setex' | 'getBuffer'> = {
|
||||
get: async key => Promise.resolve(null),
|
||||
setex: async (key, seconds, value) => Promise.resolve('OK'),
|
||||
getBuffer: (key, callback) => Promise.resolve(null),
|
||||
};
|
||||
|
||||
if (toUseRedis && redisUrl) redis = new Redis(redisUrl);
|
||||
else redis = null;
|
||||
const redis = toUseRedis && redisUrl ? new Redis(redisUrl) : stub;
|
||||
|
||||
export default redis;
|
||||
|
|
Loading…
Reference in a new issue