11"use client"
22
3- import React , { useState , useEffect } from "react"
3+ import React , { useState , useEffect , useRef } from "react"
44import { Breadcrumbs , Typography , styled , useMediaQuery } from "ol-components"
55import type { Theme } from "ol-components"
66import { Button , ActionButton } from "@mitodl/smoot-design"
7- import { RiPlayFill } from "@remixicon/react"
7+ import { RiPlayFill , RiPauseFill } from "@remixicon/react"
88import PodcastPlayer , { PLAYER_HEIGHT } from "./PodcastPlayer"
9- import type { PodcastTrack } from "./PodcastPlayer"
9+ import type { PodcastTrack , PodcastPlayerHandle } from "./PodcastPlayer"
1010import {
1111 useLearningResourcesDetail ,
1212 useInfiniteLearningResourceItems ,
@@ -156,13 +156,18 @@ const EpisodeList = styled.ul({
156156 gridTemplateColumns : "1fr" ,
157157} )
158158
159- const EpisodeRow = styled . li ( ( { theme } ) => ( {
159+ const EpisodeRow = styled ( "li" , {
160+ shouldForwardProp : ( prop ) => prop !== "isEpisodePage" ,
161+ } ) < { isEpisodePage ?: boolean } > ( ( { theme, isEpisodePage } ) => ( {
160162 margin : 0 ,
161163 display : "flex" ,
162164 flexDirection : "row" ,
163165 alignItems : "center" ,
164166 justifyContent : "space-between" ,
165- padding : "28px 16px" ,
167+ padding : ! isEpisodePage ? "28px 16px" : "28px 0px" ,
168+ ...( isEpisodePage && {
169+ "&:first-of-type" : { paddingTop : 0 , boxShadow : "none" } ,
170+ } ) ,
166171 boxShadow : `0 -1px 0 ${ theme . custom . colors . lightGray2 } ` ,
167172 gap : "16px" ,
168173 "&:last-child" : {
@@ -199,14 +204,17 @@ const EpisodeTitleLink = styled.span(({ theme }) => ({
199204 color : theme . custom . colors . darkGray2 ,
200205 textDecoration : "none" ,
201206 display : "block" ,
202- marginBottom : "8px" ,
203207 fontSize : "18px" ,
204208 fontStyle : "normal" ,
205209 fontWeight : theme . typography . fontWeightBold ,
206210 lineHeight : "26px" ,
207211} ) )
208212
209213const StyledButton = styled ( Button ) ( ( { theme } ) => ( {
214+ padding : "16px 20px" ,
215+ ...theme . typography . body1 ,
216+ fontWeight : theme . typography . fontWeightMedium ,
217+ lineHeight : "16px" ,
210218 [ theme . breakpoints . down ( "sm" ) ] : {
211219 width : "100%" ,
212220 } ,
@@ -225,8 +233,13 @@ const StyledShowMore = styled(Button)(({ theme }) => ({
225233 } ,
226234} ) )
227235
236+ const StyledIcon = styled ( RiPlayFill ) ( {
237+ width : "24px !important" ,
238+ height : "24px !important" ,
239+ } )
240+
228241const BreadcrumbBar = styled . div ( ( { theme } ) => ( {
229- padding : "32px 0 16px 0" ,
242+ padding : "20px 0 4px 0" ,
230243 borderBottom : `2px solid ${ theme . custom . colors . red } ` ,
231244 [ theme . breakpoints . down ( "sm" ) ] : {
232245 padding : "16px 0 0px 0" ,
@@ -254,7 +267,7 @@ const StyledDot = styled.span(({ theme }) => ({
254267} ) )
255268
256269const PageSection = styled . div ( ( { theme } ) => ( {
257- backgroundColor : theme . custom . colors . lightGray1 ,
270+ backgroundColor : theme . custom . colors . white ,
258271} ) )
259272
260273const EpisodeMeta = styled ( Typography ) ( ( { theme } ) => ( {
@@ -269,7 +282,10 @@ const PlayButton = styled(ActionButton, {
269282 isPlaying : boolean
270283} > ( ( { theme, isPlaying } ) => [
271284 {
285+ width : "48px" ,
286+ height : "48px" ,
272287 color : theme . custom . colors . darkGray2 ,
288+ backgroundColor : theme . custom . colors . white ,
273289 borderColor : "currentColor" ,
274290 "&:hover:not(:disabled)" : {
275291 color : theme . custom . colors . red ,
@@ -287,18 +303,22 @@ const PlayButton = styled(ActionButton, {
287303
288304/* ── Episode row component ── */
289305
290- type EpisodeItemProps = {
306+ export type EpisodeItemProps = {
291307 episode : LearningResource
292308 onPlayClick : ( episode : LearningResource ) => void
309+ onPauseClick ?: ( ) => void
293310 isPlaying : boolean
294311 isPlayable : boolean
312+ isEpisodePage ?: boolean
295313}
296314
297- const EpisodeItem : React . FC < EpisodeItemProps > = ( {
315+ export const EpisodeItem : React . FC < EpisodeItemProps > = ( {
298316 episode,
299317 onPlayClick,
318+ onPauseClick,
300319 isPlaying,
301320 isPlayable,
321+ isEpisodePage = false ,
302322} ) => {
303323 const podcastEpisode =
304324 episode . resource_type === "podcast_episode" ? episode . podcast_episode : null
@@ -314,7 +334,10 @@ const EpisodeItem: React.FC<EpisodeItemProps> = ({
314334 const metaParts = [ duration ? `${ duration } min` : null , date ] . filter ( Boolean )
315335
316336 return (
317- < EpisodeRow onClick = { ( ) => onPlayClick ( episode ) } >
337+ < EpisodeRow
338+ onClick = { ( ) => ( isPlaying ? onPauseClick ?.( ) : onPlayClick ( episode ) ) }
339+ isEpisodePage = { isEpisodePage }
340+ >
318341 < EpisodeInfo >
319342 < EpisodeTitleLink className = "episode-title" >
320343 { episode . title }
@@ -333,13 +356,15 @@ const EpisodeItem: React.FC<EpisodeItemProps> = ({
333356 </ EpisodeMeta >
334357 ) }
335358 < PlayButton
336- aria-label = { `Play ${ episode . title } ` }
359+ aria-label = {
360+ isPlaying ? `Pause ${ episode . title } ` : `Play ${ episode . title } `
361+ }
337362 isPlaying = { isPlaying }
338363 disabled = { ! isPlayable }
339364 variant = "secondary"
340365 className = "play-button"
341366 >
342- < RiPlayFill size = { 20 } />
367+ { isPlaying ? < RiPauseFill size = { 20 } /> : < RiPlayFill size = { 20 } /> }
343368 </ PlayButton >
344369 </ EpisodeRight >
345370 </ EpisodeRow >
@@ -366,6 +391,8 @@ export const PodcastDetailPage: React.FC<PodcastDetailPageProps> = ({
366391 const [ playingEpisode , setPlayingEpisode ] = useState < LearningResource | null > (
367392 null ,
368393 )
394+ const [ isAudioPlaying , setIsAudioPlaying ] = useState ( false )
395+ const playerRef = useRef < PodcastPlayerHandle > ( null )
369396
370397 const { data : resource } = useLearningResourcesDetail ( id )
371398
@@ -425,7 +452,11 @@ export const PodcastDetailPage: React.FC<PodcastDetailPageProps> = ({
425452
426453 const handlePlayClick = ( episode : LearningResource ) => {
427454 if ( ! getEpisodeAudioUrl ( episode ) ) return
428- setPlayingEpisode ( episode )
455+ if ( playingEpisode ?. id === episode . id ) {
456+ playerRef . current ?. resume ( )
457+ } else {
458+ setPlayingEpisode ( episode )
459+ }
429460 }
430461
431462 const currentTrack : PodcastTrack | null = playingEpisode
@@ -517,7 +548,7 @@ export const PodcastDetailPage: React.FC<PodcastDetailPageProps> = ({
517548 < StyledButton
518549 onClick = { ( ) => handlePlayClick ( latestEpisode ) }
519550 variant = "primary"
520- startIcon = { < RiPlayFill /> }
551+ startIcon = { < StyledIcon /> }
521552 disabled = { ! getEpisodeAudioUrl ( latestEpisode ) }
522553 >
523554 Play Latest Episode
@@ -540,7 +571,10 @@ export const PodcastDetailPage: React.FC<PodcastDetailPageProps> = ({
540571 key = { episode . id }
541572 episode = { episode }
542573 onPlayClick = { handlePlayClick }
543- isPlaying = { playingEpisode ?. id === episode . id }
574+ onPauseClick = { ( ) => playerRef . current ?. pause ( ) }
575+ isPlaying = {
576+ playingEpisode ?. id === episode . id && isAudioPlaying
577+ }
544578 isPlayable = { Boolean ( getEpisodeAudioUrl ( episode ) ) }
545579 />
546580 ) ) }
@@ -568,8 +602,10 @@ export const PodcastDetailPage: React.FC<PodcastDetailPageProps> = ({
568602 </ PageSection >
569603 { currentTrack && (
570604 < PodcastPlayer
605+ ref = { playerRef }
571606 track = { currentTrack }
572607 onClose = { ( ) => setPlayingEpisode ( null ) }
608+ onPlayStateChange = { setIsAudioPlaying }
573609 />
574610 ) }
575611 </ >
0 commit comments