diff --git a/src/pages/podcast.tsx b/src/pages/podcast.tsx index 6d7995f..364d023 100644 --- a/src/pages/podcast.tsx +++ b/src/pages/podcast.tsx @@ -1,10 +1,14 @@ import { useParams } from "react-router-dom"; -import { useTopPodcastsQuery } from "../services/podcasts/podcast.hooks"; +import { + usePodcastEpisodesQuery, + useTopPodcastsQuery, +} from "../services/podcasts/podcast.hooks"; export function Podcast() { const { podcastId } = useParams<{ podcastId: string }>(); const { data: podcasts } = useTopPodcastsQuery(); + const { data: episodesData } = usePodcastEpisodesQuery(podcastId); if (!podcastId) { throw new Error( @@ -17,6 +21,58 @@ export function Podcast() { } const podcast = podcasts.find((podcast) => podcast.id === podcastId); - console.log(podcast); - return

Podcast page

; + + if (!podcast) { + return

Podcast not found

; + } + // TODO: break into smaller components + return ( +
+
+

{podcast.title}

+

{podcast.author}

+ {podcast.title} +

{podcast.description}

+
+
Episodes: {episodesData?.podcast.trackCount}
+
+ + + + + + + + + + {episodesData?.episodes?.map((episode) => { + const formattedDate = formatDate(episode.releaseDate); + const formattedDuration = formatDuration(episode.durationSeconds); + + return ( + + + + + + ); + })} + +
TitleRelease DateDuration
{episode.title}{formattedDate}{formattedDuration}
+
+
+ ); +} + +function formatDuration(duration?: number) { + if (!duration) { + return "N/A"; + } + const minutes = Math.floor(duration / 60); + const seconds = Math.floor(duration % 60); + return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; +} + +function formatDate(date: string) { + return new Date(date).toLocaleDateString(); } diff --git a/src/services/podcasts/dto/episode.dto.ts b/src/services/podcasts/dto/episode.dto.ts new file mode 100644 index 0000000..e5d2e3b --- /dev/null +++ b/src/services/podcasts/dto/episode.dto.ts @@ -0,0 +1,17 @@ +import { Episode } from "../podcasts.types"; + +export class EpisodeDto { + id: number; + releaseDate: string; + audioUrl: string; + title: string; + durationSeconds: number; + + constructor(episode: Episode) { + this.id = episode.trackId; + this.title = episode.trackName; + this.releaseDate = episode.releaseDate; + this.audioUrl = episode.episodeUrl; + this.durationSeconds = episode.trackTimeMillis / 1000; + } +} diff --git a/src/services/podcasts/dto/podcast-extra.dto.ts b/src/services/podcasts/dto/podcast-extra.dto.ts new file mode 100644 index 0000000..f8db4c7 --- /dev/null +++ b/src/services/podcasts/dto/podcast-extra.dto.ts @@ -0,0 +1,17 @@ +import { PodcastDetails } from "../podcasts.types"; + +export class PodcastExtraDTO { + id: number; + trackCount: number; + images: { small: string; medium: string; large: string }; + + constructor(data: PodcastDetails) { + this.id = data.trackId; + this.trackCount = data.trackCount; + this.images = { + small: data.artworkUrl60, + medium: data.artworkUrl100, + large: data.artworkUrl600, + }; + } +} diff --git a/src/services/podcasts/podcast.dto.ts b/src/services/podcasts/dto/podcast.dto.ts similarity index 93% rename from src/services/podcasts/podcast.dto.ts rename to src/services/podcasts/dto/podcast.dto.ts index 96f3b6b..efd1f09 100644 --- a/src/services/podcasts/podcast.dto.ts +++ b/src/services/podcasts/dto/podcast.dto.ts @@ -1,4 +1,4 @@ -import { Entry, ImImage } from "./podcasts.types"; +import { Entry, ImImage } from "../podcasts.types"; export class PodcastDTO { id: string; diff --git a/src/services/podcasts/podcast.hooks.ts b/src/services/podcasts/podcast.hooks.ts index 8b6f2db..03c94b9 100644 --- a/src/services/podcasts/podcast.hooks.ts +++ b/src/services/podcasts/podcast.hooks.ts @@ -3,6 +3,7 @@ import { podcastsService } from "./podcasts.service"; const QUERY_KEYS = { TOP_PODCASTS: "podcasts/top", + PODCAST_EPISODES: "podcasts/episodes", } as const; export function useTopPodcastsQuery() { @@ -11,3 +12,11 @@ export function useTopPodcastsQuery() { queryFn: () => podcastsService.getTopPodcasts(), }); } + +export function usePodcastEpisodesQuery(podcastId?: string) { + return useQuery({ + queryKey: [QUERY_KEYS.PODCAST_EPISODES, podcastId], + queryFn: () => podcastsService.getEpisodesByPodcastId(podcastId ?? ""), + enabled: !!podcastId, + }); +} diff --git a/src/services/podcasts/podcasts.service.ts b/src/services/podcasts/podcasts.service.ts index a101b1b..5eb9a3b 100644 --- a/src/services/podcasts/podcasts.service.ts +++ b/src/services/podcasts/podcasts.service.ts @@ -1,5 +1,7 @@ import { GetEpisodesResponse, GetTopPodcastsResponse } from "./podcasts.types"; -import { PodcastDTO } from "./podcast.dto"; +import { PodcastDTO } from "./dto/podcast.dto"; +import { EpisodeDto } from "./dto/episode.dto"; +import { PodcastExtraDTO } from "./dto/podcast-extra.dto"; class PodcastsService { baseUrl = "https://itunes.apple.com"; @@ -23,11 +25,30 @@ class PodcastsService { }; url.search = new URLSearchParams(params).toString(); - const response = await fetch(url); - const data: GetEpisodesResponse = await response.json(); + const response: GetEpisodesResponse = await this.fetchWithoutCors( + url.toString(), + ); + let podcast: PodcastExtraDTO = {} as PodcastExtraDTO; + const episodes: EpisodeDto[] = []; - //TODO: add dto - return data; + response.results.forEach((entry) => { + if (entry.kind === "podcast-episode") { + episodes.push(new EpisodeDto(entry)); + } else if (entry.kind === "podcast") { + podcast = new PodcastExtraDTO(entry); + } + }); + + return { episodes, podcast: podcast }; + } + + //TODO: move into a separate service + private async fetchWithoutCors(url: string) { + const response = await fetch( + `https://api.allorigins.win/get?url=${encodeURIComponent(url)}`, + ); + const data = await response.json(); + return JSON.parse(data.contents); } } diff --git a/src/services/podcasts/podcasts.types.ts b/src/services/podcasts/podcasts.types.ts index 94f02d0..653c26e 100644 --- a/src/services/podcasts/podcasts.types.ts +++ b/src/services/podcasts/podcasts.types.ts @@ -165,46 +165,72 @@ export interface Id2 { export interface GetEpisodesResponse { resultCount: number; - results: PodcastDetails[]; + results: Array; +} + +export interface Episode { + country: string; + artworkUrl160: string; + episodeFileExtension: string; + episodeContentType: string; + artworkUrl600: string; + collectionViewUrl: string; + artistIds: number[]; + feedUrl: string; + closedCaptioning: string; + collectionId: number; + collectionName: string; + trackTimeMillis: number; + trackId: number; + trackName: string; + shortDescription: string; + description: string; + contentAdvisoryRating: string; + trackViewUrl: string; + previewUrl: string; + artworkUrl60: string; + episodeUrl: string; + releaseDate: string; + episodeGuid: string; + genres: Genre[]; + kind: "podcast-episode"; + wrapperType: string; +} + +export interface Genre { + name: string; + id: string; } export interface PodcastDetails { wrapperType: string; - kind: string; + kind: "podcast"; collectionId: number; trackId: number; - artistName?: string; + artistName: string; collectionName: string; trackName: string; - collectionCensoredName?: string; - trackCensoredName?: string; + collectionCensoredName: string; + trackCensoredName: string; collectionViewUrl: string; feedUrl: string; trackViewUrl: string; - artworkUrl30?: string; + artworkUrl30: string; artworkUrl60: string; - artworkUrl100?: string; - collectionPrice?: number; - trackPrice?: number; - collectionHdPrice?: number; + artworkUrl100: string; + collectionPrice: number; + trackPrice: number; + collectionHdPrice: number; releaseDate: string; - collectionExplicitness?: string; - trackExplicitness?: string; - trackCount?: number; + collectionExplicitness: string; + trackExplicitness: string; + trackCount: number; trackTimeMillis: number; country: string; - currency?: string; - primaryGenreName?: string; + currency: string; + primaryGenreName: string; contentAdvisoryRating: string; artworkUrl600: string; - genreIds?: string[]; - closedCaptioning?: string; - shortDescription?: string; - episodeUrl?: string; - episodeGuid?: string; - description?: string; - artworkUrl160?: string; - episodeContentType?: string; - episodeFileExtension?: string; - previewUrl?: string; + genreIds: string[]; + genres: string[]; }