67
resources/client/channels/carousel/channel-content-carousel.tsx
Executable file
67
resources/client/channels/carousel/channel-content-carousel.tsx
Executable file
@@ -0,0 +1,67 @@
|
||||
import React, {Fragment} from 'react';
|
||||
import {ChannelContentProps} from '@app/channels/channel-content';
|
||||
import {ChannelHeader} from '@app/channels/channel-header/channel-header';
|
||||
import {ChannelContentGridItem} from '@app/channels/content-grid/channel-content-grid-item';
|
||||
import {useCarousel} from '@app/channels/carousel/use-carousel';
|
||||
import clsx from 'clsx';
|
||||
import {ContentGridProps} from '@app/channels/content-grid/content-grid-layout';
|
||||
import {IconButton} from '@common/ui/buttons/icon-button';
|
||||
import {KeyboardArrowLeftIcon} from '@common/icons/material/KeyboardArrowLeft';
|
||||
import {KeyboardArrowRightIcon} from '@common/icons/material/KeyboardArrowRight';
|
||||
|
||||
interface Props extends ChannelContentProps {
|
||||
variant?: ContentGridProps['variant'];
|
||||
}
|
||||
export function ChannelContentCarousel(props: Props) {
|
||||
const {channel, variant} = props;
|
||||
const {
|
||||
scrollContainerRef,
|
||||
canScrollForward,
|
||||
canScrollBackward,
|
||||
scrollToPreviousPage,
|
||||
scrollToNextPage,
|
||||
containerClassName,
|
||||
itemClassName,
|
||||
} = useCarousel();
|
||||
|
||||
const gridClassName =
|
||||
variant === 'landscape'
|
||||
? 'content-grid-landscape'
|
||||
: 'content-grid-portrait';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ChannelHeader
|
||||
{...props}
|
||||
actions={
|
||||
<Fragment>
|
||||
<IconButton
|
||||
disabled={!canScrollBackward}
|
||||
onClick={() => scrollToPreviousPage()}
|
||||
aria-label="Previous page"
|
||||
>
|
||||
<KeyboardArrowLeftIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
disabled={!canScrollForward}
|
||||
onClick={() => scrollToNextPage()}
|
||||
aria-label="Next page"
|
||||
>
|
||||
<KeyboardArrowRightIcon />
|
||||
</IconButton>
|
||||
</Fragment>
|
||||
}
|
||||
/>
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className={clsx(containerClassName, gridClassName)}
|
||||
>
|
||||
{channel.content?.data.map(item => (
|
||||
<div className={itemClassName} key={`${item.id}-${item.model_type}`}>
|
||||
<ChannelContentGridItem item={item} variant={variant} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
112
resources/client/channels/carousel/use-carousel.ts
Executable file
112
resources/client/channels/carousel/use-carousel.ts
Executable file
@@ -0,0 +1,112 @@
|
||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import debounce from 'just-debounce-it';
|
||||
import {useLayoutEffect} from '@react-aria/utils';
|
||||
|
||||
interface Options {
|
||||
rotate?: boolean;
|
||||
}
|
||||
|
||||
const containerClassName =
|
||||
'content-carousel content-grid relative w-full grid grid-flow-col grid-rows-[auto] overflow-x-auto overflow-y-hidden gap-24 snap-always snap-x snap-mandatory hidden-scrollbar scroll-smooth';
|
||||
const itemClassName = 'snap-start snap-normal';
|
||||
|
||||
export function useCarousel({rotate = false}: Options = {}) {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const itemWidth = useRef<number>(0);
|
||||
const perPage = useRef<number>(5);
|
||||
|
||||
const [canScrollBackward, setCanScrollBackward] = useState(rotate);
|
||||
const [canScrollForward, setCanScrollForward] = useState(true);
|
||||
const [activePage, setActivePage] = useState(0);
|
||||
|
||||
const updateNavStatus = useCallback(() => {
|
||||
const el = scrollContainerRef.current;
|
||||
if (el && itemWidth.current) {
|
||||
if (!rotate) {
|
||||
setCanScrollForward(
|
||||
el.scrollWidth - 1 > el.scrollLeft + el.clientWidth
|
||||
);
|
||||
setCanScrollBackward(el.scrollLeft > 0);
|
||||
}
|
||||
const pageWidth = el.clientWidth;
|
||||
const activePage = Math.round(el.scrollLeft / pageWidth);
|
||||
setActivePage(activePage);
|
||||
}
|
||||
}, [rotate]);
|
||||
|
||||
// enable/disable navigation buttons based on element scroll offset
|
||||
useEffect(() => {
|
||||
const el = scrollContainerRef.current;
|
||||
const handleScroll = debounce(() => updateNavStatus(), 100);
|
||||
if (el) {
|
||||
el.addEventListener('scroll', handleScroll);
|
||||
}
|
||||
return () => el?.removeEventListener('scroll', handleScroll);
|
||||
}, [updateNavStatus]);
|
||||
|
||||
// get width for first grid item
|
||||
useLayoutEffect(() => {
|
||||
const el = scrollContainerRef.current;
|
||||
if (el) {
|
||||
perPage.current = Number(
|
||||
getComputedStyle(el).getPropertyValue('--nVisibleItems')
|
||||
);
|
||||
const firstGridItem = el.children.item(0);
|
||||
const observer = new ResizeObserver(entries => {
|
||||
itemWidth.current = entries[0].contentRect.width;
|
||||
updateNavStatus();
|
||||
});
|
||||
if (firstGridItem) {
|
||||
observer.observe(firstGridItem);
|
||||
}
|
||||
return () => observer.unobserve(el);
|
||||
}
|
||||
}, [updateNavStatus]);
|
||||
|
||||
const scrollToIndex = useCallback((index: number) => {
|
||||
if (scrollContainerRef.current) {
|
||||
setActivePage(index);
|
||||
const amount = itemWidth.current * index;
|
||||
scrollContainerRef.current.scrollTo({left: amount});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const scrollToPreviousPage = useCallback(() => {
|
||||
if (scrollContainerRef.current) {
|
||||
const pageWidth = scrollContainerRef.current.clientWidth;
|
||||
const currentScroll = scrollContainerRef.current.scrollLeft;
|
||||
const scrollLeft =
|
||||
!currentScroll && rotate
|
||||
? scrollContainerRef.current.scrollWidth - pageWidth
|
||||
: currentScroll - pageWidth;
|
||||
scrollContainerRef.current.scrollTo({
|
||||
left: scrollLeft,
|
||||
});
|
||||
}
|
||||
}, [rotate]);
|
||||
|
||||
const scrollToNextPage = useCallback(() => {
|
||||
if (scrollContainerRef.current) {
|
||||
const pageWidth = scrollContainerRef.current.clientWidth;
|
||||
const currentScroll = scrollContainerRef.current.scrollLeft;
|
||||
const scrollLeft =
|
||||
rotate &&
|
||||
currentScroll + pageWidth >= scrollContainerRef.current.scrollWidth
|
||||
? 0
|
||||
: (activePage + 1) * pageWidth;
|
||||
scrollContainerRef.current.scrollTo({left: scrollLeft});
|
||||
}
|
||||
}, [activePage, rotate]);
|
||||
|
||||
return {
|
||||
scrollContainerRef,
|
||||
scrollToIndex,
|
||||
scrollToPreviousPage,
|
||||
scrollToNextPage,
|
||||
canScrollForward,
|
||||
canScrollBackward,
|
||||
activePage,
|
||||
containerClassName,
|
||||
itemClassName,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user