1- import React , { useState } from 'react' ;
1+ import React , { useState , useEffect } from 'react' ;
22import Layout from '@theme/Layout' ;
33import type { ReactElement } from 'react' ;
44import { useHistory } from '@docusaurus/router' ;
@@ -8,6 +8,7 @@ interface PodcastData {
88 id : string ;
99 spotifyUrl : string ;
1010 type : 'episode' | 'show' | 'playlist' ;
11+ title ?: string ; // Add optional title here
1112}
1213
1314// Function to extract Spotify ID from URL
@@ -36,90 +37,75 @@ const podcastUrls: string[] = [
3637 "https://open.spotify.com/episode/21yp6PDe1XN8B1goR5qMI3?si=k6JURkMRTQq2Ltbujq9qLw" ,
3738] ;
3839
39- const podcastData : PodcastData [ ] = podcastUrls . map ( ( url , index ) => ( {
40+ // Initialize podcast data without titles first
41+ const initialPodcastData : PodcastData [ ] = podcastUrls . map ( ( url , index ) => ( {
4042 id : String ( index + 1 ) ,
4143 spotifyUrl : url ,
42- type : getSpotifyContentType ( url )
44+ type : getSpotifyContentType ( url ) ,
4345} ) ) ;
4446
45- interface SpotifyTitleProps {
46- spotifyUrl : string ;
47- type : 'episode' | 'show' | 'playlist' ;
48- }
49-
50- // Fetches the podcast/show/episode title from Spotify oEmbed API
51- const SpotifyTitle : React . FC < SpotifyTitleProps > = ( { spotifyUrl, type } ) => {
52- const [ title , setTitle ] = React . useState < string > ( '' ) ;
53- const [ loading , setLoading ] = React . useState ( true ) ;
54-
55- React . useEffect ( ( ) => {
56- let cancelled = false ;
57- setLoading ( true ) ;
58- fetch ( `https://open.spotify.com/oembed?url=${ encodeURIComponent ( spotifyUrl ) } ` )
59- . then ( res => res . json ( ) )
60- . then ( data => {
61- if ( ! cancelled ) {
62- setTitle ( data . title ) ;
63- setLoading ( false ) ;
64- }
65- } )
66- . catch ( ( ) => {
67- if ( ! cancelled ) {
68- setTitle ( '' ) ;
69- setLoading ( false ) ;
70- }
71- } ) ;
72- return ( ) => { cancelled = true ; } ;
73- } , [ spotifyUrl ] ) ;
74-
75- return (
76- < div className = "podcast-title" >
77- { loading ? (
78- < div className = "title-skeleton" >
79- < div className = "skeleton-line" > </ div >
80- < div className = "skeleton-line short" > </ div >
81- </ div >
82- ) : (
83- < >
84- < div className = "podcast-type-badge" >
85- < span className = "type-icon" >
86- { type === 'episode' ? '🎙️' : type === 'show' ? '📻' : '🎵' }
87- </ span >
88- { type . charAt ( 0 ) . toUpperCase ( ) + type . slice ( 1 ) }
89- </ div >
90- < h3 className = "podcast-title-text" >
91- { title || `${ type . charAt ( 0 ) . toUpperCase ( ) + type . slice ( 1 ) } #${ Math . floor ( Math . random ( ) * 100 ) + 1 } ` }
92- </ h3 >
93- </ >
94- ) }
47+ // Component to display Spotify title and type badge for each podcast
48+ const SpotifyTitle : React . FC < { title ?: string ; type : 'episode' | 'show' | 'playlist' } > = ( { title, type } ) => (
49+ < div className = "podcast-title" >
50+ < div className = "podcast-type-badge" >
51+ < span className = "type-icon" >
52+ { type === 'episode' ? '🎙️' : type === 'show' ? '📻' : '🎵' }
53+ </ span >
54+ { type . charAt ( 0 ) . toUpperCase ( ) + type . slice ( 1 ) }
9555 </ div >
96- ) ;
97- } ;
56+ < h3 className = "podcast-title-text" > { title || 'Loading title...' } </ h3 >
57+ </ div >
58+ ) ;
9859
9960export default function Podcasts ( ) : ReactElement {
10061 const history = useHistory ( ) ;
10162 const [ currentPage , setCurrentPage ] = useState ( 1 ) ;
10263 const [ searchTerm , setSearchTerm ] = useState ( "" ) ;
103- const [ selectedFilter , setSelectedFilter ] = useState <
104- "all" | "episode" | "show" | "playlist"
105- > ( "all" ) ;
64+ const [ selectedFilter , setSelectedFilter ] = useState < "all" | "episode" | "show" | "playlist" > ( "all" ) ;
10665 const [ favorites , setFavorites ] = useState < string [ ] > ( ( ) => {
107- // Load favorites from localStorage on component mount
10866 if ( typeof window !== "undefined" ) {
10967 const saved = localStorage . getItem ( "podcast-favorites" ) ;
11068 return saved ? JSON . parse ( saved ) : [ ] ;
11169 }
11270 return [ ] ;
11371 } ) ;
72+ const [ podcasts , setPodcasts ] = useState < PodcastData [ ] > ( initialPodcastData ) ;
11473 const podcastsPerPage = 9 ;
11574
116- // Filter podcasts based on search and filter
117- const filteredPodcasts = podcastData . filter ( podcast => {
75+ // Fetch all podcast titles once on mount
76+ useEffect ( ( ) => {
77+ let cancelled = false ;
78+ Promise . all (
79+ podcasts . map ( p =>
80+ fetch ( `https://open.spotify.com/oembed?url=${ encodeURIComponent ( p . spotifyUrl ) } ` )
81+ . then ( res => res . json ( ) )
82+ . then ( data => ( { id : p . id , title : data . title } ) )
83+ . catch ( ( ) => ( { id : p . id , title : '' } ) )
84+ )
85+ ) . then ( results => {
86+ if ( ! cancelled ) {
87+ // Merge fetched titles into podcasts state
88+ setPodcasts ( prev =>
89+ prev . map ( p => {
90+ const found = results . find ( r => r . id === p . id ) ;
91+ return found ? { ...p , title : found . title } : p ;
92+ } )
93+ ) ;
94+ }
95+ } ) ;
96+ return ( ) => { cancelled = true ; } ;
97+ } , [ ] ) ;
98+
99+ // Filter podcasts based on search and filter using title now
100+ const filteredPodcasts = podcasts . filter ( podcast => {
118101 const matchesFilter = selectedFilter === 'all' || podcast . type === selectedFilter ;
119- return matchesFilter ;
102+ const matchesSearch =
103+ searchTerm === '' ||
104+ ( podcast . title && podcast . title . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ) ;
105+ return matchesFilter && matchesSearch ;
120106 } ) ;
121107
122- // Calculate podcasts for current page
108+ // Pagination calculations
123109 const indexOfLastPodcast = currentPage * podcastsPerPage ;
124110 const indexOfFirstPodcast = indexOfLastPodcast - podcastsPerPage ;
125111 const currentPodcasts = filteredPodcasts . slice ( indexOfFirstPodcast , indexOfLastPodcast ) ;
@@ -137,8 +123,7 @@ export default function Podcasts(): ReactElement {
137123 title : `Check out this ${ podcast . type } ` ,
138124 url : podcast . spotifyUrl ,
139125 } ) ;
140- } catch ( err ) {
141- // Fallback to clipboard
126+ } catch {
142127 navigator . clipboard . writeText ( podcast . spotifyUrl ) ;
143128 }
144129 } else {
@@ -147,20 +132,14 @@ export default function Podcasts(): ReactElement {
147132 } ;
148133
149134 const handleFavorite = ( podcast : PodcastData , event : React . MouseEvent ) => {
150- // Prevent card click when clicking favorite button
151135 event . stopPropagation ( ) ;
152136
153- setFavorites ( ( prev ) => {
137+ setFavorites ( prev => {
154138 const isFavorited = prev . includes ( podcast . id ) ;
155- const newFavorites = isFavorited
156- ? prev . filter ( ( id ) => id !== podcast . id ) // Remove from favorites
157- : [ ...prev , podcast . id ] ; // Add to favorites
158-
159- // Save to localStorage for persistence
139+ const newFavorites = isFavorited ? prev . filter ( id => id !== podcast . id ) : [ ...prev , podcast . id ] ;
160140 if ( typeof window !== "undefined" ) {
161141 localStorage . setItem ( "podcast-favorites" , JSON . stringify ( newFavorites ) ) ;
162142 }
163-
164143 return newFavorites ;
165144 } ) ;
166145 } ;
@@ -171,12 +150,11 @@ export default function Podcasts(): ReactElement {
171150 ) => {
172151 const target = event . target as HTMLElement ;
173152
174- // Prevent navigation if clicking on buttons or action area
175153 if (
176154 target . tagName === "IFRAME" ||
177155 target . closest ( ".podcast-embed" ) ||
178- target . closest ( ".action-btn" ) || // Don't navigate if clicking buttons
179- target . closest ( ".card-actions" ) || // Don't navigate if clicking action area
156+ target . closest ( ".action-btn" ) ||
157+ target . closest ( ".card-actions" ) ||
180158 target . classList . contains ( "action-btn" ) ||
181159 target . classList . contains ( "favorite" ) ||
182160 target . classList . contains ( "share" )
@@ -197,17 +175,13 @@ export default function Podcasts(): ReactElement {
197175 < span className = "badge-icon" > 🎙️</ span >
198176 < span className = "badge-text" > Premium Audio Content</ span >
199177 </ div >
200- < h1 className = "podcast-hero-title" >
201- Discover Top Podcasts
202- </ h1 >
178+ < h1 className = "podcast-hero-title" > Discover Top Podcasts</ h1 >
203179 < p className = "podcast-hero-description" >
204180 Stream the best podcasts from your favorite stations. Dive into episodes that inspire, educate, and entertain from leading voices in tech, business, and beyond.
205181 </ p >
206-
207- { /* Stats */ }
208182 < div className = "podcast-stats" >
209183 < div className = "stat-item" >
210- < div className = "stat-number" > { podcastData . length } +</ div >
184+ < div className = "stat-number" > { podcasts . length } +</ div >
211185 < div className = "stat-label" > Episodes</ div >
212186 </ div >
213187 < div className = "stat-item" >
@@ -230,7 +204,7 @@ export default function Podcasts(): ReactElement {
230204 type = "text"
231205 placeholder = "Search podcasts..."
232206 value = { searchTerm }
233- onChange = { ( e ) => setSearchTerm ( e . target . value ) }
207+ onChange = { e => setSearchTerm ( e . target . value ) }
234208 className = "search-input"
235209 />
236210 </ div >
@@ -240,28 +214,28 @@ export default function Podcasts(): ReactElement {
240214 onClick = { ( ) => setSelectedFilter ( 'all' ) }
241215 >
242216 < span className = "tab-icon" > 📊</ span >
243- All ({ podcastData . length } )
217+ All ({ podcasts . length } )
244218 </ button >
245219 < button
246220 className = { `filter-tab ${ selectedFilter === 'episode' ? 'active' : '' } ` }
247221 onClick = { ( ) => setSelectedFilter ( 'episode' ) }
248222 >
249223 < span className = "tab-icon" > 🎙️</ span >
250- Episodes ({ podcastData . filter ( p => p . type === 'episode' ) . length } )
224+ Episodes ({ podcasts . filter ( p => p . type === 'episode' ) . length } )
251225 </ button >
252226 < button
253227 className = { `filter-tab ${ selectedFilter === 'show' ? 'active' : '' } ` }
254228 onClick = { ( ) => setSelectedFilter ( 'show' ) }
255229 >
256230 < span className = "tab-icon" > 📻</ span >
257- Shows ({ podcastData . filter ( p => p . type === 'show' ) . length } )
231+ Shows ({ podcasts . filter ( p => p . type === 'show' ) . length } )
258232 </ button >
259233 < button
260234 className = { `filter-tab ${ selectedFilter === 'playlist' ? 'active' : '' } ` }
261235 onClick = { ( ) => setSelectedFilter ( 'playlist' ) }
262236 >
263237 < span className = "tab-icon" > 🎵</ span >
264- Playlists ({ podcastData . filter ( p => p . type === 'playlist' ) . length } )
238+ Playlists ({ podcasts . filter ( p => p . type === 'playlist' ) . length } )
265239 </ button >
266240 </ div >
267241 </ div >
@@ -275,59 +249,60 @@ export default function Podcasts(): ReactElement {
275249 < div
276250 key = { podcast . id }
277251 className = "enhanced-podcast-card"
278- onClick = { ( e ) => handlePodcastClick ( podcast , e ) }
252+ onClick = { e => handlePodcastClick ( podcast , e ) }
279253 role = "button"
280254 tabIndex = { 0 }
281- onKeyDown = { ( e ) => {
255+ onKeyDown = { e => {
282256 if ( e . key === 'Enter' || e . key === ' ' ) {
283257 handlePodcastClick ( podcast , e ) ;
284258 }
285259 } }
286260 style = { { animationDelay : `${ index * 0.1 } s` } }
287261 >
288262 < div className = "podcast-card-header" >
289- < SpotifyTitle
290- spotifyUrl = { podcast . spotifyUrl }
291- type = { podcast . type }
292- />
263+ < SpotifyTitle title = { podcast . title } type = { podcast . type } />
293264 < div
294265 className = "card-actions"
295- onClick = { ( e ) => {
266+ onClick = { e => {
296267 e . stopPropagation ( ) ;
297268 e . preventDefault ( ) ;
298269 } }
299- onMouseDown = { ( e ) => {
270+ onMouseDown = { e => {
300271 e . stopPropagation ( ) ;
301272 } }
302273 >
303274 < button
304275 className = { `action-btn favorite unfavorited ${
305- favorites . includes ( podcast . id ) ? " favorited" : ""
276+ favorites . includes ( podcast . id ) ? ' favorited' : ''
306277 } `}
307278 title = {
308279 favorites . includes ( podcast . id )
309- ? " Remove from favorites"
310- : " Add to favorites"
280+ ? ' Remove from favorites'
281+ : ' Add to favorites'
311282 }
312- onClick = { ( e ) => {
283+ onClick = { e => {
313284 e . preventDefault ( ) ;
314285 e . stopPropagation ( ) ;
315286 e . nativeEvent . stopImmediatePropagation ( ) ;
316287 handleFavorite ( podcast , e ) ;
317288 } }
318289 >
319- { favorites . includes ( podcast . id ) ? '🤍' : '❤️' }
290+ { favorites . includes ( podcast . id ) ? '🤍' : '❤️' }
320291 </ button >
321- < button className = "action-btn share" title = "Share podcast" onClick = { ( e ) => {
292+ < button
293+ className = "action-btn share"
294+ title = "Share podcast"
295+ onClick = { e => {
322296 e . stopPropagation ( ) ;
323297 handleShare ( podcast ) ;
324- } } >
298+ } }
299+ >
325300 🔗
326301 </ button >
327302 </ div >
328303 </ div >
329304
330- < div className = "podcast-embed" onClick = { ( e ) => e . stopPropagation ( ) } >
305+ < div className = "podcast-embed" onClick = { e => e . stopPropagation ( ) } >
331306 < iframe
332307 src = { `https://open.spotify.com/embed/${ podcast . type } /${ getSpotifyEmbedId ( podcast . spotifyUrl ) } ` }
333308 width = "100%"
@@ -362,14 +337,14 @@ export default function Podcasts(): ReactElement {
362337 </ button >
363338
364339 < div className = "pagination-numbers" >
365- { Array . from ( { length : totalPages } , ( _ , i ) => i + 1 ) . map ( ( number ) => (
366- < button
367- key = { number }
340+ { Array . from ( { length : totalPages } , ( _ , i ) => i + 1 ) . map ( number => (
341+ < button
342+ key = { number }
368343 className = { `pagination-number ${ currentPage === number ? 'active' : '' } ` }
369- onClick = { ( ) => handlePageChange ( number ) }
370- >
371- { number }
372- </ button >
344+ onClick = { ( ) => handlePageChange ( number ) }
345+ >
346+ { number }
347+ </ button >
373348 ) ) }
374349 </ div >
375350
0 commit comments