Skip to content

Commit

Permalink
Refactor: Use SeriesPoster component for all series posters outside o…
Browse files Browse the repository at this point in the history
…f main collection page
  • Loading branch information
harshithmohan committed Sep 20, 2024
1 parent 8ac186e commit f19fe52
Show file tree
Hide file tree
Showing 10 changed files with 316 additions and 277 deletions.
108 changes: 108 additions & 0 deletions src/components/SeriesPoster.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, { useMemo } from 'react';
import { Link } from 'react-router-dom';
import cx from 'classnames';

import BackgroundImagePlaceholderDiv from '@/components/BackgroundImagePlaceholderDiv';

import type { ImageType } from '@/core/types/api/common';

type Props = {
children?: React.ReactNode;
title: string;
subtitle?: string;
image: ImageType | null;
shokoId?: number | null;
anidbSeriesId?: number;
anidbEpisodeId?: number;
inCollection?: boolean;
};

const baseClassName = 'w-56 flex flex-col shrink-0';

const SeriesPoster = React.memo((props: Props) => {
const {
anidbEpisodeId,
anidbSeriesId,
children,
image,
inCollection,
shokoId,
subtitle,
title,
} = props;

const isAnidb = useMemo(() => {
if (shokoId) return false;
return anidbSeriesId !== undefined || anidbEpisodeId !== undefined;
}, [anidbEpisodeId, anidbSeriesId, shokoId]);

const content = (
<>
<BackgroundImagePlaceholderDiv
image={image}
className="h-80 rounded-lg border border-panel-border drop-shadow-md"
hidePlaceholderOnHover
overlayOnHover
zoomOnHover
>
{isAnidb && (
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-y-3 text-sm font-semibold opacity-0 transition-opacity group-hover:opacity-100">
<div className="metadata-link-icon AniDB" />
View on AniDB
</div>
)}

{children}

{inCollection && (
<div className="absolute bottom-4 left-3 flex w-[90%] justify-center rounded-lg bg-panel-background-overlay py-2 text-sm font-semibold text-panel-text opacity-100 transition-opacity group-hover:opacity-0">
In Collection
</div>
)}
</BackgroundImagePlaceholderDiv>

<div
className="mt-3 truncate text-center text-sm font-semibold"
data-tooltip-id="tooltip"
data-tooltip-content={title}
data-tooltip-delay-show={500}
>
{title}
</div>

{subtitle && (
<div
className="truncate text-center text-sm font-semibold opacity-65"
title={subtitle}
>
{subtitle}
</div>
)}
</>
);

if (shokoId) {
return (
<Link className={cx(baseClassName, 'group')} to={`/webui/collection/series/${shokoId}`}>
{content}
</Link>
);
}

if (anidbSeriesId ?? anidbEpisodeId) {
return (
<a
href={`https://anidb.net/${anidbSeriesId ? `anime/${anidbSeriesId}` : `episode/${anidbEpisodeId}`}`}
className={cx(baseClassName, 'group')}
target="_blank"
rel="noopener noreferrer"
>
{content}
</a>
);
}

return <div className={baseClassName}>{content}</div>;
});

export default SeriesPoster;
1 change: 1 addition & 0 deletions src/core/types/api/series.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export type SeriesIDsType = {

export type AniDBSeriesType = {
ID: number;
ShokoID?: number;
Type: SeriesTypeEnum;
Restricted: boolean;
Title: string;
Expand Down
110 changes: 37 additions & 73 deletions src/pages/collection/series/SeriesOverview.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import React, { useMemo, useState } from 'react';
import { useParams } from 'react-router';
import { Link } from 'react-router-dom';
import { mdiEarth, mdiOpenInNew } from '@mdi/js';
import { Icon } from '@mdi/react';
import cx from 'classnames';
import { flatMap, get, round, toNumber } from 'lodash';
import { flatMap, get, map, round, toNumber } from 'lodash';

import BackgroundImagePlaceholderDiv from '@/components/BackgroundImagePlaceholderDiv';
import CharacterImage from '@/components/CharacterImage';
import EpisodeSummary from '@/components/Collection/Episode/EpisodeSummary';
import SeriesMetadata from '@/components/Collection/SeriesMetadata';
import MultiStateButton from '@/components/Input/MultiStateButton';
import ShokoPanel from '@/components/Panels/ShokoPanel';
import SeriesPoster from '@/components/SeriesPoster';
import {
useRelatedAnimeQuery,
useSeriesCastQuery,
Expand Down Expand Up @@ -169,82 +168,47 @@ const SeriesOverview = () => {
</div>

{relatedAnime.length > 0 && (
<ShokoPanel title="Related Anime" className="w-full" transparent>
<div className={cx('flex gap-x-5', relatedAnime.length > 5 && ('mb-4'))}>
{relatedAnime.map((item) => {
const thumbnail = get(item, 'Poster', null);
const itemRelation = item.Relation.replace(/([a-z])([A-Z])/g, '$1 $2');
return (
<Link
key={item.ID}
to={`/webui/collection/series/${item.ShokoID}`}
className={cx(
'flex w-[13.875rem] shrink-0 flex-col gap-y-2 text-center font-semibold',
!item.ShokoID && 'pointer-events-none',
)}
>
<BackgroundImagePlaceholderDiv
image={thumbnail}
className="group h-[19.875rem] w-[13.875rem] rounded-lg border border-panel-border drop-shadow-md"
hidePlaceholderOnHover
overlayOnHover
zoomOnHover
>
{item.ShokoID && (
<div className="absolute bottom-4 left-3 flex w-[90%] justify-center rounded-lg bg-panel-background-overlay py-2 text-sm font-semibold text-panel-text opacity-100 transition-opacity group-hover:opacity-0">
In Collection
</div>
)}
</BackgroundImagePlaceholderDiv>
<span className="line-clamp-1 text-ellipsis text-sm">{item.Title}</span>
<span className="text-sm text-panel-text-important">{itemRelation}</span>
</Link>
);
})}
</div>
<ShokoPanel
title="Related Anime"
className="w-full"
transparent
contentClassName={cx('!flex-row gap-x-6', relatedAnime.length > 7 && 'pb-4')}
>
{map(relatedAnime, item => (
<SeriesPoster
key={item.ID}
image={item.Poster}
title={item.Title}
subtitle={item.Relation.replace(/([a-z])([A-Z])/g, '$1 $2')}
shokoId={item.ShokoID}
anidbSeriesId={item.ID}
inCollection={!!item.ShokoID}
/>
))}
</ShokoPanel>
)}

{similarAnime.length > 0 && (
<ShokoPanel title="Similar Anime" className="w-full" transparent>
<div className={cx('shoko-scrollbar flex gap-x-5', similarAnime.length > 5 && ('mb-4'))}>
{similarAnime.map((item) => {
const thumbnail = get(item, 'Poster', null);
return (
<Link
key={item.ID}
to={`/webui/collection/series/${item.ShokoID}`}
className={cx(
'flex w-[13.875rem] shrink-0 flex-col gap-y-2 text-center font-semibold',
!item.ShokoID && 'pointer-events-none',
)}
>
<BackgroundImagePlaceholderDiv
image={thumbnail}
className="group h-[19.875rem] w-[13.875rem] rounded-lg border border-panel-border drop-shadow-md"
hidePlaceholderOnHover
overlayOnHover
zoomOnHover
>
{item.ShokoID && (
<div className="absolute bottom-4 left-3 flex w-[90%] justify-center rounded-lg bg-panel-background-overlay py-2 text-sm font-semibold text-panel-text opacity-100 transition-opacity group-hover:opacity-0">
In Collection
</div>
)}
</BackgroundImagePlaceholderDiv>
<span className="line-clamp-1 text-ellipsis text-sm">{item.Title}</span>
<span className="text-sm text-panel-text-important">
{round(item.UserApproval.Value, 2)}
% (
{item.UserApproval.Votes}
&nbsp;votes)
</span>
</Link>
);
})}
</div>
<ShokoPanel
title="Similar Anime"
className="w-full"
transparent
contentClassName={cx('!flex-row gap-x-6', similarAnime.length > 7 && 'pb-4')}
>
{map(similarAnime, item => (
<SeriesPoster
key={item.ID}
image={item.Poster}
title={item.Title}
subtitle={`${round(item.UserApproval.Value, 2)}% (${item.UserApproval.Votes} votes)`}
shokoId={item.ShokoID}
anidbSeriesId={item.ID}
inCollection={!!item.ShokoID}
/>
))}
</ShokoPanel>
)}

<ShokoPanel title="Top 20 Seiyuu" className="w-full" transparent>
<div className="z-10 flex w-full gap-x-6">
{cast?.filter(credit => credit.RoleName === 'Seiyuu' && credit.Character).slice(0, 20).map(seiyuu => (
Expand Down
78 changes: 21 additions & 57 deletions src/pages/dashboard/components/EpisodeDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import React, { useMemo } from 'react';
import { Link } from 'react-router-dom';
import cx from 'classnames';

import BackgroundImagePlaceholderDiv from '@/components/BackgroundImagePlaceholderDiv';
import SeriesPoster from '@/components/SeriesPoster';
import { EpisodeTypeEnum } from '@/core/types/api/episode';
import { convertTimeSpanToMs, dayjs } from '@/core/util';

Expand All @@ -23,41 +21,6 @@ const CalendarConfig = {
sameElse: 'dddd',
};

const DateSection: React.FC<{ airDate: dayjs.Dayjs, relativeTime: string }> = ({ airDate, relativeTime }) => (
<div>
<p className="truncate text-center text-sm font-semibold">{airDate.format('MMMM Do, YYYY')}</p>
<p className="truncate text-center text-sm font-semibold opacity-65">{relativeTime}</p>
</div>
);

const ImageSection: React.FC<
{ episode: DashboardEpisodeDetailsType, percentage: string | null, isInCollection: boolean }
> = ({ episode, isInCollection, percentage }) => (
<BackgroundImagePlaceholderDiv
image={episode.SeriesPoster}
className="h-80 rounded-lg border border-panel-border drop-shadow-md"
hidePlaceholderOnHover
overlayOnHover
zoomOnHover
>
{percentage && <div className="absolute bottom-0 left-0 h-1 bg-panel-text-primary" style={{ width: percentage }} />}
{isInCollection && (
<div className="absolute bottom-4 left-3 flex w-[90%] justify-center rounded-lg bg-panel-background-overlay py-2 text-sm font-semibold text-panel-text opacity-100 transition-opacity group-hover:opacity-0">
In Collection
</div>
)}
</BackgroundImagePlaceholderDiv>
);

const TitleSection: React.FC<{ episode: DashboardEpisodeDetailsType, title: string }> = ({ episode, title }) => (
<div>
<p className="truncate text-center text-sm font-semibold" title={episode.SeriesTitle}>
{episode.SeriesTitle}
</p>
<p className="truncate text-center text-sm font-semibold opacity-65" title={title}>{title}</p>
</div>
);

const anidbEpisodePrefixes = (type: EpisodeTypeEnum, epNumber: number): string => {
const fullPrefixes = (prefix: string) => `${prefix}${epNumber}`;
// Prefixes for episode types base on https://wiki.anidb.net/Content:Episodes#Type
Expand Down Expand Up @@ -95,29 +58,30 @@ function EpisodeDetails({ episode, isInCollection = false, showDate = false }: P
[episode.Type, episode.Title, episode.Number],
);

const content = (
<>
{showDate && <DateSection airDate={airDate} relativeTime={relativeTime} />}
<ImageSection episode={episode} percentage={percentage} isInCollection={isInCollection} />
<TitleSection episode={episode} title={title} />
</>
);

return (
<div
key={`episode-${episode.IDs.ID}`}
className={cx(
'mr-6 flex w-56 shrink-0 flex-col justify-center gap-y-3 last:mr-0',
episode.IDs.ShokoSeries && 'group',
)}
className="flex w-56 shrink-0 flex-col gap-y-3"
>
{episode.IDs.ShokoSeries
? (
<Link className="flex flex-col gap-y-3" to={`/webui/collection/series/${episode.IDs.ShokoSeries}`}>
{content}
</Link>
)
: content}
{showDate && (
<div>
<div className="truncate text-center text-sm font-semibold">{airDate.format('MMMM Do, YYYY')}</div>
<div className="truncate text-center text-sm font-semibold opacity-65">{relativeTime}</div>
</div>
)}

<SeriesPoster
image={episode.SeriesPoster}
title={episode.SeriesTitle}
subtitle={title}
shokoId={episode.IDs.ShokoSeries}
anidbEpisodeId={episode.IDs.ID}
inCollection={isInCollection}
>
{percentage && (
<div className="absolute bottom-0 left-0 h-1 bg-panel-text-primary" style={{ width: percentage }} />
)}
</SeriesPoster>
</div>
);
}
Expand Down
Loading

0 comments on commit f19fe52

Please sign in to comment.