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