From c5908bd2b7b65e1068281e4f8cb89bbc7271f788 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 15:49:12 +0000 Subject: [PATCH 1/3] fix: replace white scrollbar on artist page with chevron arrow carousel Replace the native overflow-x-auto scrollbar on the artist albums section with a hidden-scrollbar carousel matching the home page pattern. Left/right chevron arrow buttons appear dynamically based on scroll position. Closes #28 https://claude.ai/code/session_015duffePt3aymjzz7T84rEB --- src/pages/ArtistDetail.tsx | 65 ++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/src/pages/ArtistDetail.tsx b/src/pages/ArtistDetail.tsx index d7039ee..0aa024d 100644 --- a/src/pages/ArtistDetail.tsx +++ b/src/pages/ArtistDetail.tsx @@ -1,21 +1,70 @@ +import { useRef, useState, useEffect } from 'react'; import { useParams } from 'react-router-dom'; -import { Play } from 'lucide-react'; +import { Play, ChevronLeft, ChevronRight } from 'lucide-react'; import { useArtist, useArtistAlbums } from '../hooks/useSpotifyQueries'; 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'; +const CARD_WIDTH = 168; + +function ScrollArrow({ dir, onClick }: Readonly<{ dir: 'left' | 'right'; onClick: () => void }>) { + const isLeft = dir === 'left'; + const Icon = isLeft ? ChevronLeft : ChevronRight; + return ( +
+ +
+ ); +} + export default function ArtistDetail() { const { id } = useParams<{ id: string }>(); 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 = useRef(null); + const [canScrollLeft, setCanScrollLeft] = useState(false); + const [canScrollRight, setCanScrollRight] = useState(false); const isLoading = artistLoading || albumsLoading; const isError = artistError || albumsError; + const albums = (albumsData?.items ?? []).slice(0, 10); + const artistImage = artist?.images?.[0]?.url; + + 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); + }; + }, [albums]); + + const scroll = (dir: 'left' | 'right') => { + scrollRef.current?.scrollBy({ left: dir === 'left' ? -CARD_WIDTH * 2 : CARD_WIDTH * 2, behavior: 'smooth' }); + }; + if (isLoading) return ; if (isError || !artist) { return ( @@ -26,9 +75,6 @@ export default function ArtistDetail() { ); } - const albums = (albumsData?.items ?? []).slice(0, 10); - const artistImage = artist.images?.[0]?.url; - return (
{/* Hero section */} @@ -73,12 +119,19 @@ export default function ArtistDetail() {

Albums

-
-
+
+ {canScrollLeft && scroll('left')} />} +
{albums.map((album) => ( ))}
+ {canScrollRight && scroll('right')} />}
From ed3d45c2350f010da00f724b5f9816289bd30aac Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 15:52:41 +0000 Subject: [PATCH 2/3] refactor: extract ScrollArrow into shared component to fix duplication Move the duplicated ScrollArrow component from PlaylistCarousel and ArtistDetail into a shared src/components/ui/ScrollArrow.tsx, eliminating code duplication flagged by SonarQube. https://claude.ai/code/session_015duffePt3aymjzz7T84rEB --- .../features/home/PlaylistCarousel.tsx | 20 +-------------- src/components/ui/ScrollArrow.tsx | 25 +++++++++++++++++++ src/pages/ArtistDetail.tsx | 25 +++---------------- 3 files changed, 30 insertions(+), 40 deletions(-) create mode 100644 src/components/ui/ScrollArrow.tsx diff --git a/src/components/features/home/PlaylistCarousel.tsx b/src/components/features/home/PlaylistCarousel.tsx index c76f2a4..7f84cfa 100644 --- a/src/components/features/home/PlaylistCarousel.tsx +++ b/src/components/features/home/PlaylistCarousel.tsx @@ -1,31 +1,13 @@ 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'; 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[]; diff --git a/src/components/ui/ScrollArrow.tsx b/src/components/ui/ScrollArrow.tsx new file mode 100644 index 0000000..1fc1022 --- /dev/null +++ b/src/components/ui/ScrollArrow.tsx @@ -0,0 +1,25 @@ +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/pages/ArtistDetail.tsx b/src/pages/ArtistDetail.tsx index 0aa024d..865a5d9 100644 --- a/src/pages/ArtistDetail.tsx +++ b/src/pages/ArtistDetail.tsx @@ -1,32 +1,15 @@ import { useRef, useState, useEffect } from 'react'; import { useParams } from 'react-router-dom'; -import { Play, ChevronLeft, ChevronRight } from 'lucide-react'; +import { Play } from 'lucide-react'; import { useArtist, useArtistAlbums } from '../hooks/useSpotifyQueries'; 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'; const CARD_WIDTH = 168; -function ScrollArrow({ dir, onClick }: Readonly<{ dir: 'left' | 'right'; onClick: () => void }>) { - const isLeft = dir === 'left'; - const Icon = isLeft ? ChevronLeft : ChevronRight; - return ( -
- -
- ); -} - export default function ArtistDetail() { const { id } = useParams<{ id: string }>(); @@ -120,7 +103,7 @@ export default function ArtistDetail() {

Albums

- {canScrollLeft && scroll('left')} />} + {canScrollLeft && scroll('left')} fromColor="from-[#121212]" />}
))}
- {canScrollRight && scroll('right')} />} + {canScrollRight && scroll('right')} fromColor="from-[#121212]" />}
From 57575c9aaba29aecf99b79092d4e24784b858d8b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 15:57:15 +0000 Subject: [PATCH 3/3] refactor: extract useCarouselScroll hook and fix ScrollArrow button type - Extract scroll state/effect logic into useCarouselScroll hook shared by PlaylistCarousel and ArtistDetail, eliminating code duplication - Fix useEffect dependency to use stable albumsData?.items instead of the albums array recreated on every render - Add type="button" to ScrollArrow button to prevent accidental form submit https://claude.ai/code/session_015duffePt3aymjzz7T84rEB --- .../features/home/PlaylistCarousel.tsx | 29 ++-------------- src/components/ui/ScrollArrow.tsx | 1 + src/hooks/useCarouselScroll.ts | 34 +++++++++++++++++++ src/pages/ArtistDetail.tsx | 30 ++-------------- 4 files changed, 39 insertions(+), 55 deletions(-) create mode 100644 src/hooks/useCarouselScroll.ts diff --git a/src/components/features/home/PlaylistCarousel.tsx b/src/components/features/home/PlaylistCarousel.tsx index 7f84cfa..d3363f4 100644 --- a/src/components/features/home/PlaylistCarousel.tsx +++ b/src/components/features/home/PlaylistCarousel.tsx @@ -1,11 +1,10 @@ -import { useRef, useState, useEffect } from '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']; interface PlaylistCarouselProps { @@ -27,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 index 1fc1022..fc3783d 100644 --- a/src/components/ui/ScrollArrow.tsx +++ b/src/components/ui/ScrollArrow.tsx @@ -16,6 +16,7 @@ export function ScrollArrow({ dir, onClick, fromColor = 'from-surface' }: Readon