11import useVideos from '@/components/hooks/use-videos.ts'
22import { BREAKPOINTS } from '@/constants.ts'
33import { getMyList , toggleToMyList } from '@/utils/my-list'
4- import { Button , buttonStyles , Carousel , Heading , Link , type CarouselApi } from 'ui'
4+ import type { CarouselApi } from 'ui'
5+ import { Button , buttonStyles , Carousel , Heading , Link } from 'ui'
56
67import { IconCheck , IconMute , IconPlus , IconVolumeFull } from '@intentui/icons'
78import clsx from 'clsx'
89import Autoplay from 'embla-carousel-autoplay'
10+ import type { UseEmblaCarouselType } from 'embla-carousel-react'
911import { useEffect , useRef , useState , type ComponentProps } from 'react'
1012
1113interface Poster {
@@ -40,7 +42,7 @@ function Featured({ items }: FeaturedProps) {
4042 const [ api , setApi ] = useState < CarouselApi > ( )
4143 const [ myList , setMyList ] = useState ( getMyList ( ) )
4244
43- const [ slide , setSlide ] = useState ( 1 )
45+ const [ slide , setSlide ] = useState ( 0 )
4446 const [ videos , setVideos ] = useVideos ( items , ( item ) => item . id )
4547 const [ playing , setPlaying ] = useState ( Object . fromEntries ( items . map ( ( item ) => [ item . id , false ] ) ) )
4648 const [ timeouts , setTimeouts ] = useState < NodeJS . Timeout [ ] > ( [ ] )
@@ -93,6 +95,8 @@ function Featured({ items }: FeaturedProps) {
9395 slide . ref . setAttribute ( 'data-timeout' , 'true' )
9496
9597 const fadeIn = setTimeout ( ( ) => {
98+ if ( index != api . selectedScrollSnap ( ) ) return
99+
96100 slide . video . ref . play ( )
97101 slide . video . ref . volume = 0
98102
@@ -149,66 +153,74 @@ function Featured({ items }: FeaturedProps) {
149153 const intersectionObserver = new IntersectionObserver (
150154 ( entries ) => {
151155 entries . forEach ( ( entry ) => {
152- setIntersecting ( entry . isIntersecting )
153- if ( entry . isIntersecting ) return
156+ const isCurrentlyIntersecting = entry . isIntersecting
157+
158+ if ( intersecting !== isCurrentlyIntersecting ) {
159+ setIntersecting ( isCurrentlyIntersecting )
154160
155- const slide = {
156- id : $slides [ index ] . getAttribute ( 'data-id' ) as string ,
157- ref : $slides [ index ] ,
158- timeout : $slides [ index ] . getAttribute ( 'data-timeout' ) === 'true' ? true : false ,
161+ if ( isCurrentlyIntersecting ) {
162+ fadeInOut ( index )
163+ return
164+ }
159165
160- video : {
161- ref : $slides [ index ] . querySelector ( 'video' ) as HTMLVideoElement ,
162- duration : items [ index ] . duration ,
163- fadeInDelay : items [ index ] . fadeInDelay ,
164- fadeOutDelay : items [ index ] . fadeOutDelay ,
165- } ,
166- } as const
166+ const slide = {
167+ id : $slides [ index ] . getAttribute ( 'data-id' ) as string ,
168+ ref : $slides [ index ] ,
169+ timeout : $slides [ index ] . getAttribute ( 'data-timeout' ) === 'true' ? true : false ,
167170
168- for ( const timeout of timeouts ) clearTimeout ( timeout )
171+ video : {
172+ ref : $slides [ index ] . querySelector ( 'video' ) as HTMLVideoElement ,
173+ duration : items [ index ] . duration ,
174+ fadeInDelay : items [ index ] . fadeInDelay ,
175+ fadeOutDelay : items [ index ] . fadeOutDelay ,
176+ } ,
177+ } as const
169178
170- const _playing = { ... playing }
179+ for ( const timeout of timeouts ) clearTimeout ( timeout )
171180
172- for ( const $slide of $slides ) {
173- const id = $slide . getAttribute ( 'data-id' ) as string
174- const $video = $slide . querySelector ( 'video' ) as HTMLVideoElement
181+ const _playing = { ...playing }
175182
176- $video . pause ( )
177- $video . volume = 0
178- $video . currentTime = 0
179- $slide . setAttribute ( 'data-timeout' , 'false' )
183+ for ( const $slide of $slides ) {
184+ const id = $slide . getAttribute ( 'data-id' ) as string
185+ const $video = $slide . querySelector ( 'video' ) as HTMLVideoElement
180186
181- _playing [ id ] = false
182- }
187+ $video . pause ( )
188+ $video . volume = 0
189+ $video . currentTime = 0
190+ $slide . setAttribute ( 'data-timeout' , 'false' )
183191
184- setPlaying ( _playing )
185-
186- const fadeOut = setTimeout (
187- ( ) => {
188- setPlaying ( ( prev ) => ( { ...prev , [ slide . id ] : false } ) )
189-
190- const steps : number = 50
191- const duration : number = parseInt ( slide . video . ref . getAttribute ( 'data-fade-in-out' ) as string )
192- const interval : number = duration / steps
193- const decrement : number = slide . video . ref . volume / steps
194-
195- const fadeOutVolume = setInterval ( ( ) => {
196- const newVolume : number = slide . video . ref . volume - decrement
197-
198- if ( newVolume > 0 ) {
199- slide . video . ref . volume = Math . max ( 0 , newVolume )
200- } else {
201- slide . video . ref . pause ( )
202- slide . video . ref . volume = 0
203- slide . video . ref . currentTime = 0
204- clearInterval ( fadeOutVolume as NodeJS . Timeout )
205- }
206- } , interval )
207- } ,
208- slide . video . fadeInDelay + slide . video . duration - slide . video . fadeOutDelay
209- )
210-
211- setTimeouts ( [ fadeOut ] )
192+ _playing [ id ] = false
193+ }
194+
195+ setPlaying ( _playing )
196+
197+ const fadeOut = setTimeout (
198+ ( ) => {
199+ setPlaying ( ( prev ) => ( { ...prev , [ slide . id ] : false } ) )
200+
201+ const steps : number = 50
202+ const duration : number = parseInt ( slide . video . ref . getAttribute ( 'data-fade-in-out' ) as string )
203+ const interval : number = duration / steps
204+ const decrement : number = slide . video . ref . volume / steps
205+
206+ const fadeOutVolume = setInterval ( ( ) => {
207+ const newVolume : number = slide . video . ref . volume - decrement
208+
209+ if ( newVolume > 0 ) {
210+ slide . video . ref . volume = Math . max ( 0 , newVolume )
211+ } else {
212+ slide . video . ref . pause ( )
213+ slide . video . ref . volume = 0
214+ slide . video . ref . currentTime = 0
215+ clearInterval ( fadeOutVolume as NodeJS . Timeout )
216+ }
217+ } , interval )
218+ } ,
219+ slide . video . fadeInDelay + slide . video . duration - slide . video . fadeOutDelay
220+ )
221+
222+ setTimeouts ( [ fadeOut ] )
223+ }
212224 } )
213225 } ,
214226 { threshold : 0.5 }
@@ -218,17 +230,21 @@ function Featured({ items }: FeaturedProps) {
218230
219231 intersectionObserver . observe ( $carousel )
220232
221- fadeInOut ( index )
222233 setSlide ( index )
223234
224- api . on ( 'select' , ( event ) => {
235+ const handleAPISelect = ( event : NonNullable < UseEmblaCarouselType [ 1 ] > ) => {
225236 const index = event . selectedScrollSnap ( )
226237
227238 fadeInOut ( index )
228239 setSlide ( index )
229- } )
240+ }
241+
242+ api . on ( 'select' , handleAPISelect )
230243
231- return ( ) => intersectionObserver . disconnect ( )
244+ return ( ) => {
245+ intersectionObserver . disconnect ( )
246+ api . off ( 'select' , handleAPISelect )
247+ }
232248 } , [ api , intersecting , playing , timeouts ] )
233249
234250 useEffect ( ( ) => {
@@ -259,7 +275,7 @@ function Featured({ items }: FeaturedProps) {
259275 return (
260276 < >
261277 < Carousel
262- className = '**: select-none'
278+ className = 'select-none'
263279 opts = { { loop : true , slidesToScroll : 1 } }
264280 setApi = { setApi }
265281 plugins = { [ autoPlay . current ] }
@@ -277,7 +293,8 @@ function Featured({ items }: FeaturedProps) {
277293 return (
278294 < Carousel . Item
279295 id = { item . id }
280- aria-label = { item . highlight }
296+ textValue = { `${ item . highlight } : "${ item . title } "` }
297+ aria-label = { playing [ item . id ] ? `${ videos [ item . id ] . muted ? 'Activar' : 'Silenciar' } sonido` : '' }
281298 data-id = { item . id }
282299 onAction = { ( ) => setVideos . toggleMuted ( item . id ) }
283300 >
@@ -286,18 +303,18 @@ function Featured({ items }: FeaturedProps) {
286303 < div className = 'absolute inset-0 grid w-full grid-cols-[1fr_3rem] p-2 sm:p-3 lg:grid-cols-[1fr_6rem] lg:p-7 lg:pt-3.5 lg:pr-3.5' >
287304 < div className = 'mt-auto' >
288305 < header className = 'relative flex w-full flex-col gap-1 *:relative *:w-fit *:before:absolute *:before:-top-2 *:before:-left-6 *:before:-z-[1] *:before:block *:before:h-[calc(100%_+_1rem)] *:before:w-[calc(100%_+_3rem)] *:before:bg-neutral-950 *:before:blur-2xl *:before:sm:blur-3xl lg:gap-4' >
289- < span className = 'dark:text-fg text-bg bg-highlight/50 relative w-fit rounded-full px-2 text-[0.5rem] font-medium lg:px-4 lg:text-base' >
306+ < span className = 'dark:text-fg text-bg bg-highlight/50 relative w-fit rounded-full px-2 text-[0.5rem] font-medium sm:text-xs lg:px-4 lg:text-base' >
290307 { item . highlight }
291308 </ span >
292309 < Heading
293- className = 'dark:text-fg text-bg relative flex w-fit sm:text-2xl lg:text-4xl'
310+ className = 'dark:text-fg text-bg relative flex w-fit text-balance sm:text-2xl lg:text-4xl'
294311 tracking = 'wider'
295312 level = { i ? 2 : 1 }
296313 >
297314 { item . title }
298315 </ Heading >
299316 < div >
300- < p className = 'dark:text-fg/80 text-bg/80 line-clamp-2 text-xs font-normal sm:text-lg lg:text-2xl' >
317+ < p className = 'dark:text-fg/80 text-bg/80 line-clamp-2 text-sm font-normal text-pretty sm:text-lg lg:text-2xl' >
301318 { item . description }
302319 </ p >
303320 </ div >
@@ -344,7 +361,7 @@ function Featured({ items }: FeaturedProps) {
344361 'dark:text-fg/50 text-bg/50 relative ml-auto size-4 transition-opacity delay-400 duration-600 ease-in-out *:absolute *:size-full *:transition-all before:absolute before:-top-1.5 before:-left-2 before:-z-[1] before:block before:size-[calc(100%_+_1rem)] before:bg-neutral-950 before:blur-2xl sm:size-6 md:size-7 lg:size-8' ,
345362 playing [ item . id ] && videos [ item . id ] . volumeIcon ? 'opacity-100' : 'opacity-0'
346363 ) }
347- aria-hidden = 'true'
364+ aria-hidden
348365 >
349366 < IconMute className = { videos [ item . id ] . muted ? 'scale-100 opacity-100' : 'scale-0 opacity-0' } />
350367 < IconVolumeFull
@@ -360,7 +377,7 @@ function Featured({ items }: FeaturedProps) {
360377 muted = { videos [ item . id ] . muted }
361378 poster = { item . poster . src }
362379 preload = 'metadata'
363- aria-hidden = { ! playing [ item . id ] }
380+ aria-hidden
364381 data-fade-in-out = '1000'
365382 loop
366383 playsInline
@@ -396,6 +413,7 @@ function Featured({ items }: FeaturedProps) {
396413 ) }
397414 alt = { item . poster . alt }
398415 src = { item . poster . src }
416+ aria-hidden = { playing [ item . id ] }
399417 />
400418 </ div >
401419 </ article >
0 commit comments