1
- import React , { useState } from 'react' ;
1
+ import React , { useState , useEffect } from 'react' ;
2
2
import Layout from '@theme/Layout' ;
3
3
import type { ReactElement } from 'react' ;
4
4
import { useHistory } from '@docusaurus/router' ;
@@ -8,6 +8,7 @@ interface PodcastData {
8
8
id : string ;
9
9
spotifyUrl : string ;
10
10
type : 'episode' | 'show' | 'playlist' ;
11
+ title ?: string ; // Add optional title here
11
12
}
12
13
13
14
// Function to extract Spotify ID from URL
@@ -36,90 +37,75 @@ const podcastUrls: string[] = [
36
37
"https://open.spotify.com/episode/21yp6PDe1XN8B1goR5qMI3?si=k6JURkMRTQq2Ltbujq9qLw" ,
37
38
] ;
38
39
39
- const podcastData : PodcastData [ ] = podcastUrls . map ( ( url , index ) => ( {
40
+ // Initialize podcast data without titles first
41
+ const initialPodcastData : PodcastData [ ] = podcastUrls . map ( ( url , index ) => ( {
40
42
id : String ( index + 1 ) ,
41
43
spotifyUrl : url ,
42
- type : getSpotifyContentType ( url )
44
+ type : getSpotifyContentType ( url ) ,
43
45
} ) ) ;
44
46
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 ) }
95
55
</ div >
96
- ) ;
97
- } ;
56
+ < h3 className = "podcast-title-text" > { title || 'Loading title...' } </ h3 >
57
+ </ div >
58
+ ) ;
98
59
99
60
export default function Podcasts ( ) : ReactElement {
100
61
const history = useHistory ( ) ;
101
62
const [ currentPage , setCurrentPage ] = useState ( 1 ) ;
102
63
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" ) ;
106
65
const [ favorites , setFavorites ] = useState < string [ ] > ( ( ) => {
107
- // Load favorites from localStorage on component mount
108
66
if ( typeof window !== "undefined" ) {
109
67
const saved = localStorage . getItem ( "podcast-favorites" ) ;
110
68
return saved ? JSON . parse ( saved ) : [ ] ;
111
69
}
112
70
return [ ] ;
113
71
} ) ;
72
+ const [ podcasts , setPodcasts ] = useState < PodcastData [ ] > ( initialPodcastData ) ;
114
73
const podcastsPerPage = 9 ;
115
74
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 => {
118
101
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 ;
120
106
} ) ;
121
107
122
- // Calculate podcasts for current page
108
+ // Pagination calculations
123
109
const indexOfLastPodcast = currentPage * podcastsPerPage ;
124
110
const indexOfFirstPodcast = indexOfLastPodcast - podcastsPerPage ;
125
111
const currentPodcasts = filteredPodcasts . slice ( indexOfFirstPodcast , indexOfLastPodcast ) ;
@@ -137,8 +123,7 @@ export default function Podcasts(): ReactElement {
137
123
title : `Check out this ${ podcast . type } ` ,
138
124
url : podcast . spotifyUrl ,
139
125
} ) ;
140
- } catch ( err ) {
141
- // Fallback to clipboard
126
+ } catch {
142
127
navigator . clipboard . writeText ( podcast . spotifyUrl ) ;
143
128
}
144
129
} else {
@@ -147,20 +132,14 @@ export default function Podcasts(): ReactElement {
147
132
} ;
148
133
149
134
const handleFavorite = ( podcast : PodcastData , event : React . MouseEvent ) => {
150
- // Prevent card click when clicking favorite button
151
135
event . stopPropagation ( ) ;
152
136
153
- setFavorites ( ( prev ) => {
137
+ setFavorites ( prev => {
154
138
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 ] ;
160
140
if ( typeof window !== "undefined" ) {
161
141
localStorage . setItem ( "podcast-favorites" , JSON . stringify ( newFavorites ) ) ;
162
142
}
163
-
164
143
return newFavorites ;
165
144
} ) ;
166
145
} ;
@@ -171,12 +150,11 @@ export default function Podcasts(): ReactElement {
171
150
) => {
172
151
const target = event . target as HTMLElement ;
173
152
174
- // Prevent navigation if clicking on buttons or action area
175
153
if (
176
154
target . tagName === "IFRAME" ||
177
155
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" ) ||
180
158
target . classList . contains ( "action-btn" ) ||
181
159
target . classList . contains ( "favorite" ) ||
182
160
target . classList . contains ( "share" )
@@ -197,17 +175,13 @@ export default function Podcasts(): ReactElement {
197
175
< span className = "badge-icon" > 🎙️</ span >
198
176
< span className = "badge-text" > Premium Audio Content</ span >
199
177
</ div >
200
- < h1 className = "podcast-hero-title" >
201
- Discover Top Podcasts
202
- </ h1 >
178
+ < h1 className = "podcast-hero-title" > Discover Top Podcasts</ h1 >
203
179
< p className = "podcast-hero-description" >
204
180
Stream the best podcasts from your favorite stations. Dive into episodes that inspire, educate, and entertain from leading voices in tech, business, and beyond.
205
181
</ p >
206
-
207
- { /* Stats */ }
208
182
< div className = "podcast-stats" >
209
183
< div className = "stat-item" >
210
- < div className = "stat-number" > { podcastData . length } +</ div >
184
+ < div className = "stat-number" > { podcasts . length } +</ div >
211
185
< div className = "stat-label" > Episodes</ div >
212
186
</ div >
213
187
< div className = "stat-item" >
@@ -230,7 +204,7 @@ export default function Podcasts(): ReactElement {
230
204
type = "text"
231
205
placeholder = "Search podcasts..."
232
206
value = { searchTerm }
233
- onChange = { ( e ) => setSearchTerm ( e . target . value ) }
207
+ onChange = { e => setSearchTerm ( e . target . value ) }
234
208
className = "search-input"
235
209
/>
236
210
</ div >
@@ -240,28 +214,28 @@ export default function Podcasts(): ReactElement {
240
214
onClick = { ( ) => setSelectedFilter ( 'all' ) }
241
215
>
242
216
< span className = "tab-icon" > 📊</ span >
243
- All ({ podcastData . length } )
217
+ All ({ podcasts . length } )
244
218
</ button >
245
219
< button
246
220
className = { `filter-tab ${ selectedFilter === 'episode' ? 'active' : '' } ` }
247
221
onClick = { ( ) => setSelectedFilter ( 'episode' ) }
248
222
>
249
223
< span className = "tab-icon" > 🎙️</ span >
250
- Episodes ({ podcastData . filter ( p => p . type === 'episode' ) . length } )
224
+ Episodes ({ podcasts . filter ( p => p . type === 'episode' ) . length } )
251
225
</ button >
252
226
< button
253
227
className = { `filter-tab ${ selectedFilter === 'show' ? 'active' : '' } ` }
254
228
onClick = { ( ) => setSelectedFilter ( 'show' ) }
255
229
>
256
230
< span className = "tab-icon" > 📻</ span >
257
- Shows ({ podcastData . filter ( p => p . type === 'show' ) . length } )
231
+ Shows ({ podcasts . filter ( p => p . type === 'show' ) . length } )
258
232
</ button >
259
233
< button
260
234
className = { `filter-tab ${ selectedFilter === 'playlist' ? 'active' : '' } ` }
261
235
onClick = { ( ) => setSelectedFilter ( 'playlist' ) }
262
236
>
263
237
< span className = "tab-icon" > 🎵</ span >
264
- Playlists ({ podcastData . filter ( p => p . type === 'playlist' ) . length } )
238
+ Playlists ({ podcasts . filter ( p => p . type === 'playlist' ) . length } )
265
239
</ button >
266
240
</ div >
267
241
</ div >
@@ -275,59 +249,60 @@ export default function Podcasts(): ReactElement {
275
249
< div
276
250
key = { podcast . id }
277
251
className = "enhanced-podcast-card"
278
- onClick = { ( e ) => handlePodcastClick ( podcast , e ) }
252
+ onClick = { e => handlePodcastClick ( podcast , e ) }
279
253
role = "button"
280
254
tabIndex = { 0 }
281
- onKeyDown = { ( e ) => {
255
+ onKeyDown = { e => {
282
256
if ( e . key === 'Enter' || e . key === ' ' ) {
283
257
handlePodcastClick ( podcast , e ) ;
284
258
}
285
259
} }
286
260
style = { { animationDelay : `${ index * 0.1 } s` } }
287
261
>
288
262
< div className = "podcast-card-header" >
289
- < SpotifyTitle
290
- spotifyUrl = { podcast . spotifyUrl }
291
- type = { podcast . type }
292
- />
263
+ < SpotifyTitle title = { podcast . title } type = { podcast . type } />
293
264
< div
294
265
className = "card-actions"
295
- onClick = { ( e ) => {
266
+ onClick = { e => {
296
267
e . stopPropagation ( ) ;
297
268
e . preventDefault ( ) ;
298
269
} }
299
- onMouseDown = { ( e ) => {
270
+ onMouseDown = { e => {
300
271
e . stopPropagation ( ) ;
301
272
} }
302
273
>
303
274
< button
304
275
className = { `action-btn favorite unfavorited ${
305
- favorites . includes ( podcast . id ) ? " favorited" : ""
276
+ favorites . includes ( podcast . id ) ? ' favorited' : ''
306
277
} `}
307
278
title = {
308
279
favorites . includes ( podcast . id )
309
- ? " Remove from favorites"
310
- : " Add to favorites"
280
+ ? ' Remove from favorites'
281
+ : ' Add to favorites'
311
282
}
312
- onClick = { ( e ) => {
283
+ onClick = { e => {
313
284
e . preventDefault ( ) ;
314
285
e . stopPropagation ( ) ;
315
286
e . nativeEvent . stopImmediatePropagation ( ) ;
316
287
handleFavorite ( podcast , e ) ;
317
288
} }
318
289
>
319
- { favorites . includes ( podcast . id ) ? '🤍' : '❤️' }
290
+ { favorites . includes ( podcast . id ) ? '🤍' : '❤️' }
320
291
</ 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 => {
322
296
e . stopPropagation ( ) ;
323
297
handleShare ( podcast ) ;
324
- } } >
298
+ } }
299
+ >
325
300
🔗
326
301
</ button >
327
302
</ div >
328
303
</ div >
329
304
330
- < div className = "podcast-embed" onClick = { ( e ) => e . stopPropagation ( ) } >
305
+ < div className = "podcast-embed" onClick = { e => e . stopPropagation ( ) } >
331
306
< iframe
332
307
src = { `https://open.spotify.com/embed/${ podcast . type } /${ getSpotifyEmbedId ( podcast . spotifyUrl ) } ` }
333
308
width = "100%"
@@ -362,14 +337,14 @@ export default function Podcasts(): ReactElement {
362
337
</ button >
363
338
364
339
< 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 }
368
343
className = { `pagination-number ${ currentPage === number ? 'active' : '' } ` }
369
- onClick = { ( ) => handlePageChange ( number ) }
370
- >
371
- { number }
372
- </ button >
344
+ onClick = { ( ) => handlePageChange ( number ) }
345
+ >
346
+ { number }
347
+ </ button >
373
348
) ) }
374
349
</ div >
375
350
0 commit comments