diff --git a/src/components/features/home/PlaylistCarousel.tsx b/src/components/features/home/PlaylistCarousel.tsx index c76f2a4..d3363f4 100644 --- a/src/components/features/home/PlaylistCarousel.tsx +++ b/src/components/features/home/PlaylistCarousel.tsx @@ -1,31 +1,12 @@ -import { useRef, useState, useEffect } from 'react'; -import { ChevronLeft, ChevronRight } from 'lucide-react'; import type { Playlist } from '../../../types/spotify'; import { HomeSection } from './HomeSection'; import { Skeleton } from '../../ui/skeleton'; import { PlaylistCard } from './PlaylistCard'; +import { ScrollArrow } from '../../ui/ScrollArrow'; +import { useCarouselScroll } from '../../../hooks/useCarouselScroll'; -const CARD_WIDTH = 168; const SKELETON_KEYS = ['s1', 's2', 's3', 's4', 's5', 's6', 's7', 's8']; -function ScrollArrow({ dir, onClick }: Readonly<{ dir: 'left' | 'right'; onClick: () => void }>) { - const isLeft = dir === 'left'; - const Icon = isLeft ? ChevronLeft : ChevronRight; - return ( -
- -
- ); -} - interface PlaylistCarouselProps { title: string; playlists: Playlist[]; @@ -45,31 +26,7 @@ export function PlaylistCarousel({ onPlay, onPause, }: Readonly) { - const scrollRef = useRef(null); - const [canScrollLeft, setCanScrollLeft] = useState(false); - const [canScrollRight, setCanScrollRight] = useState(false); - - useEffect(() => { - const el = scrollRef.current; - if (!el) return; - - const updateArrows = () => { - setCanScrollLeft(el.scrollLeft > 0); - setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1); - }; - - updateArrows(); - el.addEventListener('scroll', updateArrows); - globalThis.addEventListener('resize', updateArrows); - return () => { - el.removeEventListener('scroll', updateArrows); - globalThis.removeEventListener('resize', updateArrows); - }; - }, [playlists]); - - const scroll = (dir: 'left' | 'right') => { - scrollRef.current?.scrollBy({ left: dir === 'left' ? -CARD_WIDTH * 2 : CARD_WIDTH * 2, behavior: 'smooth' }); - }; + const { scrollRef, canScrollLeft, canScrollRight, scroll } = useCarouselScroll(playlists); if (isLoading) { return ( diff --git a/src/components/ui/ScrollArrow.tsx b/src/components/ui/ScrollArrow.tsx new file mode 100644 index 0000000..fc3783d --- /dev/null +++ b/src/components/ui/ScrollArrow.tsx @@ -0,0 +1,26 @@ +import { ChevronLeft, ChevronRight } from 'lucide-react'; + +interface ScrollArrowProps { + dir: 'left' | 'right'; + onClick: () => void; + fromColor?: string; +} + +export function ScrollArrow({ dir, onClick, fromColor = 'from-surface' }: Readonly) { + const isLeft = dir === 'left'; + const Icon = isLeft ? ChevronLeft : ChevronRight; + return ( +
+ +
+ ); +} diff --git a/src/hooks/useCarouselScroll.ts b/src/hooks/useCarouselScroll.ts new file mode 100644 index 0000000..a2d1ba9 --- /dev/null +++ b/src/hooks/useCarouselScroll.ts @@ -0,0 +1,34 @@ +import { useRef, useState, useEffect } from 'react'; + +const CARD_WIDTH = 168; + +export function useCarouselScroll(dependency: unknown) { + const scrollRef = useRef(null); + const [canScrollLeft, setCanScrollLeft] = useState(false); + const [canScrollRight, setCanScrollRight] = useState(false); + + useEffect(() => { + const el = scrollRef.current; + if (!el) return; + + const updateArrows = () => { + setCanScrollLeft(el.scrollLeft > 0); + setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1); + }; + + updateArrows(); + el.addEventListener('scroll', updateArrows); + globalThis.addEventListener('resize', updateArrows); + return () => { + el.removeEventListener('scroll', updateArrows); + globalThis.removeEventListener('resize', updateArrows); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dependency]); + + const scroll = (dir: 'left' | 'right') => { + scrollRef.current?.scrollBy({ left: dir === 'left' ? -CARD_WIDTH * 2 : CARD_WIDTH * 2, behavior: 'smooth' }); + }; + + return { scrollRef, canScrollLeft, canScrollRight, scroll }; +} diff --git a/src/pages/ArtistDetail.tsx b/src/pages/ArtistDetail.tsx index d7039ee..de6b50c 100644 --- a/src/pages/ArtistDetail.tsx +++ b/src/pages/ArtistDetail.tsx @@ -5,6 +5,8 @@ import { usePlaybackControls } from '../hooks/useSpotifyMutations'; import { ErrorState } from '../components/ui/ErrorState'; import { ArtistDetailSkeleton } from '../components/features/artist/ArtistDetailSkeleton'; import { ArtistAlbumCard } from '../components/features/artist/ArtistAlbumCard'; +import { ScrollArrow } from '../components/ui/ScrollArrow'; +import { useCarouselScroll } from '../hooks/useCarouselScroll'; export default function ArtistDetail() { const { id } = useParams<{ id: string }>(); @@ -12,10 +14,14 @@ export default function ArtistDetail() { const { data: artist, isLoading: artistLoading, isError: artistError, refetch: refetchArtist } = useArtist(id ?? ''); const { data: albumsData, isLoading: albumsLoading, isError: albumsError, refetch: refetchAlbums } = useArtistAlbums(id ?? ''); const { play } = usePlaybackControls(); + const { scrollRef, canScrollLeft, canScrollRight, scroll } = useCarouselScroll(albumsData?.items); const isLoading = artistLoading || albumsLoading; const isError = artistError || albumsError; + const albums = (albumsData?.items ?? []).slice(0, 10); + const artistImage = artist?.images?.[0]?.url; + if (isLoading) return ; if (isError || !artist) { return ( @@ -26,9 +32,6 @@ export default function ArtistDetail() { ); } - const albums = (albumsData?.items ?? []).slice(0, 10); - const artistImage = artist.images?.[0]?.url; - return (
{/* Hero section */} @@ -73,12 +76,19 @@ export default function ArtistDetail() {

Albums

-
-
+
+ {canScrollLeft && scroll('left')} fromColor="from-[#121212]" />} +
{albums.map((album) => ( ))}
+ {canScrollRight && scroll('right')} fromColor="from-[#121212]" />}