mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
Add initial home route
This commit is contained in:
@@ -0,0 +1,302 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Center } from '@mantine/core';
|
||||||
|
import { RiAlbumFill } from 'react-icons/ri';
|
||||||
|
import { generatePath, useNavigate } from 'react-router';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { SimpleImg } from 'react-simple-img';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Text } from '/@/components/text';
|
||||||
|
import type { LibraryItem, CardRow, CardRoute, Play } from '/@/types';
|
||||||
|
import { Skeleton } from '/@/components/skeleton';
|
||||||
|
import CardControls from '/@/features/shared/components/card-controls';
|
||||||
|
|
||||||
|
const CardWrapper = styled.div<{
|
||||||
|
link?: boolean;
|
||||||
|
}>`
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--card-default-bg);
|
||||||
|
border-radius: var(--card-default-radius);
|
||||||
|
cursor: ${({ link }) => link && 'pointer'};
|
||||||
|
transition: border 0.2s ease-in-out, background 0.2s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--card-default-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover div {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover * {
|
||||||
|
&::before {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 1px solid #fff;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledCard = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: var(--card-default-radius);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ImageSection = styled.div`
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: var(--card-default-radius);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(0deg, rgba(0, 0, 0, 100%) 35%, rgba(0, 0, 0, 0%) 100%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
content: '';
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Image = styled(SimpleImg)`
|
||||||
|
border-radius: var(--card-default-radius);
|
||||||
|
box-shadow: 2px 2px 10px 10px rgba(0, 0, 0, 20%);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ControlsContainer = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 50;
|
||||||
|
width: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DetailSection = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Row = styled.div<{ $secondary?: boolean }>`
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 22px;
|
||||||
|
padding: 0 0.2rem;
|
||||||
|
overflow: hidden;
|
||||||
|
color: ${({ $secondary }) => ($secondary ? 'var(--main-fg-secondary)' : 'var(--main-fg)')};
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
user-select: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface BaseGridCardProps {
|
||||||
|
controls: {
|
||||||
|
cardRows: CardRow[];
|
||||||
|
itemType: LibraryItem;
|
||||||
|
playButtonBehavior: Play;
|
||||||
|
route: CardRoute;
|
||||||
|
};
|
||||||
|
data: any;
|
||||||
|
loading?: boolean;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AlbumCard = ({ loading, size, data, controls }: BaseGridCardProps) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { itemType, cardRows, route } = controls;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<CardWrapper>
|
||||||
|
<StyledCard style={{ alignItems: 'center', display: 'flex' }}>
|
||||||
|
<Skeleton
|
||||||
|
visible
|
||||||
|
height={size}
|
||||||
|
radius="sm"
|
||||||
|
width={size}
|
||||||
|
>
|
||||||
|
<ImageSection />
|
||||||
|
</Skeleton>
|
||||||
|
<DetailSection style={{ width: '100%' }}>
|
||||||
|
{cardRows.map((row: CardRow, index: number) => (
|
||||||
|
<Skeleton
|
||||||
|
visible
|
||||||
|
height={15}
|
||||||
|
my={3}
|
||||||
|
radius="md"
|
||||||
|
width={!data ? (index > 0 ? '50%' : '90%') : '100%'}
|
||||||
|
>
|
||||||
|
<Row />
|
||||||
|
</Skeleton>
|
||||||
|
))}
|
||||||
|
</DetailSection>
|
||||||
|
</StyledCard>
|
||||||
|
</CardWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardWrapper
|
||||||
|
link
|
||||||
|
onClick={() =>
|
||||||
|
navigate(
|
||||||
|
generatePath(
|
||||||
|
route.route,
|
||||||
|
route.slugs?.reduce((acc, slug) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[slug.slugProperty]: data[slug.idProperty],
|
||||||
|
};
|
||||||
|
}, {}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledCard>
|
||||||
|
<ImageSection>
|
||||||
|
{data?.imageUrl ? (
|
||||||
|
<Image
|
||||||
|
animationDuration={0.3}
|
||||||
|
height={size}
|
||||||
|
imgStyle={{ objectFit: 'cover' }}
|
||||||
|
placeholder="var(--card-default-bg)"
|
||||||
|
src={data?.imageUrl}
|
||||||
|
width={size}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Center
|
||||||
|
sx={{
|
||||||
|
background: 'var(--placeholder-bg)',
|
||||||
|
borderRadius: 'var(--card-default-radius)',
|
||||||
|
height: `${size}px`,
|
||||||
|
width: `${size}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RiAlbumFill
|
||||||
|
color="var(--placeholder-fg)"
|
||||||
|
size={35}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
<ControlsContainer>
|
||||||
|
<CardControls
|
||||||
|
itemData={data}
|
||||||
|
itemType={itemType}
|
||||||
|
/>
|
||||||
|
</ControlsContainer>
|
||||||
|
</ImageSection>
|
||||||
|
<DetailSection>
|
||||||
|
{cardRows.map((row: CardRow, index: number) => {
|
||||||
|
if (row.arrayProperty && row.route) {
|
||||||
|
return (
|
||||||
|
<Row
|
||||||
|
key={`row-${row.property}-${index}`}
|
||||||
|
$secondary={index > 0}
|
||||||
|
>
|
||||||
|
{data[row.property].map((item: any, itemIndex: number) => (
|
||||||
|
<React.Fragment key={`${data.id}-${item.id}`}>
|
||||||
|
{itemIndex > 0 && (
|
||||||
|
<Text
|
||||||
|
$noSelect
|
||||||
|
sx={{
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '0 2px 0 1px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
,
|
||||||
|
</Text>
|
||||||
|
)}{' '}
|
||||||
|
<Text
|
||||||
|
$link
|
||||||
|
$noSelect
|
||||||
|
$secondary={index > 0}
|
||||||
|
component={Link}
|
||||||
|
overflow="hidden"
|
||||||
|
size={index > 0 ? 'xs' : 'md'}
|
||||||
|
to={generatePath(
|
||||||
|
row.route!.route,
|
||||||
|
row.route!.slugs?.reduce((acc, slug) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[slug.slugProperty]: data[slug.idProperty],
|
||||||
|
};
|
||||||
|
}, {}),
|
||||||
|
)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{row.arrayProperty && item[row.arrayProperty]}
|
||||||
|
</Text>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.arrayProperty) {
|
||||||
|
return (
|
||||||
|
<Row key={`row-${row.property}`}>
|
||||||
|
{data[row.property].map((item: any) => (
|
||||||
|
<Text
|
||||||
|
key={`${data.id}-${item.id}`}
|
||||||
|
$noSelect
|
||||||
|
$secondary={index > 0}
|
||||||
|
overflow="hidden"
|
||||||
|
size={index > 0 ? 'xs' : 'md'}
|
||||||
|
>
|
||||||
|
{row.arrayProperty && item[row.arrayProperty]}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row key={`row-${row.property}`}>
|
||||||
|
{row.route ? (
|
||||||
|
<Text
|
||||||
|
$link
|
||||||
|
$noSelect
|
||||||
|
component={Link}
|
||||||
|
overflow="hidden"
|
||||||
|
to={generatePath(
|
||||||
|
row.route.route,
|
||||||
|
row.route.slugs?.reduce((acc, slug) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[slug.slugProperty]: data[slug.idProperty],
|
||||||
|
};
|
||||||
|
}, {}),
|
||||||
|
)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{data && data[row.property]}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text
|
||||||
|
$noSelect
|
||||||
|
$secondary={index > 0}
|
||||||
|
overflow="hidden"
|
||||||
|
size={index > 0 ? 'xs' : 'md'}
|
||||||
|
>
|
||||||
|
{data && data[row.property]}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DetailSection>
|
||||||
|
</StyledCard>
|
||||||
|
</CardWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './album-card';
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
import { Group, Stack } from '@mantine/core';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { RiArrowLeftSFill, RiArrowRightSFill } from 'react-icons/ri';
|
||||||
|
import { AlbumCard, Button } from '/@/components';
|
||||||
|
import { AppRoute } from '/@/router/routes';
|
||||||
|
import type { CardRow } from '/@/types';
|
||||||
|
import { LibraryItem, Play } from '/@/types';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
interface GridCarouselProps {
|
||||||
|
cardRows: CardRow[];
|
||||||
|
children: React.ReactElement;
|
||||||
|
containerWidth: number;
|
||||||
|
data: any[] | undefined;
|
||||||
|
loading?: boolean;
|
||||||
|
pagination?: {
|
||||||
|
handleNextPage?: () => void;
|
||||||
|
handlePreviousPage?: () => void;
|
||||||
|
hasNextPage?: boolean;
|
||||||
|
hasPreviousPage?: boolean;
|
||||||
|
itemsPerPage?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const GridCarouselContext = createContext<any>(null);
|
||||||
|
|
||||||
|
export const GridCarousel = ({
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
cardRows,
|
||||||
|
pagination,
|
||||||
|
children,
|
||||||
|
containerWidth,
|
||||||
|
}: GridCarouselProps) => {
|
||||||
|
const gridHeight = (containerWidth * 1.2 - 36) / (pagination?.itemsPerPage || 4);
|
||||||
|
const imageSize = gridHeight * 0.66;
|
||||||
|
const providerValue = { cardRows, data, gridHeight, imageSize, loading, pagination };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridCarouselContext.Provider value={providerValue}>
|
||||||
|
<Stack>
|
||||||
|
{children}
|
||||||
|
{data && (
|
||||||
|
<Carousel
|
||||||
|
cardRows={cardRows}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</GridCarouselContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const GridContainer = styled.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;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Carousel = ({ data, cardRows }: any) => {
|
||||||
|
const { loading, pagination, gridHeight, imageSize } = useContext(GridCarouselContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
animate={{
|
||||||
|
opacity: loading ? 0.5 : 1,
|
||||||
|
scale: loading ? 0.95 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GridContainer
|
||||||
|
height={gridHeight}
|
||||||
|
itemsPerPage={pagination.itemsPerPage}
|
||||||
|
>
|
||||||
|
{data?.map((item: any) => (
|
||||||
|
<AlbumCard
|
||||||
|
controls={{
|
||||||
|
cardRows,
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
|
playButtonBehavior: Play.NOW,
|
||||||
|
route: {
|
||||||
|
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||||
|
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
data={item}
|
||||||
|
loading={loading}
|
||||||
|
size={imageSize}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</GridContainer>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TitleProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Title = ({ children }: TitleProps) => {
|
||||||
|
const { pagination } = useContext(GridCarouselContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group position="apart">
|
||||||
|
{children}
|
||||||
|
<Group>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
disabled={pagination?.hasPreviousPage === false}
|
||||||
|
variant="default"
|
||||||
|
onClick={pagination?.handlePreviousPage}
|
||||||
|
>
|
||||||
|
<RiArrowLeftSFill size={20} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
variant="default"
|
||||||
|
onClick={pagination?.handleNextPage}
|
||||||
|
>
|
||||||
|
<RiArrowRightSFill size={20} />
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
GridCarousel.Title = Title;
|
||||||
|
GridCarousel.Carousel = Carousel;
|
||||||
@@ -23,3 +23,5 @@ export * from './virtual-table';
|
|||||||
export * from './skeleton';
|
export * from './skeleton';
|
||||||
export * from './page-header';
|
export * from './page-header';
|
||||||
export * from './text-title';
|
export * from './text-title';
|
||||||
|
export * from './grid-carousel';
|
||||||
|
export * from './card';
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { useShouldPadTitlebar } from '/@/hooks';
|
import { useShouldPadTitlebar } from '/@/hooks';
|
||||||
|
|
||||||
const Container = styled(motion.div)``;
|
const Container = styled(motion.div)<{ $useOpacity?: boolean; height?: string }>`
|
||||||
|
z-index: 100;
|
||||||
|
width: 100%;
|
||||||
|
height: ${(props) => props.height || '55px'};
|
||||||
|
opacity: ${(props) => props.$useOpacity && 'var(--header-opacity)'};
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
`;
|
||||||
|
|
||||||
const Header = styled(motion.div)<{ $padRight?: boolean }>`
|
const Header = styled(motion.div)<{ $padRight?: boolean }>`
|
||||||
|
height: 100%;
|
||||||
margin-right: ${(props) => props.$padRight && '170px'};
|
margin-right: ${(props) => props.$padRight && '170px'};
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
@@ -16,21 +24,29 @@ const Header = styled(motion.div)<{ $padRight?: boolean }>`
|
|||||||
|
|
||||||
interface PageHeaderProps {
|
interface PageHeaderProps {
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
children: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
height?: string;
|
||||||
|
useOpacity?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PageHeader = ({ backgroundColor, children }: PageHeaderProps) => {
|
export const PageHeader = ({ height, backgroundColor, useOpacity, children }: PageHeaderProps) => {
|
||||||
|
const ref = useRef(null);
|
||||||
const padRight = useShouldPadTitlebar();
|
const padRight = useShouldPadTitlebar();
|
||||||
|
|
||||||
console.log('padRight :>> ', padRight);
|
useEffect(() => {
|
||||||
|
const rootElement = document.querySelector(':root') as HTMLElement;
|
||||||
|
rootElement?.style?.setProperty('--header-opacity', '0');
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
|
ref={ref}
|
||||||
|
$useOpacity={useOpacity}
|
||||||
animate={{
|
animate={{
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
transition: { duration: 1.5 },
|
transition: { duration: 1.5 },
|
||||||
}}
|
}}
|
||||||
initial={{ backgroundColor: 'transparent', color: 'white' }}
|
height={height}
|
||||||
>
|
>
|
||||||
<Header $padRight={padRight}>{children}</Header>
|
<Header $padRight={padRight}>{children}</Header>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
const DashboardRoute = () => {
|
|
||||||
return <></>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DashboardRoute;
|
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
import { Box, Stack } from '@mantine/core';
|
||||||
|
import { AlbumListSort, SortOrder } from '/@/api/types';
|
||||||
|
import { GridCarousel, PageHeader, ScrollArea, TextTitle } from '/@/components';
|
||||||
|
import { useAlbumList } from '/@/features/albums';
|
||||||
|
import { useRecentlyPlayed } from '/@/features/home/queries/recently-played-query';
|
||||||
|
import { AnimatedPage, useContainerQuery } from '/@/features/shared';
|
||||||
|
import { AppRoute } from '/@/router/routes';
|
||||||
|
import { useSetState } from '@mantine/hooks';
|
||||||
|
import { throttle } from 'lodash';
|
||||||
|
|
||||||
|
const HomeRoute = () => {
|
||||||
|
const rootElement = document.querySelector(':root') as HTMLElement;
|
||||||
|
const cq = useContainerQuery();
|
||||||
|
|
||||||
|
const itemsPerPage = cq.isXl ? 9 : cq.isLg ? 7 : cq.isMd ? 5 : cq.isSm ? 4 : 3;
|
||||||
|
const [pagination, setPagination] = useSetState({
|
||||||
|
mostPlayed: 0,
|
||||||
|
random: 0,
|
||||||
|
recentlyAdded: 0,
|
||||||
|
recentlyPlayed: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const random = useAlbumList(
|
||||||
|
{
|
||||||
|
limit: itemsPerPage,
|
||||||
|
sortBy: AlbumListSort.RANDOM,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: pagination.random * itemsPerPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
staleTime: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const recentlyPlayed = useRecentlyPlayed(
|
||||||
|
{
|
||||||
|
limit: itemsPerPage,
|
||||||
|
sortBy: AlbumListSort.RECENTLY_PLAYED,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: pagination.recentlyPlayed * itemsPerPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
staleTime: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const recentlyAdded = useAlbumList(
|
||||||
|
{
|
||||||
|
limit: itemsPerPage,
|
||||||
|
sortBy: AlbumListSort.RECENTLY_ADDED,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: pagination.recentlyAdded * itemsPerPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
staleTime: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const mostPlayed = useAlbumList(
|
||||||
|
{
|
||||||
|
limit: itemsPerPage,
|
||||||
|
sortBy: AlbumListSort.PLAY_COUNT,
|
||||||
|
sortOrder: SortOrder.DESC,
|
||||||
|
startIndex: pagination.mostPlayed * itemsPerPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
staleTime: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNextPage = (key: 'mostPlayed' | 'random' | 'recentlyAdded' | 'recentlyPlayed') => {
|
||||||
|
setPagination({
|
||||||
|
[key]: pagination[key as keyof typeof pagination] + 1,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePreviousPage = (
|
||||||
|
key: 'mostPlayed' | 'random' | 'recentlyAdded' | 'recentlyPlayed',
|
||||||
|
) => {
|
||||||
|
setPagination({
|
||||||
|
[key]: pagination[key as keyof typeof pagination] - 1,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = (position: { x: number; y: number }) => {
|
||||||
|
if (position.y <= 15) {
|
||||||
|
return rootElement?.style?.setProperty('--header-opacity', '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
return rootElement?.style?.setProperty('--header-opacity', '1');
|
||||||
|
};
|
||||||
|
|
||||||
|
const throttledScroll = throttle(handleScroll, 200);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AnimatedPage>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||||
|
<PageHeader
|
||||||
|
useOpacity
|
||||||
|
backgroundColor="var(--sidebar-bg)"
|
||||||
|
/>
|
||||||
|
<ScrollArea
|
||||||
|
mb="1rem"
|
||||||
|
mt="-1.5rem"
|
||||||
|
px="1rem"
|
||||||
|
sx={{
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'auto',
|
||||||
|
}}
|
||||||
|
onScrollPositionChange={throttledScroll}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
ref={cq.ref}
|
||||||
|
sx={{
|
||||||
|
height: '100%',
|
||||||
|
maxWidth: '1920px',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={35}>
|
||||||
|
<GridCarousel
|
||||||
|
cardRows={[
|
||||||
|
{
|
||||||
|
property: 'name',
|
||||||
|
route: {
|
||||||
|
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||||
|
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arrayProperty: 'name',
|
||||||
|
property: 'albumArtists',
|
||||||
|
route: {
|
||||||
|
route: AppRoute.LIBRARY_ALBUMARTISTS_DETAIL,
|
||||||
|
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
containerWidth={cq.width}
|
||||||
|
data={random?.data?.items}
|
||||||
|
loading={random.isLoading || random.isFetching}
|
||||||
|
pagination={{
|
||||||
|
handleNextPage: () => handleNextPage('random'),
|
||||||
|
handlePreviousPage: () => handlePreviousPage('random'),
|
||||||
|
hasPreviousPage: pagination.random > 0,
|
||||||
|
itemsPerPage,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GridCarousel.Title>
|
||||||
|
<TextTitle
|
||||||
|
fw="bold"
|
||||||
|
order={3}
|
||||||
|
>
|
||||||
|
Explore from your library
|
||||||
|
</TextTitle>
|
||||||
|
</GridCarousel.Title>
|
||||||
|
</GridCarousel>
|
||||||
|
<GridCarousel
|
||||||
|
cardRows={[
|
||||||
|
{
|
||||||
|
property: 'name',
|
||||||
|
route: {
|
||||||
|
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||||
|
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arrayProperty: 'name',
|
||||||
|
property: 'albumArtists',
|
||||||
|
route: {
|
||||||
|
route: AppRoute.LIBRARY_ALBUMARTISTS_DETAIL,
|
||||||
|
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
containerWidth={cq.width}
|
||||||
|
data={recentlyPlayed?.data?.items}
|
||||||
|
loading={recentlyPlayed.isLoading || recentlyPlayed.isFetching}
|
||||||
|
pagination={{
|
||||||
|
handleNextPage: () => handleNextPage('recentlyPlayed'),
|
||||||
|
handlePreviousPage: () => handlePreviousPage('recentlyPlayed'),
|
||||||
|
hasPreviousPage: pagination.recentlyPlayed > 0,
|
||||||
|
itemsPerPage,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GridCarousel.Title>
|
||||||
|
<TextTitle
|
||||||
|
fw="bold"
|
||||||
|
order={3}
|
||||||
|
>
|
||||||
|
Recently played
|
||||||
|
</TextTitle>
|
||||||
|
</GridCarousel.Title>
|
||||||
|
</GridCarousel>
|
||||||
|
<GridCarousel
|
||||||
|
cardRows={[
|
||||||
|
{
|
||||||
|
property: 'name',
|
||||||
|
route: {
|
||||||
|
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||||
|
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arrayProperty: 'name',
|
||||||
|
property: 'albumArtists',
|
||||||
|
route: {
|
||||||
|
route: AppRoute.LIBRARY_ALBUMARTISTS_DETAIL,
|
||||||
|
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
containerWidth={cq.width}
|
||||||
|
data={recentlyAdded?.data?.items}
|
||||||
|
loading={recentlyAdded.isLoading || recentlyAdded.isFetching}
|
||||||
|
pagination={{
|
||||||
|
handleNextPage: () => handleNextPage('recentlyAdded'),
|
||||||
|
handlePreviousPage: () => handlePreviousPage('recentlyAdded'),
|
||||||
|
hasPreviousPage: pagination.recentlyAdded > 0,
|
||||||
|
itemsPerPage,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GridCarousel.Title>
|
||||||
|
<TextTitle
|
||||||
|
fw="bold"
|
||||||
|
order={3}
|
||||||
|
>
|
||||||
|
Newly added releases
|
||||||
|
</TextTitle>
|
||||||
|
</GridCarousel.Title>
|
||||||
|
</GridCarousel>
|
||||||
|
<GridCarousel
|
||||||
|
cardRows={[
|
||||||
|
{
|
||||||
|
property: 'name',
|
||||||
|
route: {
|
||||||
|
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||||
|
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arrayProperty: 'name',
|
||||||
|
property: 'albumArtists',
|
||||||
|
route: {
|
||||||
|
route: AppRoute.LIBRARY_ALBUMARTISTS_DETAIL,
|
||||||
|
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
containerWidth={cq.width}
|
||||||
|
data={mostPlayed?.data?.items}
|
||||||
|
loading={mostPlayed.isLoading || mostPlayed.isFetching}
|
||||||
|
pagination={{
|
||||||
|
handleNextPage: () => handleNextPage('mostPlayed'),
|
||||||
|
handlePreviousPage: () => handlePreviousPage('mostPlayed'),
|
||||||
|
hasPreviousPage: pagination.mostPlayed > 0,
|
||||||
|
itemsPerPage,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GridCarousel.Title>
|
||||||
|
<TextTitle
|
||||||
|
fw="bold"
|
||||||
|
order={3}
|
||||||
|
>
|
||||||
|
Most played
|
||||||
|
</TextTitle>
|
||||||
|
</GridCarousel.Title>
|
||||||
|
</GridCarousel>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
|
</AnimatedPage>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HomeRoute;
|
||||||
@@ -11,7 +11,7 @@ import { AppRoute } from './routes';
|
|||||||
import { RouteErrorBoundary } from '/@/features/action-required';
|
import { RouteErrorBoundary } from '/@/features/action-required';
|
||||||
import { TitlebarOutlet } from '/@/router/titlebar-outlet';
|
import { TitlebarOutlet } from '/@/router/titlebar-outlet';
|
||||||
|
|
||||||
const DashboardRoute = lazy(() => import('/@/features/dashboard/routes/DashboardRoute'));
|
const HomeRoute = lazy(() => import('/@/features/home/routes/home-route'));
|
||||||
|
|
||||||
const NowPlayingRoute = lazy(() => import('/@/features/now-playing/routes/now-playing-route'));
|
const NowPlayingRoute = lazy(() => import('/@/features/now-playing/routes/now-playing-route'));
|
||||||
|
|
||||||
@@ -35,10 +35,10 @@ export const AppRouter = () => {
|
|||||||
<Route element={<DefaultLayout />}>
|
<Route element={<DefaultLayout />}>
|
||||||
<Route
|
<Route
|
||||||
index
|
index
|
||||||
element={<DashboardRoute />}
|
element={<HomeRoute />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
element={<DashboardRoute />}
|
element={<HomeRoute />}
|
||||||
path={AppRoute.HOME}
|
path={AppRoute.HOME}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
Reference in New Issue
Block a user