You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

146 lines
4.8 KiB

  1. "use client";
  2. import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
  3. import { AnimatePresence, motion, MotionConfig } from "framer-motion";
  4. import Image, { StaticImageData } from "next/image";
  5. import { useState } from "react";
  6. import { useSwipeable } from "react-swipeable";
  7. import { variants } from "@/utils/animationVariants";
  8. import Link from "next/link";
  9. import { Route } from "@/routers/types";
  10. export interface GallerySliderProps {
  11. className?: string;
  12. galleryImgs: (StaticImageData | string)[];
  13. ratioClass?: string;
  14. uniqueID: string;
  15. href?: Route<string>;
  16. imageClass?: string;
  17. galleryClass?: string;
  18. navigation?: boolean;
  19. }
  20. export default function GallerySlider({
  21. className = "",
  22. galleryImgs,
  23. ratioClass = "aspect-w-4 aspect-h-3",
  24. imageClass = "",
  25. uniqueID = "uniqueID",
  26. galleryClass = "rounded-xl",
  27. href = "/listing-stay-detail",
  28. navigation = true,
  29. }: GallerySliderProps) {
  30. const [loaded, setLoaded] = useState(false);
  31. const [index, setIndex] = useState(0);
  32. const [direction, setDirection] = useState(0);
  33. const images = galleryImgs;
  34. function changePhotoId(newVal: number) {
  35. if (newVal > index) {
  36. setDirection(1);
  37. } else {
  38. setDirection(-1);
  39. }
  40. setIndex(newVal);
  41. }
  42. const handlers = useSwipeable({
  43. onSwipedLeft: () => {
  44. if (index < images?.length - 1) {
  45. changePhotoId(index + 1);
  46. }
  47. },
  48. onSwipedRight: () => {
  49. if (index > 0) {
  50. changePhotoId(index - 1);
  51. }
  52. },
  53. trackMouse: true,
  54. });
  55. let currentImage = images[index];
  56. return (
  57. <MotionConfig
  58. transition={{
  59. x: { type: "spring", stiffness: 300, damping: 30 },
  60. opacity: { duration: 0.2 },
  61. }}
  62. >
  63. <div
  64. className={`relative group group/cardGallerySlider ${className}`}
  65. {...handlers}
  66. >
  67. {/* Main image */}
  68. <div className={`w-full overflow-hidden ${galleryClass}`}>
  69. <Link
  70. href={href}
  71. className={`relative flex items-center justify-center ${ratioClass}`}
  72. >
  73. <AnimatePresence initial={false} custom={direction}>
  74. <motion.div
  75. key={index}
  76. custom={direction}
  77. variants={variants(340, 1)}
  78. initial="enter"
  79. animate="center"
  80. exit="exit"
  81. className="absolute inset-0"
  82. >
  83. <Image
  84. src={currentImage || ""}
  85. fill
  86. alt="listing card gallery"
  87. className={`object-cover ${imageClass}`}
  88. onLoadingComplete={() => setLoaded(true)}
  89. sizes="(max-width: 1025px) 100vw, 300px"
  90. />
  91. </motion.div>
  92. </AnimatePresence>
  93. </Link>
  94. </div>
  95. {/* Buttons + bottom nav bar */}
  96. <>
  97. {/* Buttons */}
  98. {loaded && navigation && (
  99. <div className="opacity-0 group-hover/cardGallerySlider:opacity-100 transition-opacity ">
  100. {index > 0 && (
  101. <button
  102. className="absolute w-8 h-8 left-3 top-[calc(50%-16px)] bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-6000 dark:hover:border-neutral-500 rounded-full flex items-center justify-center hover:border-neutral-300 focus:outline-none"
  103. style={{ transform: "translate3d(0, 0, 0)" }}
  104. onClick={() => changePhotoId(index - 1)}
  105. >
  106. <ChevronLeftIcon className="h-4 w-4" />
  107. </button>
  108. )}
  109. {index + 1 < images.length && (
  110. <button
  111. className="absolute w-8 h-8 right-3 top-[calc(50%-16px)] bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-6000 dark:hover:border-neutral-500 rounded-full flex items-center justify-center hover:border-neutral-300 focus:outline-none"
  112. style={{ transform: "translate3d(0, 0, 0)" }}
  113. onClick={() => changePhotoId(index + 1)}
  114. >
  115. <ChevronRightIcon className="h-4 w-4" />
  116. </button>
  117. )}
  118. </div>
  119. )}
  120. {/* Bottom Nav bar */}
  121. <div className="absolute bottom-0 inset-x-0 h-10 bg-gradient-to-t from-neutral-900 opacity-50 rounded-b-lg"></div>
  122. <div className="flex items-center justify-center absolute bottom-2 left-1/2 transform -translate-x-1/2 space-x-1.5">
  123. {images.map((_, i) => (
  124. <button
  125. className={`w-1.5 h-1.5 rounded-full ${
  126. i === index ? "bg-white" : "bg-white/60 "
  127. }`}
  128. onClick={() => changePhotoId(i)}
  129. key={i}
  130. />
  131. ))}
  132. </div>
  133. </>
  134. </div>
  135. </MotionConfig>
  136. );
  137. }