-
Notifications
You must be signed in to change notification settings - Fork 114
Fix #596: Implement favorite button functionality on podcast details … #633
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7eafcc7
92a2e0c
b74455e
a872569
ec21f07
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -359,6 +359,7 @@ html[data-theme='light'] { | |||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); | ||||||
cursor: pointer; | ||||||
position: relative; | ||||||
z-index: 1; | ||||||
overflow: hidden; | ||||||
animation: fadeInUp 0.6s ease-out both; | ||||||
} | ||||||
|
@@ -404,12 +405,40 @@ html[data-theme='light'] { | |||||
gap: 8px; | ||||||
opacity: 0; | ||||||
transition: opacity 0.3s ease; | ||||||
position: relative; | ||||||
z-index: 10; | ||||||
pointer-events: auto; | ||||||
} | ||||||
|
||||||
.enhanced-podcast-card:hover .card-actions { | ||||||
opacity: 1; | ||||||
} | ||||||
|
||||||
.action-btn.share { | ||||||
background-color: rgba(123, 124, 128, 0.15); /* same subtle bg as unfavorited */ | ||||||
border: 1px solid rgba(123, 124, 128, 0.3); /* same border */ | ||||||
transition: all 0.3s ease; | ||||||
color: inherit; /* keep the existing icon color */ | ||||||
border-radius: 8px; | ||||||
width: 36px; | ||||||
height: 36px; | ||||||
cursor: pointer; | ||||||
font-size: 14px; | ||||||
position: relative; | ||||||
z-index: 15; | ||||||
user-select: none; | ||||||
} | ||||||
|
||||||
.action-btn.share:hover { | ||||||
background-color: rgba(123, 124, 128, 0.25); /* slightly darker on hover */ | ||||||
border-color: rgba(123, 124, 128, 0.5); | ||||||
transform: scale(1.1); | ||||||
} | ||||||
|
||||||
.action-btn.share:active { | ||||||
transform: scale(0.95); | ||||||
} | ||||||
|
||||||
.action-btn { | ||||||
width: 36px; | ||||||
height: 36px; | ||||||
|
@@ -419,12 +448,35 @@ html[data-theme='light'] { | |||||
cursor: pointer; | ||||||
transition: all 0.3s ease; | ||||||
font-size: 14px; | ||||||
position: relative; | ||||||
z-index: 15; | ||||||
pointer-events: auto !important; | ||||||
user-select: none; | ||||||
} | ||||||
|
||||||
.action-btn:hover { | ||||||
background: var(--podcast-bg-card-hover); | ||||||
transform: scale(1.1); | ||||||
} | ||||||
/* Enhanced favorite button styling */ | ||||||
.action-btn.favorite { | ||||||
transition: all 0.2s ease; | ||||||
border: 1px solid transparent; | ||||||
} | ||||||
|
||||||
.action-btn.favorite:active { | ||||||
transform: scale(0.95); | ||||||
} | ||||||
|
||||||
.action-btn.favorite.unfavorited { | ||||||
background-color: rgba(123, 124, 128, 0.15); /* light purple-blue tint */ | ||||||
color: white; /* keep the heart white */ | ||||||
border-color: rgba(123, 124, 128, 0.3); | ||||||
} | ||||||
|
||||||
.action-btn.favorite.unfavorited:hover { | ||||||
background-color: rgba(123, 124, 128, 0.15); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The hover state for unfavorited buttons has the same background-color as the default state, making the hover effect invisible. Consider using a different color or opacity for the hover state.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||
} | ||||||
|
||||||
/* Podcast Title */ | ||||||
.podcast-title { | ||||||
|
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -99,8 +99,18 @@ const SpotifyTitle: React.FC<SpotifyTitleProps> = ({ spotifyUrl, type }) => { | |||||||||
export default function Podcasts(): ReactElement { | ||||||||||
const history = useHistory(); | ||||||||||
const [currentPage, setCurrentPage] = useState(1); | ||||||||||
const [searchTerm, setSearchTerm] = useState(''); | ||||||||||
const [selectedFilter, setSelectedFilter] = useState<'all' | 'episode' | 'show' | 'playlist'>('all'); | ||||||||||
const [searchTerm, setSearchTerm] = useState(""); | ||||||||||
const [selectedFilter, setSelectedFilter] = useState< | ||||||||||
"all" | "episode" | "show" | "playlist" | ||||||||||
>("all"); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nitpick] Inconsistent quote style. The codebase should use consistent quotes throughout - either single quotes (') or double quotes ("). Consider using single quotes to match the existing pattern in the file.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||
const [favorites, setFavorites] = useState<string[]>(() => { | ||||||||||
// Load favorites from localStorage on component mount | ||||||||||
if (typeof window !== "undefined") { | ||||||||||
const saved = localStorage.getItem("podcast-favorites"); | ||||||||||
return saved ? JSON.parse(saved) : []; | ||||||||||
} | ||||||||||
return []; | ||||||||||
}); | ||||||||||
const podcastsPerPage = 9; | ||||||||||
|
||||||||||
// Filter podcasts based on search and filter | ||||||||||
|
@@ -136,17 +146,46 @@ export default function Podcasts(): ReactElement { | |||||||||
} | ||||||||||
}; | ||||||||||
|
||||||||||
const handlePodcastClick = (podcast: PodcastData, event: React.MouseEvent | React.KeyboardEvent) => { | ||||||||||
const handleFavorite = (podcast: PodcastData, event: React.MouseEvent) => { | ||||||||||
// Prevent card click when clicking favorite button | ||||||||||
event.stopPropagation(); | ||||||||||
|
||||||||||
setFavorites((prev) => { | ||||||||||
const isFavorited = prev.includes(podcast.id); | ||||||||||
const newFavorites = isFavorited | ||||||||||
? prev.filter((id) => id !== podcast.id) // Remove from favorites | ||||||||||
: [...prev, podcast.id]; // Add to favorites | ||||||||||
|
||||||||||
// Save to localStorage for persistence | ||||||||||
if (typeof window !== "undefined") { | ||||||||||
localStorage.setItem("podcast-favorites", JSON.stringify(newFavorites)); | ||||||||||
} | ||||||||||
|
||||||||||
return newFavorites; | ||||||||||
}); | ||||||||||
}; | ||||||||||
|
||||||||||
const handlePodcastClick = ( | ||||||||||
podcast: PodcastData, | ||||||||||
event: React.MouseEvent | React.KeyboardEvent | ||||||||||
) => { | ||||||||||
const target = event.target as HTMLElement; | ||||||||||
if (target.tagName === 'IFRAME' || target.closest('.podcast-embed')) { | ||||||||||
|
||||||||||
// Prevent navigation if clicking on buttons or action area | ||||||||||
if ( | ||||||||||
target.tagName === "IFRAME" || | ||||||||||
target.closest(".podcast-embed") || | ||||||||||
target.closest(".action-btn") || // Don't navigate if clicking buttons | ||||||||||
target.closest(".card-actions") || // Don't navigate if clicking action area | ||||||||||
target.classList.contains("action-btn") || | ||||||||||
target.classList.contains("favorite") || | ||||||||||
target.classList.contains("share") | ||||||||||
) { | ||||||||||
return; | ||||||||||
} | ||||||||||
history.push('/podcasts/details', { podcast }); | ||||||||||
}; | ||||||||||
|
||||||||||
React.useEffect(() => { | ||||||||||
setCurrentPage(1); | ||||||||||
}, [searchTerm, selectedFilter]); | ||||||||||
history.push("/podcasts/details", { podcast }); | ||||||||||
}; | ||||||||||
|
||||||||||
return ( | ||||||||||
<Layout> | ||||||||||
|
@@ -164,7 +203,7 @@ export default function Podcasts(): ReactElement { | |||||||||
<p className="podcast-hero-description"> | ||||||||||
Stream the best podcasts from your favorite stations. Dive into episodes that inspire, educate, and entertain from leading voices in tech, business, and beyond. | ||||||||||
</p> | ||||||||||
|
||||||||||
{/* Stats */} | ||||||||||
<div className="podcast-stats"> | ||||||||||
<div className="stat-item"> | ||||||||||
|
@@ -247,20 +286,47 @@ export default function Podcasts(): ReactElement { | |||||||||
style={{ animationDelay: `${index * 0.1}s` }} | ||||||||||
> | ||||||||||
<div className="podcast-card-header"> | ||||||||||
<SpotifyTitle spotifyUrl={podcast.spotifyUrl} type={podcast.type} /> | ||||||||||
<div className="card-actions"> | ||||||||||
<button className="action-btn favorite" title="Add to favorites"> | ||||||||||
❤️ | ||||||||||
<SpotifyTitle | ||||||||||
spotifyUrl={podcast.spotifyUrl} | ||||||||||
type={podcast.type} | ||||||||||
/> | ||||||||||
<div | ||||||||||
className="card-actions" | ||||||||||
onClick={(e) => { | ||||||||||
e.stopPropagation(); | ||||||||||
e.preventDefault(); | ||||||||||
}} | ||||||||||
onMouseDown={(e) => { | ||||||||||
e.stopPropagation(); | ||||||||||
}} | ||||||||||
> | ||||||||||
<button | ||||||||||
className={`action-btn favorite unfavorited ${ | ||||||||||
favorites.includes(podcast.id) ? "favorited" : "" | ||||||||||
}`} | ||||||||||
Comment on lines
+304
to
+306
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The className logic is confusing - applying both 'unfavorited' and potentially 'favorited' classes. Consider using conditional logic:
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||
title={ | ||||||||||
favorites.includes(podcast.id) | ||||||||||
? "Remove from favorites" | ||||||||||
: "Add to favorites" | ||||||||||
} | ||||||||||
onClick={(e) => { | ||||||||||
e.preventDefault(); | ||||||||||
e.stopPropagation(); | ||||||||||
e.nativeEvent.stopImmediatePropagation(); | ||||||||||
Comment on lines
+313
to
+315
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Multiple event propagation prevention methods are redundant.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||
handleFavorite(podcast, e); | ||||||||||
}} | ||||||||||
> | ||||||||||
{favorites.includes(podcast.id) ? '❤️' : '🤍'} | ||||||||||
</button> | ||||||||||
<button className="action-btn share" title="Share podcast" onClick={(e) => { | ||||||||||
e.stopPropagation(); | ||||||||||
handleShare(podcast); | ||||||||||
e.stopPropagation(); | ||||||||||
handleShare(podcast); | ||||||||||
}}> | ||||||||||
🔗 | ||||||||||
</button> | ||||||||||
</div> | ||||||||||
</div> | ||||||||||
|
||||||||||
<div className="podcast-embed" onClick={(e) => e.stopPropagation()}> | ||||||||||
<iframe | ||||||||||
src={`https://open.spotify.com/embed/${podcast.type}/${getSpotifyEmbedId(podcast.spotifyUrl)}`} | ||||||||||
|
@@ -273,7 +339,7 @@ export default function Podcasts(): ReactElement { | |||||||||
title={`Spotify embed ${podcast.id}`} | ||||||||||
/> | ||||||||||
</div> | ||||||||||
|
||||||||||
<div className="podcast-card-footer"> | ||||||||||
<button className="listen-button"> | ||||||||||
<span className="listen-icon">▶️</span> | ||||||||||
|
@@ -294,19 +360,19 @@ export default function Podcasts(): ReactElement { | |||||||||
> | ||||||||||
← Previous | ||||||||||
</button> | ||||||||||
|
||||||||||
<div className="pagination-numbers"> | ||||||||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map((number) => ( | ||||||||||
<button | ||||||||||
key={number} | ||||||||||
<button | ||||||||||
key={number} | ||||||||||
className={`pagination-number ${currentPage === number ? 'active' : ''}`} | ||||||||||
onClick={() => handlePageChange(number)} | ||||||||||
> | ||||||||||
{number} | ||||||||||
</button> | ||||||||||
onClick={() => handlePageChange(number)} | ||||||||||
> | ||||||||||
{number} | ||||||||||
</button> | ||||||||||
))} | ||||||||||
</div> | ||||||||||
|
||||||||||
<button | ||||||||||
className="pagination-nav" | ||||||||||
onClick={() => handlePageChange(Math.min(totalPages, currentPage + 1))} | ||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function parameter
podcast
is optional but the function body doesn't handle the undefined case. This will cause runtime errors when accessingpodcast.type
andpodcast.spotifyUrl
if no podcast is passed.Copilot uses AI. Check for mistakes.