mirror of
https://github.com/Sosokker/B2D-Ventures.git
synced 2025-12-19 14:04:06 +01:00
feat: enhance carousel component with synchronization and improve accessibility in project deal page
This commit is contained in:
parent
52204c4a6f
commit
63efe5ffa2
@ -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>
|
||||||
|
|||||||
@ -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]);
|
|
||||||
|
|
||||||
const handleClick = (index: number) => {
|
|
||||||
if (!mainApi || !thumbnailApi) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
thumbnailApi.scrollTo(index);
|
|
||||||
mainApi.scrollTo(index);
|
|
||||||
setCurrent(index);
|
|
||||||
};
|
};
|
||||||
|
}, [mainApi, thumbnailApi, current, syncCarousels, isReady]);
|
||||||
|
|
||||||
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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user