feat: add episodes info to podcast page

This commit is contained in:
2024-04-19 00:32:01 +02:00
parent 9370066464
commit db7d0fd841
7 changed files with 180 additions and 34 deletions

View File

@@ -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 <h1>Podcast page</h1>;
if (!podcast) {
return <h1>Podcast not found</h1>;
}
// TODO: break into smaller components
return (
<div>
<div>
<h1>{podcast.title}</h1>
<h2>{podcast.author}</h2>
<img src={episodesData?.podcast?.images.large} alt={podcast.title} />
<p>{podcast.description}</p>
</div>
<div>Episodes: {episodesData?.podcast.trackCount}</div>
<div>
<table>
<thead>
<tr>
<th>Title</th>
<th>Release Date</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
{episodesData?.episodes?.map((episode) => {
const formattedDate = formatDate(episode.releaseDate);
const formattedDuration = formatDuration(episode.durationSeconds);
return (
<tr key={episode.id}>
<td>{episode.title}</td>
<td>{formattedDate}</td>
<td>{formattedDuration}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}
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();
}

View File

@@ -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;
}
}

View File

@@ -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,
};
}
}

View File

@@ -1,4 +1,4 @@
import { Entry, ImImage } from "./podcasts.types";
import { Entry, ImImage } from "../podcasts.types";
export class PodcastDTO {
id: string;

View File

@@ -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,
});
}

View File

@@ -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);
}
}

View File

@@ -165,46 +165,72 @@ export interface Id2 {
export interface GetEpisodesResponse {
resultCount: number;
results: PodcastDetails[];
results: Array<Episode | PodcastDetails>;
}
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[];
}