mirror of
https://github.com/Sosokker/B2D-Ventures.git
synced 2025-12-20 14:34:05 +01:00
feat: implement image gallery component for project deal page
This commit is contained in:
parent
7e4a237075
commit
493d2805e6
@ -5,19 +5,19 @@ 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 { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/components/ui/carousel";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
|
||||||
import { Progress } from "@/components/ui/progress";
|
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 { DisplayFullImage } from "./displayImage";
|
|
||||||
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";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { isOwnerOfProject } from "./query";
|
import { isOwnerOfProject } from "./query";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
|
import Gallery from "@/components/carousel";
|
||||||
|
|
||||||
const PHOTO_MATERIAL_ID = 2;
|
const PHOTO_MATERIAL_ID = 2;
|
||||||
|
|
||||||
@ -75,7 +75,6 @@ export default async function ProjectDealPage({ params }: { params: { id: number
|
|||||||
? projectMaterial.flatMap((item) =>
|
? projectMaterial.flatMap((item) =>
|
||||||
(item.material_url || ["/boiler1.jpg"]).map((url: string) => ({
|
(item.material_url || ["/boiler1.jpg"]).map((url: string) => ({
|
||||||
src: url,
|
src: url,
|
||||||
alt: "Image",
|
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
: [{ src: "/boiler1.jpg", alt: "Default Boiler Image" }];
|
: [{ src: "/boiler1.jpg", alt: "Default Boiler Image" }];
|
||||||
@ -106,35 +105,8 @@ export default async function ProjectDealPage({ params }: { params: { id: number
|
|||||||
</div>
|
</div>
|
||||||
<div id="sub-content" className="flex flex-row mt-5">
|
<div id="sub-content" className="flex flex-row mt-5">
|
||||||
{/* image carousel */}
|
{/* image carousel */}
|
||||||
<div id="image-carousel" className="shrink-0 w-[700px] flex flex-col">
|
<div id="image-carousel" className="w-full">
|
||||||
{/* first carousel */}
|
<Gallery images={carouselData} />
|
||||||
<Carousel className="w-full h-[400px] ml-1 overflow-hidden">
|
|
||||||
<CarouselContent className="flex h-full">
|
|
||||||
{carouselData.map((item, index) => (
|
|
||||||
<CarouselItem key={index}>
|
|
||||||
<Image src={item.src} alt={item.alt} width={700} height={400} className="rounded-lg object-cover" />
|
|
||||||
</CarouselItem>
|
|
||||||
))}
|
|
||||||
</CarouselContent>
|
|
||||||
<CarouselPrevious className="absolute left-2 top-1/2 transform -translate-y-1/2 z-10 text-white bg-black opacity-50 hover:opacity-100" />
|
|
||||||
<CarouselNext className="absolute right-2 top-1/2 transform -translate-y-1/2 z-10 text-white bg-black opacity-50 hover:opacity-100" />
|
|
||||||
</Carousel>
|
|
||||||
{/* second carousel */}
|
|
||||||
<Carousel className="w-full ml-1 h-[100px] mt-5 overflow-hidden">
|
|
||||||
<CarouselContent className="flex space-x-1 h-[100px]">
|
|
||||||
{carouselData.map((item, index) => (
|
|
||||||
<CarouselItem key={index} className="flex">
|
|
||||||
<DisplayFullImage
|
|
||||||
src={item.src}
|
|
||||||
alt={item.alt}
|
|
||||||
width={200}
|
|
||||||
height={100}
|
|
||||||
className="rounded-lg object-cover h-[100px] w-[200px]"
|
|
||||||
/>
|
|
||||||
</CarouselItem>
|
|
||||||
))}
|
|
||||||
</CarouselContent>
|
|
||||||
</Carousel>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="w-[80%] ml-10 shadow-sm">
|
<Card className="w-[80%] ml-10 shadow-sm">
|
||||||
|
|||||||
88
src/components/carousel.tsx
Normal file
88
src/components/carousel.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
"use client";
|
||||||
|
import { useEffect, useState, useMemo } from "react";
|
||||||
|
import { Carousel, CarouselContent, CarouselItem, type CarouselApi } from "./ui/carousel";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
interface GalleryProps {
|
||||||
|
images: { src: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Gallery = ({ images }: GalleryProps) => {
|
||||||
|
const [mainApi, setMainApi] = useState<CarouselApi>();
|
||||||
|
const [thumbnailApi, setThumbnailApi] = useState<CarouselApi>();
|
||||||
|
const [current, setCurrent] = useState(0);
|
||||||
|
|
||||||
|
const mainImage = useMemo(
|
||||||
|
() =>
|
||||||
|
images.map((image, index) => (
|
||||||
|
<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" }} />
|
||||||
|
</CarouselItem>
|
||||||
|
)),
|
||||||
|
[images]
|
||||||
|
);
|
||||||
|
|
||||||
|
const thumbnailImages = useMemo(
|
||||||
|
() =>
|
||||||
|
images.map((image, index) => (
|
||||||
|
<CarouselItem key={index} className="relative aspect-square basis-1/4" onClick={() => handleClick(index)}>
|
||||||
|
<Image
|
||||||
|
className={`${index === current ? "border-2" : ""}`}
|
||||||
|
src={image.src}
|
||||||
|
fill
|
||||||
|
alt={`Carousel Thumbnail Image ${index + 1}`}
|
||||||
|
style={{ objectFit: "contain" }}
|
||||||
|
/>
|
||||||
|
</CarouselItem>
|
||||||
|
)),
|
||||||
|
[images, current]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mainApi || !thumbnailApi) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTopSelect = () => {
|
||||||
|
const selected = mainApi.selectedScrollSnap();
|
||||||
|
setCurrent(selected);
|
||||||
|
thumbnailApi.scrollTo(selected);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBottomSelect = () => {
|
||||||
|
const selected = thumbnailApi.selectedScrollSnap();
|
||||||
|
setCurrent(selected);
|
||||||
|
mainApi.scrollTo(selected);
|
||||||
|
};
|
||||||
|
|
||||||
|
mainApi.on("select", handleTopSelect);
|
||||||
|
thumbnailApi.on("select", handleBottomSelect);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mainApi.off("select", handleTopSelect);
|
||||||
|
thumbnailApi.off("select", handleBottomSelect);
|
||||||
|
};
|
||||||
|
}, [mainApi, thumbnailApi]);
|
||||||
|
|
||||||
|
const handleClick = (index: number) => {
|
||||||
|
if (!mainApi || !thumbnailApi) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
thumbnailApi.scrollTo(index);
|
||||||
|
mainApi.scrollTo(index);
|
||||||
|
setCurrent(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-xl sm:w-auto">
|
||||||
|
<Carousel setApi={setMainApi}>
|
||||||
|
<CarouselContent className="m-1">{mainImage}</CarouselContent>
|
||||||
|
</Carousel>
|
||||||
|
<Carousel setApi={setThumbnailApi}>
|
||||||
|
<CarouselContent className="m-1 h-16">{thumbnailImages}</CarouselContent>
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Gallery;
|
||||||
Loading…
Reference in New Issue
Block a user