feat(dashboard-v2): add Slider component
This commit is contained in:
parent
fad846ee7a
commit
f6496f0358
|
@ -0,0 +1,28 @@
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
export default function Bullets({ visibleSlides, activeIndex, allSlides, changeSlide }) {
|
||||||
|
if (allSlides <= visibleSlides) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-3 pt-6">
|
||||||
|
{Array(allSlides - visibleSlides + 1)
|
||||||
|
.fill(null)
|
||||||
|
.map((_, i) => (
|
||||||
|
<span
|
||||||
|
key={`slider-bullets-${i}`}
|
||||||
|
className={`rounded-full w-3 h-3 ${activeIndex === i ? "bg-primary" : "border-2 cursor-pointer"}`}
|
||||||
|
onClick={() => changeSlide(i)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bullets.propTypes = {
|
||||||
|
allSlides: PropTypes.number.isRequired,
|
||||||
|
activeIndex: PropTypes.number.isRequired,
|
||||||
|
visibleSlides: PropTypes.number.isRequired,
|
||||||
|
changeSlide: PropTypes.func.isRequired,
|
||||||
|
};
|
|
@ -0,0 +1,12 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
const Slide = styled.div.attrs(({ isVisible }) => ({
|
||||||
|
className: `slider-slide transition-opacity ${isVisible ? "" : "opacity-50 cursor-pointer"}`,
|
||||||
|
}))``;
|
||||||
|
|
||||||
|
Slide.propTypes = {
|
||||||
|
isVisible: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Slide;
|
|
@ -0,0 +1,105 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import styled, { css } from "styled-components";
|
||||||
|
import theme from "../../lib/theme";
|
||||||
|
|
||||||
|
import useActiveBreakpoint from "./useActiveBreakpoint";
|
||||||
|
import Bullets from "./Bullets";
|
||||||
|
import Slide from "./Slide";
|
||||||
|
|
||||||
|
const Container = styled.div.attrs({
|
||||||
|
className: "slider w-full",
|
||||||
|
})``;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Styles applied to the movable element when the number of slide elements
|
||||||
|
* exceeds the number of visible slides for the current breakpoint
|
||||||
|
* */
|
||||||
|
const scrollableStyles = css`
|
||||||
|
${({ $allSlides, $visibleSlides, $activeIndex }) => `
|
||||||
|
transform: translateX(calc(-1 * ${$activeIndex} * ((100% + 1rem) / ${$visibleSlides})));
|
||||||
|
grid-template-columns: repeat(${$allSlides}, calc((100% - ${$visibleSlides - 1}rem) / ${$visibleSlides}));
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Scroller = styled.div.attrs({
|
||||||
|
className: "slider-scroller grid gap-4 transition-transform",
|
||||||
|
})`
|
||||||
|
${({ $scrollable }) => ($scrollable ? scrollableStyles : "")}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Slider = ({ slides, breakpoints }) => {
|
||||||
|
const { visibleSlides, scrollable } = useActiveBreakpoint(breakpoints);
|
||||||
|
const [activeIndex, setActiveIndex] = React.useState(0);
|
||||||
|
const changeSlide = React.useCallback(
|
||||||
|
(index) => {
|
||||||
|
setActiveIndex(Math.min(index, slides.length - visibleSlides)); // Don't let it scroll too far
|
||||||
|
},
|
||||||
|
[slides, visibleSlides, setActiveIndex]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Scroller
|
||||||
|
$visibleSlides={visibleSlides}
|
||||||
|
$allSlides={slides.length}
|
||||||
|
$activeIndex={activeIndex}
|
||||||
|
$scrollable={scrollable}
|
||||||
|
>
|
||||||
|
{slides.map((slide, i) => {
|
||||||
|
const isVisible = i >= activeIndex && i < activeIndex + visibleSlides;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={`slide-${i}`}>
|
||||||
|
<Slide
|
||||||
|
isVisible={isVisible || !scrollable}
|
||||||
|
onClick={scrollable && !isVisible ? () => changeSlide(i) : null}
|
||||||
|
>
|
||||||
|
{slide}
|
||||||
|
</Slide>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Scroller>
|
||||||
|
<Bullets
|
||||||
|
activeIndex={activeIndex}
|
||||||
|
allSlides={slides.length}
|
||||||
|
visibleSlides={visibleSlides}
|
||||||
|
changeSlide={changeSlide}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Slider.propTypes = {
|
||||||
|
slides: PropTypes.arrayOf(PropTypes.node.isRequired),
|
||||||
|
breakpoints: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
minWidth: PropTypes.number.isRequired,
|
||||||
|
visibleSlides: PropTypes.number.isRequired,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
Slider.defaultProps = {
|
||||||
|
breakpoints: [
|
||||||
|
{
|
||||||
|
minWidth: parseInt(theme.screens.xl),
|
||||||
|
scrollable: true,
|
||||||
|
visibleSlides: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
minWidth: parseInt(theme.screens.md, 10),
|
||||||
|
scrollable: true,
|
||||||
|
visibleSlides: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// For the smallest screens, we won't scroll but instead stack the slides vertically.
|
||||||
|
minWidth: -Infinity,
|
||||||
|
scrollable: false,
|
||||||
|
visibleSlides: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Slider;
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./Slider";
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { useEffect, useMemo, useCallback, useState } from "react";
|
||||||
|
import { useWindowSize } from "react-use";
|
||||||
|
|
||||||
|
export default function useActiveBreakpoint(breakpoints) {
|
||||||
|
const { width: windowWidth } = useWindowSize();
|
||||||
|
// Since our breakpoints are setup with min-width rule, we need to sort them from largest to smallest
|
||||||
|
const monitoredBreakpoints = useMemo(
|
||||||
|
() => breakpoints.slice().sort(({ minWidth: widthA }, { minWidth: widthB }) => widthB - widthA),
|
||||||
|
[breakpoints]
|
||||||
|
);
|
||||||
|
const findActiveBreakpoint = useCallback(
|
||||||
|
() => monitoredBreakpoints.find((breakpoint) => windowWidth >= breakpoint.minWidth),
|
||||||
|
[monitoredBreakpoints, windowWidth]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [activeBreakpoint, setActiveBreakpoint] = useState(findActiveBreakpoint());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveBreakpoint(findActiveBreakpoint());
|
||||||
|
}, [findActiveBreakpoint]);
|
||||||
|
|
||||||
|
return activeBreakpoint;
|
||||||
|
}
|
Reference in New Issue