Add swiper carousel component
This commit is contained in:
parent
89afa9b836
commit
d8130f48e2
2 changed files with 268 additions and 215 deletions
|
@ -1,227 +1,281 @@
|
||||||
import { createContext, useContext, useState, useCallback, useMemo } from 'react';
|
import { useCallback, ReactNode, useRef, useState, isValidElement } from 'react';
|
||||||
import { Group, Stack } from '@mantine/core';
|
import { Box, Group } from '@mantine/core';
|
||||||
import type { Variants } from 'framer-motion';
|
import { useElementSize } from '@mantine/hooks';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';
|
import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';
|
||||||
|
import { Virtual, SwiperOptions } from 'swiper';
|
||||||
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
|
import { Swiper as SwiperCore } from 'swiper/types';
|
||||||
|
import { PosterCard } from '/@/renderer/components/card/poster-card';
|
||||||
|
import { Album, AlbumArtist, Artist, LibraryItem, RelatedArtist } from '/@/renderer/api/types';
|
||||||
|
import { CardRoute, CardRow } from '/@/renderer/types';
|
||||||
|
import { TextTitle } from '/@/renderer/components/text-title';
|
||||||
import { Button } from '/@/renderer/components/button';
|
import { Button } from '/@/renderer/components/button';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { usePlayButtonBehavior } from '/@/renderer/store';
|
||||||
import type { CardRow } from '/@/renderer/types';
|
import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
|
||||||
import { Play } from '/@/renderer/types';
|
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||||
import styled from 'styled-components';
|
import { MotionStack } from '/@/renderer/components/motion';
|
||||||
import { AlbumCard } from '/@/renderer/components/card';
|
import 'swiper/css';
|
||||||
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
|
|
||||||
import { LibraryItem } from '/@/renderer/api/types';
|
|
||||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
|
||||||
|
|
||||||
interface GridCarouselProps {
|
|
||||||
cardRows: CardRow<any>[];
|
|
||||||
children: React.ReactElement;
|
|
||||||
containerWidth: number;
|
|
||||||
data: any[] | undefined;
|
|
||||||
itemType: LibraryItem;
|
|
||||||
loading?: boolean;
|
|
||||||
pagination?: {
|
|
||||||
handleNextPage?: () => void;
|
|
||||||
handlePreviousPage?: () => void;
|
|
||||||
hasNextPage?: boolean;
|
|
||||||
hasPreviousPage?: boolean;
|
|
||||||
itemsPerPage?: number;
|
|
||||||
};
|
|
||||||
uniqueId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GridCarouselContext = createContext<any>({});
|
|
||||||
|
|
||||||
const GridContainer = styled(motion.div)<{ height: number; itemsPerPage: number }>`
|
|
||||||
display: grid;
|
|
||||||
grid-auto-rows: 0;
|
|
||||||
grid-gap: 18px;
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
grid-template-columns: repeat(${(props) => props.itemsPerPage || 4}, minmax(0, 1fr));
|
|
||||||
height: ${(props) => props.height}px;
|
|
||||||
overflow: hidden;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const variants: Variants = {
|
|
||||||
animate: (custom: { direction: number; loading: boolean }) => {
|
|
||||||
return {
|
|
||||||
opacity: custom.loading ? 0.5 : 1,
|
|
||||||
scale: custom.loading ? 0.95 : 1,
|
|
||||||
transition: {
|
|
||||||
opacity: { duration: 0.2 },
|
|
||||||
x: { damping: 30, stiffness: 300, type: 'spring' },
|
|
||||||
},
|
|
||||||
x: 0,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
exit: (custom: { direction: number; loading: boolean }) => {
|
|
||||||
return {
|
|
||||||
opacity: 0,
|
|
||||||
transition: {
|
|
||||||
opacity: { duration: 0.2 },
|
|
||||||
x: { damping: 30, stiffness: 300, type: 'spring' },
|
|
||||||
},
|
|
||||||
x: custom.direction > 0 ? -1000 : 1000,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
initial: (custom: { direction: number; loading: boolean }) => {
|
|
||||||
return {
|
|
||||||
opacity: 0,
|
|
||||||
x: custom.direction > 0 ? 1000 : -1000,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const Carousel = ({ data, cardRows }: any) => {
|
|
||||||
const { loading, pagination, gridHeight, imageSize, direction, uniqueId, itemType } =
|
|
||||||
useContext(GridCarouselContext);
|
|
||||||
|
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
|
||||||
|
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<AnimatePresence
|
|
||||||
custom={{ direction, loading }}
|
|
||||||
initial={false}
|
|
||||||
mode="popLayout"
|
|
||||||
>
|
|
||||||
<GridContainer
|
|
||||||
key={`carousel-${uniqueId}-${data[0]?.id}`}
|
|
||||||
animate="animate"
|
|
||||||
custom={{ direction, loading }}
|
|
||||||
exit="exit"
|
|
||||||
height={gridHeight}
|
|
||||||
initial="initial"
|
|
||||||
itemsPerPage={pagination.itemsPerPage}
|
|
||||||
variants={variants}
|
|
||||||
>
|
|
||||||
{data?.map((item: any, index: number) => (
|
|
||||||
<AlbumCard
|
|
||||||
key={`card-${uniqueId}-${index}`}
|
|
||||||
controls={{
|
|
||||||
cardRows,
|
|
||||||
itemType: itemType || LibraryItem.ALBUM,
|
|
||||||
playButtonBehavior: playButtonBehavior || Play.NOW,
|
|
||||||
route: cardRows[0]?.route || {
|
|
||||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
|
||||||
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
data={item}
|
|
||||||
handlePlayQueueAdd={handlePlayQueueAdd}
|
|
||||||
size={imageSize}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</GridContainer>
|
|
||||||
</AnimatePresence>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GridCarousel = ({
|
|
||||||
data,
|
|
||||||
loading,
|
|
||||||
cardRows,
|
|
||||||
pagination,
|
|
||||||
children,
|
|
||||||
containerWidth,
|
|
||||||
uniqueId,
|
|
||||||
itemType,
|
|
||||||
}: GridCarouselProps) => {
|
|
||||||
const [direction, setDirection] = useState(0);
|
|
||||||
|
|
||||||
const gridHeight = useMemo(
|
|
||||||
() => (containerWidth * 1.2 - 36) / (pagination?.itemsPerPage || 4),
|
|
||||||
[containerWidth, pagination?.itemsPerPage],
|
|
||||||
);
|
|
||||||
|
|
||||||
const imageSize = useMemo(() => gridHeight * 0.66, [gridHeight]);
|
|
||||||
|
|
||||||
const providerValue = useMemo(
|
|
||||||
() => ({
|
|
||||||
cardRows,
|
|
||||||
data,
|
|
||||||
direction,
|
|
||||||
gridHeight,
|
|
||||||
imageSize,
|
|
||||||
itemType,
|
|
||||||
loading,
|
|
||||||
pagination,
|
|
||||||
setDirection,
|
|
||||||
uniqueId,
|
|
||||||
}),
|
|
||||||
[cardRows, data, direction, gridHeight, imageSize, itemType, loading, pagination, uniqueId],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GridCarouselContext.Provider value={providerValue}>
|
|
||||||
<Stack>
|
|
||||||
{children}
|
|
||||||
{data && (
|
|
||||||
<Carousel
|
|
||||||
cardRows={cardRows}
|
|
||||||
data={data}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</GridCarouselContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface TitleProps {
|
interface TitleProps {
|
||||||
children?: React.ReactNode;
|
handleNext?: () => void;
|
||||||
|
handlePrev?: () => void;
|
||||||
|
label?: string | ReactNode;
|
||||||
|
pagination: {
|
||||||
|
hasNextPage: boolean;
|
||||||
|
hasPreviousPage: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const Title = ({ children }: TitleProps) => {
|
const Title = ({ label, handleNext, handlePrev, pagination }: TitleProps) => {
|
||||||
const { pagination, setDirection } = useContext(GridCarouselContext);
|
|
||||||
const showPaginationButtons = pagination?.handleNextPage && pagination?.handlePreviousPage;
|
|
||||||
|
|
||||||
const handleNextPage = useCallback(() => {
|
|
||||||
setDirection(1);
|
|
||||||
pagination?.handleNextPage?.();
|
|
||||||
}, [pagination, setDirection]);
|
|
||||||
|
|
||||||
const handlePreviousPage = useCallback(() => {
|
|
||||||
setDirection(-1);
|
|
||||||
pagination?.handlePreviousPage?.();
|
|
||||||
}, [pagination, setDirection]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group position="apart">
|
<Group position="apart">
|
||||||
{children}
|
{isValidElement(label) ? (
|
||||||
{showPaginationButtons && (
|
label
|
||||||
<Group spacing="sm">
|
) : (
|
||||||
<Button
|
<TextTitle
|
||||||
compact
|
order={2}
|
||||||
disabled={!pagination?.hasPreviousPage}
|
weight={700}
|
||||||
size="md"
|
>
|
||||||
variant="default"
|
{label}
|
||||||
onClick={handlePreviousPage}
|
</TextTitle>
|
||||||
>
|
|
||||||
<RiArrowLeftSLine size={15} />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
compact
|
|
||||||
size="md"
|
|
||||||
variant="default"
|
|
||||||
onClick={handleNextPage}
|
|
||||||
>
|
|
||||||
<RiArrowRightSLine size={15} />
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Group spacing="sm">
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
disabled={!pagination.hasPreviousPage}
|
||||||
|
size="lg"
|
||||||
|
variant="default"
|
||||||
|
onClick={handlePrev}
|
||||||
|
>
|
||||||
|
<RiArrowLeftSLine />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
disabled={!pagination.hasNextPage}
|
||||||
|
size="lg"
|
||||||
|
variant="default"
|
||||||
|
onClick={handleNext}
|
||||||
|
>
|
||||||
|
<RiArrowRightSLine />
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
GridCarousel.Title = Title;
|
interface SwiperGridCarouselProps {
|
||||||
GridCarousel.Carousel = Carousel;
|
cardRows: CardRow<Album>[] | CardRow<Artist>[] | CardRow<AlbumArtist>[];
|
||||||
|
data: Album[] | AlbumArtist[] | Artist[] | RelatedArtist[] | undefined;
|
||||||
|
isLoading?: boolean;
|
||||||
|
itemType: LibraryItem;
|
||||||
|
route: CardRoute;
|
||||||
|
swiperProps?: SwiperOptions;
|
||||||
|
title?: {
|
||||||
|
children?: ReactNode;
|
||||||
|
hasPagination?: boolean;
|
||||||
|
icon?: ReactNode;
|
||||||
|
label: string | ReactNode;
|
||||||
|
};
|
||||||
|
uniqueId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variants = {
|
||||||
|
hidden: {
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
show: {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SwiperGridCarousel = ({
|
||||||
|
cardRows,
|
||||||
|
data,
|
||||||
|
itemType,
|
||||||
|
route,
|
||||||
|
swiperProps,
|
||||||
|
title,
|
||||||
|
isLoading,
|
||||||
|
uniqueId,
|
||||||
|
}: SwiperGridCarouselProps) => {
|
||||||
|
const { ref, width } = useElementSize();
|
||||||
|
const swiperRef = useRef<SwiperCore | any>(null);
|
||||||
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
|
|
||||||
|
const slidesPerView = width > 1500 ? 9 : width > 1200 ? 6 : width > 768 ? 5 : width > 600 ? 3 : 2;
|
||||||
|
|
||||||
|
const [pagination, setPagination] = useState({
|
||||||
|
hasNextPage: (data?.length || 0) > Math.round(slidesPerView),
|
||||||
|
hasPreviousPage: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createFavoriteMutation = useCreateFavorite({});
|
||||||
|
const deleteFavoriteMutation = useDeleteFavorite({});
|
||||||
|
|
||||||
|
const handleFavorite = useCallback(
|
||||||
|
(options: { id: string[]; isFavorite: boolean; itemType: LibraryItem; serverId: string }) => {
|
||||||
|
const { id, itemType, isFavorite, serverId } = options;
|
||||||
|
if (isFavorite) {
|
||||||
|
deleteFavoriteMutation.mutate({
|
||||||
|
query: {
|
||||||
|
id,
|
||||||
|
type: itemType,
|
||||||
|
},
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createFavoriteMutation.mutate({
|
||||||
|
query: {
|
||||||
|
id,
|
||||||
|
type: itemType,
|
||||||
|
},
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[createFavoriteMutation, deleteFavoriteMutation],
|
||||||
|
);
|
||||||
|
|
||||||
|
const slides = data
|
||||||
|
? data.map((el) => (
|
||||||
|
<PosterCard
|
||||||
|
controls={{
|
||||||
|
cardRows,
|
||||||
|
handleFavorite,
|
||||||
|
handlePlayQueueAdd,
|
||||||
|
itemType,
|
||||||
|
playButtonBehavior,
|
||||||
|
route,
|
||||||
|
}}
|
||||||
|
data={el}
|
||||||
|
isLoading={isLoading}
|
||||||
|
uniqueId={uniqueId}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
: Array.from(Array(10).keys()).map((el) => (
|
||||||
|
<PosterCard
|
||||||
|
controls={{
|
||||||
|
cardRows,
|
||||||
|
handleFavorite,
|
||||||
|
handlePlayQueueAdd,
|
||||||
|
itemType,
|
||||||
|
playButtonBehavior,
|
||||||
|
route,
|
||||||
|
}}
|
||||||
|
data={el}
|
||||||
|
isLoading={isLoading}
|
||||||
|
uniqueId={uniqueId}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
const handleNext = useCallback(() => {
|
||||||
|
const activeIndex = swiperRef?.current?.activeIndex || 0;
|
||||||
|
const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || 5));
|
||||||
|
swiperRef?.current?.slideTo(activeIndex + slidesPerView);
|
||||||
|
}, [swiperProps?.slidesPerView]);
|
||||||
|
|
||||||
|
const handlePrev = useCallback(() => {
|
||||||
|
const activeIndex = swiperRef?.current?.activeIndex || 0;
|
||||||
|
const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || 5));
|
||||||
|
swiperRef?.current?.slideTo(activeIndex - slidesPerView);
|
||||||
|
}, [swiperProps?.slidesPerView]);
|
||||||
|
|
||||||
|
const handleOnSlideChange = useCallback(
|
||||||
|
(e: SwiperCore) => {
|
||||||
|
const { slides, isEnd, isBeginning } = e;
|
||||||
|
if (isEnd || isBeginning) return;
|
||||||
|
|
||||||
|
setPagination({
|
||||||
|
hasNextPage: slidesPerView < slides.length,
|
||||||
|
hasPreviousPage: slidesPerView < slides.length,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[slidesPerView],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOnReachEnd = useCallback(
|
||||||
|
(e: SwiperCore) => {
|
||||||
|
const { slides } = e;
|
||||||
|
|
||||||
|
setPagination({
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPreviousPage: slidesPerView < slides.length,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[slidesPerView],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOnReachBeginning = useCallback(
|
||||||
|
(e: SwiperCore) => {
|
||||||
|
const { slides } = e;
|
||||||
|
|
||||||
|
setPagination({
|
||||||
|
hasNextPage: slidesPerView < slides.length,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[slidesPerView],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence
|
||||||
|
initial
|
||||||
|
mode="sync"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
ref={ref}
|
||||||
|
className="grid-carousel"
|
||||||
|
>
|
||||||
|
{width ? (
|
||||||
|
<MotionStack
|
||||||
|
animate="show"
|
||||||
|
initial="hidden"
|
||||||
|
spacing="md"
|
||||||
|
variants={variants}
|
||||||
|
>
|
||||||
|
{title && (
|
||||||
|
<Title
|
||||||
|
{...title}
|
||||||
|
handleNext={handleNext}
|
||||||
|
handlePrev={handlePrev}
|
||||||
|
pagination={pagination}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Swiper
|
||||||
|
ref={swiperRef}
|
||||||
|
grid={{
|
||||||
|
fill: 'column',
|
||||||
|
rows: 2,
|
||||||
|
}}
|
||||||
|
modules={[Virtual]}
|
||||||
|
slidesPerView={swiperProps?.slidesPerView || slidesPerView || 5}
|
||||||
|
spaceBetween={20}
|
||||||
|
style={{ height: '100%', width: '100%' }}
|
||||||
|
onBeforeInit={(swiper) => {
|
||||||
|
swiperRef.current = swiper;
|
||||||
|
}}
|
||||||
|
onReachBeginning={handleOnReachBeginning}
|
||||||
|
onReachEnd={handleOnReachEnd}
|
||||||
|
onSlideChange={handleOnSlideChange}
|
||||||
|
{...swiperProps}
|
||||||
|
>
|
||||||
|
{slides.map((slideContent, index) => {
|
||||||
|
return (
|
||||||
|
<SwiperSlide
|
||||||
|
key={`${uniqueId}-${slideContent?.props?.data?.id}-${index}`}
|
||||||
|
virtualIndex={index}
|
||||||
|
>
|
||||||
|
{slideContent}
|
||||||
|
</SwiperSlide>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Swiper>
|
||||||
|
</MotionStack>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -6,7 +6,6 @@ export * from './card';
|
||||||
export * from './date-picker';
|
export * from './date-picker';
|
||||||
export * from './dropdown-menu';
|
export * from './dropdown-menu';
|
||||||
export * from './feature-carousel';
|
export * from './feature-carousel';
|
||||||
export * from './grid-carousel';
|
|
||||||
export * from './input';
|
export * from './input';
|
||||||
export * from './modal';
|
export * from './modal';
|
||||||
export * from './page-header';
|
export * from './page-header';
|
||||||
|
|
Reference in a new issue