Skip to content

Commit 32af870

Browse files
authored
Merge pull request #633 from ShreyasLakhani/fix/596-favorite-button-functionality
Fix #596: Implement favorite button functionality on podcast details …
2 parents b516664 + ec21f07 commit 32af870

File tree

4 files changed

+195
-37
lines changed

4 files changed

+195
-37
lines changed

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: 42 additions & 11 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: [
@@ -128,7 +147,7 @@ export default function PodcastDetails(): ReactElement {
128147
history.goBack();
129148
};
130149

131-
const handleShare = async () => {
150+
const handleShare = async (podcast?: PodcastData) => {
132151
if (navigator.share) {
133152
try {
134153
await navigator.share({
@@ -174,13 +193,25 @@ 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(podcast)}
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>
184215
</div>
185216

186217
{/* Main Content */}
@@ -247,7 +278,7 @@ export default function PodcastDetails(): ReactElement {
247278
</span>
248279
</div>
249280
<div className="embed-actions">
250-
<button className="embed-action" onClick={handleShare}>
281+
<button className="embed-action" onClick={() => handleShare(podcast)}>
251282
<span className="action-icon">📤</span>
252283
Share
253284
</button>

src/pages/podcasts/index.css

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ 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
}
@@ -404,12 +405,40 @@ html[data-theme='light'] {
404405
gap: 8px;
405406
opacity: 0;
406407
transition: opacity 0.3s ease;
408+
position: relative;
409+
z-index: 10;
410+
pointer-events: auto;
407411
}
408412

409413
.enhanced-podcast-card:hover .card-actions {
410414
opacity: 1;
411415
}
412416

417+
.action-btn.share {
418+
background-color: rgba(123, 124, 128, 0.15); /* same subtle bg as unfavorited */
419+
border: 1px solid rgba(123, 124, 128, 0.3); /* same border */
420+
transition: all 0.3s ease;
421+
color: inherit; /* keep the existing icon color */
422+
border-radius: 8px;
423+
width: 36px;
424+
height: 36px;
425+
cursor: pointer;
426+
font-size: 14px;
427+
position: relative;
428+
z-index: 15;
429+
user-select: none;
430+
}
431+
432+
.action-btn.share:hover {
433+
background-color: rgba(123, 124, 128, 0.25); /* slightly darker on hover */
434+
border-color: rgba(123, 124, 128, 0.5);
435+
transform: scale(1.1);
436+
}
437+
438+
.action-btn.share:active {
439+
transform: scale(0.95);
440+
}
441+
413442
.action-btn {
414443
width: 36px;
415444
height: 36px;
@@ -419,12 +448,35 @@ html[data-theme='light'] {
419448
cursor: pointer;
420449
transition: all 0.3s ease;
421450
font-size: 14px;
451+
position: relative;
452+
z-index: 15;
453+
pointer-events: auto !important;
454+
user-select: none;
422455
}
423456

424457
.action-btn:hover {
425458
background: var(--podcast-bg-card-hover);
426459
transform: scale(1.1);
427460
}
461+
/* Enhanced favorite button styling */
462+
.action-btn.favorite {
463+
transition: all 0.2s ease;
464+
border: 1px solid transparent;
465+
}
466+
467+
.action-btn.favorite:active {
468+
transform: scale(0.95);
469+
}
470+
471+
.action-btn.favorite.unfavorited {
472+
background-color: rgba(123, 124, 128, 0.15); /* light purple-blue tint */
473+
color: white; /* keep the heart white */
474+
border-color: rgba(123, 124, 128, 0.3);
475+
}
476+
477+
.action-btn.favorite.unfavorited:hover {
478+
background-color: rgba(123, 124, 128, 0.15);
479+
}
428480

429481
/* Podcast Title */
430482
.podcast-title {

src/pages/podcasts/index.tsx

Lines changed: 92 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,18 @@ 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+
const [favorites, setFavorites] = useState<string[]>(() => {
107+
// Load favorites from localStorage on component mount
108+
if (typeof window !== "undefined") {
109+
const saved = localStorage.getItem("podcast-favorites");
110+
return saved ? JSON.parse(saved) : [];
111+
}
112+
return [];
113+
});
104114
const podcastsPerPage = 9;
105115

106116
// Filter podcasts based on search and filter
@@ -136,17 +146,46 @@ export default function Podcasts(): ReactElement {
136146
}
137147
};
138148

139-
const handlePodcastClick = (podcast: PodcastData, event: React.MouseEvent | React.KeyboardEvent) => {
149+
const handleFavorite = (podcast: PodcastData, event: React.MouseEvent) => {
150+
// Prevent card click when clicking favorite button
151+
event.stopPropagation();
152+
153+
setFavorites((prev) => {
154+
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
160+
if (typeof window !== "undefined") {
161+
localStorage.setItem("podcast-favorites", JSON.stringify(newFavorites));
162+
}
163+
164+
return newFavorites;
165+
});
166+
};
167+
168+
const handlePodcastClick = (
169+
podcast: PodcastData,
170+
event: React.MouseEvent | React.KeyboardEvent
171+
) => {
140172
const target = event.target as HTMLElement;
141-
if (target.tagName === 'IFRAME' || target.closest('.podcast-embed')) {
173+
174+
// Prevent navigation if clicking on buttons or action area
175+
if (
176+
target.tagName === "IFRAME" ||
177+
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
180+
target.classList.contains("action-btn") ||
181+
target.classList.contains("favorite") ||
182+
target.classList.contains("share")
183+
) {
142184
return;
143185
}
144-
history.push('/podcasts/details', { podcast });
145-
};
146186

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

151190
return (
152191
<Layout>
@@ -164,7 +203,7 @@ export default function Podcasts(): ReactElement {
164203
<p className="podcast-hero-description">
165204
Stream the best podcasts from your favorite stations. Dive into episodes that inspire, educate, and entertain from leading voices in tech, business, and beyond.
166205
</p>
167-
206+
168207
{/* Stats */}
169208
<div className="podcast-stats">
170209
<div className="stat-item">
@@ -247,20 +286,47 @@ export default function Podcasts(): ReactElement {
247286
style={{ animationDelay: `${index * 0.1}s` }}
248287
>
249288
<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-
❤️
289+
<SpotifyTitle
290+
spotifyUrl={podcast.spotifyUrl}
291+
type={podcast.type}
292+
/>
293+
<div
294+
className="card-actions"
295+
onClick={(e) => {
296+
e.stopPropagation();
297+
e.preventDefault();
298+
}}
299+
onMouseDown={(e) => {
300+
e.stopPropagation();
301+
}}
302+
>
303+
<button
304+
className={`action-btn favorite unfavorited ${
305+
favorites.includes(podcast.id) ? "favorited" : ""
306+
}`}
307+
title={
308+
favorites.includes(podcast.id)
309+
? "Remove from favorites"
310+
: "Add to favorites"
311+
}
312+
onClick={(e) => {
313+
e.preventDefault();
314+
e.stopPropagation();
315+
e.nativeEvent.stopImmediatePropagation();
316+
handleFavorite(podcast, e);
317+
}}
318+
>
319+
{favorites.includes(podcast.id) ? '❤️' : '🤍'}
254320
</button>
255321
<button className="action-btn share" title="Share podcast" onClick={(e) => {
256-
e.stopPropagation();
257-
handleShare(podcast);
322+
e.stopPropagation();
323+
handleShare(podcast);
258324
}}>
259325
🔗
260326
</button>
261327
</div>
262328
</div>
263-
329+
264330
<div className="podcast-embed" onClick={(e) => e.stopPropagation()}>
265331
<iframe
266332
src={`https://open.spotify.com/embed/${podcast.type}/${getSpotifyEmbedId(podcast.spotifyUrl)}`}
@@ -273,7 +339,7 @@ export default function Podcasts(): ReactElement {
273339
title={`Spotify embed ${podcast.id}`}
274340
/>
275341
</div>
276-
342+
277343
<div className="podcast-card-footer">
278344
<button className="listen-button">
279345
<span className="listen-icon">▶️</span>
@@ -294,19 +360,19 @@ export default function Podcasts(): ReactElement {
294360
>
295361
← Previous
296362
</button>
297-
363+
298364
<div className="pagination-numbers">
299365
{Array.from({ length: totalPages }, (_, i) => i + 1).map((number) => (
300-
<button
301-
key={number}
366+
<button
367+
key={number}
302368
className={`pagination-number ${currentPage === number ? 'active' : ''}`}
303-
onClick={() => handlePageChange(number)}
304-
>
305-
{number}
306-
</button>
369+
onClick={() => handlePageChange(number)}
370+
>
371+
{number}
372+
</button>
307373
))}
308374
</div>
309-
375+
310376
<button
311377
className="pagination-nav"
312378
onClick={() => handlePageChange(Math.min(totalPages, currentPage + 1))}

0 commit comments

Comments
 (0)