diff --git a/src/api/ApiHooks.ts b/src/api/ApiHooks.ts new file mode 100644 index 0000000000000000000000000000000000000000..b05dc36f929ec5f6e375a0b9216b75d25d8987c2 --- /dev/null +++ b/src/api/ApiHooks.ts @@ -0,0 +1,98 @@ +import {Content} from "./models/Content"; +import {useQuery} from "react-query"; +import {fetchJson} from "../util/fetchJson"; +import {Instalment} from "./models/Instalment"; +import {useApiEndpoint} from "./ApiEndpointContext"; +import {ContentMeta} from "./models/dto/ContentMeta"; +import {Genre} from "./models/Genre"; + +export function useShowEpisodes( + item: Content | undefined +): [Instalment[], boolean, unknown] { + const apiEndpoint = useApiEndpoint(); + + const {data, isLoading, error} = useQuery( + ["episodes", item?.ids?.uuid], + () => item?.kind === "show" + ? fetchJson<Instalment[]>( + apiEndpoint, + `api/v1/content/${item.ids.uuid}/episodes`, + { + method: "GET" + } + ) + : [] + ) + + return [data || [], isLoading, error]; +} + +export function useSingleContent( + id: string +): [ContentMeta | null, boolean, unknown] { + const apiEndpoint = useApiEndpoint(); + + const {data, isLoading, error} = useQuery( + ["media", id], + () => fetchJson<ContentMeta>( + apiEndpoint, + `api/v1/content/${id}`, + { + method: "GET" + } + ) + ) + + return [data || null, isLoading, error]; +} + +export function useAllContent(): [Content[], boolean, unknown] { + const apiEndpoint = useApiEndpoint(); + + const {data, isLoading, error} = useQuery( + "media", + () => fetchJson<Content[]>( + apiEndpoint, + `api/v1/content`, + { + method: "GET" + } + ) + ); + + return [data || [], isLoading, error]; +} + +export function useGenres(): [Genre[], boolean, unknown] { + const apiEndpoint = useApiEndpoint(); + + const {data, isLoading, error} = useQuery( + "media", + () => fetchJson<Genre[]>( + apiEndpoint, + `api/v1/genres`, + { + method: "GET" + } + ) + ); + + return [data || [], isLoading, error]; +} + +export function useGenreContent(genreId: string): [Content[], boolean, unknown] { + const apiEndpoint = useApiEndpoint(); + + const {data, isLoading, error} = useQuery( + "media", + () => fetchJson<Content[]>( + apiEndpoint, + `api/v1/genres/${genreId}`, + { + method: "GET" + } + ) + ); + + return [data || [], isLoading, error]; +} diff --git a/src/api/hooks.ts b/src/api/hooks.ts deleted file mode 100644 index 074f081cf326a5798da140080e036755f9c9bf04..0000000000000000000000000000000000000000 --- a/src/api/hooks.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {Content} from "./models/Content"; -import {useQuery} from "react-query"; -import {getContent, listEpisodes} from "./queries"; -import {Instalment} from "./models/Instalment"; -import {useApiEndpoint} from "./ApiEndpointContext"; -import {ContentMeta} from "./models/dto/ContentMeta"; - -export function useEpisodes(item: Content | undefined): [Instalment[], boolean, unknown] { - const apiEndpoint = useApiEndpoint(); - - const {data, isLoading, error} = useQuery( - ["episodes", item?.ids?.uuid], - () => item?.kind === "show" - ? listEpisodes(apiEndpoint, item.ids.uuid) - : [] - ) - - return [data || [], isLoading, error]; -} - -export function useContent(id: string): [ContentMeta | null, boolean, unknown] { - const apiEndpoint = useApiEndpoint(); - - const {data, isLoading, error} = useQuery( - ["media", id], - () => getContent(apiEndpoint, id) - ) - - return [data || null, isLoading, error]; -} diff --git a/src/api/queries.ts b/src/api/queries.ts deleted file mode 100644 index d7ff6f01595c760515189fab238aaf83519f5e3f..0000000000000000000000000000000000000000 --- a/src/api/queries.ts +++ /dev/null @@ -1,53 +0,0 @@ -import {Genre} from "./models/Genre"; -import {GenreWithContent} from "./models/dto/GenreWithContent"; -import {Content} from "./models/Content"; -import {ContentMeta} from "./models/dto/ContentMeta"; -import {Instalment} from "./models/Instalment"; -import {RequestErrorKind} from "../util/request/RequestErrorKind"; -import {RequestError} from "../util/request/RequestError"; - -async function request<T>( - baseUrl: URL, - path: string, - headers?: RequestInit -): Promise<T> { - const url = new URL(path, baseUrl).toString(); - const response = await fetch(url, headers); - if (response.ok) { - return await response.json(); - } else if (response.status in RequestErrorKind) { - throw new RequestError(response.statusText, response.status) - } else { - throw new RequestError(response.statusText, RequestErrorKind.UnknownError) - } -} - -export async function listGenres(baseUrl: URL): Promise<Genre[]> { - return await request(baseUrl, `api/v1/genres`, { - method: "GET" - }); -} - -export async function getGenre(baseUrl: URL, genreId: string): Promise<GenreWithContent> { - return await request(baseUrl, `api/v1/genres/${genreId}`, { - method: "GET" - }); -} - -export async function listContent(baseUrl: URL): Promise<Content[]> { - return await request(baseUrl, `api/v1/content`, { - method: "GET" - }); -} - -export async function getContent(baseUrl: URL, contentId: string): Promise<ContentMeta> { - return await request(baseUrl, `api/v1/content/${contentId}`, { - method: "GET" - }); -} - -export async function listEpisodes(baseUrl: URL, contentId: string): Promise<Instalment[]> { - return await request(baseUrl, `api/v1/content/${contentId}/episodes`, { - method: "GET" - }); -} diff --git a/src/routes/ContentRoute.tsx b/src/routes/ContentRoute.tsx index 5a9ec8b86d96d1da36d34e39382b967182cf75cb..23d9665f95a9fdea789d8a9bf6b2b1153116d004 100644 --- a/src/routes/ContentRoute.tsx +++ b/src/routes/ContentRoute.tsx @@ -1,12 +1,12 @@ import {PropsWithChildren} from "react"; import {useParams} from "react-router"; import {CurrentContentProvider} from "../util/CurrentContentContext"; -import {useContent} from "../api/hooks"; +import {useSingleContent} from "../api/ApiHooks"; export function ContentRoute(props: PropsWithChildren<{}>) { const {children} = props; const {contentId} = useParams<{ contentId: string }>(); - const [meta/*, isLoading, error*/] = useContent(contentId); + const [meta/*, isLoading, error*/] = useSingleContent(contentId); return ( <CurrentContentProvider value={meta}> diff --git a/src/routes/main/MainPage.tsx b/src/routes/main/MainPage.tsx index 752224f8e033d317c3db3f3ac2a3c8898fc264a6..ef2804e9de5b7b0f3b14968e52bc3f1dc40b9a5e 100644 --- a/src/routes/main/MainPage.tsx +++ b/src/routes/main/MainPage.tsx @@ -1,25 +1,23 @@ -import {useApiEndpoint} from "../../api/ApiEndpointContext"; import {Content, getLocalizedName} from "../../api/models/Content"; import {sortLexicallyAsc} from "../../util/sort/sortLexically"; -import {useQuery} from "react-query"; -import {listContent} from "../../api/queries"; import {MediaEntry} from "./MediaEntry"; import {useLocale} from "../../util/locale/LocalizedContext"; +import {useAllContent} from "../../api/ApiHooks"; export function MainPage() { - const apiEndpoint = useApiEndpoint(); const locale = useLocale(); - const {data/*, isLoading, isError*/} = useQuery( - "media", - () => listContent(apiEndpoint) - ); - const media = data || []; + + const [media/*, isLoading, error*/] = useAllContent(); return ( <div> {media .sort(sortLexicallyAsc(item => getLocalizedName(item, locale)?.name || "")) - .map((item: Content) => (<MediaEntry item={item}/>))} + .map((item: Content) => ( + <MediaEntry + key={item.ids.uuid} + item={item}/> + ))} </div> ); } diff --git a/src/routes/main/MediaEntry.tsx b/src/routes/main/MediaEntry.tsx index 042417c385837461b238ed5aee8b05436970cb13..125472b155a3566b0e6943b2b7e09fb8739cebfe 100644 --- a/src/routes/main/MediaEntry.tsx +++ b/src/routes/main/MediaEntry.tsx @@ -3,7 +3,7 @@ import {Fragment} from "react"; import {Link} from "react-router-dom"; import {createUseStyles} from "react-jss"; import {useLocale} from "../../util/locale/LocalizedContext"; -import {useEpisodes} from "../../api/hooks"; +import {useShowEpisodes} from "../../api/ApiHooks"; import {MediaEpisode} from "./MediaEpisode"; export interface Props { @@ -16,7 +16,7 @@ export function MediaEntry( const locale = useLocale(); const classes = useStyles(); - const [episodes/*, episodesLoading*/, episodesError] = useEpisodes(item); + const [episodes/*, episodesLoading*/, episodesError] = useShowEpisodes(item); const title = getLocalizedName(item, locale); const description = getLocalizedDescription(item, locale); @@ -25,7 +25,7 @@ export function MediaEntry( const backdrop = item.images.find(it => it.kind === "backdrop"); return ( - <div key={item.ids.uuid} className={classes.movie}> + <div className={classes.movie}> <h1>{title?.name}</h1> <p><strong>{rating?.certification}</strong></p> <p><strong>{description?.tagline}</strong></p> @@ -38,11 +38,14 @@ export function MediaEntry( {item.kind === "show" && ( <Fragment> {episodesError ? ( - <p>{""+episodesError}</p> + <p>{"" + episodesError}</p> ) : ( <ul> {episodes.map(episode => - <MediaEpisode item={episode} />)} + <MediaEpisode + key={episode.content.ids.uuid} + item={episode} + />)} </ul> )} </Fragment> diff --git a/src/routes/main/MediaEpisode.tsx b/src/routes/main/MediaEpisode.tsx index 498b80a95240094f572736b779cfc5b7da4cfc7a..ec815e17fc238380bf509f2e7301faf0b96f0120 100644 --- a/src/routes/main/MediaEpisode.tsx +++ b/src/routes/main/MediaEpisode.tsx @@ -14,7 +14,7 @@ export function MediaEpisode({item, disabled}: Props) { const episodeDescription = getLocalizedDescription(item.content, locale); return ( - <li key={item.content.ids.uuid}> + <li> <p> <strong>S{item.season}E{item.episode}</strong> – {item.airDate} </p> diff --git a/src/routes/player/Player.tsx b/src/routes/player/Player.tsx index b529f79f3dc45eff2f022606eca222e29d6f5496..aac33f4e10c32c71abd2cbe5f532d4b512a64aa7 100644 --- a/src/routes/player/Player.tsx +++ b/src/routes/player/Player.tsx @@ -17,7 +17,7 @@ import {Subtitle} from "../../api/models/Subtitle"; import {MousePosition} from "../../util/mouse/MousePosition"; import {VideoProvider} from "./video/VideoContext"; import {SubtitleRenderer} from "./subtitles/SubtitleRenderer"; -import {useEpisodes} from "../../api/hooks"; +import {useShowEpisodes} from "../../api/ApiHooks"; import {MediaEpisode} from "../main/MediaEpisode"; import {usePaused} from "../../util/media/usePaused"; @@ -33,7 +33,7 @@ export function Player( const classes = useStyles(); const locale = useLocale(); - const [relatedEpisodes/*, relatedEpisodesLoading*/, relatedEpisodesError] = useEpisodes(instalment?.content); + const [relatedEpisodes/*, relatedEpisodesLoading*/, relatedEpisodesError] = useShowEpisodes(instalment?.content); const name = getLocalizedName(content, locale); const description = getLocalizedDescription(content, locale); diff --git a/src/util/fetchJson.ts b/src/util/fetchJson.ts new file mode 100644 index 0000000000000000000000000000000000000000..f42e348178b8a22f82cadb7d7e6a815b02a8ea32 --- /dev/null +++ b/src/util/fetchJson.ts @@ -0,0 +1,18 @@ +import {RequestErrorKind} from "./request/RequestErrorKind"; +import {RequestError} from "./request/RequestError"; + +export async function fetchJson<T>( + baseUrl: URL, + path: string, + options?: RequestInit +): Promise<T> { + const url = new URL(path, baseUrl).toString(); + const response = await fetch(url, options); + if (response.ok) { + return await response.json(); + } else if (response.status in RequestErrorKind) { + throw new RequestError(response.statusText, response.status) + } else { + throw new RequestError(response.statusText, RequestErrorKind.UnknownError) + } +}