-
-
Notifications
You must be signed in to change notification settings - Fork 891
feat(title-card): display ratings on media cards #3154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 3 commits
52b9cb3
0c53a3a
0f6bd81
4e50292
068c05b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| import RTAudFresh from '@app/assets/rt_aud_fresh.svg'; | ||
| import RTAudRotten from '@app/assets/rt_aud_rotten.svg'; | ||
| import RTFresh from '@app/assets/rt_fresh.svg'; | ||
| import RTRotten from '@app/assets/rt_rotten.svg'; | ||
| import ImdbLogo from '@app/assets/services/imdb.svg'; | ||
| import TmdbLogo from '@app/assets/services/tmdb.svg'; | ||
| import defineMessages from '@app/utils/defineMessages'; | ||
| import type { RTRating } from '@server/api/rating/rottentomatoes'; | ||
| import type { RatingResponse } from '@server/api/ratings'; | ||
| import type { MediaType } from '@server/models/Search'; | ||
| import { useEffect, useState } from 'react'; | ||
| import { useIntl } from 'react-intl'; | ||
| import useSWR from 'swr'; | ||
|
|
||
| interface TitleCardRatingsProps { | ||
| id: number; | ||
| mediaType: Extract<MediaType, 'movie' | 'tv'>; | ||
| userScore?: number; | ||
| visible: boolean; | ||
| } | ||
|
|
||
| const RATINGS_REQUEST_DELAY_MS = 300; | ||
|
|
||
| const messages = defineMessages('components.TitleCard.TitleCardRatings', { | ||
| ratings: 'Ratings', | ||
| rottenTomatoesAudienceScore: 'Rotten Tomatoes Audience Score: {score}%', | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why use the score inside the aria-label ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The provider logos are hidden from assistive technologies, so the score in the label lets a screen reader announce the provider and its value together. Without it, the visible number would not have useful context.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, but the note is already inside the tag that’s why I’m asking. It seems a bit redundant 😅
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are right, the visible score is already announced. I will remove the parent |
||
| rottenTomatoesCriticsScore: 'Rotten Tomatoes Critics Score: {score}%', | ||
| imdbUserScore: 'IMDb User Score: {score}', | ||
| tmdbUserScore: 'TMDB User Score: {score}%', | ||
| }); | ||
|
|
||
| const TitleCardRatings = ({ | ||
| id, | ||
| mediaType, | ||
| userScore, | ||
| visible, | ||
| }: TitleCardRatingsProps) => { | ||
| const intl = useIntl(); | ||
| const [ratingsRequested, setRatingsRequested] = useState(false); | ||
|
|
||
| useEffect(() => { | ||
| if (!visible || ratingsRequested) { | ||
| return; | ||
| } | ||
|
|
||
| const timeout = setTimeout(() => { | ||
| setRatingsRequested(true); | ||
| }, RATINGS_REQUEST_DELAY_MS); | ||
|
|
||
| return () => clearTimeout(timeout); | ||
| }, [ratingsRequested, visible]); | ||
|
|
||
| const { data: movieRatings } = useSWR<RatingResponse>( | ||
| ratingsRequested && mediaType === 'movie' | ||
| ? `/api/v1/movie/${id}/ratingscombined` | ||
| : null, | ||
| { | ||
| revalidateOnFocus: false, | ||
| shouldRetryOnError: false, | ||
| } | ||
| ); | ||
| const { data: tvRatings } = useSWR<RTRating>( | ||
| ratingsRequested && mediaType === 'tv' ? `/api/v1/tv/${id}/ratings` : null, | ||
| { | ||
| revalidateOnFocus: false, | ||
| shouldRetryOnError: false, | ||
| } | ||
| ); | ||
|
|
||
| const rtRating = mediaType === 'movie' ? movieRatings?.rt : tvRatings; | ||
| const imdbRating = movieRatings?.imdb; | ||
| const hasRtAudienceScore = | ||
| typeof rtRating?.audienceScore === 'number' && | ||
| rtRating.audienceRating !== undefined; | ||
| const hasRtCriticsScore = | ||
| !hasRtAudienceScore && | ||
| typeof rtRating?.criticsScore === 'number' && | ||
| rtRating.criticsRating !== undefined; | ||
|
u61d marked this conversation as resolved.
|
||
| const hasImdbScore = typeof imdbRating?.criticsScore === 'number'; | ||
| const hasTmdbScore = typeof userScore === 'number' && userScore > 0; | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| if ( | ||
| !hasRtAudienceScore && | ||
| !hasRtCriticsScore && | ||
| !hasImdbScore && | ||
| !hasTmdbScore | ||
| ) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <div | ||
| className="mt-1 flex min-h-4 min-w-0 items-center gap-1 whitespace-nowrap text-[10px] font-medium md:gap-2 md:text-xs" | ||
| aria-label={intl.formatMessage(messages.ratings)} | ||
| > | ||
| {hasRtAudienceScore && ( | ||
| <span | ||
| className="flex min-w-0 items-center gap-0.5 md:gap-1" | ||
| aria-label={intl.formatMessage(messages.rottenTomatoesAudienceScore, { | ||
| score: rtRating.audienceScore, | ||
| })} | ||
| > | ||
| {rtRating.audienceRating === 'Spilled' ? ( | ||
| <RTAudRotten | ||
| className="h-3.5 w-3.5 shrink-0 md:h-4 md:w-4" | ||
| aria-hidden="true" | ||
| /> | ||
| ) : ( | ||
| <RTAudFresh | ||
| className="h-3.5 w-3.5 shrink-0 md:h-4 md:w-4" | ||
| aria-hidden="true" | ||
| /> | ||
| )} | ||
| <span>{rtRating.audienceScore}%</span> | ||
| </span> | ||
| )} | ||
| {hasRtCriticsScore && ( | ||
| <span | ||
| className="flex min-w-0 items-center gap-0.5 md:gap-1" | ||
| aria-label={intl.formatMessage(messages.rottenTomatoesCriticsScore, { | ||
| score: rtRating.criticsScore, | ||
| })} | ||
| > | ||
| {rtRating.criticsRating === 'Rotten' ? ( | ||
| <RTRotten | ||
| className="h-3.5 w-3.5 shrink-0 md:h-4 md:w-4" | ||
| aria-hidden="true" | ||
| /> | ||
| ) : ( | ||
| <RTFresh | ||
| className="h-3.5 w-3.5 shrink-0 md:h-4 md:w-4" | ||
| aria-hidden="true" | ||
| /> | ||
| )} | ||
| <span>{rtRating.criticsScore}%</span> | ||
| </span> | ||
| )} | ||
| {hasImdbScore && ( | ||
| <span | ||
| className="flex min-w-0 items-center gap-0.5 md:gap-1" | ||
| aria-label={intl.formatMessage(messages.imdbUserScore, { | ||
| score: imdbRating.criticsScore, | ||
| })} | ||
| > | ||
| <ImdbLogo | ||
| className="h-2.5 w-4 shrink-0 md:h-3 md:w-5" | ||
| aria-hidden="true" | ||
| /> | ||
| <span>{imdbRating.criticsScore}</span> | ||
| </span> | ||
| )} | ||
| {hasTmdbScore && ( | ||
| <span | ||
| className="flex min-w-0 items-center gap-0.5 md:gap-1" | ||
| aria-label={intl.formatMessage(messages.tmdbUserScore, { | ||
| score: Math.round(userScore * 10), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we keep the original rating scale (I prefer this, as users will see the same value as on the external website) or standardize everything to a 0–100% scale?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We keep the original rating scale. We just display the score from the websites, we're not customizing / modifying it on Seerr.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Understood. I will keep the original provider scales and display the TMDB score on its 0-10 scale.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correction after checking the provider display: TMDB presents its User Score as a percentage, including in the approved Design A examples. I will therefore keep RT and TMDB as percentages and IMDb on its 0-10 scale. |
||
| })} | ||
| > | ||
| <TmdbLogo | ||
| className="h-3.5 w-4 shrink-0 md:h-4 md:w-5" | ||
| aria-hidden="true" | ||
| /> | ||
| <span>{Math.round(userScore * 10)}%</span> | ||
| </span> | ||
| )} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default TitleCardRatings; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ import Tooltip from '@app/components/Common/Tooltip'; | |
| import RequestModal from '@app/components/RequestModal'; | ||
| import ErrorCard from '@app/components/TitleCard/ErrorCard'; | ||
| import Placeholder from '@app/components/TitleCard/Placeholder'; | ||
| import TitleCardRatings from '@app/components/TitleCard/TitleCardRatings'; | ||
| import { useIsTouch } from '@app/hooks/useIsTouch'; | ||
| import useToasts from '@app/hooks/useToasts'; | ||
| import { Permission, UserType, useUser } from '@app/hooks/useUser'; | ||
|
|
@@ -62,6 +63,7 @@ const TitleCard = ({ | |
| year, | ||
| title, | ||
| status, | ||
| userScore, | ||
| mediaType, | ||
| isAddedToWatchlist = false, | ||
| inProgress = false, | ||
|
|
@@ -522,7 +524,7 @@ const TitleCard = ({ | |
| {year && <div className="text-sm font-medium">{year}</div>} | ||
|
|
||
| <h1 | ||
| className="whitespace-normal text-xl font-bold leading-tight" | ||
| className="whitespace-normal text-lg font-bold leading-tight" | ||
| style={{ | ||
| WebkitLineClamp: 3, | ||
| display: '-webkit-box', | ||
|
|
@@ -552,6 +554,14 @@ const TitleCard = ({ | |
| > | ||
| {summary} | ||
| </div> | ||
| {(mediaType === 'movie' || mediaType === 'tv') && ( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For me, the rating card should only appear if there is a userScore not because it's a TV or movie.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it. I will render the ratings row when |
||
| <TitleCardRatings | ||
| id={id} | ||
| mediaType={mediaType} | ||
| userScore={userScore} | ||
| visible={showDetail} | ||
| /> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </Link> | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here it should be handled as you did below with
mediaType === 'xxxx'when making a request to the providerThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. I will restore the general
MediaTypehere and keep the movie/TV checks only on the relevant provider requests.