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",
|
"arrowParens": "avoid",
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": 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>
|
<title id='gnu-title'>GNU and Tux</title>
|
||||||
<desc id='gnu-desc'>
|
<desc id='gnu-desc'>
|
||||||
A pencil drawing of a big gnu and a small penguin, both very sad.
|
A pencil drawing of a big gnu and a small penguin, both very sad. GNU is despondently
|
||||||
GNU is despondently sitting on a bench, and Tux stands beside him,
|
sitting on a bench, and Tux stands beside him, looking down and patting him on the back.
|
||||||
looking down and patting him on the back.
|
|
||||||
</desc>
|
</desc>
|
||||||
<use href='/svg/sadgnu.svg#sad-gnu'></use>
|
<use href='/svg/sadgnu.svg#sad-gnu'></use>
|
||||||
</svg>
|
</svg>
|
||||||
<h1 className={`heading heading__primary ${styles.heading}`}>
|
<h1 className={`heading heading__primary ${styles.heading}`}>{title}</h1>
|
||||||
{title}
|
|
||||||
</h1>
|
|
||||||
{misc ? (
|
{misc ? (
|
||||||
<>
|
<>
|
||||||
<p>{misc.subtext}</p>
|
<p>{misc.subtext}</p>
|
||||||
|
@ -52,7 +49,7 @@ const ErrorInfo = ({ message, statusCode, misc }: Props) => {
|
||||||
) : (
|
) : (
|
||||||
<p>
|
<p>
|
||||||
Go back to{' '}
|
Go back to{' '}
|
||||||
<Link href='/about'>
|
<Link href='/'>
|
||||||
<a className='link'>the homepage</a>
|
<a className='link'>the homepage</a>
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
|
|
|
@ -1,22 +1,13 @@
|
||||||
|
import { CardResult } from 'src/components/card';
|
||||||
import { Companies } from 'src/interfaces/shared/search';
|
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 = {
|
const Company = ({ company }: Props) => (
|
||||||
company: Companies[0];
|
<CardResult name={company.name} link={`/search/title?companies=${company.id}`}>
|
||||||
};
|
{company.country && <p>{company.country}</p>}
|
||||||
|
{!!company.type && <p>{company.type}</p>}
|
||||||
const Company = ({ company }: Props) => {
|
</CardResult>
|
||||||
return (
|
);
|
||||||
<li className={styles.company}>
|
|
||||||
<Link href={`name/${company.id}`}>
|
|
||||||
<a className={`heading ${styles.heading}`}>{company.name}</a>
|
|
||||||
</Link>
|
|
||||||
{company.country && <p>{company.country}</p>}
|
|
||||||
{!!company.type && <p>{company.type}</p>}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Company;
|
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 { Keywords } from 'src/interfaces/shared/search';
|
||||||
import styles from 'src/styles/modules/components/find/keyword.module.scss';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = { keyword: Keywords[number] };
|
||||||
keyword: Keywords[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
const Keyword = ({ keyword }: Props) => {
|
const Keyword = ({ keyword }: Props) => (
|
||||||
return (
|
<CardResult link={`/search/keyword?keywords=${keyword.text}`} name={keyword.text}>
|
||||||
<li className={styles.keyword}>
|
{keyword.numTitles && <p>{keyword.numTitles} titles</p>}
|
||||||
<Link href={`name/${keyword.id}`}>
|
</CardResult>
|
||||||
<a className={`heading ${styles.heading}`}>{keyword.text}</a>
|
);
|
||||||
</Link>
|
|
||||||
{keyword.numTitles && <p>{keyword.numTitles} titles</p>}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Keyword;
|
export default Keyword;
|
||||||
|
|
|
@ -1,44 +1,19 @@
|
||||||
import Image from 'next/future/image';
|
import { CardResult } from 'src/components/card';
|
||||||
import Link from 'next/link';
|
|
||||||
import { People } from 'src/interfaces/shared/search';
|
import { People } from 'src/interfaces/shared/search';
|
||||||
import { modifyIMDbImg } from 'src/utils/helpers';
|
|
||||||
import styles from 'src/styles/modules/components/find/person.module.scss';
|
import styles from 'src/styles/modules/components/find/person.module.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = { person: People[number] };
|
||||||
person: People[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
const Person = ({ person }: Props) => {
|
const Person = ({ person }: Props) => {
|
||||||
return (
|
return (
|
||||||
<li className={styles.person}>
|
<CardResult showImage name={person.name} link={`/name/${person.id}`} image={person.image?.url}>
|
||||||
<div className={styles.imgContainer} style={{ position: 'relative' }}>
|
<p>{person.aka}</p>
|
||||||
{person.image ? (
|
<p>{person.jobCateogry}</p>
|
||||||
<Image
|
<ul className={styles.basicInfo} aria-label='quick facts'>
|
||||||
src={modifyIMDbImg(person.image.url, 400)}
|
{person.knownForTitle && <li>{person.knownForTitle}</li>}
|
||||||
alt={person.image.caption}
|
{person.knownInYear && <li>{person.knownInYear}</li>}
|
||||||
fill
|
</ul>
|
||||||
className={styles.img}
|
</CardResult>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<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) && (
|
|
||||||
<ul className={styles.basicInfo} aria-label='quick facts'>
|
|
||||||
{person.knownForTitle && <li>{person.knownForTitle}</li>}
|
|
||||||
{person.knownInYear && <li>{person.knownInYear}</li>}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,58 +1,36 @@
|
||||||
import Image from 'next/future/image';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { CardResult } from 'src/components/card';
|
||||||
import { Titles } from 'src/interfaces/shared/search';
|
import { Titles } from 'src/interfaces/shared/search';
|
||||||
import { modifyIMDbImg } from 'src/utils/helpers';
|
|
||||||
import styles from 'src/styles/modules/components/find/title.module.scss';
|
import styles from 'src/styles/modules/components/find/title.module.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = { title: Titles[number] };
|
||||||
title: Titles[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
const Title = ({ title }: Props) => {
|
const Title = ({ title }: Props) => {
|
||||||
return (
|
return (
|
||||||
<li className={styles.title}>
|
<CardResult showImage name={title.name} link={`/title/${title.id}`} image={title.image?.url}>
|
||||||
<div className={styles.imgContainer}>
|
<ul aria-label='quick facts' className={styles.basicInfo}>
|
||||||
{title.image ? (
|
<li>{title.type}</li>
|
||||||
<Image
|
<li>{title.sAndE}</li>
|
||||||
src={modifyIMDbImg(title.image.url, 400)}
|
<li>{title.releaseYear}</li>
|
||||||
alt={title.image.caption}
|
</ul>
|
||||||
fill
|
{!!title.credits.length && (
|
||||||
className={styles.img}
|
<p className={styles.stars}>
|
||||||
/>
|
<span>Stars: </span>
|
||||||
) : (
|
{title.credits.join(', ')}
|
||||||
<svg className={styles.imgNA}>
|
</p>
|
||||||
<use href='/svg/sprite.svg#icon-image-slash' />
|
)}
|
||||||
</svg>
|
{title.seriesId && (
|
||||||
)}
|
<ul aria-label='quick series facts' className={styles.seriesInfo}>
|
||||||
</div>
|
{title.seriesType && <li>{title.seriesType}</li>}
|
||||||
<div className={styles.info}>
|
<li>
|
||||||
<Link href={`/title/${title.id}`}>
|
<Link href={`/title/${title.seriesId}`}>
|
||||||
<a className={`heading ${styles.heading}`}>{title.name}</a>
|
<a className='link'>{title.seriesName}</a>
|
||||||
</Link>
|
</Link>
|
||||||
<ul aria-label='quick facts' className={styles.basicInfo}>
|
</li>
|
||||||
{title.type && <li>{title.type}</li>}
|
{title.seriesReleaseYear && <li>{title.seriesReleaseYear}</li>}
|
||||||
{title.sAndE && <li>{title.sAndE}</li>}
|
|
||||||
{title.releaseYear && <li>{title.releaseYear}</li>}
|
|
||||||
</ul>
|
</ul>
|
||||||
{!!title.credits.length && (
|
)}
|
||||||
<p className={styles.stars}>
|
</CardResult>
|
||||||
<span>Stars: </span>
|
|
||||||
{title.credits.join(', ')}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{title.seriesId && (
|
|
||||||
<ul aria-label='quick series facts' className={styles.seriesInfo}>
|
|
||||||
{title.seriesType && <li>{title.seriesType}</li>}
|
|
||||||
<li>
|
|
||||||
<Link href={`/title/${title.seriesId}`}>
|
|
||||||
<a className='link'>{title.seriesName}</a>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
{title.seriesReleaseYear && <li>{title.seriesReleaseYear}</li>}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -12,18 +12,16 @@ type Props = {
|
||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const resultsExist = (results: Props['results']) => {
|
const resultsExist = (
|
||||||
if (
|
results: Props['results']
|
||||||
!results ||
|
): results is NonNullable<Props['results']> =>
|
||||||
(!results.people.length &&
|
Boolean(
|
||||||
!results.keywords.length &&
|
results &&
|
||||||
!results.companies.length &&
|
(results.people.length ||
|
||||||
!results.titles.length)
|
results.keywords.length ||
|
||||||
)
|
results.companies.length ||
|
||||||
return false;
|
results.titles.length)
|
||||||
|
);
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// MAIN COMPONENT
|
// MAIN COMPONENT
|
||||||
const Results = ({ results, className, title }: Props) => {
|
const Results = ({ results, className, title }: Props) => {
|
||||||
|
@ -34,7 +32,7 @@ const Results = ({ results, className, title }: Props) => {
|
||||||
</h1>
|
</h1>
|
||||||
);
|
);
|
||||||
|
|
||||||
const { titles, people, keywords, companies, meta } = results!;
|
const { titles, people, keywords, companies, meta } = results;
|
||||||
const titlesSectionHeading = getResTitleTypeHeading(
|
const titlesSectionHeading = getResTitleTypeHeading(
|
||||||
meta.type,
|
meta.type,
|
||||||
meta.titleType
|
meta.titleType
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import Image from 'next/future/image';
|
import Image from 'next/future/image';
|
||||||
import Link from 'next/link';
|
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 { 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 = {
|
type Props = {
|
||||||
className: string;
|
className: string;
|
||||||
media: Media;
|
media: Media;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: refactor this component.
|
||||||
|
|
||||||
const Media = ({ className, media }: Props) => {
|
const Media = ({ className, media }: Props) => {
|
||||||
return (
|
return (
|
||||||
<div className={`${className} ${styles.media}`}>
|
<div className={`${className} ${styles.media}`}>
|
||||||
|
@ -21,13 +23,9 @@ const Media = ({ className, media }: Props) => {
|
||||||
<div className={styles.trailer}>
|
<div className={styles.trailer}>
|
||||||
<video
|
<video
|
||||||
aria-label='trailer video'
|
aria-label='trailer video'
|
||||||
// it's a relatively new tag. hence jsx-all1 complains
|
|
||||||
aria-description={media.trailer.caption}
|
|
||||||
controls
|
controls
|
||||||
playsInline
|
playsInline
|
||||||
poster={getProxiedIMDbImgUrl(
|
poster={getProxiedIMDbImgUrl(modifyIMDbImg(media.trailer.thumbnail))}
|
||||||
modifyIMDbImg(media.trailer.thumbnail)
|
|
||||||
)}
|
|
||||||
className={styles.trailer__video}
|
className={styles.trailer__video}
|
||||||
preload='none'
|
preload='none'
|
||||||
>
|
>
|
||||||
|
@ -76,9 +74,7 @@ const Media = ({ className, media }: Props) => {
|
||||||
fill
|
fill
|
||||||
sizes='400px'
|
sizes='400px'
|
||||||
/>
|
/>
|
||||||
<figcaption className={styles.image__caption}>
|
<figcaption className={styles.image__caption}>{image.caption.plainText}</figcaption>
|
||||||
{image.caption.plainText}
|
|
||||||
</figcaption>
|
|
||||||
</figure>
|
</figure>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
|
@ -1,4 +1,5 @@
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -6,11 +7,15 @@ type Props = {
|
||||||
imgUrl?: string;
|
imgUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const BASE_URL = process.env.NEXT_PUBLIC_URL ?? 'https://iket.me';
|
||||||
|
|
||||||
const Meta = ({
|
const Meta = ({
|
||||||
title,
|
title,
|
||||||
description = 'libremdb, a free & open source IMDb front-end.',
|
description = 'libremdb, a free & open source IMDb front-end.',
|
||||||
imgUrl = 'icon.svg',
|
imgUrl = 'icon.svg',
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const url = new URL(imgUrl, BASE_URL);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Head>
|
<Head>
|
||||||
<meta charSet='UTF-8' />
|
<meta charSet='UTF-8' />
|
||||||
|
@ -30,10 +35,7 @@ const Meta = ({
|
||||||
<meta property='og:site_name' content='libremdb' />
|
<meta property='og:site_name' content='libremdb' />
|
||||||
<meta property='og:locale' content='en_US' />
|
<meta property='og:locale' content='en_US' />
|
||||||
<meta property='og:type' content='video.movie' />
|
<meta property='og:type' content='video.movie' />
|
||||||
<meta
|
<meta property='og:image' content={url.toString()} />
|
||||||
property='og:image'
|
|
||||||
content={`${process.env.NEXT_PUBLIC_URL}/${imgUrl}`}
|
|
||||||
/>
|
|
||||||
</Head>
|
</Head>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import Image from 'next/future/image';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { CardBasic } from 'src/components/card';
|
||||||
import { Basic } from 'src/interfaces/shared/title';
|
import { Basic } from 'src/interfaces/shared/title';
|
||||||
import {
|
import { formatNumber, formatTime } from 'src/utils/helpers';
|
||||||
formatNumber,
|
|
||||||
formatTime,
|
|
||||||
getProxiedIMDbImgUrl,
|
|
||||||
modifyIMDbImg,
|
|
||||||
} from 'src/utils/helpers';
|
|
||||||
import styles from 'src/styles/modules/components/title/basic.module.scss';
|
import styles from 'src/styles/modules/components/title/basic.module.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -23,135 +18,92 @@ const Basic = ({ data, className }: Props) => {
|
||||||
: data.releaseYear?.start;
|
: data.releaseYear?.start;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<CardBasic
|
||||||
// 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}`}
|
className={`${styles.container} ${className}`}
|
||||||
|
image={data.poster?.url}
|
||||||
|
title={data.title}
|
||||||
>
|
>
|
||||||
<div
|
<ul className={styles.meta} aria-label='quick facts'>
|
||||||
className={styles.imageContainer}
|
{data.status && data.status.id !== 'released' && (
|
||||||
style={{
|
<li className={styles.meta__text}>{data.status.text}</li>
|
||||||
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>
|
<li className={styles.meta__text}>{data.type.name}</li>
|
||||||
<div className={styles.info}>
|
{data.releaseYear && <li className={styles.meta__text}>{releaseTime}</li>}
|
||||||
<h1 className={`${styles.title} heading heading__primary`}>
|
{data.ceritficate && <li className={styles.meta__text}>{data.ceritficate}</li>}
|
||||||
{data.title}
|
{data.runtime && <li className={styles.meta__text}>{formatTime(data.runtime)}</li>}
|
||||||
</h1>
|
</ul>
|
||||||
<ul className={styles.meta} aria-label='quick facts'>
|
<div className={styles.ratings}>
|
||||||
{data.status && data.status.id !== 'released' && (
|
{data.ratings.avg && (
|
||||||
<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}>
|
<p className={styles.rating}>
|
||||||
<span className={styles.rating__num}>
|
<span className={styles.rating__num}>{data.ratings.avg}</span>
|
||||||
{formatNumber(data.ranking.position)}
|
|
||||||
</span>
|
|
||||||
<svg className={styles.rating__icon}>
|
<svg className={styles.rating__icon}>
|
||||||
<use href='/svg/sprite.svg#icon-graph-rising'></use>
|
<use href='/svg/sprite.svg#icon-rating'></use>
|
||||||
</svg>
|
</svg>
|
||||||
<span className={styles.rating__text}>
|
<span className={styles.rating__text}> Avg. rating</span>
|
||||||
{' '}
|
|
||||||
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>
|
</p>
|
||||||
)}
|
<p className={styles.rating}>
|
||||||
</div>
|
<span className={styles.rating__num}>{formatNumber(data.ratings.numVotes)}</span>
|
||||||
|
<svg className={styles.rating__icon}>
|
||||||
{!!data.genres.length && (
|
<use href='/svg/sprite.svg#icon-like-dislike'></use>
|
||||||
<p className={styles.genres}>
|
</svg>
|
||||||
<span className={styles.genres__heading}>Genres: </span>
|
<span className={styles.rating__text}> No. of votes</span>
|
||||||
{data.genres.map((genre, i) => (
|
</p>
|
||||||
<Fragment key={genre.id}>
|
</>
|
||||||
{i > 0 && ', '}
|
)}
|
||||||
<Link href={`/search/title?genres=${genre.id}`}>
|
{data.ranking && (
|
||||||
<a className={styles.link}>{genre.text}</a>
|
<p className={styles.rating}>
|
||||||
</Link>
|
<span className={styles.rating__num}>{formatNumber(data.ranking.position)}</span>
|
||||||
</Fragment>
|
<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>
|
</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>
|
</div>
|
||||||
</section>
|
|
||||||
|
{!!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>
|
||||||
|
))}
|
||||||
|
</CardBasic>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import Image from 'next/future/image';
|
import { CardCast } from 'src/components/card';
|
||||||
import Link from 'next/link';
|
|
||||||
import { Cast } from 'src/interfaces/shared/title';
|
import { Cast } from 'src/interfaces/shared/title';
|
||||||
import { modifyIMDbImg } from 'src/utils/helpers';
|
|
||||||
import styles from 'src/styles/modules/components/title/cast.module.scss';
|
import styles from 'src/styles/modules/components/title/cast.module.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -10,46 +8,25 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const Cast = ({ className, cast }: Props) => {
|
const Cast = ({ className, cast }: Props) => {
|
||||||
if (!cast.length) return <></>;
|
if (!cast.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`${className} ${styles.container}`}>
|
<section className={`${className} ${styles.container}`}>
|
||||||
<h2 className='heading heading__secondary'>Cast</h2>
|
<h2 className='heading heading__secondary'>Cast</h2>
|
||||||
<ul className={styles.cast}>
|
<ul className={styles.cast}>
|
||||||
{cast.map(member => (
|
{cast.map(member => (
|
||||||
<li key={member.id} className={styles.member}>
|
<CardCast
|
||||||
<div className={styles.member__imgContainer}>
|
key={member.id}
|
||||||
{member.image ? (
|
link={`/name/${member.id}`}
|
||||||
<Image
|
name={member.name}
|
||||||
src={modifyIMDbImg(member.image, 400)}
|
image={member.image}
|
||||||
alt=''
|
characters={member.characters}
|
||||||
fill
|
attributes={member.attributes}
|
||||||
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>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Cast;
|
export default Cast;
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import Image from 'next/future/image';
|
import { CardTitle } from 'src/components/card';
|
||||||
import Link from 'next/link';
|
|
||||||
import { MoreLikeThis } from 'src/interfaces/shared/title';
|
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';
|
import styles from 'src/styles/modules/components/title/more-like-this.module.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -10,52 +8,22 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const MoreLikeThis = ({ className, data }: Props) => {
|
const MoreLikeThis = ({ className, data }: Props) => {
|
||||||
if (!data.length) return <></>;
|
if (!data.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`${className} ${styles.morelikethis}`}>
|
<section className={`${className} ${styles.morelikethis}`}>
|
||||||
<h2 className='heading heading__secondary'>More like this</h2>
|
<h2 className='heading heading__secondary'>More like this</h2>
|
||||||
<ul className={styles.container}>
|
<ul className={styles.container}>
|
||||||
{data.map(title => (
|
{data.map(title => (
|
||||||
<li key={title.id}>
|
<CardTitle
|
||||||
<Link href={`/title/${title.id}`}>
|
key={title.id}
|
||||||
<a className={styles.item}>
|
link={`/title/${title.id}`}
|
||||||
<div className={styles.item__imgContainer}>
|
name={title.title}
|
||||||
{title.poster ? (
|
titleType={title.type.text}
|
||||||
<Image
|
image={title.poster?.url}
|
||||||
src={modifyIMDbImg(title.poster.url, 400)}
|
year={title.releaseYear}
|
||||||
alt=''
|
ratings={title.ratings}
|
||||||
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>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import Basic from './Basic';
|
export { default as Basic } from './Basic';
|
||||||
import Cast from './Cast';
|
export { default as Cast } from './Cast';
|
||||||
import DidYouKnow from './DidYouKnow';
|
export { default as DidYouKnow } from './DidYouKnow';
|
||||||
import Info from './Info';
|
export { default as Info } from './Info';
|
||||||
import Media from './Media';
|
export { default as MoreLikeThis } from './MoreLikeThis';
|
||||||
import MoreLikeThis from './MoreLikeThis';
|
export { default as Reviews } from './Reviews';
|
||||||
import Reviews from './Reviews';
|
|
||||||
|
|
||||||
export { Basic, Cast, DidYouKnow, Info, Media, MoreLikeThis, Reviews };
|
|
||||||
|
|
|
@ -5,9 +5,9 @@ const getInitialTheme = () => {
|
||||||
// for server-side rendering, as window isn't availabe there
|
// for server-side rendering, as window isn't availabe there
|
||||||
if (typeof window === 'undefined') return 'light';
|
if (typeof window === 'undefined') return 'light';
|
||||||
|
|
||||||
const userPrefersTheme = isLocalStorageAvailable()
|
const userPrefersTheme = (
|
||||||
? window.localStorage.getItem('theme')
|
isLocalStorageAvailable() ? window.localStorage.getItem('theme') : null
|
||||||
: null;
|
) as 'light' | 'dark' | null;
|
||||||
const browserPrefersDarkTheme = window.matchMedia(
|
const browserPrefersDarkTheme = window.matchMedia(
|
||||||
'(prefers-color-scheme: dark)'
|
'(prefers-color-scheme: dark)'
|
||||||
).matches;
|
).matches;
|
||||||
|
@ -28,7 +28,7 @@ const updateMetaTheme = () => {
|
||||||
|
|
||||||
const initialContext = {
|
const initialContext = {
|
||||||
theme: '',
|
theme: '',
|
||||||
setTheme: (theme: string) => {},
|
setTheme: (theme: ReturnType<typeof getInitialTheme>) => { },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const themeContext = createContext(initialContext);
|
export const themeContext = createContext(initialContext);
|
||||||
|
@ -36,7 +36,7 @@ export const themeContext = createContext(initialContext);
|
||||||
const ThemeProvider = ({ children }: { children: ReactNode }) => {
|
const ThemeProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [curTheme, setCurTheme] = useState(getInitialTheme);
|
const [curTheme, setCurTheme] = useState(getInitialTheme);
|
||||||
|
|
||||||
const setTheme = (theme: string) => {
|
const setTheme = (theme: typeof curTheme) => {
|
||||||
setCurTheme(theme);
|
setCurTheme(theme);
|
||||||
if (isLocalStorageAvailable()) window.localStorage.setItem('theme', theme);
|
if (isLocalStorageAvailable()) window.localStorage.setItem('theme', theme);
|
||||||
document.documentElement.dataset.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 cleanTitle from 'src/utils/cleaners/title';
|
||||||
import title from 'src/utils/fetchers/title';
|
import title from 'src/utils/fetchers/title';
|
||||||
|
|
||||||
export type AxiosTitleRes = Awaited<ReturnType<typeof title>>;
|
|
||||||
|
|
||||||
// for full title
|
// for full title
|
||||||
type Title = ReturnType<typeof cleanTitle>;
|
type Title = ReturnType<typeof cleanTitle>;
|
||||||
export type { Title as default };
|
export type { Title as default };
|
||||||
|
|
|
@ -2,35 +2,32 @@ import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import styles from '../styles/modules/layout/footer.module.scss';
|
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 Footer = () => {
|
||||||
const { pathname } = useRouter();
|
const { pathname } = useRouter();
|
||||||
const className = (link: string) =>
|
|
||||||
pathname === link ? styles.nav__linkActive : styles.nav__link;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer id='footer' className={styles.footer}>
|
<footer id='footer' className={styles.footer}>
|
||||||
<nav aria-label='primary navigation' className={styles.nav}>
|
<nav aria-label='primary navigation' className={styles.nav}>
|
||||||
<ul className={styles.list}>
|
<ul className={styles.list}>
|
||||||
<li className={styles.nav__item}>
|
{links.map(link => (
|
||||||
<Link href='/about'>
|
<li className={styles.nav__item} key={link.path}>
|
||||||
<a className={className('/about')}>About</a>
|
<Link href={link.path}>
|
||||||
</Link>
|
<a
|
||||||
</li>
|
className={styles.nav__link}
|
||||||
<li className={styles.nav__item}>
|
aria-current={pathname === link.path ? 'page' : undefined}
|
||||||
<Link href='/find'>
|
>
|
||||||
<a className={className('/find')}>Search</a>
|
{link.text}
|
||||||
</Link>
|
</a>
|
||||||
</li>
|
</Link>
|
||||||
<li className={styles.nav__item}>
|
</li>
|
||||||
<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>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li className={styles.nav__item}>
|
<li className={styles.nav__item}>
|
||||||
<a href='#' className={styles.nav__link}>
|
<a href='#' className={styles.nav__link}>
|
||||||
Back to top
|
Back to top
|
||||||
|
@ -39,7 +36,7 @@ const Footer = () => {
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<p className={styles.licence}>
|
<p className={styles.licence}>
|
||||||
Licensed under
|
Licensed under{' '}
|
||||||
<a
|
<a
|
||||||
className={styles.nav__link}
|
className={styles.nav__link}
|
||||||
href='https://www.gnu.org/licenses/agpl-3.0-standalone.html'
|
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 Footer from './Footer';
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
full?: boolean;
|
full?: true;
|
||||||
children: React.ReactNode;
|
children: ReactNode;
|
||||||
className: string;
|
className: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,23 +1,20 @@
|
||||||
// external
|
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
|
||||||
import { GetServerSideProps, GetStaticProps, GetStaticPaths } from 'next';
|
|
||||||
import Head from 'next/head';
|
|
||||||
import Meta from 'src/components/meta/Meta';
|
import Meta from 'src/components/meta/Meta';
|
||||||
import Layout from 'src/layouts/Layout';
|
import Layout from 'src/layouts/Layout';
|
||||||
import ErrorInfo from 'src/components/error/ErrorInfo';
|
import ErrorInfo from 'src/components/error/ErrorInfo';
|
||||||
// prettier-ignore
|
import Media from 'src/components/media/Media';
|
||||||
import { Basic, Cast, DidYouKnow, Info, Media, MoreLikeThis, Reviews } from 'src/components/title';
|
import { Basic, Cast, DidYouKnow, Info, MoreLikeThis, Reviews } from 'src/components/title';
|
||||||
import Title from 'src/interfaces/shared/title';
|
import Title from 'src/interfaces/shared/title';
|
||||||
import { AppError } from 'src/interfaces/shared/error';
|
import { AppError } from 'src/interfaces/shared/error';
|
||||||
import title from 'src/utils/fetchers/title';
|
import title from 'src/utils/fetchers/title';
|
||||||
import { getProxiedIMDbImgUrl } from 'src/utils/helpers';
|
import { getProxiedIMDbImgUrl } from 'src/utils/helpers';
|
||||||
import styles from 'src/styles/modules/pages/title/title.module.scss';
|
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
|
// TO-DO: make a wrapper page component to display errors, if present in props
|
||||||
const TitleInfo = ({ data, error }: Props) => {
|
const TitleInfo = ({ data, error }: Props) => {
|
||||||
if (error)
|
if (error) return <ErrorInfo message={error.message} statusCode={error.statusCode} />;
|
||||||
return <ErrorInfo message={error.message} statusCode={error.statusCode} />;
|
|
||||||
|
|
||||||
const info = {
|
const info = {
|
||||||
meta: data.meta,
|
meta: data.meta,
|
||||||
|
@ -31,21 +28,10 @@ const TitleInfo = ({ data, error }: Props) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta
|
<Meta
|
||||||
title={`${data.basic.title} (${
|
title={`${data.basic.title} (${data.basic.releaseYear?.start || data.basic.type.name})`}
|
||||||
data.basic.releaseYear?.start || data.basic.type.name
|
description={data.basic.plot ?? undefined}
|
||||||
})`}
|
imgUrl={data.basic.poster?.url && getProxiedIMDbImgUrl(data.basic.poster.url)}
|
||||||
description={data.basic.plot || undefined}
|
|
||||||
/>
|
/>
|
||||||
<Head>
|
|
||||||
<meta
|
|
||||||
title='og:image'
|
|
||||||
content={
|
|
||||||
data.basic.poster?.url
|
|
||||||
? getProxiedIMDbImgUrl(data.basic.poster?.url)
|
|
||||||
: '/icon-512.png'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Head>
|
|
||||||
<Layout className={styles.title}>
|
<Layout className={styles.title}>
|
||||||
<Basic data={data.basic} className={styles.basic} />
|
<Basic data={data.basic} className={styles.basic} />
|
||||||
<Media className={styles.media} media={data.media} />
|
<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
|
// TO-DO: make a getServerSideProps wrapper for handling errors
|
||||||
export const getServerSideProps: GetServerSideProps = async ctx => {
|
type Data = { data: Title; error: null } | { error: AppError; data: null };
|
||||||
const titleId = ctx.params!.titleId as string;
|
type Params = { titleId: string };
|
||||||
|
|
||||||
|
export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx => {
|
||||||
|
const titleId = ctx.params!.titleId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await title(titleId);
|
const data = await title(titleId);
|
||||||
|
|
|
@ -21,8 +21,8 @@ $breakpoints: (
|
||||||
);
|
);
|
||||||
|
|
||||||
// 1. colors
|
// 1. colors
|
||||||
$clr-primary: hsl(240, 31%, 25%);
|
// $clr-primary: hsl(240, 31%, 25%);
|
||||||
$clr-secondary: hsl(344, 79%, 40%);
|
// $clr-secondary: hsl(344, 79%, 40%);
|
||||||
$clr-tertiary: hsl(176, 43%, 46%);
|
// $clr-tertiary: hsl(176, 43%, 46%);
|
||||||
$clr-quatenary: hsl(204, 4%, 23%);
|
// $clr-quatenary: hsl(204, 4%, 23%);
|
||||||
$clr-quintenary: hsl(0, 0%, 100%);
|
// $clr-quintenary: hsl(0, 0%, 100%);
|
||||||
|
|
|
@ -22,17 +22,13 @@ $_light: (
|
||||||
// 4.2 for borders, primarily
|
// 4.2 for borders, primarily
|
||||||
fill-muted: hsl(0, 0%, 80%),
|
fill-muted: hsl(0, 0%, 80%),
|
||||||
// shadows on cards
|
// 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
|
// keyboard, focus hightlight
|
||||||
highlight: hsl(176, 43%, 46%),
|
highlight: hsl(176, 43%, 46%),
|
||||||
// for gradient behind hero text on about page.
|
// for gradient behind hero text on about page.
|
||||||
gradient:
|
gradient:
|
||||||
(
|
(
|
||||||
radial-gradient(
|
radial-gradient(at 23% 32%, hsla(344, 79%, 40%, 0.15) 0px, transparent 70%),
|
||||||
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%)
|
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.
|
// 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;
|
.basicInfo {
|
||||||
|
|
||||||
.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 {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
@ -64,11 +11,3 @@
|
||||||
font-size: var(--fs-5);
|
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;
|
display: grid;
|
||||||
gap: var(--spacer-2);
|
gap: var(--spacer-2);
|
||||||
|
|
||||||
|
@ -18,8 +21,5 @@
|
||||||
padding: var(--spacer-2);
|
padding: var(--spacer-2);
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--spacer-4);
|
gap: var(--spacer-4);
|
||||||
// justify-self: start;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,57 +1,5 @@
|
||||||
@use '../../../abstracts' as helper;
|
@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,
|
.basicInfo,
|
||||||
.seriesInfo {
|
.seriesInfo {
|
||||||
|
@ -59,7 +7,7 @@
|
||||||
list-style: none;
|
list-style: none;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
& * + ::before {
|
& :not(:last-child)::after {
|
||||||
content: '\00b7';
|
content: '\00b7';
|
||||||
padding-inline: var(--spacer-1);
|
padding-inline: var(--spacer-1);
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
|
|
|
@ -1,97 +1,5 @@
|
||||||
@use '../../../abstracts' as helper;
|
@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 {
|
.title {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,53 +24,3 @@
|
||||||
--min-height: 30rem;
|
--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;
|
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,10 +23,10 @@
|
||||||
|
|
||||||
&__link {
|
&__link {
|
||||||
@include helper.prettify-link(var(--clr-link));
|
@include helper.prettify-link(var(--clr-link));
|
||||||
}
|
|
||||||
|
|
||||||
&__linkActive {
|
&[aria-current] {
|
||||||
@include helper.prettify-link(var(--clr-link), $animate: false);
|
@include helper.prettify-link(var(--clr-link), $animate: false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
import Redis from 'ioredis';
|
import Redis from 'ioredis';
|
||||||
|
|
||||||
const redisUrl = process.env.REDIS_URL;
|
const redisUrl = process.env.REDIS_URL;
|
||||||
const toUseRedis = process.env.USE_REDIS === 'true';
|
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);
|
const redis = toUseRedis && redisUrl ? new Redis(redisUrl) : stub;
|
||||||
else redis = null;
|
|
||||||
|
|
||||||
export default redis;
|
export default redis;
|
||||||
|
|
Loading…
Reference in a new issue