From 75732e00869f9777e87e767a48648996345f02f7 Mon Sep 17 00:00:00 2001 From: zyachel Date: Sat, 15 Apr 2023 21:02:10 +0530 Subject: [PATCH] feat(route): add name route adds much needed route fix https://github.com/zyachel/libremdb/issues/39, https://github.com/zyachel/libremdb/issues/36, https://codeberg.org/zyachel/libremdb/issues/11 --- README.md | 2 +- src/components/name/Basic.tsx | 57 + src/components/name/Bio.tsx | 12 + src/components/name/Credits.tsx | 73 ++ src/components/name/DidYouKnow.tsx | 53 + src/components/name/Info.tsx | 192 +++ src/components/name/KnownFor.tsx | 34 + src/components/name/index.tsx | 6 + src/interfaces/misc/rawName.ts | 1084 +++++++++++++++++ src/interfaces/shared/name.ts | 16 + src/pages/name/[nameId]/index.tsx | 62 + .../modules/components/name/basic.module.scss | 54 + .../components/name/credits.module.scss | 49 + .../components/name/did-you-know.module.scss | 4 + .../modules/components/name/info.module.scss | 21 + .../components/name/known-for.module.scss | 32 + .../components/name/reviews.module.scss | 26 + .../modules/pages/name/name.module.scss | 64 + src/utils/cleaners/name.ts | 281 +++++ src/utils/cleaners/title.ts | 2 +- src/utils/fetchers/name.ts | 28 + 21 files changed, 2150 insertions(+), 2 deletions(-) create mode 100644 src/components/name/Basic.tsx create mode 100644 src/components/name/Bio.tsx create mode 100644 src/components/name/Credits.tsx create mode 100644 src/components/name/DidYouKnow.tsx create mode 100644 src/components/name/Info.tsx create mode 100644 src/components/name/KnownFor.tsx create mode 100644 src/components/name/index.tsx create mode 100644 src/interfaces/misc/rawName.ts create mode 100644 src/interfaces/shared/name.ts create mode 100644 src/pages/name/[nameId]/index.tsx create mode 100644 src/styles/modules/components/name/basic.module.scss create mode 100644 src/styles/modules/components/name/credits.module.scss create mode 100644 src/styles/modules/components/name/did-you-know.module.scss create mode 100644 src/styles/modules/components/name/info.module.scss create mode 100644 src/styles/modules/components/name/known-for.module.scss create mode 100644 src/styles/modules/components/name/reviews.module.scss create mode 100644 src/styles/modules/pages/name/name.module.scss create mode 100644 src/utils/cleaners/name.ts create mode 100644 src/utils/fetchers/name.ts diff --git a/README.md b/README.md index 65efd44..cf62d0f 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ Inspired by projects like [teddit](https://codeberg.org/teddit/teddit), [nitter] - [ ] lists - [ ] moviemeter - - [ ] person info(includes directors and actors) + - [x] person info(includes directors and actors) - [ ] company info - [ ] user info diff --git a/src/components/name/Basic.tsx b/src/components/name/Basic.tsx new file mode 100644 index 0000000..b5ae294 --- /dev/null +++ b/src/components/name/Basic.tsx @@ -0,0 +1,57 @@ +import { CardBasic } from 'src/components/card'; +import { Basic as BasicType } from 'src/interfaces/shared/name'; +import { formatNumber } from 'src/utils/helpers'; +import styles from 'src/styles/modules/components/name/basic.module.scss'; + +type Props = { + className: string; + data: BasicType; +}; + +const Basic = ({ data, className }: Props) => { + return ( + +
+ {data.ranking && ( +

+ {formatNumber(data.ranking.position)} + + + + + {' '} + Popularity ( + {getRankingStats(data.ranking)}) + +

+ )} +
+ + {!!data.primaryProfessions.length && ( +

+ Profession: + {data.primaryProfessions.join(', ')} +

+ )} + { +

+ About: + {data.bio.short}... +

+ } +

+ Known for: + {data.knownFor.title} ({data.knownFor.role}) +

+
+ ); +}; + +const getRankingStats = (ranking: NonNullable) => { + if (ranking.direction === 'FLAT') return '\u2192'; + + const change = formatNumber(ranking.change); + return (ranking.direction === 'UP' ? '\u2191' : '\u2193') + change; +}; + +export default Basic; diff --git a/src/components/name/Bio.tsx b/src/components/name/Bio.tsx new file mode 100644 index 0000000..a362279 --- /dev/null +++ b/src/components/name/Bio.tsx @@ -0,0 +1,12 @@ +import styles from 'src/styles/modules/components/name/did-you-know.module.scss'; + +type Props = { bio: string }; + +const Bio = ({ bio }: Props) => ( +
+

About

+
+
+); + +export default Bio; diff --git a/src/components/name/Credits.tsx b/src/components/name/Credits.tsx new file mode 100644 index 0000000..ea93e49 --- /dev/null +++ b/src/components/name/Credits.tsx @@ -0,0 +1,73 @@ +import { Credits } from 'src/interfaces/shared/name'; +import { CardTitle } from 'src/components/card'; +import styles from 'src/styles/modules/components/name/credits.module.scss'; + +type Props = { + className: string; + data: Credits; +}; + +const Credits = ({ className, data }: Props) => { + if (!data.total) return null; + + return ( +
+

Credits

+
+

Released

+ {data.released.map( + (item, i) => + !!item.total && ( +
+ + {item.category.text} ({item.total}) + +
    + {item.titles.map(title => ( + + ))} +
+
+ ) + )} +
+
+

Unreleased

+ {data.unreleased.map( + (item, i) => + !!item.total && ( +
+ + {item.category.text} ({item.total}) + +
    + {item.titles.map(title => ( + +

    {title.productionStatus}

    +
    + ))} +
+
+ ) + )} +
+
+ ); +}; + +export default Credits; diff --git a/src/components/name/DidYouKnow.tsx b/src/components/name/DidYouKnow.tsx new file mode 100644 index 0000000..1578cd0 --- /dev/null +++ b/src/components/name/DidYouKnow.tsx @@ -0,0 +1,53 @@ +import Link from 'next/link'; +import { DidYouKnow } from 'src/interfaces/shared/name'; +import styles from 'src/styles/modules/components/name/did-you-know.module.scss'; + +type Props = { + data: DidYouKnow; +}; + +const DidYouKnow = ({ data }: Props) => ( +
+

Did you know

+
+ {!!data.trivia?.total && ( +
+

Trivia

+
+
+ )} + {!!data.quotes?.total && ( +
+

Quotes

+
+
+ )} + {!!data.trademark?.total && ( +
+

Trademark

+
+
+ )} + {!!data.nicknames.length && ( +
+

Nicknames

+

{data.nicknames.join(', ')}

+
+ )} + {!!data.salary?.total && ( +
+

Salary

+

+ {data.salary.value} in + + {data.salary.title.text} + + ({data.salary.title.year}) +

+
+ )} +
+
+); + +export default DidYouKnow; diff --git a/src/components/name/Info.tsx b/src/components/name/Info.tsx new file mode 100644 index 0000000..8b9b02e --- /dev/null +++ b/src/components/name/Info.tsx @@ -0,0 +1,192 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import Name, { PersonalDetails } from 'src/interfaces/shared/name'; +import styles from 'src/styles/modules/components/name/info.module.scss'; + +type Props = { + info: PersonalDetails; + accolades: Name['accolades']; +}; + +const PersonalDetails = ({ info, accolades }: Props) => { + const { + query: { nameId }, + } = useRouter(); + + return ( +
+
+

Accolades

+
+ {accolades.awards && ( +

+ + Won {accolades.awards.wins} {accolades.awards.name} + + (out of {accolades.awards.nominations} nominations) +

+ )} +

+ {accolades.wins} wins and {accolades.nominations} nominations in total +

+

+ + View all awards + +

+
+
+ +
+

Personal details

+
+ {!!info.officialSites.length && ( +

+ Official sites: + {info.officialSites.map((site, i) => ( + + {!!i && ', '} + + {site.name} + + + ))} +

+ )} + {!!info.alsoKnownAs.length && ( +

+ Also known as: + {info.alsoKnownAs.join(', ')} +

+ )} + {info.height && ( +

+ Height: + {info.height} +

+ )} + {info.birth && ( +

+ Born: + {info.birth.date} + + {' '} + in{' '} + + {info.birth.location} + + +

+ )} + {info.death.date && ( +

+ Died: + {info.death.date} + + {' '} + in{' '} + + {info.death.location} + + +

+ )} + {info.death.cause && ( +

+ Death cause: + {info.death.cause} +

+ )} + {!!info.spouses?.length && ( +

+ Spouses: + {info.spouses.map((spouse, i) => ( + + <> + {!!i && ', '} + {renderPersonNameWithLink(spouse)} {spouse.range} ( + {spouse.attributes.join(', ')}) + + + ))} +

+ )} + {!!info.children?.length && ( +

+ Children: + {info.children.map((child, i) => ( + + <> + {!!i && ', '} + {renderPersonNameWithLink(child)} + + + ))} +

+ )} + {!!info.parents?.length && ( +

+ Parents: + {info.parents.map((parent, i) => ( + + <> + {!!i && ', '} + {renderPersonNameWithLink(parent)} + + + ))} +

+ )} + {!!info.relatives?.length && ( +

+ Relatives: + {info.relatives.map((relative, i) => ( + + <> + {!!i && ', '} + {renderPersonNameWithLink(relative)} ({relative.relation}) + + + ))} +

+ )} + {!!info.otherWorks?.length && ( +

+ Other Works: + {info.otherWorks.map((work, i) => ( + + <> + {!!i && ', '} + + + + ))} +

+ )} + {!!info.publicity.total && ( +

+ Publicity Listings: + {info.publicity.articles} Articles,{' '} + {info.publicity.interviews} Interviews,{' '} + {info.publicity.magazines} Magazines,{' '} + {info.publicity.pictorials} Pictorials,{' '} + {info.publicity.printBiographies} Print biographies, and{' '} + {info.publicity.filmBiographies} Biographies +

+ )} +
+
+
+ ); +}; + +export default PersonalDetails; + +const renderPersonNameWithLink = (person: { name: string; id: string | null }) => + person.id ? ( + + {person.name} + + ) : ( + {person.name} + ); diff --git a/src/components/name/KnownFor.tsx b/src/components/name/KnownFor.tsx new file mode 100644 index 0000000..050e86f --- /dev/null +++ b/src/components/name/KnownFor.tsx @@ -0,0 +1,34 @@ +import type { KnownFor as KnownForType } from 'src/interfaces/shared/name'; +import { CardTitle } from 'src/components/card'; +import styles from 'src/styles/modules/components/name/known-for.module.scss'; + +type Props = { data: KnownForType }; + +const KnownFor = ({ data }: Props) => { + if (!data.length) return null; + + return ( +
+

Known For

+
    + {data.map(title => ( + +

    {getRoles(title)}

    +
    + ))} +
+
+ ); +}; + +const getRoles = (title: Props['data'][number]) => + (title.summary.characters ?? title.summary.jobs)?.join(', '); + +export default KnownFor; diff --git a/src/components/name/index.tsx b/src/components/name/index.tsx new file mode 100644 index 0000000..40d1632 --- /dev/null +++ b/src/components/name/index.tsx @@ -0,0 +1,6 @@ +export { default as Basic } from './Basic'; +export { default as DidYouKnow } from './DidYouKnow'; +export { default as Info } from './Info'; +export { default as Credits } from './Credits'; +export { default as KnownFor } from './KnownFor'; +export { default as Bio } from './Bio'; diff --git a/src/interfaces/misc/rawName.ts b/src/interfaces/misc/rawName.ts new file mode 100644 index 0000000..c3c5c36 --- /dev/null +++ b/src/interfaces/misc/rawName.ts @@ -0,0 +1,1084 @@ +export default interface Name { + props: { + pageProps: { + aboveTheFold: { + id: string; + nameText: { + text: string; + }; + disambiguator?: { + text: string; + }; + /* + searchIndexing: { + disableIndexing: boolean + }*/ + knownFor: { + edges: Array<{ + node: { + title: { + titleText: { + text: string; + }; + }; + summary: { + principalCategory: { + text: string; + }; + }; + }; + }>; + }; + /* + images: { + total: number; + };*/ + + primaryImage: { + id: string; + url: string; + height: number; + width: number; + caption: { + plainText: string; + }; + }; + /* + meta: { + canonicalId: string + publicationStatus: string + } + */ + + primaryProfessions: Array<{ + category: { + text: string; + }; + }>; + bio: { + text: { + plainText: string; + plaidHtml: string; + }; + }; + birthDate: { + displayableProperty: { + value: { + plainText: string; + }; + }; + date?: string; + dateComponents: { + day?: number; + month?: number; + year: number; + isBCE: boolean; + }; + }; + deathDate?: { + displayableProperty: { + value: { + plainText: string; + }; + }; + date?: string; + dateComponents: { + day?: number; + month?: number; + year: number; + }; + }; + deathStatus: 'ALIVE' | 'DEAD'; + meterRanking?: { + currentRank: number; + rankChange: { + changeDirection: 'UP' | 'DOWN' | 'FLAT'; + difference: number; + }; + }; + subNavBio: { + id: string; + }; + subNavTrivia: { + total: number; + }; + subNavAwardNominations: { + total: number; + }; + // videos: { + // total: number; + // }; + primaryVideos: { + edges: Array<{ + node: { + id: string; + isMature: boolean; + createdDate: string; + contentType: { + id: string; + displayName: { + value: string; + }; + }; + thumbnail: { + url: string; + height: number; + width: number; + }; + runtime: { + value: number; + }; + description?: { + value: string; + language: string; + }; + name: { + value: string; + language: string; + }; + playbackURLs: Array<{ + displayName: { + value: string; + language: string; + }; + mimeType: string; + url: string; + }>; + recommendedTimedTextTrack?: { + displayName: { + value: string; + language: string; + }; + refTagFragment: string; + language: string; + url: string; + }; + timedTextTracks: Array<{ + displayName: { + value: string; + language: string; + }; + refTagFragment: string; + language: string; + url: string; + }>; + previewURLs: Array<{ + displayName: { + value: string; + language: string; + }; + mimeType: string; + url: string; + }>; + primaryTitle: { + originalTitleText: { + text: string; + }; + titleText: { + text: string; + }; + releaseYear: { + year: number; + endYear: null; + }; + titleType: { + canHaveEpisodes: boolean; + }; + }; + }; + }>; + }; + }; + mainColumnData: { + id: string; + wins: { + total: number; + }; + nominations: { + total: number; + }; + prestigiousAwardSummary?: { + nominations: number; + wins: number; + award: { + text: string; + id: string; + event: { + id: string; + }; + }; + }; + images: { + total: number; + edges: Array<{ + node: { + id: string; + url: string; + caption: { + plainText: string; + }; + height: number; + width: number; + }; + }>; + }; + // primaryImage: { + // id: string; + // caption: { + // plainText: string; + // }; + // height: number; + // width: number; + // url: string; + // }; + // imageUploadLink: null; + // nameText: { + // text: string; + // }; + knownFor: { + edges: Array<{ + node: { + summary: { + attributes?: Array<{ + text: string; + }>; + episodeCount?: number; + principalCategory: { + text: string; + id: string; + }; + principalCharacters?: Array<{ + name: string; + }>; + principalJobs?: Array<{ + id: string; + text: string; + }>; + yearRange: { + year: number; + endYear?: number; + }; + }; + credit: { + attributes?: Array<{ + text: string; + }>; + category: { + id: string; + text: string; + }; + characters?: Array<{ + name: string; + }>; + episodeCredits: { + total: number; + yearRange?: { + year: number; + endYear: number; + }; + displayableYears: { + total: number; + edges: Array<{ + node: { + year: string; + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + }>; + }; + displayableSeasons: { + total: number; + edges: Array<{ + node: { + season: string; + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + }>; + }; + }; + jobs?: Array<{ + id: string; + text: string; + }>; + }; + title: { + id: string; + canRate: { + isRatable: boolean; + }; + certificate?: { + rating: string; + }; + originalTitleText: { + text: string; + }; + titleText: { + text: string; + }; + titleType: { + canHaveEpisodes: boolean; + displayableProperty: { + value: { + plainText: string; + }; + }; + text: string; + id: 'movie' | 'tvSeries' | 'tvEpisode' | 'videoGame'; + }; + primaryImage: { + id: string; + url: string; + height: number; + width: number; + caption: { + plainText: string; + }; + }; + ratingsSummary: { + aggregateRating?: number; + voteCount: number; + }; + latestTrailer?: { + id: string; + }; + releaseYear: { + year: number; + endYear?: number; + }; + runtime?: { + seconds: number; + }; + series: null; + episodes?: { + displayableSeasons: { + total: number; + edges: Array<{ + node: { + season: string; + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + }>; + }; + displayableYears: { + total: number; + edges: Array<{ + node: { + year: string; + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + }>; + }; + }; + titleGenres: { + genres: Array<{ + genre: { + text: string; + }; + }>; + }; + productionStatus: { + currentProductionStage: { + id: string; + text: string; + }; + }; + }; + }; + }>; + }; + primaryProfessions: Array<{ + category: { + text: string; + id: string; + }; + }>; + releasedPrimaryCredits: Array<{ + category: { + id: string; + text: string; + }; + credits: { + total: number; + edges: Array<{ + node: { + attributes?: Array<{ + text: string; + }>; + category: { + id: string; + text: string; + }; + characters?: Array<{ + name: string; + }>; + episodeCredits: { + total: number; + yearRange?: { + year: number; + endYear?: number; + }; + displayableYears: { + total: number; + edges: Array<{ + node: { + year: string; + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + }>; + }; + displayableSeasons: { + total: number; + edges: Array<{ + node: { + season: string; + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + }>; + }; + }; + title: { + id: string; + canRate: { + isRatable: boolean; + }; + certificate?: { + rating: string; + }; + originalTitleText: { + text: string; + }; + titleText: { + text: string; + }; + titleType: { + canHaveEpisodes: boolean; + displayableProperty: { + value: { + plainText: string; + }; + }; + text: string; + id: string; + }; + primaryImage?: { + id: string; + url: string; + height: number; + width: number; + caption: { + plainText: string; + }; + }; + ratingsSummary: { + aggregateRating?: number; + voteCount: number; + }; + latestTrailer?: { + id: string; + }; + releaseYear: { + year: number; + endYear?: number; + }; + runtime?: { + seconds: number; + }; + series: null; + episodes?: { + displayableSeasons: { + total: number; + edges: Array<{ + node: { + season: string; + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + }>; + }; + displayableYears: { + total: number; + edges: Array<{ + node: { + year: string; + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + }>; + }; + }; + titleGenres: { + genres: Array<{ + genre: { + text: string; + }; + }>; + }; + productionStatus: { + currentProductionStage: { + id: string; + text: string; + }; + }; + }; + jobs?: Array<{ + id: string; + text: string; + }>; + }; + }>; + // pageInfo: { + // hasNextPage: boolean; + // hasPreviousPage: boolean; + // endCursor: string; + // }; + }; + }>; + unreleasedPrimaryCredits: Array<{ + category: { + id: string; + text: string; + }; + credits: { + total: number; + edges: Array<{ + node: { + attributes?: Array<{ + text: string; + }>; + category: { + id: string; + text: string; + }; + characters?: Array<{ + name: string; + }>; + episodeCredits: { + total: number; + yearRange: null; + displayableYears: { + total: number; + edges: Array<{ + node: { + year: string; + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + }>; + }; + displayableSeasons: { + total: number; + edges: Array<{ + node: { + season: string; + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + }>; + }; + }; + title: { + id: string; + canRate: { + isRatable: boolean; + }; + certificate?: { + rating: string; + }; + originalTitleText: { + text: string; + }; + titleText: { + text: string; + }; + titleType: { + canHaveEpisodes: boolean; + displayableProperty: { + value: { + plainText: string; + }; + }; + text: string; + id: string; + }; + primaryImage?: { + id: string; + url: string; + height: number; + width: number; + caption: { + plainText: string; + }; + }; + ratingsSummary: { + aggregateRating: null; + voteCount: number; + }; + latestTrailer?: { + id: string; + }; + releaseYear?: { + year: number; + endYear: null; + }; + runtime?: { + seconds: number; + }; + series: null; + episodes?: { + displayableSeasons: { + total: number; + edges: Array<{ + node: { + season: string; + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + }>; + }; + displayableYears: { + total: number; + edges: Array<{ + node: { + year: string; + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + }>; + }; + }; + titleGenres: { + genres: Array<{ + genre: { + text: string; + }; + }>; + }; + productionStatus: { + currentProductionStage: { + id: string; + text: string; + }; + }; + }; + jobs?: Array<{ + id: string; + text: string; + }>; + }; + }>; + // pageInfo: { + // hasNextPage: boolean; + // hasPreviousPage: boolean; + // endCursor: string; + // }; + }; + }>; + jobs: Array<{ + category: { + id: string; + text: string; + }; + credits: { + total: number; + }; + }>; + totalCredits: { + total: number; + // restriction?: { + // unrestrictedTotal: number; + // explanations: Array<{ + // reason: string; + // text: string; + // }>; + // }; + }; + creditSummary: { + titleTypeCategories: Array<{ + total: number; + titleTypeCategory: { + id: string; + text: string; + }; + }>; + genres: Array<{ + total: number; + genre: { + id: string; + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + }>; + }; + videos: { + total: number; + edges: Array<{ + node: { + id: string; + contentType: { + displayName: { + value: string; + }; + }; + name: { + value: string; + }; + runtime: { + value: number; + }; + thumbnail: { + height: number; + url: string; + width: number; + }; + primaryTitle: { + originalTitleText: { + text: string; + }; + titleText: { + text: string; + }; + releaseYear: { + year: number; + endYear?: number; + }; + titleType: { + canHaveEpisodes: boolean; + }; + }; + }; + }>; + }; + height?: { + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + // birthDate: { + // dateComponents: { + // day?: number; + // month?: number; + // year: number; + // }; + // displayableProperty: { + // value: { + // plainText: string; + // }; + // }; + // }; + birthLocation: { + text: string; + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + // deathDate?: { + // dateComponents: { + // day?: number; + // month?: number; + // year: number; + // }; + // displayableProperty: { + // value: { + // plainText: string; + // }; + // }; + // }; + deathLocation?: { + text: string; + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + deathCause?: { + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + akas: { + edges: Array<{ + node: { + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + }>; + }; + otherWorks: { + edges: Array<{ + node: { + category?: { + text: string; + }; + text: { + plaidHtml: string; + }; + }; + }>; + total: number; + }; + personalDetailsSpouses?: Array<{ + spouse: { + name?: { + id: string; + nameText: { + text: string; + }; + }; + asMarkdown: { + plainText: string; + }; + }; + attributes: Array<{ + id: string; + text: string; + }>; + timeRange: { + displayableProperty: { + value: { + plaidHtml: string; + }; + }; + }; + }>; + parents: { + total: number; + // pageInfo: { + // hasNextPage: boolean; + // endCursor?: string; + // }; + edges: Array<{ + node: { + relationshipType: { + id: string; + text: string; + }; + relationName: { + name?: { + id: string; + }; + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + }; + }>; + }; + children: { + total: number; + // pageInfo: { + // hasNextPage: boolean; + // endCursor?: string; + // }; + edges: Array<{ + node: { + relationshipType: { + id: string; + text: string; + }; + relationName: { + name?: { + id: string; + }; + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + }; + }>; + }; + others: { + total: number; + // pageInfo: { + // hasNextPage: boolean; + // endCursor?: string; + // }; + edges: Array<{ + node: { + relationshipType: { + id: string; + text: string; + }; + relationName: { + name: { + id: string; + }; + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + }; + }>; + }; + personalDetailsExternalLinks: { + edges: Array<{ + node: { + url: string; + label: string; + }; + }>; + total: number; + }; + publicityListings: { + total: number; + }; + nameFilmBiography: { + total: number; + }; + namePrintBiography: { + total: number; + }; + namePortrayal: { + total: number; + }; + publicityInterview: { + total: number; + }; + publicityArticle: { + total: number; + }; + publicityPictorial: { + total: number; + }; + publicityMagazineCover: { + total: number; + }; + demographicData: Array; + triviaTotal: { + total: number; + }; + trivia: { + edges: Array<{ + node: { + displayableArticle: { + body: { + plaidHtml: string; + }; + }; + }; + }>; + }; + quotesTotal: { + total: number; + }; + quotes: { + edges: Array<{ + node: { + displayableArticle: { + body: { + plaidHtml: string; + }; + }; + }; + }>; + }; + trademarksTotal: { + total: number; + }; + trademarks: { + edges: Array<{ + node: { + displayableArticle: { + body: { + plaidHtml: string; + }; + }; + }; + }>; + }; + nickNames: Array<{ + displayableProperty: { + value: { + plainText: string; + }; + }; + }>; + titleSalariesTotal: { + total: number; + }; + titleSalaries: { + edges: Array<{ + node: { + title: { + id: string; + titleText: { + text: string; + }; + originalTitleText: { + text: string; + }; + releaseYear: { + year: number; + }; + }; + displayableProperty: { + value: { + plainText: string; + }; + }; + }; + }>; + }; + }; + }; + }; +} diff --git a/src/interfaces/shared/name.ts b/src/interfaces/shared/name.ts new file mode 100644 index 0000000..04b743b --- /dev/null +++ b/src/interfaces/shared/name.ts @@ -0,0 +1,16 @@ +import cleanName from 'src/utils/cleaners/name'; + +type Name = ReturnType; +export type { Name as default }; + +export type Basic = Name['basic']; + +export type Media = Name['media']; + +export type Credits = Name['credits']; + +export type DidYouKnow = Name['didYouKnow']; + +export type PersonalDetails = Name['personalDetails']; + +export type KnownFor = Name['knownFor']; diff --git a/src/pages/name/[nameId]/index.tsx b/src/pages/name/[nameId]/index.tsx new file mode 100644 index 0000000..e5a6d07 --- /dev/null +++ b/src/pages/name/[nameId]/index.tsx @@ -0,0 +1,62 @@ +import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; +import Meta from 'src/components/meta/Meta'; +import Layout from 'src/layouts/Layout'; +import ErrorInfo from 'src/components/error/ErrorInfo'; +import Media from 'src/components/media/Media'; +import { Basic, Credits, DidYouKnow, Info, Bio, KnownFor } from 'src/components/name'; +import Name from 'src/interfaces/shared/name'; +import { AppError } from 'src/interfaces/shared/error'; +import name from 'src/utils/fetchers/name'; +import { getProxiedIMDbImgUrl } from 'src/utils/helpers'; +import styles from 'src/styles/modules/pages/name/name.module.scss'; + +type Props = InferGetServerSidePropsType; + +const NameInfo = ({ data, error }: Props) => { + if (error) return ; + + return ( + <> + + + + +
+ + +
+
+ + +
+ +
+ + ); +}; + +type Data = { data: Name; error: null } | { error: AppError; data: null }; +type Params = { nameId: string }; + +export const getServerSideProps: GetServerSideProps = async ctx => { + const nameId = ctx.params!.nameId; + + try { + const data = await name(nameId); + + return { props: { data, error: null } }; + } catch (error: any) { + const { message, statusCode } = error; + + ctx.res.statusCode = statusCode; + ctx.res.statusMessage = message; + + return { props: { error: { message, statusCode }, data: null } }; + } +}; + +export default NameInfo; diff --git a/src/styles/modules/components/name/basic.module.scss b/src/styles/modules/components/name/basic.module.scss new file mode 100644 index 0000000..847e04c --- /dev/null +++ b/src/styles/modules/components/name/basic.module.scss @@ -0,0 +1,54 @@ +@use '../../../abstracts' as helper; + +.ratings { + display: flex; + flex-wrap: wrap; + gap: var(--spacer-0) var(--spacer-3); + + @include helper.bp('bp-900') { + justify-content: center; + } +} + +.rating { + font-size: var(--fs-5); + + display: grid; + grid-template-columns: repeat(2, max-content); + place-items: center; + gap: 0 var(--spacer-0); + + &__num { + grid-column: 1 / 2; + font-size: 1.8em; + font-weight: var(--fw-medium); + // line-height: 1; + } + + &__icon { + --dim: 1.8em; + grid-column: -2 / -1; + line-height: 1; + height: var(--dim); + width: var(--dim); + display: grid; + place-content: center; + fill: var(--clr-fill); + } + + &__text { + grid-column: 1 / -1; + font-size: 0.9em; + line-height: 1; + + color: var(--clr-text-muted); + } +} + +.link { + @include helper.prettify-link(var(--clr-link)); +} + +.heading { + font-weight: var(--fw-bold); +} diff --git a/src/styles/modules/components/name/credits.module.scss b/src/styles/modules/components/name/credits.module.scss new file mode 100644 index 0000000..395b3c2 --- /dev/null +++ b/src/styles/modules/components/name/credits.module.scss @@ -0,0 +1,49 @@ +@use '../../../abstracts' as helper; + +.credits { + display: grid; + gap: var(--comp-whitespace); + + & > section { + overflow-x: auto; + display: grid; + gap: var(--spacer-1); + } + + details { + overflow-x: auto; + } + + summary { + cursor: pointer; + font-size: var(--fs-4); + color: var(--clr-text-accent); + font-family: var(--ff-primary); + } +} + +.container { + --max-width: 18rem; + --min-height: 40rem; + + list-style: none; + overflow-x: auto; + + display: grid; + grid-auto-flow: column; + // grid-template-columns: repeat(2, 1fr); + gap: var(--spacer-4); + padding: var(--spacer-1) var(--spacer-2) var(--spacer-3) var(--spacer-2); + + grid-auto-columns: var(--max-width); + min-height: var(--min-height); + + > li { + list-style: none; + } + + @include helper.bp('bp-700') { + grid-auto-columns: calc(var(--max-width) - 1rem); + min-height: calc(var(--min-height) - 5rem); + } +} diff --git a/src/styles/modules/components/name/did-you-know.module.scss b/src/styles/modules/components/name/did-you-know.module.scss new file mode 100644 index 0000000..28ae062 --- /dev/null +++ b/src/styles/modules/components/name/did-you-know.module.scss @@ -0,0 +1,4 @@ +.bio { + display: grid; + gap: var(--comp-whitespace); +} diff --git a/src/styles/modules/components/name/info.module.scss b/src/styles/modules/components/name/info.module.scss new file mode 100644 index 0000000..b1a0a69 --- /dev/null +++ b/src/styles/modules/components/name/info.module.scss @@ -0,0 +1,21 @@ +.info { + display: grid; + + gap: var(--doc-whitespace); +} + +.accolades, .details { + display: grid; + gap: var(--comp-whitespace); + + &__container { + display: grid; + gap: var(--spacer-0); + + // for span elements like these: 'release date:' + & > p > span:first-of-type { + font-weight: var(--fw-bold); + } + } +} + diff --git a/src/styles/modules/components/name/known-for.module.scss b/src/styles/modules/components/name/known-for.module.scss new file mode 100644 index 0000000..2383504 --- /dev/null +++ b/src/styles/modules/components/name/known-for.module.scss @@ -0,0 +1,32 @@ +@use '../../../abstracts' as helper; + +.knownFor { + display: grid; + gap: var(--comp-whitespace); +} + +.container { + --max-width: 18rem; + --min-height: 40rem; + + list-style: none; + overflow-x: auto; + + display: grid; + grid-auto-flow: column; + // grid-template-columns: repeat(2, 1fr); + gap: var(--spacer-4); + padding: 0 var(--spacer-2) var(--spacer-3) var(--spacer-2); + + grid-auto-columns: var(--max-width); + min-height: var(--min-height); + + > li { + list-style: none; + } + + @include helper.bp('bp-700') { + grid-auto-columns: calc(var(--max-width) - 1rem); + min-height: calc(var(--min-height) - 5rem); + } +} diff --git a/src/styles/modules/components/name/reviews.module.scss b/src/styles/modules/components/name/reviews.module.scss new file mode 100644 index 0000000..685748a --- /dev/null +++ b/src/styles/modules/components/name/reviews.module.scss @@ -0,0 +1,26 @@ +.reviews { + display: grid; + gap: var(--comp-whitespace); + + &__reviewContainer { + // background-color: antiquewhite; + } + + &__stats { + display: flex; + flex-wrap: wrap; + gap: var(--spacer-2); + } +} + +.review { + &__summary { + font-size: calc(var(--fs-5) * 1.1); + cursor: pointer; + } + + &__text, + &__metadata { + padding-top: var(--spacer-2); + } +} diff --git a/src/styles/modules/pages/name/name.module.scss b/src/styles/modules/pages/name/name.module.scss new file mode 100644 index 0000000..53467b6 --- /dev/null +++ b/src/styles/modules/pages/name/name.module.scss @@ -0,0 +1,64 @@ +@use '../../../abstracts' as helper; + +.name { + --doc-whitespace: var(--spacer-8); + --comp-whitespace: var(--spacer-3); + + display: grid; + + gap: var(--doc-whitespace); + padding: var(--doc-whitespace); + align-items: start; + + grid-template-columns: repeat(8, 1fr); + grid-template-areas: + 'basic basic basic basic basic basic basic basic' + 'media media media media media media media media' + 'text text text text text info info info' + 'credits credits credits credits credits credits credits credits'; + + @include helper.bp('bp-1200') { + grid-template-columns: none; + grid-template-areas: + 'basic' + 'media' + 'known' + 'text' + 'info' + 'credits'; + } + + @include helper.bp('bp-700') { + --doc-whitespace: var(--spacer-5); + } + + @include helper.bp('bp-450') { + padding: var(--spacer-3); + } +} + +.basic { + grid-area: basic; +} + +.media { + grid-area: media; +} + +.credits { + grid-area: credits; +} + +.textarea { + grid-area: text; + display: grid; + + gap: var(--doc-whitespace); +} + +.infoarea { + grid-area: info; + display: grid; + + gap: var(--doc-whitespace); +} diff --git a/src/utils/cleaners/name.ts b/src/utils/cleaners/name.ts new file mode 100644 index 0000000..55110cc --- /dev/null +++ b/src/utils/cleaners/name.ts @@ -0,0 +1,281 @@ +import RawName from 'src/interfaces/misc/rawName'; + +const cleanName = (rawData: RawName) => { + const { + props: { + pageProps: { aboveTheFold: main, mainColumnData: misc }, + }, + } = rawData; + + const cleanData = { + nameId: main.id, + basic: { + id: main.id, + name: main.nameText.text, + nameSuffix: main.disambiguator?.text ?? null, + knownFor: { + title: main.knownFor.edges[0].node.title.titleText.text, + role: main.knownFor.edges[0].node.summary.principalCategory.text, + }, + ...(main.primaryImage && { + poster: { + url: main.primaryImage.url, + id: main.primaryImage.id, + caption: main.primaryImage.caption.plainText, + }, + }), + primaryProfessions: main.primaryProfessions.map(profession => profession.category.text), + bio: { + full: main.bio.text.plaidHtml, + short: main.bio.text.plainText.slice(0, 600), + }, + birthDate: main.birthDate?.displayableProperty.value.plainText ?? null, + deathStatus: main.deathStatus, + deathDate: main.deathDate?.displayableProperty.value.plainText ?? null, + ...(main.meterRanking && { + ranking: { + position: main.meterRanking.currentRank, + change: main.meterRanking.rankChange.difference, + direction: main.meterRanking.rankChange.changeDirection, + }, + }), + }, + media: { + ...(main.primaryVideos.edges.length && { + trailer: { + id: main.primaryVideos.edges[0].node.id, + isMature: main.primaryVideos.edges[0].node.isMature, + thumbnail: main.primaryVideos.edges[0].node.thumbnail.url, + runtime: main.primaryVideos.edges[0].node.runtime.value, + caption: main.primaryVideos.edges[0].node.description?.value ?? null, + urls: main.primaryVideos.edges[0].node.playbackURLs.map(url => ({ + resolution: url.displayName.value, + mimeType: url.mimeType, + url: url.url, + })), + }, + }), + images: { + total: misc.images.total, + images: misc.images.edges.map(image => ({ + id: image.node.id, + url: image.node.url, + caption: image.node.caption, + })), + }, + videos: { + total: misc.videos.total, + videos: misc.videos.edges.map(video => ({ + id: video.node.id, + type: video.node.contentType.displayName.value, + caption: video.node.name.value, + runtime: video.node.runtime.value, + thumbnail: video.node.thumbnail.url, + })), + }, + }, + accolades: { + wins: misc.wins.total, + nominations: misc.nominations.total, + ...(misc.prestigiousAwardSummary && { + awards: { + name: misc.prestigiousAwardSummary.award.text, + id: misc.prestigiousAwardSummary.award.id, + event: misc.prestigiousAwardSummary.award.event.id, + nominations: misc.prestigiousAwardSummary.nominations, + wins: misc.prestigiousAwardSummary.wins, + }, + }), + }, + knownFor: misc.knownFor.edges.map(item => ({ + id: item.node.title.id, + title: item.node.title.titleText.text, + ...(item.node.title.primaryImage && { + poster: { + id: item.node.title.primaryImage.id, + url: item.node.title.primaryImage.url, + caption: item.node.title.primaryImage.caption.plainText, + }, + }), + type: { + id: item.node.title.titleType.id, + text: item.node.title.titleType.text, + }, + certificate: item.node.title.certificate?.rating ?? null, + ...(item.node.title.releaseYear && { + releaseYear: { + start: item.node.title.releaseYear.year, + end: item.node.title.releaseYear.endYear ?? null, + }, + }), + runtime: item.node.title.runtime?.seconds ?? null, + ratings: { + avg: item.node.title.ratingsSummary.aggregateRating ?? null, + numVotes: item.node.title.ratingsSummary.voteCount, + }, + genres: item.node.title.titleGenres.genres.map(genre => genre.genre.text), + + summary: { + numEpisodes: item.node.summary.episodeCount ?? null, + years: { + start: item.node.summary.yearRange.year, + end: item.node.summary.yearRange.endYear ?? null, + }, + characters: item.node.summary.principalCharacters?.map(character => character.name) ?? null, + jobs: item.node.summary.principalJobs?.map(job => job.text) ?? null, + }, + })), + credits: { + total: misc.totalCredits.total, + summary: { + titleType: misc.creditSummary.titleTypeCategories.map(cat => ({ + total: cat.total, + id: cat.titleTypeCategory.id, + label: cat.titleTypeCategory.text, + })), + genres: misc.creditSummary.genres.map(genre => ({ + total: genre.total, + name: genre.genre.displayableProperty.value.plainText, + })), + }, + released: getCredits(misc.releasedPrimaryCredits), + unreleased: getCredits<'unreleased'>(misc.unreleasedPrimaryCredits), + }, + personalDetails: { + officialSites: misc.personalDetailsExternalLinks.edges.map(item => ({ + name: item.node.label, + url: item.node.url, + })), + alsoKnownAs: misc.akas.edges.map(item => item.node.displayableProperty.value.plainText), + height: misc.height?.displayableProperty.value.plainText ?? null, + birth: { + location: misc.birthLocation?.text ?? null, + date: main.birthDate?.displayableProperty.value.plainText ?? null, + }, + death: { + location: misc.deathLocation?.displayableProperty.value.plainText ?? null, + cause: misc.deathCause?.displayableProperty.value.plainText ?? null, + date: main.deathDate?.displayableProperty.value.plainText ?? null, + }, + spouses: + misc.personalDetailsSpouses?.map(spouse => ({ + name: spouse.spouse.asMarkdown.plainText, + id: spouse.spouse.name?.id ?? null, + range: spouse.timeRange.displayableProperty.value.plaidHtml, + attributes: spouse.attributes.map(attr => attr.text), + })) ?? null, + children: misc.children.edges.map(child => ({ + name: child.node.relationName.displayableProperty.value.plainText, + id: child.node.relationName.name?.id ?? null, + })), + parents: misc.parents.edges.map(parent => ({ + name: parent.node.relationName.displayableProperty.value.plainText, + id: parent.node.relationName.name?.id ?? null, + })), + relatives: misc.others.edges.map(relative => ({ + relation: relative.node.relationshipType.text, + id: relative.node.relationName.name?.id ?? null, + name: relative.node.relationName.displayableProperty.value.plainText, + })), + otherWorks: misc.otherWorks.edges.map(work => ({ + summary: work.node.category?.text ?? null, + text: work.node.text.plaidHtml, + })), + publicity: { + total: misc.publicityListings.total, + filmBiographies: misc.nameFilmBiography.total, + printBiographies: misc.namePrintBiography.total, + interviews: misc.publicityInterview.total, + articles: misc.publicityArticle.total, + magazines: misc.publicityMagazineCover.total, + pictorials: misc.publicityPictorial.total, + }, + }, + didYouKnow: { + ...(misc.trivia.edges.length && { + trivia: { + total: misc.triviaTotal.total, + html: misc.trivia.edges[0].node.displayableArticle.body.plaidHtml, + }, + }), + ...(misc.trademarks.edges.length && { + trademark: { + total: misc.trademarksTotal.total, + html: misc.trademarks.edges[0].node.displayableArticle.body.plaidHtml, + }, + }), + ...(misc.quotes.edges.length && { + quotes: { + total: misc.quotesTotal.total, + html: misc.quotes.edges[0].node.displayableArticle.body.plaidHtml, + }, + }), + nicknames: misc.nickNames.map(name => name.displayableProperty.value.plainText), + ...(misc.titleSalaries.edges.length && { + salary: { + total: misc.titleSalariesTotal.total, + value: misc.titleSalaries.edges[0].node.displayableProperty.value.plainText, + title: { + id: misc.titleSalaries.edges[0].node.title.id, + year: misc.titleSalaries.edges[0].node.title.releaseYear?.year ?? null, + text: misc.titleSalaries.edges[0].node.title.titleText.text, + }, + }, + }), + }, + }; + + return cleanData; +}; + +type RawReleased = RawName['props']['pageProps']['mainColumnData']['releasedPrimaryCredits']; +type RawUnreleased = RawName['props']['pageProps']['mainColumnData']['unreleasedPrimaryCredits']; +const getCredits = ( + credits: T extends 'released' ? RawReleased : RawUnreleased +) => + credits.map(creditItem => ({ + category: creditItem.category, + total: creditItem.credits.total, + titles: creditItem.credits.edges.map(item => ({ + id: item.node.title.id, + title: item.node.title.titleText.text, + ...(item.node.title.primaryImage && { + poster: { + id: item.node.title.primaryImage.id, + url: item.node.title.primaryImage.url, + caption: item.node.title.primaryImage.caption.plainText, + }, + }), + type: { + id: item.node.title.titleType.id, + text: item.node.title.titleType.text, + }, + certificate: item.node.title.certificate?.rating ?? null, + ...(item.node.title.releaseYear && { + releaseYear: { + start: item.node.title.releaseYear.year, + end: item.node.title.releaseYear.endYear ?? null, + }, + }), + runtime: item.node.title.runtime?.seconds ?? null, + ratings: { + avg: item.node.title.ratingsSummary.aggregateRating ?? null, + numVotes: item.node.title.ratingsSummary.voteCount, + }, + test: JSON.stringify(item.node.title), + genres: item.node.title.titleGenres.genres.map(genre => genre.genre.text), + productionStatus: item.node.title.productionStatus.currentProductionStage.text, + + summary: { + numEpisodes: item.node.episodeCredits.total, + years: { + start: item.node.episodeCredits.yearRange?.year ?? null, + end: item.node.episodeCredits.yearRange?.endYear ?? null, + }, + characters: item.node.characters?.map(char => char.name) ?? null, + jobs: item.node.jobs?.map(job => job.text) ?? null, + }, + })), + })); + +export default cleanName; diff --git a/src/utils/cleaners/title.ts b/src/utils/cleaners/title.ts index 76caa23..ebf74d2 100644 --- a/src/utils/cleaners/title.ts +++ b/src/utils/cleaners/title.ts @@ -101,7 +101,7 @@ const cleanTitle = (rawData: RawTitle) => { total: misc.videos.total, videos: misc.videoStrip.edges.map(video => ({ id: video.node.id, - type: video.node.contentType.displayName, + type: video.node.contentType.displayName.value, caption: video.node.name.value, runtime: video.node.runtime.value, thumbnail: video.node.thumbnail.url, diff --git a/src/utils/fetchers/name.ts b/src/utils/fetchers/name.ts new file mode 100644 index 0000000..cea92f2 --- /dev/null +++ b/src/utils/fetchers/name.ts @@ -0,0 +1,28 @@ +import * as cheerio from 'cheerio'; +import RawName from 'src/interfaces/misc/rawName'; +import axiosInstance from 'src/utils/axiosInstance'; +import cleanName from 'src/utils/cleaners/name'; +import { AppError } from 'src/utils/helpers'; + +const name = async (nameId: string) => { + try { + // getting data + const res = await axiosInstance(`/name/${nameId}`); + const $ = cheerio.load(res.data); + const rawData = $('script#__NEXT_DATA__').text(); + // cleaning it a bit + const parsedRawData: RawName = JSON.parse(rawData); + const cleanData = cleanName(parsedRawData); + // returning + return cleanData; + } catch (err: any) { + if (err.response?.status === 404) throw new AppError('not found', 404, err.cause); + + console.warn(err); + + + throw new AppError('something went wrong', 500, err.cause); + } +}; + +export default name;