feat: enhance carousel component with synchronization and improve accessibility in project deal page

This commit is contained in:
THIS ONE IS A LITTLE BIT TRICKY KRUB 2024-11-13 15:26:40 +07:00
parent 52204c4a6f
commit 63efe5ffa2
3 changed files with 66 additions and 41 deletions

View File

@ -1,8 +1,6 @@
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import * as Tabs from "@radix-ui/react-tabs"; import * as Tabs from "@radix-ui/react-tabs";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
@ -10,7 +8,6 @@ import { Progress } from "@/components/ui/progress";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient"; import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
import FollowShareButtons from "./followShareButton"; import FollowShareButtons from "./followShareButton";
import { getProjectData } from "@/lib/data/projectQuery"; import { getProjectData } from "@/lib/data/projectQuery";
import { getDealList } from "@/app/api/dealApi"; import { getDealList } from "@/app/api/dealApi";
import { sumByKey, toPercentage } from "@/lib/utils"; import { sumByKey, toPercentage } from "@/lib/utils";
@ -89,7 +86,7 @@ export default async function ProjectDealPage({ params }: { params: { id: number
<Image src="/logo.svg" alt="logo" width={50} height={50} className="sm:scale-75" /> <Image src="/logo.svg" alt="logo" width={50} height={50} className="sm:scale-75" />
<h1 className="mt-3 font-bold text-lg md:text-3xl">{projectData?.project_name}</h1> <h1 className="mt-3 font-bold text-lg md:text-3xl">{projectData?.project_name}</h1>
</span> </span>
<FollowShareButtons userId={user!.user.id} projectId={params.id} projectName ={projectData?.project_name}/> <FollowShareButtons userId={user!.user.id} projectId={params.id} projectName={projectData?.project_name} />
</div> </div>
{/* end of pack */} {/* end of pack */}
<p className="mt-2 sm:text-sm">{projectData?.project_short_description}</p> <p className="mt-2 sm:text-sm">{projectData?.project_short_description}</p>

View File

@ -1,5 +1,5 @@
"use client"; "use client";
import { useEffect, useState, useMemo } from "react"; import { useEffect, useState, useMemo, useCallback } from "react";
import { Carousel, CarouselContent, CarouselItem, type CarouselApi } from "./ui/carousel"; import { Carousel, CarouselContent, CarouselItem, type CarouselApi } from "./ui/carousel";
import Image from "next/image"; import Image from "next/image";
@ -8,15 +8,40 @@ interface GalleryProps {
} }
const Gallery = ({ images }: GalleryProps) => { const Gallery = ({ images }: GalleryProps) => {
const [mainApi, setMainApi] = useState<CarouselApi>(); const [mainApi, setMainApi] = useState<CarouselApi | null>(null);
const [thumbnailApi, setThumbnailApi] = useState<CarouselApi>(); const [thumbnailApi, setThumbnailApi] = useState<CarouselApi | null>(null);
const [current, setCurrent] = useState(0); const [current, setCurrent] = useState(0);
const [isReady, setIsReady] = useState(false);
const syncCarousels = useCallback(
(index: number) => {
if (mainApi && thumbnailApi) {
setCurrent(index);
mainApi.scrollTo(index);
thumbnailApi.scrollTo(index);
}
},
[mainApi, thumbnailApi]
);
const handleClick = useCallback(
(index: number) => {
syncCarousels(index);
},
[syncCarousels]
);
const mainImage = useMemo( const mainImage = useMemo(
() => () =>
images.map((image, index) => ( images.map((image, index) => (
<CarouselItem key={index} className="relative aspect-video w-full border-8 border-b"> <CarouselItem key={index} className="relative aspect-video w-full border-8 border-b">
<Image src={image.src} alt={`Carousel Main Image ${index + 1}`} fill style={{ objectFit: "contain" }} /> <Image
src={image.src}
alt={`Carousel Main Image ${index + 1}`}
fill
style={{ objectFit: "contain" }}
priority={index === 0}
/>
</CarouselItem> </CarouselItem>
)), )),
[images] [images]
@ -25,61 +50,60 @@ const Gallery = ({ images }: GalleryProps) => {
const thumbnailImages = useMemo( const thumbnailImages = useMemo(
() => () =>
images.map((image, index) => ( images.map((image, index) => (
<CarouselItem key={index} className="relative aspect-square basis-1/4" onClick={() => handleClick(index)}> <CarouselItem
key={index}
className="relative aspect-square basis-1/4 cursor-pointer"
onClick={() => handleClick(index)}
>
<Image <Image
className={`${index === current ? "border-2" : ""}`} className={`transition-all duration-200 ${index === current ? "border-2 border-primary" : ""}`}
src={image.src} src={image.src}
fill fill
alt={`Carousel Thumbnail Image ${index + 1}`} alt={`Carousel Thumbnail Image ${index + 1}`}
style={{ objectFit: "contain" }} style={{ objectFit: "contain" }}
priority={index === 0}
/> />
</CarouselItem> </CarouselItem>
)), )),
// eslint-disable-next-line react-hooks/exhaustive-deps [images, current, handleClick]
[images, current]
); );
useEffect(() => { useEffect(() => {
if (!mainApi || !thumbnailApi) { if (!mainApi || !thumbnailApi) return;
return; if (isReady) return;
}
const handleTopSelect = () => { const handleMainSelect = () => {
const selected = mainApi.selectedScrollSnap(); const selected = mainApi.selectedScrollSnap();
setCurrent(selected); if (selected !== current) {
thumbnailApi.scrollTo(selected); syncCarousels(selected);
}
}; };
const handleBottomSelect = () => { const handleThumbnailSelect = () => {
const selected = thumbnailApi.selectedScrollSnap(); const selected = thumbnailApi.selectedScrollSnap();
setCurrent(selected); if (selected !== current) {
mainApi.scrollTo(selected); syncCarousels(selected);
}
}; };
mainApi.on("select", handleTopSelect); mainApi.on("select", handleMainSelect);
thumbnailApi.on("select", handleBottomSelect); thumbnailApi.on("select", handleThumbnailSelect);
syncCarousels(0);
setIsReady(true);
return () => { return () => {
mainApi.off("select", handleTopSelect); mainApi.off("select", handleMainSelect);
thumbnailApi.off("select", handleBottomSelect); thumbnailApi.off("select", handleThumbnailSelect);
}; };
}, [mainApi, thumbnailApi]); }, [mainApi, thumbnailApi, current, syncCarousels, isReady]);
const handleClick = (index: number) => {
if (!mainApi || !thumbnailApi) {
return;
}
thumbnailApi.scrollTo(index);
mainApi.scrollTo(index);
setCurrent(index);
};
return ( return (
<div className="w-full max-w-xl sm:w-auto"> <div className="w-full max-w-xl sm:w-auto">
<Carousel setApi={setMainApi}> <Carousel setApi={setMainApi} className="mb-2">
<CarouselContent className="m-1">{mainImage}</CarouselContent> <CarouselContent className="m-1">{mainImage}</CarouselContent>
</Carousel> </Carousel>
<Carousel setApi={setThumbnailApi}> <Carousel setApi={setThumbnailApi} className="cursor-pointer">
<CarouselContent className="m-1 h-16">{thumbnailImages}</CarouselContent> <CarouselContent className="m-1 h-16">{thumbnailImages}</CarouselContent>
</Carousel> </Carousel>
</div> </div>

View File

@ -115,14 +115,16 @@ const Carousel = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivEl
scrollNext, scrollNext,
canScrollPrev, canScrollPrev,
canScrollNext, canScrollNext,
}}> }}
>
<div <div
ref={ref} ref={ref}
onKeyDownCapture={handleKeyDown} onKeyDownCapture={handleKeyDown}
className={cn("relative", className)} className={cn("relative", className)}
role="region" role="region"
aria-roledescription="carousel" aria-roledescription="carousel"
{...props}> {...props}
>
{children} {children}
</div> </div>
</CarouselContext.Provider> </CarouselContext.Provider>
@ -183,7 +185,8 @@ const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProp
)} )}
disabled={!canScrollPrev} disabled={!canScrollPrev}
onClick={scrollPrev} onClick={scrollPrev}
{...props}> {...props}
>
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span> <span className="sr-only">Previous slide</span>
</Button> </Button>
@ -210,7 +213,8 @@ const CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps<ty
)} )}
disabled={!canScrollNext} disabled={!canScrollNext}
onClick={scrollNext} onClick={scrollNext}
{...props}> {...props}
>
<ArrowRight className="h-4 w-4" /> <ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span> <span className="sr-only">Next slide</span>
</Button> </Button>