68
resources/client/episodes/compact-season-episode.tsx
Executable file
68
resources/client/episodes/compact-season-episode.tsx
Executable file
@@ -0,0 +1,68 @@
|
||||
import {Episode} from '@app/titles/models/episode';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
|
||||
interface Props {
|
||||
episode?: Episode;
|
||||
seasonNum?: number;
|
||||
episodeNum?: number;
|
||||
className?: string;
|
||||
}
|
||||
export function CompactSeasonEpisode({
|
||||
episode,
|
||||
seasonNum,
|
||||
episodeNum,
|
||||
className,
|
||||
}: Props) {
|
||||
if (!seasonNum && episode) {
|
||||
seasonNum = episode.season_number;
|
||||
}
|
||||
if (!episodeNum && episode) {
|
||||
episodeNum = episode.episode_number;
|
||||
}
|
||||
|
||||
if (seasonNum && episodeNum) {
|
||||
return (
|
||||
<span className={className}>
|
||||
<Trans
|
||||
message="s:seasone:episode"
|
||||
values={{
|
||||
season: prefixWithZero(seasonNum),
|
||||
episode: prefixWithZero(episodeNum),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (seasonNum) {
|
||||
return (
|
||||
<span className={className}>
|
||||
<Trans
|
||||
message="s:season"
|
||||
values={{
|
||||
season: prefixWithZero(seasonNum),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (episodeNum) {
|
||||
return (
|
||||
<span className={className}>
|
||||
<Trans
|
||||
message="e:episode"
|
||||
values={{
|
||||
episode: prefixWithZero(episodeNum),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function prefixWithZero(value: number): string {
|
||||
return value < 10 ? `0${value}` : `${value}`;
|
||||
}
|
||||
44
resources/client/episodes/episode-link.tsx
Executable file
44
resources/client/episodes/episode-link.tsx
Executable file
@@ -0,0 +1,44 @@
|
||||
import React, {useMemo} from 'react';
|
||||
import {Title} from '@app/titles/models/title';
|
||||
import {getSeasonLink, SeasonLinkProps} from '@app/seasons/season-link';
|
||||
import {Episode} from '@app/titles/models/episode';
|
||||
import {BaseMediaLink} from '@app/base-media-link';
|
||||
|
||||
interface Props extends Omit<SeasonLinkProps, 'seasonNumber'> {
|
||||
episodeNumber?: number;
|
||||
seasonNumber?: number;
|
||||
episode?: Episode;
|
||||
}
|
||||
export function EpisodeLink({
|
||||
title,
|
||||
seasonNumber,
|
||||
episodeNumber,
|
||||
episode,
|
||||
children,
|
||||
color = 'inherit',
|
||||
...linkProps
|
||||
}: Props) {
|
||||
const link = useMemo(() => {
|
||||
return getEpisodeLink(
|
||||
title,
|
||||
seasonNumber || episode?.episode_number || 1,
|
||||
episodeNumber || episode?.episode_number || 1
|
||||
);
|
||||
}, [title, seasonNumber, episodeNumber, episode]);
|
||||
|
||||
return (
|
||||
<BaseMediaLink {...linkProps} link={link}>
|
||||
{children ?? <span>{episode?.name}</span>}
|
||||
</BaseMediaLink>
|
||||
);
|
||||
}
|
||||
|
||||
export function getEpisodeLink(
|
||||
title: Title,
|
||||
seasonNumber: number | string,
|
||||
episodeNumber: number | string,
|
||||
{absolute}: {absolute?: boolean} = {}
|
||||
): string {
|
||||
const seasonLink = getSeasonLink(title, seasonNumber, {absolute});
|
||||
return `${seasonLink}/episode/${episodeNumber}`;
|
||||
}
|
||||
70
resources/client/episodes/episode-page-header.tsx
Executable file
70
resources/client/episodes/episode-page-header.tsx
Executable file
@@ -0,0 +1,70 @@
|
||||
import {TitlePageHeaderLayout} from '@app/titles/pages/title-page/title-page-header-layout';
|
||||
import {BulletSeparatedItems} from '@app/titles/bullet-separated-items';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {FormattedDate} from '@common/i18n/formatted-date';
|
||||
import {FormattedDuration} from '@common/i18n/formatted-duration';
|
||||
import {InteractableRating} from '@app/reviews/interactable-rating';
|
||||
import {Breadcrumb} from '@common/ui/breadcrumbs/breadcrumb';
|
||||
import {BreadcrumbItem} from '@common/ui/breadcrumbs/breadcrumb-item';
|
||||
import {getTitleLink} from '@app/titles/title-link';
|
||||
import {getSeasonLink} from '@app/seasons/season-link';
|
||||
import React from 'react';
|
||||
import {Title} from '@app/titles/models/title';
|
||||
import {Episode} from '@app/titles/models/episode';
|
||||
import {useNavigate} from '@common/utils/hooks/use-navigate';
|
||||
import {TitlePoster} from '@app/titles/title-poster/title-poster';
|
||||
|
||||
interface Props {
|
||||
title: Title;
|
||||
episode: Episode;
|
||||
showPoster?: boolean;
|
||||
}
|
||||
export function EpisodePageHeader({title, episode, showPoster}: Props) {
|
||||
const navigate = useNavigate();
|
||||
const runtime = episode.runtime || title.runtime;
|
||||
return (
|
||||
<TitlePageHeaderLayout
|
||||
poster={
|
||||
showPoster ? (
|
||||
<TitlePoster title={title} size="w-80" srcSize="sm" />
|
||||
) : undefined
|
||||
}
|
||||
name={episode.name}
|
||||
description={
|
||||
<BulletSeparatedItems className="my-10 md:my-0">
|
||||
<Trans
|
||||
message="Aired :date"
|
||||
values={{
|
||||
date: <FormattedDate date={episode.release_date} />,
|
||||
}}
|
||||
/>
|
||||
<span className="uppercase">{title.certification}</span>
|
||||
{runtime ? <FormattedDuration minutes={runtime} verbose /> : null}
|
||||
</BulletSeparatedItems>
|
||||
}
|
||||
right={<InteractableRating title={title} episode={episode} />}
|
||||
>
|
||||
<Breadcrumb isNavigation>
|
||||
<BreadcrumbItem onSelected={() => navigate(getTitleLink(title))}>
|
||||
{title.name}
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbItem
|
||||
onSelected={() =>
|
||||
navigate(getSeasonLink(title, episode.season_number))
|
||||
}
|
||||
>
|
||||
<Trans
|
||||
message="Season :number"
|
||||
values={{number: episode.season_number}}
|
||||
/>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbItem>
|
||||
<Trans
|
||||
message="Episode :number"
|
||||
values={{number: episode.episode_number}}
|
||||
/>
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
</TitlePageHeaderLayout>
|
||||
);
|
||||
}
|
||||
150
resources/client/episodes/episode-page.tsx
Executable file
150
resources/client/episodes/episode-page.tsx
Executable file
@@ -0,0 +1,150 @@
|
||||
import {PageStatus} from '@common/http/page-status';
|
||||
import {Title} from '@app/titles/models/title';
|
||||
import {PageMetaTags} from '@common/http/page-meta-tags';
|
||||
import React, {Fragment} from 'react';
|
||||
import {TitlePageHeaderImage} from '@app/titles/pages/title-page/title-page-header-image';
|
||||
import {
|
||||
GetEpisodeResponse,
|
||||
useEpisode,
|
||||
} from '@app/episodes/requests/use-episode';
|
||||
import {ChipList} from '@common/ui/forms/input-field/chip-field/chip-list';
|
||||
import {Chip} from '@common/ui/forms/input-field/chip-field/chip';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {TitlePageCast} from '@app/titles/pages/title-page/sections/title-page-cast';
|
||||
import {RelatedTitlesPanel} from '@app/titles/related-titles-panel';
|
||||
import {CompactCredits} from '@app/titles/compact-credits';
|
||||
import {TitlePageAsideLayout} from '@app/titles/pages/title-page/title-page-aside-layout';
|
||||
import {WatchlistButton} from '@app/user-lists/watchlist-button';
|
||||
import {TitlePoster} from '@app/titles/title-poster/title-poster';
|
||||
import {getGenreLink} from '@app/titles/genre-link';
|
||||
import {SitePageLayout} from '@app/site-page-layout';
|
||||
import {EpisodePageHeader} from '@app/episodes/episode-page-header';
|
||||
import {TruncatedDescription} from '@common/ui/truncated-description';
|
||||
import {TitlePageVideoGrid} from '@app/titles/pages/title-page/sections/title-page-video-grid';
|
||||
import {useIsStreamingMode} from '@app/videos/use-is-streaming-mode';
|
||||
import {WatchNowButton} from '@app/titles/pages/title-page/watch-now-button';
|
||||
import {Episode} from '@app/titles/models/episode';
|
||||
import {TitlePageSections} from '@app/titles/pages/title-page/sections/title-page-sections';
|
||||
import {TitlePageEpisodeGrid} from '@app/titles/pages/title-page/sections/title-page-episode-grid';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {useSettings} from '@common/core/settings/use-settings';
|
||||
|
||||
export function EpisodePage() {
|
||||
const query = useEpisode('episodePage');
|
||||
|
||||
const content = query.data ? (
|
||||
<Fragment>
|
||||
<PageMetaTags query={query} />
|
||||
<PageContent data={query.data} />
|
||||
</Fragment>
|
||||
) : (
|
||||
<PageStatus query={query} loaderClassName="absolute inset-0 m-auto" />
|
||||
);
|
||||
|
||||
return <SitePageLayout>{content}</SitePageLayout>;
|
||||
}
|
||||
|
||||
interface PageContentProps {
|
||||
data: GetEpisodeResponse;
|
||||
}
|
||||
function PageContent({data}: PageContentProps) {
|
||||
const {episode, title} = data;
|
||||
return (
|
||||
<div>
|
||||
<TitlePageHeaderImage title={title} episode={episode} />
|
||||
<div className="container mx-auto mt-12 px-14 md:mt-40 md:px-24">
|
||||
<div className="items-start gap-54 md:flex">
|
||||
<Aside title={title} episode={episode} />
|
||||
<div className="flex-auto">
|
||||
<EpisodePageHeader title={title} episode={episode} />
|
||||
<MainContent data={data} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface MainContentProps {
|
||||
data: GetEpisodeResponse;
|
||||
}
|
||||
function MainContent({data}: MainContentProps) {
|
||||
const {episode, title, credits} = data;
|
||||
const {title_page} = useSettings();
|
||||
return (
|
||||
<main className="@container">
|
||||
{title.genres?.length ? (
|
||||
<ChipList>
|
||||
{title.genres.map(genre => (
|
||||
<Chip
|
||||
className="capitalize"
|
||||
elementType={Link}
|
||||
to={getGenreLink(genre)}
|
||||
key={genre.id}
|
||||
>
|
||||
{genre.display_name || genre.name}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipList>
|
||||
) : null}
|
||||
<TruncatedDescription
|
||||
className="mt-16"
|
||||
description={episode.description}
|
||||
/>
|
||||
<CompactCredits credits={credits} />
|
||||
{title_page?.sections.map(name => (
|
||||
<EpisodePageSection key={name} name={name} data={data} />
|
||||
))}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
interface EpisodePageSectionProps {
|
||||
data: GetEpisodeResponse;
|
||||
name: (typeof TitlePageSections)[number];
|
||||
}
|
||||
function EpisodePageSection({name, data}: EpisodePageSectionProps) {
|
||||
switch (name) {
|
||||
case 'videos':
|
||||
return <TitlePageVideoGrid title={data.title} episode={data.episode} />;
|
||||
case 'cast':
|
||||
return <TitlePageCast credits={data.credits?.actors} />;
|
||||
case 'related':
|
||||
return <RelatedTitlesPanel title={data.title} />;
|
||||
case 'episodes':
|
||||
return (
|
||||
<TitlePageEpisodeGrid
|
||||
data={data}
|
||||
label={<Trans message="Other episodes" />}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
interface AsideProps {
|
||||
title: Title;
|
||||
episode: Episode;
|
||||
}
|
||||
function Aside({title, episode}: AsideProps) {
|
||||
const isStreamingMode = useIsStreamingMode();
|
||||
return (
|
||||
<TitlePageAsideLayout
|
||||
className="max-md:hidden"
|
||||
poster={<TitlePoster title={title} size="w-full" srcSize="lg" />}
|
||||
>
|
||||
{isStreamingMode && episode.primary_video && (
|
||||
<WatchNowButton
|
||||
video={episode.primary_video}
|
||||
variant="flat"
|
||||
defaultLabel
|
||||
/>
|
||||
)}
|
||||
<WatchlistButton
|
||||
item={title}
|
||||
variant={isStreamingMode ? 'outline' : 'flat'}
|
||||
/>
|
||||
</TitlePageAsideLayout>
|
||||
);
|
||||
}
|
||||
113
resources/client/episodes/episode-poster/episode-poster.tsx
Executable file
113
resources/client/episodes/episode-poster/episode-poster.tsx
Executable file
@@ -0,0 +1,113 @@
|
||||
import {useTrans} from '@common/i18n/use-trans';
|
||||
import {message} from '@common/i18n/message';
|
||||
import clsx from 'clsx';
|
||||
import {ImageSize, useImageSrc} from '@app/images/use-image-src';
|
||||
import {Episode} from '@app/titles/models/episode';
|
||||
import {EpisodeLink} from '@app/episodes/episode-link';
|
||||
import {Title} from '@app/titles/models/title';
|
||||
import {ReactNode} from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {IconButton} from '@common/ui/buttons/icon-button';
|
||||
import {getWatchLink} from '@app/videos/watch-page/get-watch-link';
|
||||
import {MediaPlayIcon} from '@common/icons/media/media-play';
|
||||
import {MovieIcon} from '@common/icons/material/Movie';
|
||||
|
||||
interface Props {
|
||||
title: Title;
|
||||
episode: Episode;
|
||||
seasonNumber?: number;
|
||||
className?: string;
|
||||
size?: string;
|
||||
lazy?: boolean;
|
||||
srcSize?: ImageSize;
|
||||
children?: ReactNode;
|
||||
aspect?: string;
|
||||
link?: string;
|
||||
wrapWithLink?: boolean;
|
||||
showPlayButton?: boolean;
|
||||
rightAction?: ReactNode;
|
||||
}
|
||||
export function EpisodePoster({
|
||||
episode,
|
||||
title,
|
||||
seasonNumber,
|
||||
className,
|
||||
size,
|
||||
srcSize,
|
||||
lazy = true,
|
||||
children,
|
||||
aspect = 'aspect-video',
|
||||
link,
|
||||
wrapWithLink = true,
|
||||
showPlayButton,
|
||||
rightAction,
|
||||
}: Props) {
|
||||
const {trans} = useTrans();
|
||||
const src = useImageSrc(episode.poster, {size: srcSize});
|
||||
|
||||
const imageClassName = clsx(
|
||||
'w-full h-full object-cover bg-fg-base/4',
|
||||
!src ? 'flex items-center justify-center' : 'block',
|
||||
);
|
||||
|
||||
let image = src ? (
|
||||
<img
|
||||
className={imageClassName}
|
||||
draggable={false}
|
||||
loading={lazy ? 'lazy' : 'eager'}
|
||||
src={src}
|
||||
alt={trans(message('Poster for :name', {values: {name: episode.name}}))}
|
||||
/>
|
||||
) : (
|
||||
<span className={imageClassName}>
|
||||
<MovieIcon className="max-w-[60%] text-divider" size="text-6xl" />
|
||||
</span>
|
||||
);
|
||||
|
||||
const playButton =
|
||||
showPlayButton && episode.primary_video ? (
|
||||
<IconButton
|
||||
color="white"
|
||||
variant="flat"
|
||||
className="absolute bottom-12 left-12 z-10 shadow-md"
|
||||
elementType={Link}
|
||||
radius="rounded-full"
|
||||
to={getWatchLink(episode.primary_video)}
|
||||
>
|
||||
<MediaPlayIcon />
|
||||
</IconButton>
|
||||
) : null;
|
||||
|
||||
if (wrapWithLink) {
|
||||
image = link ? (
|
||||
<Link to={link}>{image}</Link>
|
||||
) : (
|
||||
<EpisodeLink
|
||||
title={title}
|
||||
episode={episode}
|
||||
seasonNumber={episode.season_number ?? seasonNumber}
|
||||
displayContents
|
||||
>
|
||||
{image}
|
||||
</EpisodeLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('group relative flex-shrink-0', size, aspect, className)}
|
||||
>
|
||||
{image}
|
||||
{playButton}
|
||||
{children && <div className="absolute bottom-14 left-14">{children}</div>}
|
||||
{wrapWithLink && (
|
||||
<div className="pointer-events-none absolute inset-0 bg-black opacity-0 transition-opacity group-hover:opacity-10" />
|
||||
)}
|
||||
{rightAction && (
|
||||
<div className="absolute bottom-12 right-12 z-10 shadow-md">
|
||||
{rightAction}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
58
resources/client/episodes/epispde-full-credits-page.tsx
Executable file
58
resources/client/episodes/epispde-full-credits-page.tsx
Executable file
@@ -0,0 +1,58 @@
|
||||
import React, {Fragment} from 'react';
|
||||
import {PageMetaTags} from '@common/http/page-meta-tags';
|
||||
import {PageStatus} from '@common/http/page-status';
|
||||
import {TitlePageHeaderImage} from '@app/titles/pages/title-page/title-page-header-image';
|
||||
import {SitePageLayout} from '@app/site-page-layout';
|
||||
import {SiteSectionHeading} from '@app/titles/site-section-heading';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {TitleCreditsGrid} from '@app/titles/pages/title-page/title-credits-grid/title-credits-grid';
|
||||
import {
|
||||
GetEpisodeResponse,
|
||||
useEpisode,
|
||||
} from '@app/episodes/requests/use-episode';
|
||||
import {EpisodePageHeader} from '@app/episodes/episode-page-header';
|
||||
|
||||
export function EpisodeFullCreditsPage() {
|
||||
const query = useEpisode('episodeCreditsPage');
|
||||
|
||||
const content = query.data ? (
|
||||
<Fragment>
|
||||
<PageMetaTags query={query} />
|
||||
<PageContent data={query.data} />
|
||||
</Fragment>
|
||||
) : (
|
||||
<PageStatus query={query} loaderClassName="absolute inset-0 m-auto" />
|
||||
);
|
||||
|
||||
return <SitePageLayout>{content}</SitePageLayout>;
|
||||
}
|
||||
|
||||
interface PageContentProps {
|
||||
data: GetEpisodeResponse;
|
||||
}
|
||||
function PageContent({
|
||||
data: {title, episode, credits: groupedCredits},
|
||||
}: PageContentProps) {
|
||||
return (
|
||||
<div>
|
||||
<TitlePageHeaderImage title={title} episode={episode} />
|
||||
<div className="container mx-auto mt-24 px-14 md:mt-40 md:px-24">
|
||||
<EpisodePageHeader title={title} episode={episode} showPoster />
|
||||
<div className="mt-48 @container">
|
||||
<SiteSectionHeading headingType="h2" className="mb-40">
|
||||
<Trans message="Full cast and crew" />
|
||||
</SiteSectionHeading>
|
||||
{groupedCredits &&
|
||||
Object.entries(groupedCredits).map(([department, credits]) => (
|
||||
<div key={department}>
|
||||
<h3 className="mb-16 text-2xl font-bold capitalize">
|
||||
<Trans message={department} />
|
||||
</h3>
|
||||
<TitleCreditsGrid credits={credits} className="mb-68" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
48
resources/client/episodes/requests/use-create-episode.ts
Executable file
48
resources/client/episodes/requests/use-create-episode.ts
Executable file
@@ -0,0 +1,48 @@
|
||||
import {BackendResponse} from '@common/http/backend-response/backend-response';
|
||||
import {useMutation} from '@tanstack/react-query';
|
||||
import {apiClient, queryClient} from '@common/http/query-client';
|
||||
import {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';
|
||||
import {UseFormReturn} from 'react-hook-form';
|
||||
import {onFormQueryError} from '@common/errors/on-form-query-error';
|
||||
import {seasonQueryKey} from '@app/seasons/requests/use-season';
|
||||
import {Episode} from '@app/titles/models/episode';
|
||||
|
||||
interface Response extends BackendResponse {
|
||||
episode: Episode;
|
||||
}
|
||||
|
||||
export interface CreateEpisodePayload {
|
||||
name: string;
|
||||
description: string;
|
||||
release_date: string;
|
||||
runtime: number;
|
||||
popularity: number;
|
||||
poster: string;
|
||||
}
|
||||
|
||||
export function useCreateEpisode(
|
||||
form: UseFormReturn<CreateEpisodePayload>,
|
||||
titleId: number,
|
||||
season: number | string,
|
||||
) {
|
||||
return useMutation({
|
||||
mutationFn: (payload: CreateEpisodePayload) =>
|
||||
createEpisode(titleId, season, payload),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: seasonQueryKey(titleId, season),
|
||||
});
|
||||
},
|
||||
onError: r => (form ? onFormQueryError(r, form) : showHttpErrorToast(r)),
|
||||
});
|
||||
}
|
||||
|
||||
function createEpisode(
|
||||
titleId: number,
|
||||
season: number | string,
|
||||
payload: CreateEpisodePayload,
|
||||
): Promise<Response> {
|
||||
return apiClient
|
||||
.post(`titles/${titleId}/seasons/${season}/episodes`, payload)
|
||||
.then(r => r.data);
|
||||
}
|
||||
27
resources/client/episodes/requests/use-delete-episode.ts
Executable file
27
resources/client/episodes/requests/use-delete-episode.ts
Executable file
@@ -0,0 +1,27 @@
|
||||
import {BackendResponse} from '@common/http/backend-response/backend-response';
|
||||
import {useMutation} from '@tanstack/react-query';
|
||||
import {apiClient, queryClient} from '@common/http/query-client';
|
||||
import {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';
|
||||
import {toast} from '@common/ui/toast/toast';
|
||||
import {message} from '@common/i18n/message';
|
||||
import {Episode} from '@app/titles/models/episode';
|
||||
import {seasonQueryKey} from '@app/seasons/requests/use-season';
|
||||
|
||||
interface Response extends BackendResponse {}
|
||||
|
||||
export function useDeleteEpisode(episode: Episode) {
|
||||
return useMutation({
|
||||
mutationFn: () => deleteEpisode(episode.id),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: seasonQueryKey(episode.title_id, episode.season_number),
|
||||
});
|
||||
toast(message('Episode deleted'));
|
||||
},
|
||||
onError: r => showHttpErrorToast(r),
|
||||
});
|
||||
}
|
||||
|
||||
function deleteEpisode(seasonId: number | string): Promise<Response> {
|
||||
return apiClient.delete(`episodes/${seasonId}`).then(r => r.data);
|
||||
}
|
||||
55
resources/client/episodes/requests/use-episode.ts
Executable file
55
resources/client/episodes/requests/use-episode.ts
Executable file
@@ -0,0 +1,55 @@
|
||||
import {useQuery} from '@tanstack/react-query';
|
||||
import {apiClient} from '@common/http/query-client';
|
||||
import {useParams} from 'react-router-dom';
|
||||
import {BackendResponse} from '@common/http/backend-response/backend-response';
|
||||
import {Title} from '@app/titles/models/title';
|
||||
import {Episode} from '@app/titles/models/episode';
|
||||
import {seasonQueryKey} from '@app/seasons/requests/use-season';
|
||||
import {GroupTitleCredits} from '@app/titles/requests/use-title';
|
||||
import {getBootstrapData} from '@common/core/bootstrap-data/use-backend-bootstrap-data';
|
||||
|
||||
export interface GetEpisodeResponse extends BackendResponse {
|
||||
episode: Episode;
|
||||
title: Title;
|
||||
credits?: GroupTitleCredits;
|
||||
}
|
||||
|
||||
export function useEpisode(
|
||||
loader: 'episodePage' | 'episodeCreditsPage' | 'episode',
|
||||
) {
|
||||
const {titleId, season, episode} = useParams();
|
||||
return useQuery({
|
||||
queryKey: [
|
||||
...seasonQueryKey(titleId!, season!),
|
||||
'episodes',
|
||||
`${episode}`,
|
||||
loader,
|
||||
],
|
||||
queryFn: () => fetchEpisode(titleId!, season!, episode!, loader),
|
||||
initialData: () => {
|
||||
const data = getBootstrapData().loaders?.[loader];
|
||||
if (
|
||||
data?.title.id == titleId &&
|
||||
data?.episode.season_number == season &&
|
||||
data?.episode.episode_number == episode
|
||||
) {
|
||||
return data;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function fetchEpisode(
|
||||
titleId: string,
|
||||
seasonNumber: string,
|
||||
episodeNumber: string,
|
||||
loader: string,
|
||||
) {
|
||||
return apiClient
|
||||
.get<GetEpisodeResponse>(
|
||||
`titles/${titleId}/seasons/${seasonNumber}/episodes/${episodeNumber}`,
|
||||
{params: {loader}},
|
||||
)
|
||||
.then(response => response.data);
|
||||
}
|
||||
41
resources/client/episodes/requests/use-update-episode.ts
Executable file
41
resources/client/episodes/requests/use-update-episode.ts
Executable file
@@ -0,0 +1,41 @@
|
||||
import {BackendResponse} from '@common/http/backend-response/backend-response';
|
||||
import {useMutation} from '@tanstack/react-query';
|
||||
import {apiClient, queryClient} from '@common/http/query-client';
|
||||
import {UseFormReturn} from 'react-hook-form';
|
||||
import {onFormQueryError} from '@common/errors/on-form-query-error';
|
||||
import {seasonQueryKey} from '@app/seasons/requests/use-season';
|
||||
import {CreateEpisodePayload} from '@app/episodes/requests/use-create-episode';
|
||||
import {Episode} from '@app/titles/models/episode';
|
||||
|
||||
interface Response extends BackendResponse {
|
||||
episode: Episode;
|
||||
}
|
||||
|
||||
export function useUpdateEpisode(
|
||||
titleId: number | string,
|
||||
season: number | string,
|
||||
episode: number | string,
|
||||
form: UseFormReturn<CreateEpisodePayload>,
|
||||
) {
|
||||
return useMutation({
|
||||
mutationFn: (payload: CreateEpisodePayload) =>
|
||||
updateEpisode(titleId, season, episode, payload),
|
||||
onSuccess: async ({episode}) => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: seasonQueryKey(episode.title_id, episode.season_number),
|
||||
});
|
||||
},
|
||||
onError: r => onFormQueryError(r, form),
|
||||
});
|
||||
}
|
||||
|
||||
function updateEpisode(
|
||||
titleId: number | string,
|
||||
season: number | string,
|
||||
episode: number | string,
|
||||
payload: CreateEpisodePayload,
|
||||
): Promise<Response> {
|
||||
return apiClient
|
||||
.put(`titles/${titleId}/seasons/${season}/episodes/${episode}`, payload)
|
||||
.then(r => r.data);
|
||||
}
|
||||
Reference in New Issue
Block a user