init commit,
This commit is contained in:
266
03_source/frontend/src/sections/home/home-highlight-features.tsx
Normal file
266
03_source/frontend/src/sections/home/home-highlight-features.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
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>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user