@@ -2,13 +2,14 @@ import React, { useState, useEffect, useCallback, FC, MouseEvent, KeyboardEvent
22import { useSwipeable } from 'react-swipeable' ;
33import { getHistoricalFigures } from '../api/figures' ;
44import { CategoryTab , ActionButton } from './Button' ;
5- import { BookOpen , Check , Sparkle } from "@phosphor-icons/react" ;
5+ import { BookOpen , Check , Sparkle , Play , Pause } from "@phosphor-icons/react" ;
66import OptimizedImage from './OptimizedImage' ;
77import { useTranslation } from '../hooks/useTranslation' ;
88import WisdomMapModal from './WisdomMapModal' ;
99import type { Figure , Seed } from '../types/global' ;
1010import { useUIStore } from '../stores/uiStore' ;
1111import EchoExplainerHelp , { ECHO_EXPLAINER_HELP_ID } from './EchoExplainerHelp' ;
12+ import { useFigureTrailer } from '../hooks/useFigureTrailer' ;
1213import './FigureCarousel.css' ;
1314
1415interface FigureCarouselProps {
@@ -49,6 +50,9 @@ const FigureCarousel: FC<FigureCarouselProps> = ({
4950} ) => {
5051 const { tString, tNode, language } = useTranslation ( ) ;
5152
53+ // Figure page trailer — standalone audio player (play-on-tap, never autoplay)
54+ const trailer = useFigureTrailer ( ) ;
55+
5256 // Echo explainer helper — show once for users who haven't seen it
5357 const shouldShowHelp = useUIStore ( ( state ) => state . shouldShowHelp ) ;
5458 const [ showEchoHelp , setShowEchoHelp ] = useState < boolean > ( ( ) => shouldShowHelp ( ECHO_EXPLAINER_HELP_ID ) ) ;
@@ -300,6 +304,12 @@ const FigureCarousel: FC<FigureCarouselProps> = ({
300304 return ( ) => clearTimeout ( preloadTimer ) ;
301305 } , [ currentIndex , figures , hasNavigated , preloadedImages ] ) ;
302306
307+ // Stop any trailer when the figure changes or the carousel closes — never
308+ // leave audio playing behind a swipe, a category switch, or a close.
309+ useEffect ( ( ) => {
310+ trailer . stop ( ) ;
311+ } , [ currentFigure ?. id , isOpen , trailer . stop ] ) ;
312+
303313 const handleSelect = ( ) => {
304314 if ( onSelectFigure ) onSelectFigure ( currentFigure ) ;
305315 else console . error ( 'onSelectFigure is not defined' ) ;
@@ -460,6 +470,18 @@ const FigureCarousel: FC<FigureCarouselProps> = ({
460470 const handleInfoOverlayClick = ( ) => {
461471 setShowFullInfo ( ! showFullInfo ) ;
462472 } ;
473+
474+ const trailerStatus = trailer . activeId === currentFigure . id ? trailer . status : 'idle' ;
475+ const trailerEngaged = trailerStatus === 'loading' || trailerStatus === 'playing' ;
476+
477+ const handleTrailerClick = ( e : MouseEvent < HTMLButtonElement > ) => {
478+ e . stopPropagation ( ) ; // don't toggle the about overlay
479+ trailer . toggle ( currentFigure . id , language ) ;
480+ } ;
481+ // Keep Enter/Space on the trailer button from also toggling the overlay.
482+ const handleTrailerKeyDown = ( e : KeyboardEvent < HTMLButtonElement > ) => {
483+ if ( e . key === 'Enter' || e . key === ' ' ) e . stopPropagation ( ) ;
484+ } ;
463485
464486 return (
465487 < div className = "main-image-section" >
@@ -486,15 +508,32 @@ const FigureCarousel: FC<FigureCarouselProps> = ({
486508 aria-label = { tString ( 'carousel.toggleInfo' , 'Toggle figure information' ) }
487509 >
488510 < h2 > { getDisplayName ( currentFigure ) } </ h2 >
511+
512+ { currentFigure . learn && (
513+ < p className = "figure-learn" > { currentFigure . learn } </ p >
514+ ) }
515+
516+ < button
517+ type = "button"
518+ className = { `figure-trailer-btn ${ trailerEngaged ? 'is-active' : '' } ` }
519+ onClick = { handleTrailerClick }
520+ onKeyDown = { handleTrailerKeyDown }
521+ >
522+ { trailerEngaged
523+ ? < Pause size = { 16 } weight = "fill" />
524+ : < Play size = { 16 } weight = "fill" /> }
525+ < span className = "figure-trailer-label" >
526+ { trailerEngaged
527+ ? tString ( 'figures.trailerPause' , 'Pause' )
528+ : tString ( 'figures.trailerPlay' , 'Play intro' ) }
529+ </ span >
530+ </ button >
531+
489532 { showFullInfo && currentFigure . about && (
490533 < div className = "figure-about-text" >
491- { currentFigure . about . split ( '\n\n' ) . map ( ( line , index ) => {
492- // Check if this line contains bullet points (tags)
493- if ( line . includes ( '•' ) ) {
494- return < div key = { index } className = "golden-tags no-justify" > { line } </ div > ;
495- }
496- return < div key = { index } > { line } </ div > ;
497- } ) }
534+ { currentFigure . about . split ( '\n\n' ) . map ( ( para , index ) => (
535+ < div key = { index } > { para } </ div >
536+ ) ) }
498537 </ div >
499538 ) }
500539 </ div >
0 commit comments