feishin/src/renderer/components/feature-carousel/index.tsx

261 lines
6.8 KiB
TypeScript
Raw Normal View History

2022-12-19 15:59:14 -08:00
import type { MouseEvent } from 'react';
import { useState } from 'react';
import { Group, Image, Stack } from '@mantine/core';
import type { Variants } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';
import { Link, generatePath } from 'react-router-dom';
import styled from 'styled-components';
import { Album, LibraryItem } from '/@/renderer/api/types';
2022-12-19 15:59:14 -08:00
import { Button } from '/@/renderer/components/button';
import { TextTitle } from '/@/renderer/components/text-title';
import { Badge } from '/@/renderer/components/badge';
import { AppRoute } from '/@/renderer/router/routes';
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
import { Play } from '/@/renderer/types';
2022-12-19 15:59:14 -08:00
const Carousel = styled(motion.div)`
position: relative;
height: 35vh;
2022-12-29 17:52:11 -08:00
min-height: 250px;
2022-12-19 15:59:14 -08:00
padding: 2rem;
overflow: hidden;
background: linear-gradient(180deg, var(--main-bg), rgba(25, 26, 28, 60%));
border-radius: 1rem;
2022-12-19 15:59:14 -08:00
`;
const Grid = styled.div`
display: grid;
grid-auto-columns: 1fr;
grid-template-areas: 'image info';
grid-template-rows: 1fr;
grid-template-columns: 200px minmax(0, 1fr);
2022-12-19 15:59:14 -08:00
width: 100%;
max-width: 100%;
height: 100%;
`;
const ImageColumn = styled.div`
z-index: 15;
display: flex;
grid-area: image;
2022-12-29 17:52:11 -08:00
align-items: flex-end;
2022-12-19 15:59:14 -08:00
`;
const InfoColumn = styled.div`
z-index: 15;
display: flex;
grid-area: info;
2022-12-29 17:52:11 -08:00
align-items: flex-end;
2022-12-19 15:59:14 -08:00
width: 100%;
max-width: 100%;
2022-12-29 17:52:11 -08:00
padding-left: 1rem;
2022-12-19 15:59:14 -08:00
`;
const BackgroundImage = styled.img`
position: absolute;
top: 0;
left: 0;
z-index: 0;
width: 150%;
height: 150%;
object-fit: cover;
object-position: 0 30%;
filter: blur(24px);
user-select: none;
`;
const BackgroundImageOverlay = styled.div`
position: absolute;
top: 0;
left: 0;
z-index: 10;
width: 100%;
height: 100%;
background: linear-gradient(180deg, rgba(25, 26, 28, 30%), var(--main-bg));
`;
const Wrapper = styled(Link)`
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
`;
const TitleWrapper = styled.div`
2022-12-29 18:52:37 -08:00
/* stylelint-disable-next-line value-no-vendor-prefix */
display: -webkit-box;
2022-12-29 17:52:11 -08:00
-webkit-line-clamp: 2;
2022-12-19 15:59:14 -08:00
-webkit-box-orient: vertical;
overflow: hidden;
`;
const variants: Variants = {
animate: {
opacity: 1,
transition: { opacity: { duration: 0.5 } },
},
exit: {
opacity: 0,
transition: { opacity: { duration: 0.5 } },
},
initial: {
opacity: 0,
},
};
interface FeatureCarouselProps {
data: Album[] | undefined;
}
export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
const handlePlayQueueAdd = usePlayQueueAdd();
2022-12-19 15:59:14 -08:00
const [itemIndex, setItemIndex] = useState(0);
const [direction, setDirection] = useState(0);
const currentItem = data?.[itemIndex];
const handleNext = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
setDirection(1);
if (itemIndex === (data?.length || 0) - 1 || 0) {
setItemIndex(0);
return;
}
2022-12-19 15:59:14 -08:00
setItemIndex((prev) => prev + 1);
};
const handlePrevious = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
setDirection(-1);
if (itemIndex === 0) {
setItemIndex((data?.length || 0) - 1);
return;
}
2022-12-19 15:59:14 -08:00
setItemIndex((prev) => prev - 1);
};
return (
<Wrapper to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: currentItem?.id || '' })}>
<AnimatePresence
custom={direction}
initial={false}
mode="popLayout"
>
{data && (
<Carousel
key={`image-${itemIndex}`}
animate="animate"
custom={direction}
exit="exit"
initial="initial"
variants={variants}
>
<Grid>
<ImageColumn>
<Image
2022-12-29 17:52:11 -08:00
height={225}
2022-12-19 15:59:14 -08:00
placeholder="var(--card-default-bg)"
radius="md"
2022-12-19 15:59:14 -08:00
src={data[itemIndex]?.imageUrl}
sx={{ objectFit: 'cover' }}
2022-12-29 17:52:11 -08:00
width={225}
2022-12-19 15:59:14 -08:00
/>
</ImageColumn>
<InfoColumn>
<Stack
spacing="md"
sx={{ width: '100%' }}
>
2022-12-19 15:59:14 -08:00
<TitleWrapper>
<TextTitle
lh="5rem"
order={1}
overflow="hidden"
sx={{ fontSize: '4rem' }}
weight={900}
>
{currentItem?.name}
</TextTitle>
2022-12-19 15:59:14 -08:00
</TitleWrapper>
<TitleWrapper>
{currentItem?.albumArtists.slice(0, 1).map((artist) => (
2022-12-19 15:59:14 -08:00
<TextTitle
2022-12-20 19:12:18 -08:00
key={`carousel-artist-${artist.id}`}
order={2}
weight={600}
2022-12-19 15:59:14 -08:00
>
{artist.name}
</TextTitle>
))}
</TitleWrapper>
<Group>
{currentItem?.genres?.slice(0, 1).map((genre) => (
<Badge
key={`carousel-genre-${genre.id}`}
size="lg"
>
{genre.name}
</Badge>
2022-12-19 15:59:14 -08:00
))}
<Badge size="lg">{currentItem?.releaseYear}</Badge>
<Badge size="lg">{currentItem?.songCount} tracks</Badge>
2022-12-19 15:59:14 -08:00
</Group>
</Stack>
</InfoColumn>
</Grid>
<BackgroundImage
draggable="false"
src={currentItem?.imageUrl || undefined}
/>
<BackgroundImageOverlay />
</Carousel>
)}
</AnimatePresence>
<Group
spacing="sm"
sx={{ bottom: '1rem', position: 'absolute', right: '1rem', zIndex: 20 }}
2022-12-19 15:59:14 -08:00
>
<Button
radius="lg"
size="md"
variant="outline"
2022-12-19 15:59:14 -08:00
onClick={handlePrevious}
>
<RiArrowLeftSLine size="2rem" />
2022-12-19 15:59:14 -08:00
</Button>
<Button
radius="lg"
size="md"
variant="outline"
2022-12-19 15:59:14 -08:00
onClick={handleNext}
>
<RiArrowRightSLine size="2rem" />
2022-12-19 15:59:14 -08:00
</Button>
<Button
radius="lg"
size="md"
variant="outline"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (!currentItem) return;
handlePlayQueueAdd?.({
byItemType: {
id: [currentItem.id],
type: LibraryItem.ALBUM,
},
playType: Play.NOW,
});
}}
>
Play
</Button>
2022-12-19 15:59:14 -08:00
</Group>
</Wrapper>
);
};