Skip to content

Commit 7eafcc7

Browse files
Fix #596: Implement favorite button functionality on podcast details page
1 parent f60f26b commit 7eafcc7

4 files changed

Lines changed: 201 additions & 35 deletions

File tree

src/pages/podcasts/details.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,15 @@ html[data-theme='light'] {
206206
justify-content: center;
207207
}
208208

209+
.nav-action-button:active {
210+
box-shadow: 0 1px 8px rgba(40,50,70,0.06);
211+
}
212+
213+
.nav-action-button.favorite.favorited {
214+
color: #ff4d4d; /* red heart */
215+
background: #fff0f0; /* soft pink background when liked */
216+
}
217+
209218
.nav-action-button:hover {
210219
background: var(--details-bg-card-hover);
211220
color: var(--details-text-primary);

src/pages/podcasts/details.tsx

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useState, useEffect } from 'react';
22
import Layout from '@theme/Layout';
33
import type { ReactElement } from 'react';
44
import { useLocation, useHistory } from '@docusaurus/router';
@@ -85,7 +85,26 @@ export default function PodcastDetails(): ReactElement {
8585
const history = useHistory();
8686
const state = location.state as LocationState;
8787
const podcast = state?.podcast;
88-
88+
89+
const [favorites, setFavorites] = useState<string[]>(() => {
90+
if (typeof window !== "undefined") {
91+
const saved = localStorage.getItem("podcast-favorites");
92+
return saved ? JSON.parse(saved) : [];
93+
}
94+
return [];
95+
});
96+
const isFavorited = podcast ? favorites.includes(podcast.id) : false;
97+
const toggleFavorite = () => {
98+
if (!podcast) return;
99+
setFavorites(prev => {
100+
const updated = prev.includes(podcast.id)
101+
? prev.filter(id => id !== podcast.id)
102+
: [...prev, podcast.id];
103+
localStorage.setItem("podcast-favorites", JSON.stringify(updated));
104+
return updated;
105+
});
106+
};
107+
89108
// Enhanced descriptions with categories
90109
const descriptions = {
91110
episode: [
@@ -174,13 +193,26 @@ export default function PodcastDetails(): ReactElement {
174193
<span className="nav-text">Back to Podcasts</span>
175194
</button>
176195
<div className="nav-actions">
177-
<button className="nav-action-button" onClick={handleShare} title="Share">
178-
<span className="action-icon">🔗</span>
179-
</button>
180-
<button className="nav-action-button" title="Add to favorites">
181-
<span className="action-icon">❤️</span>
182-
</button>
183-
</div>
196+
<button
197+
className="nav-action-button"
198+
onClick={handleShare}
199+
title="Share"
200+
>
201+
<span className="action-icon">🔗</span>
202+
</button>
203+
<button
204+
className={`nav-action-button favorite ${isFavorited ? "favorited" : ""}`}
205+
title={isFavorited ? "Remove from favorites" : "Add to favorites"}
206+
onClick={e => {
207+
e.preventDefault();
208+
e.stopPropagation();
209+
toggleFavorite();
210+
}}
211+
>
212+
<span className="action-icon">{isFavorited ? "❤️" : "🤍"}</span>
213+
</button>
214+
</div>
215+
184216
</div>
185217

186218
{/* Main Content */}

src/pages/podcasts/index.css

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,10 +359,12 @@ html[data-theme='light'] {
359359
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
360360
cursor: pointer;
361361
position: relative;
362+
z-index: 1;
362363
overflow: hidden;
363364
animation: fadeInUp 0.6s ease-out both;
364365
}
365366

367+
366368
.enhanced-podcast-card::before {
367369
content: '';
368370
position: absolute;
@@ -404,12 +406,42 @@ html[data-theme='light'] {
404406
gap: 8px;
405407
opacity: 0;
406408
transition: opacity 0.3s ease;
409+
position: relative;
410+
z-index: 10;
411+
pointer-events: auto;
407412
}
408413

414+
409415
.enhanced-podcast-card:hover .card-actions {
410416
opacity: 1;
411417
}
412418

419+
.action-btn.share {
420+
background-color: rgba(123, 124, 128, 0.15); /* same subtle bg as unfavorited */
421+
border: 1px solid rgba(123, 124, 128, 0.3); /* same border */
422+
transition: all 0.3s ease;
423+
color: inherit; /* keep the existing icon color */
424+
border-radius: 8px;
425+
width: 36px;
426+
height: 36px;
427+
cursor: pointer;
428+
font-size: 14px;
429+
position: relative;
430+
z-index: 15;
431+
user-select: none;
432+
}
433+
434+
.action-btn.share:hover {
435+
background-color: rgba(123, 124, 128, 0.25); /* slightly darker on hover */
436+
border-color: rgba(123, 124, 128, 0.5);
437+
transform: scale(1.1);
438+
}
439+
440+
.action-btn.share:active {
441+
transform: scale(0.95);
442+
}
443+
444+
413445
.action-btn {
414446
width: 36px;
415447
height: 36px;
@@ -419,12 +451,36 @@ html[data-theme='light'] {
419451
cursor: pointer;
420452
transition: all 0.3s ease;
421453
font-size: 14px;
454+
position: relative;
455+
z-index: 15;
456+
pointer-events: auto !important;
457+
user-select: none;
422458
}
423459

460+
424461
.action-btn:hover {
425462
background: var(--podcast-bg-card-hover);
426463
transform: scale(1.1);
427464
}
465+
/* Enhanced favorite button styling */
466+
.action-btn.favorite {
467+
transition: all 0.2s ease;
468+
border: 1px solid transparent;
469+
}
470+
471+
.action-btn.favorite:active {
472+
transform: scale(0.95);
473+
}
474+
475+
.action-btn.favorite.unfavorited {
476+
background-color: rgba(123, 124, 128, 0.15); /* light purple-blue tint */
477+
color: white; /* keep the heart white */
478+
border-color: rgba(123, 124, 128, 0.3);
479+
}
480+
481+
.action-btn.favorite.unfavorited:hover {
482+
background-color: rgba(123, 124, 128, 0.15);
483+
}
428484

429485
/* Podcast Title */
430486
.podcast-title {

src/pages/podcasts/index.tsx

Lines changed: 95 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,19 @@ const SpotifyTitle: React.FC<SpotifyTitleProps> = ({ spotifyUrl, type }) => {
9999
export default function Podcasts(): ReactElement {
100100
const history = useHistory();
101101
const [currentPage, setCurrentPage] = useState(1);
102-
const [searchTerm, setSearchTerm] = useState('');
103-
const [selectedFilter, setSelectedFilter] = useState<'all' | 'episode' | 'show' | 'playlist'>('all');
102+
const [searchTerm, setSearchTerm] = useState("");
103+
const [selectedFilter, setSelectedFilter] = useState<
104+
"all" | "episode" | "show" | "playlist"
105+
>("all");
106+
// ADD THIS NEW STATE FOR FAVORITES
107+
const [favorites, setFavorites] = useState<string[]>(() => {
108+
// Load favorites from localStorage on component mount
109+
if (typeof window !== "undefined") {
110+
const saved = localStorage.getItem("podcast-favorites");
111+
return saved ? JSON.parse(saved) : [];
112+
}
113+
return [];
114+
});
104115
const podcastsPerPage = 9;
105116

106117
// Filter podcasts based on search and filter
@@ -136,17 +147,47 @@ export default function Podcasts(): ReactElement {
136147
}
137148
};
138149

139-
const handlePodcastClick = (podcast: PodcastData, event: React.MouseEvent | React.KeyboardEvent) => {
150+
// ADD THIS NEW FUNCTION FOR FAVORITES
151+
const handleFavorite = (podcast: PodcastData, event: React.MouseEvent) => {
152+
// Prevent card click when clicking favorite button
153+
event.stopPropagation();
154+
155+
setFavorites((prev) => {
156+
const isFavorited = prev.includes(podcast.id);
157+
const newFavorites = isFavorited
158+
? prev.filter((id) => id !== podcast.id) // Remove from favorites
159+
: [...prev, podcast.id]; // Add to favorites
160+
161+
// Save to localStorage for persistence
162+
if (typeof window !== "undefined") {
163+
localStorage.setItem("podcast-favorites", JSON.stringify(newFavorites));
164+
}
165+
166+
return newFavorites;
167+
});
168+
};
169+
170+
const handlePodcastClick = (
171+
podcast: PodcastData,
172+
event: React.MouseEvent | React.KeyboardEvent
173+
) => {
140174
const target = event.target as HTMLElement;
141-
if (target.tagName === 'IFRAME' || target.closest('.podcast-embed')) {
175+
176+
// Prevent navigation if clicking on buttons or action area
177+
if (
178+
target.tagName === "IFRAME" ||
179+
target.closest(".podcast-embed") ||
180+
target.closest(".action-btn") || // Don't navigate if clicking buttons
181+
target.closest(".card-actions") || // Don't navigate if clicking action area
182+
target.classList.contains("action-btn") ||
183+
target.classList.contains("favorite") ||
184+
target.classList.contains("share")
185+
) {
142186
return;
143187
}
144-
history.push('/podcasts/details', { podcast });
145-
};
146188

147-
React.useEffect(() => {
148-
setCurrentPage(1);
149-
}, [searchTerm, selectedFilter]);
189+
history.push("/podcasts/details", { podcast });
190+
};
150191

151192
return (
152193
<Layout>
@@ -164,7 +205,7 @@ export default function Podcasts(): ReactElement {
164205
<p className="podcast-hero-description">
165206
Stream the best podcasts from your favorite stations. Dive into episodes that inspire, educate, and entertain from leading voices in tech, business, and beyond.
166207
</p>
167-
208+
168209
{/* Stats */}
169210
<div className="podcast-stats">
170211
<div className="stat-item">
@@ -247,20 +288,48 @@ export default function Podcasts(): ReactElement {
247288
style={{ animationDelay: `${index * 0.1}s` }}
248289
>
249290
<div className="podcast-card-header">
250-
<SpotifyTitle spotifyUrl={podcast.spotifyUrl} type={podcast.type} />
251-
<div className="card-actions">
252-
<button className="action-btn favorite" title="Add to favorites">
253-
❤️
291+
<SpotifyTitle
292+
spotifyUrl={podcast.spotifyUrl}
293+
type={podcast.type}
294+
/>
295+
<div
296+
className="card-actions"
297+
onClick={(e) => {
298+
e.stopPropagation();
299+
e.preventDefault();
300+
}}
301+
onMouseDown={(e) => {
302+
e.stopPropagation();
303+
}}
304+
>
305+
<button
306+
className={`action-btn favorite unfavorited ${
307+
favorites.includes(podcast.id) ? "favorited" : ""
308+
}`}
309+
title={
310+
favorites.includes(podcast.id)
311+
? "Remove from favorites"
312+
: "Add to favorites"
313+
}
314+
onClick={(e) => {
315+
e.preventDefault();
316+
e.stopPropagation();
317+
e.nativeEvent.stopImmediatePropagation();
318+
handleFavorite(podcast, e);
319+
}}
320+
>
321+
{favorites.includes(podcast.id) ? '❤️' : '🤍'}
322+
254323
</button>
255324
<button className="action-btn share" title="Share podcast" onClick={(e) => {
256-
e.stopPropagation();
257-
handleShare(podcast);
325+
e.stopPropagation();
326+
handleShare(podcast);
258327
}}>
259328
🔗
260329
</button>
261330
</div>
262331
</div>
263-
332+
264333
<div className="podcast-embed" onClick={(e) => e.stopPropagation()}>
265334
<iframe
266335
src={`https://open.spotify.com/embed/${podcast.type}/${getSpotifyEmbedId(podcast.spotifyUrl)}`}
@@ -273,7 +342,7 @@ export default function Podcasts(): ReactElement {
273342
title={`Spotify embed ${podcast.id}`}
274343
/>
275344
</div>
276-
345+
277346
<div className="podcast-card-footer">
278347
<button className="listen-button">
279348
<span className="listen-icon">▶️</span>
@@ -294,19 +363,19 @@ export default function Podcasts(): ReactElement {
294363
>
295364
← Previous
296365
</button>
297-
366+
298367
<div className="pagination-numbers">
299368
{Array.from({ length: totalPages }, (_, i) => i + 1).map((number) => (
300-
<button
301-
key={number}
369+
<button
370+
key={number}
302371
className={`pagination-number ${currentPage === number ? 'active' : ''}`}
303-
onClick={() => handlePageChange(number)}
304-
>
305-
{number}
306-
</button>
372+
onClick={() => handlePageChange(number)}
373+
>
374+
{number}
375+
</button>
307376
))}
308377
</div>
309-
378+
310379
<button
311380
className="pagination-nav"
312381
onClick={() => handlePageChange(Math.min(totalPages, currentPage + 1))}

0 commit comments

Comments
 (0)