mirror of
https://github.com/ershisan99/podcaster.git
synced 2025-12-17 20:59:27 +00:00
feat: add episodes info to podcast page
This commit is contained in:
@@ -1,10 +1,14 @@
|
|||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useTopPodcastsQuery } from "../services/podcasts/podcast.hooks";
|
import {
|
||||||
|
usePodcastEpisodesQuery,
|
||||||
|
useTopPodcastsQuery,
|
||||||
|
} from "../services/podcasts/podcast.hooks";
|
||||||
|
|
||||||
export function Podcast() {
|
export function Podcast() {
|
||||||
const { podcastId } = useParams<{ podcastId: string }>();
|
const { podcastId } = useParams<{ podcastId: string }>();
|
||||||
|
|
||||||
const { data: podcasts } = useTopPodcastsQuery();
|
const { data: podcasts } = useTopPodcastsQuery();
|
||||||
|
const { data: episodesData } = usePodcastEpisodesQuery(podcastId);
|
||||||
|
|
||||||
if (!podcastId) {
|
if (!podcastId) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -17,6 +21,58 @@ export function Podcast() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const podcast = podcasts.find((podcast) => podcast.id === podcastId);
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/services/podcasts/dto/episode.dto.ts
Normal file
17
src/services/podcasts/dto/episode.dto.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/services/podcasts/dto/podcast-extra.dto.ts
Normal file
17
src/services/podcasts/dto/podcast-extra.dto.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Entry, ImImage } from "./podcasts.types";
|
import { Entry, ImImage } from "../podcasts.types";
|
||||||
|
|
||||||
export class PodcastDTO {
|
export class PodcastDTO {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -3,6 +3,7 @@ import { podcastsService } from "./podcasts.service";
|
|||||||
|
|
||||||
const QUERY_KEYS = {
|
const QUERY_KEYS = {
|
||||||
TOP_PODCASTS: "podcasts/top",
|
TOP_PODCASTS: "podcasts/top",
|
||||||
|
PODCAST_EPISODES: "podcasts/episodes",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export function useTopPodcastsQuery() {
|
export function useTopPodcastsQuery() {
|
||||||
@@ -11,3 +12,11 @@ export function useTopPodcastsQuery() {
|
|||||||
queryFn: () => podcastsService.getTopPodcasts(),
|
queryFn: () => podcastsService.getTopPodcasts(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function usePodcastEpisodesQuery(podcastId?: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [QUERY_KEYS.PODCAST_EPISODES, podcastId],
|
||||||
|
queryFn: () => podcastsService.getEpisodesByPodcastId(podcastId ?? ""),
|
||||||
|
enabled: !!podcastId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { GetEpisodesResponse, GetTopPodcastsResponse } from "./podcasts.types";
|
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 {
|
class PodcastsService {
|
||||||
baseUrl = "https://itunes.apple.com";
|
baseUrl = "https://itunes.apple.com";
|
||||||
@@ -23,11 +25,30 @@ class PodcastsService {
|
|||||||
};
|
};
|
||||||
url.search = new URLSearchParams(params).toString();
|
url.search = new URLSearchParams(params).toString();
|
||||||
|
|
||||||
const response = await fetch(url);
|
const response: GetEpisodesResponse = await this.fetchWithoutCors(
|
||||||
const data: GetEpisodesResponse = await response.json();
|
url.toString(),
|
||||||
|
);
|
||||||
|
let podcast: PodcastExtraDTO = {} as PodcastExtraDTO;
|
||||||
|
const episodes: EpisodeDto[] = [];
|
||||||
|
|
||||||
//TODO: add dto
|
response.results.forEach((entry) => {
|
||||||
return data;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -165,46 +165,72 @@ export interface Id2 {
|
|||||||
|
|
||||||
export interface GetEpisodesResponse {
|
export interface GetEpisodesResponse {
|
||||||
resultCount: number;
|
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 {
|
export interface PodcastDetails {
|
||||||
wrapperType: string;
|
wrapperType: string;
|
||||||
kind: string;
|
kind: "podcast";
|
||||||
collectionId: number;
|
collectionId: number;
|
||||||
trackId: number;
|
trackId: number;
|
||||||
artistName?: string;
|
artistName: string;
|
||||||
collectionName: string;
|
collectionName: string;
|
||||||
trackName: string;
|
trackName: string;
|
||||||
collectionCensoredName?: string;
|
collectionCensoredName: string;
|
||||||
trackCensoredName?: string;
|
trackCensoredName: string;
|
||||||
collectionViewUrl: string;
|
collectionViewUrl: string;
|
||||||
feedUrl: string;
|
feedUrl: string;
|
||||||
trackViewUrl: string;
|
trackViewUrl: string;
|
||||||
artworkUrl30?: string;
|
artworkUrl30: string;
|
||||||
artworkUrl60: string;
|
artworkUrl60: string;
|
||||||
artworkUrl100?: string;
|
artworkUrl100: string;
|
||||||
collectionPrice?: number;
|
collectionPrice: number;
|
||||||
trackPrice?: number;
|
trackPrice: number;
|
||||||
collectionHdPrice?: number;
|
collectionHdPrice: number;
|
||||||
releaseDate: string;
|
releaseDate: string;
|
||||||
collectionExplicitness?: string;
|
collectionExplicitness: string;
|
||||||
trackExplicitness?: string;
|
trackExplicitness: string;
|
||||||
trackCount?: number;
|
trackCount: number;
|
||||||
trackTimeMillis: number;
|
trackTimeMillis: number;
|
||||||
country: string;
|
country: string;
|
||||||
currency?: string;
|
currency: string;
|
||||||
primaryGenreName?: string;
|
primaryGenreName: string;
|
||||||
contentAdvisoryRating: string;
|
contentAdvisoryRating: string;
|
||||||
artworkUrl600: string;
|
artworkUrl600: string;
|
||||||
genreIds?: string[];
|
genreIds: string[];
|
||||||
closedCaptioning?: string;
|
genres: string[];
|
||||||
shortDescription?: string;
|
|
||||||
episodeUrl?: string;
|
|
||||||
episodeGuid?: string;
|
|
||||||
description?: string;
|
|
||||||
artworkUrl160?: string;
|
|
||||||
episodeContentType?: string;
|
|
||||||
episodeFileExtension?: string;
|
|
||||||
previewUrl?: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user