feat(platform): fix carousel on store page (#9230)
- resolves #8973 Adding smooth scrolling and solving some weird interaction on carousal ### Changes - Update `CarouselPrevious`, `CarouselPrevious` and add `CarouselIndicator` in `carousel.tsx` - Add `CarouselPrevious`, `CarouselPrevious` and `CarouselIndicator` support in `FeaturedSection.tsx` ### Demo https://github.com/user-attachments/assets/ba9a22fa-ddf2-469f-ba8a-aee1a7fc5f78pull/9234/head^2
parent
32c908ae13
commit
9c702516fd
|
@ -6,9 +6,11 @@ import {
|
|||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
CarouselIndicator,
|
||||
} from "@/components/ui/carousel";
|
||||
import { useCallback, useState } from "react";
|
||||
import { IconLeftArrow, IconRightArrow } from "@/components/ui/icons";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const BACKGROUND_COLORS = [
|
||||
|
@ -63,27 +65,24 @@ export const FeaturedSection: React.FC<FeaturedSectionProps> = ({
|
|||
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center justify-center">
|
||||
<div className="w-full">
|
||||
<h2 className="font-poppins mb-8 text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
|
||||
<div className="w-[99vw]">
|
||||
<h2 className="font-poppins mx-auto mb-8 max-w-[1360px] px-4 text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
|
||||
Featured agents
|
||||
</h2>
|
||||
|
||||
<div>
|
||||
<div className="w-[99vw] pb-[60px]">
|
||||
<Carousel
|
||||
className="mx-auto pb-10"
|
||||
opts={{
|
||||
loop: true,
|
||||
startIndex: currentSlide,
|
||||
duration: 500,
|
||||
align: "start",
|
||||
align: "center",
|
||||
containScroll: "trimSnaps",
|
||||
}}
|
||||
className="w-full overflow-x-hidden"
|
||||
>
|
||||
<CarouselContent className="transition-transform duration-500">
|
||||
<CarouselContent className="ml-[calc(50vw-690px)]">
|
||||
{featuredAgents.map((agent, index) => (
|
||||
<CarouselItem
|
||||
key={index}
|
||||
className="max-w-[460px] flex-[0_0_auto] pr-8"
|
||||
className="max-w-[460px] flex-[0_0_auto]"
|
||||
>
|
||||
<FeaturedStoreCard
|
||||
agentName={agent.agent_name}
|
||||
|
@ -99,37 +98,13 @@ export const FeaturedSection: React.FC<FeaturedSectionProps> = ({
|
|||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<div className="relative mx-auto w-full max-w-[1360px] pl-4">
|
||||
<CarouselIndicator />
|
||||
<CarouselPrevious afterClick={handlePrevSlide} />
|
||||
<CarouselNext afterClick={handleNextSlide} />
|
||||
</div>
|
||||
</Carousel>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex w-full items-center justify-between">
|
||||
<div className="flex h-3 items-center gap-2">
|
||||
{featuredAgents.map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`${
|
||||
currentSlide === index
|
||||
? "h-3 w-[52px] rounded-[39px] bg-neutral-800 transition-all duration-500 dark:bg-neutral-200"
|
||||
: "h-3 w-3 rounded-full bg-neutral-300 transition-all duration-500 dark:bg-neutral-600"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="mb-[60px] flex items-center gap-3">
|
||||
<button
|
||||
onClick={handlePrevSlide}
|
||||
className="mb:h-12 mb:w-12 flex h-10 w-10 items-center justify-center rounded-full border border-neutral-400 bg-white dark:border-neutral-600 dark:bg-neutral-800"
|
||||
>
|
||||
<IconLeftArrow className="h-8 w-8 text-neutral-800 dark:text-neutral-200" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleNextSlide}
|
||||
className="mb:h-12 mb:w-12 flex h-10 w-10 items-center justify-center rounded-full border border-neutral-900 bg-white dark:border-neutral-600 dark:bg-neutral-800"
|
||||
>
|
||||
<IconRightArrow className="h-8 w-8 text-neutral-800 dark:text-neutral-200" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
// This file has been updated for the Store's "Featured Agent Section". If you want to add Carousel, keep these components in mind: CarouselIndicator, CarouselPrevious, and CarouselNext.
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from "embla-carousel-react";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
@ -196,67 +197,137 @@ CarouselItem.displayName = "CarouselItem";
|
|||
|
||||
const CarouselPrevious = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
React.ComponentProps<typeof Button> & { afterClick?: () => void }
|
||||
>(
|
||||
(
|
||||
{ className, afterClick, variant = "outline", size = "icon", ...props },
|
||||
ref,
|
||||
) => {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-8 w-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-left-12 top-1/2 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-[52px] w-[52px] rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-bottom-20 right-24 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={() => {
|
||||
scrollPrev();
|
||||
if (afterClick) {
|
||||
afterClick();
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeft className="h-8 w-8" strokeWidth={1.25} />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
);
|
||||
CarouselPrevious.displayName = "CarouselPrevious";
|
||||
|
||||
const CarouselNext = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
React.ComponentProps<typeof Button> & { afterClick?: () => void }
|
||||
>(
|
||||
(
|
||||
{ className, afterClick, variant = "outline", size = "icon", ...props },
|
||||
ref,
|
||||
) => {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
|
||||
const handleClick = () => {
|
||||
scrollNext();
|
||||
if (afterClick) {
|
||||
afterClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-[52px] w-[52px] rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-bottom-20 right-4 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={handleClick}
|
||||
{...props}
|
||||
>
|
||||
<ChevronRight className="h-8 w-8" strokeWidth={1.25} />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
);
|
||||
CarouselNext.displayName = "CarouselNext";
|
||||
|
||||
const CarouselIndicator = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { api } = useCarousel();
|
||||
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
||||
const [scrollSnaps, setScrollSnaps] = React.useState<number[]>([]);
|
||||
|
||||
const scrollTo = React.useCallback(
|
||||
(index: number) => {
|
||||
api?.scrollTo(index);
|
||||
},
|
||||
[api],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) return;
|
||||
|
||||
setScrollSnaps(api.scrollSnapList());
|
||||
api.on("select", () => {
|
||||
setSelectedIndex(api.selectedScrollSnap());
|
||||
});
|
||||
}, [api]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
<div
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-8 w-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-right-12 top-1/2 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
className={cn("relative top-10 flex h-3 items-center gap-2", className)}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
{scrollSnaps.map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => scrollTo(index)}
|
||||
className={cn(
|
||||
selectedIndex === index
|
||||
? "h-3 w-[52px] rounded-[39px] bg-neutral-800 transition-all duration-500 dark:bg-neutral-200"
|
||||
: "h-3 w-3 rounded-full bg-neutral-300 transition-all duration-500 dark:bg-neutral-600",
|
||||
"cursor-pointer",
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
CarouselNext.displayName = "CarouselNext";
|
||||
CarouselIndicator.displayName = "CarouselIndicator";
|
||||
|
||||
export {
|
||||
type CarouselApi,
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselIndicator,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue