diff --git a/.prettierrc b/.prettierrc index 25453e9..323e777 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,5 +4,6 @@ "arrowParens": "avoid", "semi": true, "singleQuote": true, - "jsxSingleQuote": true + "jsxSingleQuote": true, + "printWidth": 100 } diff --git a/src/components/card/Card.tsx b/src/components/card/Card.tsx new file mode 100644 index 0000000..10209cc --- /dev/null +++ b/src/components/card/Card.tsx @@ -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 are correct based on the value of 'as' prop. +// a cheap implementation of as prop found in libraries like CharkaUI or MaterialUI. +type Props = { + children: ReactNode; + as?: T | 'section'; + hoverable?: true; +} & ComponentPropsWithoutRef; + +const Card = ({ + children, + as, + hoverable, + className, + ...rest +}: Props) => { + const Component = as ?? 'li'; + const classNames = `${hoverable ? styles.hoverable : ''} ${styles.card} ${className}`; + + return ( + + {children} + + ); +}; + +export default Card; diff --git a/src/components/card/CardBasic.tsx b/src/components/card/CardBasic.tsx new file mode 100644 index 0000000..eb702d0 --- /dev/null +++ b/src/components/card/CardBasic.tsx @@ -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 ( + +
+ {image ? ( + + ) : ( + + + + )} +
+
+

{title}

+ {children} +
+
+ ); +}; + +export default CardBasic; diff --git a/src/components/card/CardCast.tsx b/src/components/card/CardCast.tsx new file mode 100644 index 0000000..e40276c --- /dev/null +++ b/src/components/card/CardCast.tsx @@ -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 ( + + + +
+ {image ? ( + + ) : ( + + + + )} +
+
+

{name}

+

+ {characters?.join(', ')} + {attributes && ({attributes.join(', ')})} +

+ {children} +
+
+ +
+ ); +}; + +export default CardCast; diff --git a/src/components/card/CardResult.tsx b/src/components/card/CardResult.tsx new file mode 100644 index 0000000..8dfe838 --- /dev/null +++ b/src/components/card/CardResult.tsx @@ -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 ? ( + + ) : ( + + + + ); + + return ( + + + +
{ImageComponent}
+
+

{name}

+ {children} +
+
+ +
+ ); +}; + +export default CardResult; diff --git a/src/components/card/CardTitle.tsx b/src/components/card/CardTitle.tsx new file mode 100644 index 0000000..f7526a5 --- /dev/null +++ b/src/components/card/CardTitle.tsx @@ -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 ( + + + +
+ {image ? ( + + ) : ( + + + + )} +
+
+

{name}

+

+ {titleType} + {years && ` (${years})`} +

+ {ratings?.avg && ( +

+ {ratings.avg} + + + + ({formatNumber(ratings.numVotes)} votes) +

+ )} + {children} +
+
+ +
+ ); +}; + +export default CardTitle; diff --git a/src/components/card/index.tsx b/src/components/card/index.tsx new file mode 100644 index 0000000..5553931 --- /dev/null +++ b/src/components/card/index.tsx @@ -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'; diff --git a/src/components/error/ErrorInfo.tsx b/src/components/error/ErrorInfo.tsx index 58621f3..e42b55e 100644 --- a/src/components/error/ErrorInfo.tsx +++ b/src/components/error/ErrorInfo.tsx @@ -33,15 +33,12 @@ const ErrorInfo = ({ message, statusCode, misc }: Props) => { > GNU and Tux - 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. -

- {title} -

+

{title}

{misc ? ( <>

{misc.subtext}

@@ -52,7 +49,7 @@ const ErrorInfo = ({ message, statusCode, misc }: Props) => { ) : (

Go back to{' '} - + the homepage . diff --git a/src/components/find/Company.tsx b/src/components/find/Company.tsx index 969bbde..fddaa8b 100644 --- a/src/components/find/Company.tsx +++ b/src/components/find/Company.tsx @@ -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 ( -

  • - - {company.name} - - {company.country &&

    {company.country}

    } - {!!company.type &&

    {company.type}

    } -
  • - ); -}; +const Company = ({ company }: Props) => ( + + {company.country &&

    {company.country}

    } + {!!company.type &&

    {company.type}

    } +
    +); export default Company; diff --git a/src/components/find/Keyword.tsx b/src/components/find/Keyword.tsx index 6ca0856..5e4432b 100644 --- a/src/components/find/Keyword.tsx +++ b/src/components/find/Keyword.tsx @@ -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 ( -
  • - - {keyword.text} - - {keyword.numTitles &&

    {keyword.numTitles} titles

    } -
  • - ); -}; +const Keyword = ({ keyword }: Props) => ( + + {keyword.numTitles &&

    {keyword.numTitles} titles

    } +
    +); export default Keyword; diff --git a/src/components/find/Person.tsx b/src/components/find/Person.tsx index ee86037..34e5171 100644 --- a/src/components/find/Person.tsx +++ b/src/components/find/Person.tsx @@ -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 ( -
  • -
    - {person.image ? ( - {person.image.caption} - ) : ( - - - - )} -
    -
    - - {person.name} - - {person.aka &&

    {person.aka}

    } - {person.jobCateogry &&

    {person.jobCateogry}

    } - {(person.knownForTitle || person.knownInYear) && ( -
      - {person.knownForTitle &&
    • {person.knownForTitle}
    • } - {person.knownInYear &&
    • {person.knownInYear}
    • } -
    - )} -
    -
  • + +

    {person.aka}

    +

    {person.jobCateogry}

    +
      + {person.knownForTitle &&
    • {person.knownForTitle}
    • } + {person.knownInYear &&
    • {person.knownInYear}
    • } +
    +
    ); }; diff --git a/src/components/find/Title.tsx b/src/components/find/Title.tsx index e94678b..ebaabca 100644 --- a/src/components/find/Title.tsx +++ b/src/components/find/Title.tsx @@ -1,58 +1,36 @@ -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 ( -
  • -
    - {title.image ? ( - {title.image.caption} - ) : ( - - - - )} -
    -
    - - {title.name} - -
      - {title.type &&
    • {title.type}
    • } - {title.sAndE &&
    • {title.sAndE}
    • } - {title.releaseYear &&
    • {title.releaseYear}
    • } + +
        +
      • {title.type}
      • +
      • {title.sAndE}
      • +
      • {title.releaseYear}
      • +
      + {!!title.credits.length && ( +

      + Stars: + {title.credits.join(', ')} +

      + )} + {title.seriesId && ( +
        + {title.seriesType &&
      • {title.seriesType}
      • } +
      • + + {title.seriesName} + +
      • + {title.seriesReleaseYear &&
      • {title.seriesReleaseYear}
      • }
      - {!!title.credits.length && ( -

      - Stars: - {title.credits.join(', ')} -

      - )} - {title.seriesId && ( -
        - {title.seriesType &&
      • {title.seriesType}
      • } -
      • - - {title.seriesName} - -
      • - {title.seriesReleaseYear &&
      • {title.seriesReleaseYear}
      • } -
      - )} -
    -
  • + )} + ); }; diff --git a/src/components/find/index.tsx b/src/components/find/index.tsx index 4d1b53e..8946fe8 100644 --- a/src/components/find/index.tsx +++ b/src/components/find/index.tsx @@ -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 => + 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) => { ); - const { titles, people, keywords, companies, meta } = results!; + const { titles, people, keywords, companies, meta } = results; const titlesSectionHeading = getResTitleTypeHeading( meta.type, meta.titleType diff --git a/src/components/title/Media.tsx b/src/components/media/Media.tsx similarity index 83% rename from src/components/title/Media.tsx rename to src/components/media/Media.tsx index 868df64..464f214 100644 --- a/src/components/title/Media.tsx +++ b/src/components/media/Media.tsx @@ -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 (
    @@ -21,13 +23,9 @@ const Media = ({ className, media }: Props) => {
    diff --git a/src/components/meta/Meta.tsx b/src/components/meta/Meta.tsx index d6c7062..e068d88 100644 --- a/src/components/meta/Meta.tsx +++ b/src/components/meta/Meta.tsx @@ -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 ( @@ -30,10 +35,7 @@ const Meta = ({ - + ); }; diff --git a/src/components/title/Basic.tsx b/src/components/title/Basic.tsx index 0f5df9b..dfba9c3 100644 --- a/src/components/title/Basic.tsx +++ b/src/components/title/Basic.tsx @@ -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,135 +18,92 @@ const Basic = ({ data, className }: Props) => { : data.releaseYear?.start; return ( -
    -
    - {data.poster ? ( - {data.poster.caption} - ) : ( - - - +
      + {data.status && data.status.id !== 'released' && ( +
    • {data.status.text}
    • )} -
    -
    -

    - {data.title} -

    -
      - {data.status && data.status.id !== 'released' && ( -
    • {data.status.text}
    • - )} -
    • {data.type.name}
    • - {data.releaseYear && ( -
    • {releaseTime}
    • - )} - {data.ceritficate && ( -
    • {data.ceritficate}
    • - )} - {data.runtime && ( -
    • {formatTime(data.runtime)}
    • - )} -
    -
    - {data.ratings.avg && ( - <> -

    - {data.ratings.avg} - - - - Avg. rating -

    -

    - - {formatNumber(data.ratings.numVotes)} - - - - - No. of votes -

    - - )} - {data.ranking && ( +
  • {data.type.name}
  • + {data.releaseYear &&
  • {releaseTime}
  • } + {data.ceritficate &&
  • {data.ceritficate}
  • } + {data.runtime &&
  • {formatTime(data.runtime)}
  • } + +
    + {data.ratings.avg && ( + <>

    - - {formatNumber(data.ranking.position)} - + {data.ratings.avg} - + - - {' '} - Popularity ( - - {data.ranking.direction === 'UP' - ? `\u2191${formatNumber(data.ranking.change)}` - : data.ranking.direction === 'DOWN' - ? `\u2193${formatNumber(data.ranking.change)}` - : ''} - - ) - + Avg. rating

    - )} -
    - - {!!data.genres.length && ( -

    - Genres: - {data.genres.map((genre, i) => ( - - {i > 0 && ', '} - - {genre.text} - - - ))} +

    + {formatNumber(data.ratings.numVotes)} + + + + No. of votes +

    + + )} + {data.ranking && ( +

    + {formatNumber(data.ranking.position)} + + + + + {' '} + Popularity ( + + {data.ranking.direction === 'UP' + ? `\u2191${formatNumber(data.ranking.change)}` + : data.ranking.direction === 'DOWN' + ? `\u2193${formatNumber(data.ranking.change)}` + : ''} + + ) +

    )} - { -

    - Plot: - {data.plot || '-'} -

    - } - {data.primaryCrew.map(crewType => ( -

    - - {`${crewType.type.category}: `} - - {crewType.crew.map((crew, i) => ( - - {i > 0 && ', '} - - {crew.name} - - - ))} -

    - ))}
    -
    + + {!!data.genres.length && ( +

    + Genres: + {data.genres.map((genre, i) => ( + + {i > 0 && ', '} + + {genre.text} + + + ))} +

    + )} +

    + Plot: + {data.plot || '-'} +

    + {data.primaryCrew.map(crewType => ( +

    + {`${crewType.type.category}: `} + {crewType.crew.map((crew, i) => ( + + {i > 0 && ', '} + + {crew.name} + + + ))} +

    + ))} + ); }; diff --git a/src/components/title/Cast.tsx b/src/components/title/Cast.tsx index 563da35..2e6b9c9 100644 --- a/src/components/title/Cast.tsx +++ b/src/components/title/Cast.tsx @@ -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 (

    Cast

      {cast.map(member => ( -
    • -
      - {member.image ? ( - - ) : ( - - - - )} -
      -
      -

      - - {member.name} - -

      -

      - {member.characters?.join(', ')} - {member.attributes && ( - ({member.attributes.join(', ')}) - )} -

      -
      -
    • + ))}
    ); }; + export default Cast; diff --git a/src/components/title/MoreLikeThis.tsx b/src/components/title/MoreLikeThis.tsx index 60bc726..5364d21 100644 --- a/src/components/title/MoreLikeThis.tsx +++ b/src/components/title/MoreLikeThis.tsx @@ -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 (

    More like this

    diff --git a/src/components/title/index.tsx b/src/components/title/index.tsx index 55ea4bf..8e391d9 100644 --- a/src/components/title/index.tsx +++ b/src/components/title/index.tsx @@ -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'; diff --git a/src/context/theme-context.tsx b/src/context/theme-context.tsx index 0724496..d959da1 100644 --- a/src/context/theme-context.tsx +++ b/src/context/theme-context.tsx @@ -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) => { }, }; 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; diff --git a/src/interfaces/shared/index.ts b/src/interfaces/shared/index.ts new file mode 100644 index 0000000..779c9b0 --- /dev/null +++ b/src/interfaces/shared/index.ts @@ -0,0 +1,3 @@ +import type Name from './name'; + +export type Media = Name['media']; // exactly the same in title and name diff --git a/src/interfaces/shared/title.ts b/src/interfaces/shared/title.ts index 4380a77..2490570 100644 --- a/src/interfaces/shared/title.ts +++ b/src/interfaces/shared/title.ts @@ -1,8 +1,6 @@ import cleanTitle from 'src/utils/cleaners/title'; import title from 'src/utils/fetchers/title'; -export type AxiosTitleRes = Awaited>; - // for full title type Title = ReturnType; export type { Title as default }; diff --git a/src/layouts/Footer.tsx b/src/layouts/Footer.tsx index 5001a86..2db0f3f 100644 --- a/src/layouts/Footer.tsx +++ b/src/layouts/Footer.tsx @@ -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 (