197 lines
5.9 KiB
TypeScript
Executable File
197 lines
5.9 KiB
TypeScript
Executable File
import {Trans} from '@common/i18n/trans';
|
|
import {ChartLayout, ChartLayoutProps} from '@common/charts/chart-layout';
|
|
import React, {Fragment, ReactElement} from 'react';
|
|
import {ReportMetric} from '@common/admin/analytics/report-metric';
|
|
import {ChartLoadingIndicator} from '@common/charts/chart-loading-indicator';
|
|
import {TopModelDatasetItem} from '@app/admin/reports/requests/use-insights-report';
|
|
import {InfoIcon} from '@common/icons/material/Info';
|
|
import {FormattedNumber} from '@common/i18n/formatted-number';
|
|
import {Link} from 'react-router-dom';
|
|
import {UserAvatar} from '@common/ui/images/user-avatar';
|
|
import {TitlePoster} from '@app/titles/title-poster/title-poster';
|
|
import {TitleLink} from '@app/titles/title-link';
|
|
import {UserProfileLink} from '@common/users/user-profile-link';
|
|
import {MediaPlayIcon} from '@common/icons/media/media-play';
|
|
import {getWatchLink} from '@app/videos/watch-page/get-watch-link';
|
|
import {SeasonPoster} from '@app/seasons/season-poster';
|
|
import {SeasonLink} from '@app/seasons/season-link';
|
|
import {EpisodePoster} from '@app/episodes/episode-poster/episode-poster';
|
|
import {EpisodeLink} from '@app/episodes/episode-link';
|
|
import clsx from 'clsx';
|
|
|
|
interface Props extends Partial<ChartLayoutProps> {
|
|
data?: ReportMetric<TopModelDatasetItem>;
|
|
title: ReactElement;
|
|
}
|
|
export function TopModelsChartLayout({data, isLoading, ...layoutProps}: Props) {
|
|
const dataItems = data?.datasets[0].data || [];
|
|
|
|
return (
|
|
<ChartLayout
|
|
{...layoutProps}
|
|
className="w-1/2 min-w-500 md:min-w-0"
|
|
contentIsFlex={isLoading}
|
|
contentClassName="max-h-[370px] overflow-y-auto compact-scrollbar"
|
|
>
|
|
{isLoading && <ChartLoadingIndicator />}
|
|
{dataItems.map(item => (
|
|
<div
|
|
key={item.model.id}
|
|
className="mb-20 flex items-center justify-between gap-24 text-sm"
|
|
>
|
|
<div className="flex items-center gap-8">
|
|
<Image
|
|
model={item.model}
|
|
size="w-42 h-42"
|
|
className="flex-shrink-0 rounded"
|
|
/>
|
|
<div>
|
|
<div className="text-sm">
|
|
<Name model={item.model} />
|
|
</div>
|
|
<div className="text-xs text-muted">
|
|
<Description model={item.model} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-shrink-0 items-center gap-4">
|
|
<MediaPlayIcon className="text-muted" size="sm" />
|
|
<Trans
|
|
message=":count plays"
|
|
values={{count: <FormattedNumber value={item.value} />}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{!isLoading && !dataItems.length ? (
|
|
<div className="flex items-center gap-8 text-muted">
|
|
<InfoIcon size="sm" />
|
|
<Trans message="No plays in selected timeframe." />
|
|
</div>
|
|
) : null}
|
|
</ChartLayout>
|
|
);
|
|
}
|
|
|
|
interface ImageProps {
|
|
model: TopModelDatasetItem['model'];
|
|
size: string;
|
|
className: string;
|
|
}
|
|
function Image({model, size, className}: ImageProps) {
|
|
const link = `/admin/${model.model_type}s/${model.id}`;
|
|
|
|
switch (model.model_type) {
|
|
case 'title':
|
|
return (
|
|
<TitlePoster
|
|
title={model}
|
|
size={size}
|
|
srcSize="sm"
|
|
className={className}
|
|
link={`/admin/titles/${model.id}/insights`}
|
|
/>
|
|
);
|
|
case 'season':
|
|
return (
|
|
<SeasonPoster
|
|
season={model}
|
|
title={model.title!}
|
|
size={size}
|
|
srcSize="sm"
|
|
className={className}
|
|
link={`/admin/titles/${model.title_id}/insights/seasons/${model.number}`}
|
|
/>
|
|
);
|
|
case 'episode':
|
|
return (
|
|
<EpisodePoster
|
|
episode={model}
|
|
title={model.title!}
|
|
size={size}
|
|
srcSize="sm"
|
|
className={className}
|
|
link={`/admin/titles/${model.title_id}/insights/seasons/${model.season_number}/episodes/${model.episode_number}`}
|
|
/>
|
|
);
|
|
case 'video':
|
|
return model.thumbnail ? (
|
|
<Link to={link} className={clsx(size, className)}>
|
|
<img src={model.thumbnail} className="h-full w-full" alt="" />
|
|
</Link>
|
|
) : (
|
|
<TitlePoster
|
|
title={model.title!}
|
|
size={size}
|
|
srcSize="sm"
|
|
className={className}
|
|
link={`/admin/videos/${model.id}/insights`}
|
|
/>
|
|
);
|
|
case 'user':
|
|
// there's no separate insights page for user
|
|
return <UserAvatar user={model} size={size} className={className} />;
|
|
}
|
|
}
|
|
|
|
interface NameProps {
|
|
model: TopModelDatasetItem['model'];
|
|
}
|
|
function Name({model}: NameProps) {
|
|
switch (model.model_type) {
|
|
case 'title':
|
|
return <TitleLink title={model} target="_blank" />;
|
|
case 'season':
|
|
return (
|
|
<SeasonLink
|
|
title={model.title!}
|
|
seasonNumber={model.number}
|
|
target="_blank"
|
|
/>
|
|
);
|
|
case 'episode':
|
|
return (
|
|
<EpisodeLink
|
|
title={model.title!}
|
|
episode={model}
|
|
seasonNumber={model.season_number}
|
|
target="_blank"
|
|
/>
|
|
);
|
|
case 'video':
|
|
return (
|
|
<Link
|
|
to={getWatchLink(model)}
|
|
className="hover:underline"
|
|
target="_blank"
|
|
>
|
|
{model.name}
|
|
</Link>
|
|
);
|
|
case 'user':
|
|
return model.id ? (
|
|
<UserProfileLink user={model} target="_blank" />
|
|
) : (
|
|
<Fragment>{model.display_name}</Fragment>
|
|
);
|
|
}
|
|
}
|
|
|
|
interface DescriptionProps {
|
|
model: TopModelDatasetItem['model'];
|
|
}
|
|
function Description({model}: DescriptionProps) {
|
|
switch (model.model_type) {
|
|
case 'title':
|
|
return <span>{model.year}</span>;
|
|
case 'season':
|
|
return <TitleLink title={model.title!} target="_blank" />;
|
|
case 'episode':
|
|
return <TitleLink title={model.title!} target="_blank" />;
|
|
case 'user':
|
|
return null;
|
|
case 'video':
|
|
return <TitleLink title={model.title!} target="_blank" />;
|
|
}
|
|
}
|