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.description}
+
+
Episodes: {episodesData?.podcast.trackCount}
+
+
+
+
+ | Title |
+ Release Date |
+ Duration |
+
+
+
+ {episodesData?.episodes?.map((episode) => {
+ const formattedDate = formatDate(episode.releaseDate);
+ const formattedDuration = formatDuration(episode.durationSeconds);
+
+ return (
+
+ | {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[];
}