Files
HKSingleParty/03_source/frontend/src/sections/home/home-highlight-features.tsx
2025-05-28 09:55:51 +08:00

267 lines
8.5 KiB
TypeScript

import type { BoxProps } from '@mui/material/Box';
import type { UseClientRectReturn } from 'minimal-shared/hooks';
import { useRef, useState } from 'react';
import { varAlpha } from 'minimal-shared/utils';
import { useClientRect } from 'minimal-shared/hooks';
import { m, useSpring, useScroll, useTransform, useMotionValueEvent } from 'framer-motion';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import SvgIcon from '@mui/material/SvgIcon';
import Container from '@mui/material/Container';
import Typography from '@mui/material/Typography';
import { styled, useTheme } from '@mui/material/styles';
import { CONFIG } from 'src/global-config';
import { primaryColorPresets } from 'src/theme/with-settings';
import { Iconify } from 'src/components/iconify';
import { varFade, MotionViewport } from 'src/components/animate';
import { SectionTitle } from './components/section-title';
import { FloatLine, FloatPlusIcon } from './components/svg-elements';
// ----------------------------------------------------------------------
const renderLines = () => (
<>
<FloatPlusIcon sx={{ top: 72, left: 72 }} />
<FloatLine sx={{ top: 80, left: 0 }} />
<FloatLine vertical sx={{ top: 0, left: 80 }} />
</>
);
export function HomeHighlightFeatures({ sx, ...other }: BoxProps) {
const containerRoot = useClientRect<HTMLDivElement>();
return (
<Box
component="section"
sx={[
{
position: 'relative',
pt: { xs: 10, md: 20 },
},
...(Array.isArray(sx) ? sx : [sx]),
]}
{...other}
>
<MotionViewport>
{renderLines()}
<Container>
<Stack
ref={containerRoot.elementRef}
spacing={5}
sx={[
{
textAlign: { xs: 'center', md: 'left' },
alignItems: { xs: 'center', md: 'flex-start' },
},
]}
>
<SectionTitle caption="App Features" title="Highlight" txtGradient="features" />
<SvgIcon
component={m.svg}
variants={varFade('inDown', { distance: 24 })}
sx={[{ width: 28, height: 28, color: 'grey.500' }]}
>
<path
d="M13.9999 6.75956L7.74031 0.5H20.2594L13.9999 6.75956Z"
fill="currentColor"
opacity={0.12}
/>
<path
d="M13.9998 23.8264L2.14021 11.9668H25.8593L13.9998 23.8264Z"
fill="currentColor"
opacity={0.24}
/>
</SvgIcon>
</Stack>
</Container>
</MotionViewport>
<ScrollableContent containerRoot={containerRoot} />
</Box>
);
}
// ----------------------------------------------------------------------
const ITEMS = [
{
title: 'Dark mode',
subtitle: 'A dark theme that feels easier on the eyes.',
icon: 'solar:cloudy-moon-bold-duotone',
imgUrl: [`${CONFIG.assetsDir}/assets/images/home/highlight-darkmode.webp`],
},
{
title: 'Color presets',
subtitle: 'Express your own style with just one click.',
icon: 'solar:pallete-2-bold-duotone',
imgUrl: [
`${CONFIG.assetsDir}/assets/images/home/highlight-presets-1.webp`,
`${CONFIG.assetsDir}/assets/images/home/highlight-presets-2.webp`,
`${CONFIG.assetsDir}/assets/images/home/highlight-presets-3.webp`,
`${CONFIG.assetsDir}/assets/images/home/highlight-presets-4.webp`,
`${CONFIG.assetsDir}/assets/images/home/highlight-presets-5.webp`,
],
},
{
title: 'Right-to-left',
subtitle: 'Support languages such as Arabic, Persian, and Hebrew.',
icon: 'solar:align-right-bold-duotone',
imgUrl: [`${CONFIG.assetsDir}/assets/images/home/highlight-rtl.webp`],
},
] as const;
type ScrollContentProps = {
containerRoot: UseClientRectReturn;
};
function ScrollableContent({ containerRoot }: ScrollContentProps) {
const theme = useTheme();
const isRtl = theme.direction === 'rtl';
const containerRef = useRef(null);
const containeRect = useClientRect(containerRef);
const scrollRef = useRef(null);
const scrollRect = useClientRect(scrollRef);
const { scrollYProgress } = useScroll({ target: containerRef });
const [startScroll, setStartScroll] = useState(false);
const physics = { damping: 16, mass: 0.12, stiffness: 80 };
const scrollRange = (-scrollRect.scrollWidth + containeRect.width / 2) * (isRtl ? -1 : 1);
const x = useSpring(useTransform(scrollYProgress, [0, 1], [0, scrollRange]), physics);
const background = useTransform(
scrollYProgress,
[0, 0.12, 0.28, 0.48, 0.58, 0.62, 0.72, 0.92],
[
`transparent`,
`linear-gradient(180deg, ${theme.palette.primary.light}, ${theme.palette.primary.dark})`,
`linear-gradient(180deg, ${primaryColorPresets.preset1.light}, ${primaryColorPresets.preset1.dark})`,
`linear-gradient(180deg, ${primaryColorPresets.preset2.light}, ${primaryColorPresets.preset2.dark})`,
`linear-gradient(180deg, ${primaryColorPresets.preset3.light}, ${primaryColorPresets.preset3.dark})`,
`linear-gradient(180deg, ${primaryColorPresets.preset4.light}, ${primaryColorPresets.preset4.dark})`,
`linear-gradient(180deg, ${primaryColorPresets.preset5.light}, ${primaryColorPresets.preset5.dark})`,
`linear-gradient(180deg, ${theme.palette.background.neutral}, ${theme.palette.background.neutral})`,
]
);
useMotionValueEvent(scrollYProgress, 'change', (latest) => {
if (latest !== 0 && latest !== 1) {
setStartScroll(true);
} else {
setStartScroll(false);
}
});
return (
<ScrollRoot ref={containerRef} sx={{ height: scrollRect.scrollWidth, minHeight: '100vh' }}>
<ScrollContainer style={{ background }} data-scrolling={startScroll}>
<ScrollContent
ref={scrollRef}
style={{ x }}
layout
sx={{ ml: `${containerRoot.left}px` }}
transition={{ ease: 'linear', duration: 0.25 }}
>
{ITEMS.map((item) => (
<Item key={item.title} item={item} />
))}
</ScrollContent>
</ScrollContainer>
</ScrollRoot>
);
}
// ----------------------------------------------------------------------
const ScrollRoot = styled(m.div)(({ theme }) => ({
zIndex: 9,
position: 'relative',
paddingTop: theme.spacing(5),
[theme.breakpoints.up('md')]: { paddingTop: theme.spacing(8) },
}));
const ScrollContainer = styled(m.div)(({ theme }) => ({
top: 0,
height: '100vh',
display: 'flex',
position: 'sticky',
overflow: 'hidden',
flexDirection: 'column',
alignItems: 'flex-start',
transition: theme.transitions.create(['background-color']),
'&[data-scrolling="true"]': { justifyContent: 'center' },
}));
const ScrollContent = styled(m.div)(({ theme }) => ({
display: 'flex',
gap: theme.spacing(5),
paddingLeft: theme.spacing(3),
transition: theme.transitions.create(['margin-left', 'margin-top']),
[theme.breakpoints.up('md')]: { gap: theme.spacing(8), paddingLeft: theme.spacing(0) },
}));
// ----------------------------------------------------------------------
type ItemProps = BoxProps & {
item: (typeof ITEMS)[number];
};
function Item({ item, sx, ...other }: ItemProps) {
return (
<Box sx={[{ flexShrink: 0 }, ...(Array.isArray(sx) ? sx : [sx])]} {...other}>
<Box sx={{ mb: 6, gap: 2, display: 'flex' }}>
<Iconify width={28} icon={item.icon} sx={{ mt: '10px' }} />
<Stack spacing={2}>
<Typography variant="h3">{item.title}</Typography>
<Typography sx={{ color: 'text.secondary' }}>{item.subtitle}</Typography>
</Stack>
</Box>
<Box sx={{ display: 'flex', gap: { xs: 5, md: 8 } }}>
{item.imgUrl.map((url) => (
<Box
key={url}
sx={[
(theme) => ({
borderRadius: 2,
overflow: 'hidden',
boxShadow: `-40px 40px 80px 0px ${varAlpha(theme.vars.palette.grey['500Channel'], 0.16)}`,
...theme.applyStyles('dark', {
boxShadow: `-40px 40px 80px 0px ${varAlpha(theme.vars.palette.common.blackChannel, 0.16)}`,
}),
}),
]}
>
<Box
component="img"
alt={url}
src={url}
sx={{
width: {
xs: 480,
sm: 640,
md: 800,
lg: 1140,
xl: 1280,
},
}}
/>
</Box>
))}
</Box>
</Box>
);
}