generated from muhtadeetaron/nextjs-template
186 lines
4.9 KiB
TypeScript
186 lines
4.9 KiB
TypeScript
import React, {
|
|
useState,
|
|
useRef,
|
|
useEffect,
|
|
useCallback,
|
|
UIEvent,
|
|
} from "react";
|
|
import { GalleryViews } from "@/types/gallery";
|
|
|
|
interface SlidingGalleryProps {
|
|
views: GalleryViews[] | undefined;
|
|
className?: string;
|
|
showPagination?: boolean;
|
|
autoScroll?: boolean;
|
|
autoScrollInterval?: number;
|
|
onSlideChange?: (currentIndex: number) => void;
|
|
height?: string | number;
|
|
}
|
|
|
|
const SlidingGallery = ({
|
|
views,
|
|
className = "",
|
|
showPagination = true,
|
|
autoScroll = false,
|
|
autoScrollInterval = 5000,
|
|
onSlideChange = () => {},
|
|
height = "100vh",
|
|
}: SlidingGalleryProps) => {
|
|
const [activeIdx, setActiveIdx] = useState<number>(0);
|
|
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
|
|
|
const scrollRef = useRef<HTMLDivElement | null>(null);
|
|
const galleryRef = useRef<HTMLDivElement | null>(null);
|
|
const autoScrollRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
const handleScroll = (event: UIEvent<HTMLDivElement>) => {
|
|
handleUserInteraction();
|
|
const scrollLeft = event.currentTarget.scrollLeft;
|
|
const slideWidth = dimensions.width;
|
|
const index = Math.round(scrollLeft / slideWidth);
|
|
|
|
if (index !== activeIdx) {
|
|
setActiveIdx(index);
|
|
onSlideChange(index);
|
|
}
|
|
};
|
|
|
|
const goToSlide = useCallback(
|
|
(index: number) => {
|
|
if (scrollRef.current) {
|
|
scrollRef.current.scrollTo({
|
|
left: index * dimensions.width,
|
|
behavior: "smooth",
|
|
});
|
|
}
|
|
},
|
|
[dimensions.width]
|
|
);
|
|
|
|
const handleDotClick = (index: number) => {
|
|
handleUserInteraction();
|
|
goToSlide(index);
|
|
setActiveIdx(index);
|
|
onSlideChange(index);
|
|
};
|
|
|
|
// Auto-scroll functionality
|
|
useEffect(() => {
|
|
if (autoScroll && views && views.length > 1) {
|
|
autoScrollRef.current = setInterval(() => {
|
|
setActiveIdx((prevIdx) => {
|
|
const nextIdx = (prevIdx + 1) % views.length;
|
|
goToSlide(nextIdx);
|
|
return nextIdx;
|
|
});
|
|
}, autoScrollInterval);
|
|
|
|
return () => {
|
|
if (autoScrollRef.current) {
|
|
clearInterval(autoScrollRef.current);
|
|
}
|
|
};
|
|
}
|
|
}, [autoScroll, autoScrollInterval, views?.length, goToSlide, views]);
|
|
|
|
// Clear auto-scroll on user interaction
|
|
const handleUserInteraction = () => {
|
|
if (autoScrollRef.current) {
|
|
clearInterval(autoScrollRef.current);
|
|
autoScrollRef.current = null;
|
|
}
|
|
};
|
|
|
|
// Update dimensions
|
|
useEffect(() => {
|
|
const updateDimensions = () => {
|
|
if (galleryRef.current) {
|
|
setDimensions({
|
|
width: galleryRef.current.clientWidth,
|
|
height: galleryRef.current.clientHeight,
|
|
});
|
|
}
|
|
};
|
|
|
|
updateDimensions();
|
|
window.addEventListener("resize", updateDimensions);
|
|
return () => window.removeEventListener("resize", updateDimensions);
|
|
}, []);
|
|
|
|
// Recalculate index when dimension changes
|
|
useEffect(() => {
|
|
if (scrollRef.current && dimensions.width > 0) {
|
|
const scrollLeft = scrollRef.current.scrollLeft;
|
|
const slideWidth = dimensions.width;
|
|
const index = Math.round(scrollLeft / slideWidth);
|
|
setActiveIdx(index);
|
|
}
|
|
}, [dimensions]);
|
|
|
|
if (!views || views.length === 0) {
|
|
return (
|
|
<div
|
|
className={`relative w-full h-screen overflow-hidden flex flex-col ${className}`}
|
|
>
|
|
<div className="flex-1 flex items-center justify-center text-slate-400 text-lg">
|
|
<p>No content to display</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={`relative w-full h-screen overflow-hidden flex flex-col ${className}`}
|
|
ref={galleryRef}
|
|
style={{ height }}
|
|
>
|
|
<div
|
|
className="flex-1 flex overflow-x-auto"
|
|
ref={scrollRef}
|
|
onScroll={handleScroll}
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
overflowX: "scroll",
|
|
display: "flex",
|
|
scrollSnapType: "x mandatory",
|
|
scrollbarWidth: "none",
|
|
msOverflowStyle: "none",
|
|
}}
|
|
>
|
|
{views.map((item) => (
|
|
<div
|
|
key={item.id}
|
|
className="min-w-full flex items-center justify-center px-2 box-border"
|
|
style={{
|
|
width: dimensions.width,
|
|
height: "100%",
|
|
flexShrink: 0,
|
|
scrollSnapAlign: "start",
|
|
}}
|
|
>
|
|
{item.content}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{showPagination && views.length > 1 && (
|
|
<div className="absolute bottom-[15px] left-1/2 -translate-x-1/2 flex gap-1.5 z-10">
|
|
{views.map((_, index) => (
|
|
<div
|
|
key={index}
|
|
className={`w-2 h-2 rounded-full transition-all duration-300 ease-in ${
|
|
activeIdx === index ? "bg-[#113768]" : "bg-[#b1d3ff]"
|
|
}`}
|
|
onClick={() => handleDotClick(index)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SlidingGallery;
|