Skip to content

Commit e9c41fb

Browse files
committed
feat: replace scroll area on category buttons with carousel
1 parent 223c30d commit e9c41fb

File tree

3 files changed

+145
-57
lines changed

3 files changed

+145
-57
lines changed

web/src/modules/browse/components/HomePageComponent.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ export const HomePageComponent = () => {
4040
duration: 15,
4141
}}
4242
>
43-
<CarouselContent>
43+
<CarouselContent className='-ml-4'>
4444
{featuredSongsPage.map((song, i) => (
4545
<CarouselItem
46-
className='basis-full md:basis-1/2 lg:basis-1/3'
46+
className='basis-full md:basis-1/2 lg:basis-1/3 min-w-0 shrink-0 grow-0 pl-4'
4747
key={i}
4848
>
4949
<SongCard song={song} />
@@ -58,7 +58,7 @@ export const HomePageComponent = () => {
5858

5959
{/* RECENT SONGS */}
6060
<div className='flex flex-row flex-wrap justify-between items-center gap-4'>
61-
<h2 className='text-xl uppercase'>Recent songs</h2>
61+
<h2 className='text-xl uppercase z-10'>Recent songs</h2>
6262
<CategoryButtonGroup />
6363
</div>
6464
<div className='h-6' />

web/src/modules/browse/components/client/CategoryButton.tsx

Lines changed: 61 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
import { UploadConst } from '@shared/validation/song/constants';
44
import { CategoryType } from '@shared/validation/song/dto/types';
55

6+
import {
7+
Carousel,
8+
CarouselContent,
9+
CarouselItem,
10+
CarouselNextSmall,
11+
CarouselPreviousSmall,
12+
} from '@web/src/modules/shared/components/client/Carousel';
13+
614
import { useRecentSongsProvider } from './context/RecentSongs.context';
715

816
type CategoryButtonProps = {
@@ -18,30 +26,44 @@ export const CategoryButtonGroup = () => {
1826
useRecentSongsProvider();
1927

2028
return (
21-
<div className='flex w-fit gap-2 md:gap-3 overflow-x-auto'>
22-
{Object.entries(categories).map(([category, count]) => {
23-
return (
24-
<CategoryButton
25-
key={category}
26-
id={category}
27-
data-test={`category-${category}`}
28-
isActive={selectedCategory === category}
29-
onClick={() => {
30-
if (selectedCategory === category) {
31-
setSelectedCategory('');
32-
} else {
33-
setSelectedCategory(category);
34-
}
35-
}}
36-
>
37-
{UploadConst.categories[category as CategoryType]}
38-
<span className='text-sm text-zinc-400 ml-1 font-bold'>
39-
{count}
40-
</span>
41-
</CategoryButton>
42-
);
43-
})}
44-
</div>
29+
<Carousel
30+
className='w-fit max-w-full'
31+
opts={{
32+
align: 'start',
33+
loop: false,
34+
duration: 10,
35+
slidesToScroll: 2,
36+
dragFree: true,
37+
}}
38+
orientation='horizontal'
39+
>
40+
<CarouselContent className='flex gap-2'>
41+
{Object.entries(categories).map(([category, count]) => {
42+
return (
43+
<CategoryButton
44+
key={category}
45+
id={category}
46+
data-test={`category-${category}`}
47+
isActive={selectedCategory === category}
48+
onClick={() => {
49+
if (selectedCategory === category) {
50+
setSelectedCategory('');
51+
} else {
52+
setSelectedCategory(category);
53+
}
54+
}}
55+
>
56+
{UploadConst.categories[category as CategoryType]}
57+
<span className='text-sm text-zinc-400 ml-1 font-bold'>
58+
{count}
59+
</span>
60+
</CategoryButton>
61+
);
62+
})}
63+
</CarouselContent>
64+
<CarouselPreviousSmall />
65+
<CarouselNextSmall />
66+
</Carousel>
4567
);
4668
};
4769

@@ -53,18 +75,20 @@ export const CategoryButton = ({
5375
id,
5476
}: CategoryButtonProps) => {
5577
return (
56-
<button
57-
id={id}
58-
onClick={onClick}
59-
className={
60-
(isActive
61-
? 'bg-white text-black cursor-pointer font-bold'
62-
: 'bg-zinc-600 enabled:text-white hover:bg-zinc-500 disabled:bg-zinc-700 disabled:text-zinc-400 disabled:opacity-50') +
63-
' whitespace-nowrap text-sm py-1 px-2 min-w-fit w-24 rounded-full transition-all duration-200'
64-
}
65-
disabled={isDisabled}
66-
>
67-
{children}
68-
</button>
78+
<CarouselItem>
79+
<button
80+
id={id}
81+
onClick={onClick}
82+
className={
83+
(isActive
84+
? 'bg-white text-black cursor-pointer font-bold'
85+
: 'bg-zinc-600 enabled:text-white hover:bg-zinc-500 disabled:bg-zinc-700 disabled:text-zinc-400 disabled:opacity-50') +
86+
' mr-1 whitespace-nowrap text-sm py-1 px-2 w-fit min-w-24 rounded-full transition-all duration-200'
87+
}
88+
disabled={isDisabled}
89+
>
90+
{children}
91+
</button>
92+
</CarouselItem>
6993
);
7094
};

web/src/modules/shared/components/client/Carousel.tsx

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -178,19 +178,11 @@ export const CarouselContent = forwardRef<
178178
HTMLDivElement,
179179
HTMLAttributes<HTMLDivElement>
180180
>(({ className, ...props }, ref) => {
181-
const { carouselRef, orientation } = useCarousel();
181+
const { carouselRef } = useCarousel();
182182

183183
return (
184184
<div ref={carouselRef} className='overflow-hidden p-4 m-[-1rem]'>
185-
<div
186-
ref={ref}
187-
className={cn(
188-
'flex',
189-
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
190-
className,
191-
)}
192-
{...props}
193-
/>
185+
<div ref={ref} className={cn('flex', className)} {...props} />
194186
</div>
195187
);
196188
});
@@ -200,18 +192,12 @@ export const CarouselItem = forwardRef<
200192
HTMLDivElement,
201193
HTMLAttributes<HTMLDivElement>
202194
>(({ className, ...props }, ref) => {
203-
const { orientation } = useCarousel();
204-
205195
return (
206196
<div
207197
ref={ref}
208198
role='group'
209199
aria-roledescription='slide'
210-
className={cn(
211-
'min-w-0 shrink-0 grow-0 basis-full',
212-
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
213-
className,
214-
)}
200+
className={cn('w-fit', className)}
215201
{...props}
216202
/>
217203
);
@@ -286,6 +272,84 @@ export const CarouselNext = forwardRef<
286272
});
287273
CarouselNext.displayName = 'CarouselNext';
288274

275+
const CarouselButtonSmall = forwardRef<
276+
HTMLButtonElement,
277+
ButtonHTMLAttributes<HTMLButtonElement>
278+
>(({ className, ...props }, ref) => {
279+
return (
280+
<button
281+
ref={ref}
282+
className={cn(
283+
'absolute h-10 w-10 rounded-full bg-zinc-900 hover:bg-zinc-800 transition-all duration-200 ease-in-out cursor-pointer',
284+
className,
285+
)}
286+
{...props}
287+
/>
288+
);
289+
});
290+
291+
CarouselButtonSmall.displayName = 'CarouselButtonSmall';
292+
293+
export const CarouselPreviousSmall = forwardRef<
294+
HTMLButtonElement,
295+
ButtonHTMLAttributes<HTMLButtonElement>
296+
>(({ className, ...props }, ref) => {
297+
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
298+
299+
if (!canScrollPrev) {
300+
return null;
301+
}
302+
303+
return (
304+
<CarouselButtonSmall
305+
ref={ref}
306+
className={cn(
307+
orientation === 'horizontal'
308+
? '-left-5 top-1/2 -translate-y-1/2'
309+
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
310+
'shadow-[15px_0_20px_20px_rgb(24,24,27)]',
311+
className,
312+
)}
313+
onClick={scrollPrev}
314+
{...props}
315+
>
316+
<FontAwesomeIcon icon={faChevronLeft} className='h-4 w-4' />
317+
<span className='sr-only'>Previous slide</span>
318+
</CarouselButtonSmall>
319+
);
320+
});
321+
CarouselPreviousSmall.displayName = 'CarouselPreviousSmall';
322+
323+
export const CarouselNextSmall = forwardRef<
324+
HTMLButtonElement,
325+
ButtonHTMLAttributes<HTMLButtonElement>
326+
>(({ className, ...props }, ref) => {
327+
const { orientation, scrollNext, canScrollNext } = useCarousel();
328+
329+
if (!canScrollNext) {
330+
return null;
331+
}
332+
333+
return (
334+
<CarouselButtonSmall
335+
ref={ref}
336+
className={cn(
337+
orientation === 'horizontal'
338+
? '-right-5 top-1/2 -translate-y-1/2'
339+
: 'bottom-12 left-1/2 -translate-x-1/2 rotate-90',
340+
'shadow-[-15px_0_20px_20px_rgb(24,24,27)]',
341+
className,
342+
)}
343+
onClick={scrollNext}
344+
{...props}
345+
>
346+
<FontAwesomeIcon icon={faChevronRight} className='h-4 w-4' />
347+
<span className='sr-only'>Next slide</span>
348+
</CarouselButtonSmall>
349+
);
350+
});
351+
CarouselNextSmall.displayName = 'CarouselNextSmall';
352+
289353
type UseDotButtonType = {
290354
selectedIndex: number;
291355
scrollSnaps: number[];

0 commit comments

Comments
 (0)