450 lines
16 KiB
TypeScript
450 lines
16 KiB
TypeScript
import { useCallback, useState, useRef, useEffect } from "react";
|
||
import { useNavigate } from "@remix-run/react";
|
||
import { safeGoBack } from "~/utils/helpers";
|
||
import {
|
||
Carousel,
|
||
CarouselContent,
|
||
CarouselItem,
|
||
type CarouselApi,
|
||
} from "~/components/ui/carousel";
|
||
import {
|
||
Drawer,
|
||
DrawerContent,
|
||
DrawerHeader,
|
||
DrawerTitle,
|
||
} from "~/components/ui/drawer";
|
||
import { Dialog, DialogContent, DialogOverlay } from "~/components/ui/dialog";
|
||
import { Button } from "~/components/ui/button";
|
||
import { useCarousel } from "~/hooks/useCarousel";
|
||
import { isVideo } from "../MondrianProductList";
|
||
import {
|
||
Edit3,
|
||
Trash2,
|
||
X,
|
||
EllipsisVertical,
|
||
EyeOff,
|
||
Eye,
|
||
ArrowRight,
|
||
} from "lucide-react";
|
||
import {
|
||
useDeleteProduct,
|
||
useToggleProductStatus,
|
||
} from "~/requestHandler/use-product-hooks";
|
||
import placeholderImage from "~/assets/icons/product.png";
|
||
import placeholderImageDark from "~/assets/icons/product-dark.png";
|
||
import { useRootData } from "~/hooks/use-root-data";
|
||
|
||
interface ProductImageCarouselProps {
|
||
images: string[];
|
||
productId?: string;
|
||
storeId?: string;
|
||
hideIndex?: boolean;
|
||
height?: number;
|
||
borderRadius?: number;
|
||
isOwner?: boolean;
|
||
isActive?: boolean;
|
||
controlsIsOn?: boolean;
|
||
onStatusChange?: (newStatus: boolean) => void;
|
||
isChangeStatusDialogOpen?: boolean;
|
||
setIsChangeStatusDialogOpen?: (open: boolean) => void;
|
||
}
|
||
|
||
export const ProductImageCarousel = ({
|
||
images,
|
||
productId,
|
||
storeId,
|
||
hideIndex = false,
|
||
height,
|
||
borderRadius,
|
||
controlsIsOn = false,
|
||
isOwner = false,
|
||
isActive = true,
|
||
onStatusChange,
|
||
isChangeStatusDialogOpen,
|
||
setIsChangeStatusDialogOpen,
|
||
}: ProductImageCarouselProps) => {
|
||
const navigate = useNavigate();
|
||
const [currentIndex, setCurrentIndex] = useState(0);
|
||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||
const [currentActiveStatus, setCurrentActiveStatus] = useState(isActive);
|
||
const { theme } = useRootData();
|
||
const { api, setApi } = useCarousel({
|
||
onSelect: useCallback(
|
||
(carouselApi: CarouselApi) => {
|
||
if (!carouselApi) return;
|
||
setCurrentIndex(carouselApi.selectedScrollSnap());
|
||
},
|
||
[setCurrentIndex]
|
||
),
|
||
});
|
||
|
||
const videoRefs = useRef<(HTMLVideoElement | null)[]>([]);
|
||
|
||
// Hooks for product management
|
||
const deleteProductMutation = useDeleteProduct();
|
||
const toggleProductStatusMutation = useToggleProductStatus();
|
||
|
||
// Sync local state with prop changes
|
||
useEffect(() => {
|
||
setCurrentActiveStatus(isActive);
|
||
}, [isActive]);
|
||
|
||
const handleSettingsClick = () => {
|
||
setIsDrawerOpen(true);
|
||
};
|
||
|
||
const handleEditProduct = () => {
|
||
if (productId && storeId) {
|
||
navigate(`/store/add/manual/${storeId}?edit=${productId}`);
|
||
}
|
||
setIsDrawerOpen(false);
|
||
};
|
||
|
||
const handleDeleteClick = () => {
|
||
setIsDrawerOpen(false);
|
||
setIsDeleteDialogOpen(true);
|
||
};
|
||
const handleDeactiveClick = () => {
|
||
setIsDrawerOpen(false);
|
||
if (setIsChangeStatusDialogOpen) setIsChangeStatusDialogOpen(true);
|
||
};
|
||
|
||
const handleConfirmDelete = () => {
|
||
if (productId) {
|
||
deleteProductMutation.mutate(productId, {
|
||
onSuccess: () => {
|
||
setIsDeleteDialogOpen(false);
|
||
// Navigate back or refresh the page
|
||
navigate(-1);
|
||
},
|
||
});
|
||
}
|
||
};
|
||
|
||
const handleConfirmStatusChange = () => {
|
||
if (productId) {
|
||
const newStatus = !currentActiveStatus;
|
||
toggleProductStatusMutation.mutate(
|
||
{ productId, isActive: newStatus },
|
||
{
|
||
onSuccess: () => {
|
||
if (setIsChangeStatusDialogOpen) setIsChangeStatusDialogOpen(false);
|
||
// Update local state
|
||
setCurrentActiveStatus(newStatus);
|
||
// Call parent callback if provided
|
||
if (onStatusChange) {
|
||
onStatusChange(newStatus);
|
||
}
|
||
},
|
||
}
|
||
);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div
|
||
className="w-full aspect-square relative overflow-hidden"
|
||
style={{
|
||
height: height ? `${height}px` : "100%",
|
||
borderRadius: borderRadius !== undefined ? `${borderRadius}px` : "10px",
|
||
}}
|
||
>
|
||
{!hideIndex && images.length > 1 && (
|
||
<div className="absolute bottom-3 left-2 z-10 bg-DARK_OVERLAY/90 text-WHITE2 px-3 py-2 rounded-[10px] text-B12">
|
||
{images.length} / {currentIndex + 1}
|
||
</div>
|
||
)}
|
||
{isOwner && (
|
||
<>
|
||
<button
|
||
onClick={handleSettingsClick}
|
||
className="absolute top-3 left-2 z-10 bg-DARK_OVERLAY/90 text-WHITE2 rounded-full h-9 w-9 flex items-center justify-center hover:bg-DARK_OVERLAY_HOVER transition-colors"
|
||
>
|
||
<EllipsisVertical size={20} className="text-WHITE2" />
|
||
</button>
|
||
<button
|
||
onClick={() => safeGoBack(navigate)}
|
||
className="absolute top-3 right-2 z-10 bg-DARK_OVERLAY/90 text-WHITE2 rounded-full h-9 w-9 flex items-center justify-center hover:bg-DARK_OVERLAY_HOVER transition-colors"
|
||
>
|
||
<ArrowRight size={20} className="text-WHITE2" />
|
||
</button>
|
||
</>
|
||
)}
|
||
|
||
<Carousel
|
||
setApi={setApi}
|
||
opts={{
|
||
align: "start",
|
||
loop: true,
|
||
}}
|
||
className="w-full h-full"
|
||
>
|
||
<CarouselContent className="-ml-0 h-full">
|
||
{images.map((media, index) => (
|
||
<CarouselItem key={index} className={`pl-0 h-full`}>
|
||
<div className="w-full aspect-square overflow-hidden h-full relative">
|
||
{isVideo(media) ? (
|
||
<div className="w-full h-full relative">
|
||
<video
|
||
ref={(el) => (videoRefs.current[index] = el)}
|
||
src={media}
|
||
className="w-full object-cover"
|
||
style={{
|
||
height: height ? `${height}px` : "100%",
|
||
borderRadius:
|
||
borderRadius !== undefined
|
||
? `${borderRadius}px`
|
||
: "10px",
|
||
}}
|
||
controls={controlsIsOn}
|
||
muted
|
||
autoPlay
|
||
loop
|
||
playsInline
|
||
onError={(e) => {
|
||
const target = e.currentTarget;
|
||
const parent = target.parentElement;
|
||
if (parent) {
|
||
const img = document.createElement("img");
|
||
img.src =
|
||
theme === "dark"
|
||
? placeholderImageDark
|
||
: placeholderImage;
|
||
img.alt = `تصویر ${index + 1}`;
|
||
img.className = "w-full object-contain";
|
||
img.style.height = height ? `${height}px` : "100%";
|
||
img.style.borderRadius =
|
||
borderRadius !== undefined
|
||
? `${borderRadius}px`
|
||
: "10px";
|
||
img.style.backgroundColor =
|
||
theme === "dark" ? "#121212" : "#f2f0f0";
|
||
parent.replaceChild(img, target);
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
) : (
|
||
<img
|
||
src={
|
||
media ||
|
||
(theme === "dark"
|
||
? placeholderImageDark
|
||
: placeholderImage)
|
||
}
|
||
loading={index === 0 ? "eager" : "lazy"}
|
||
onError={(e) => {
|
||
e.currentTarget.src =
|
||
theme === "dark"
|
||
? placeholderImageDark
|
||
: placeholderImage;
|
||
e.currentTarget.style.objectFit = "contain";
|
||
e.currentTarget.style.backgroundColor =
|
||
theme === "dark" ? "#121212" : "#f2f0f0";
|
||
}}
|
||
alt={`تصویر ${index + 1}`}
|
||
className={`w-full ${media ? "object-cover" : "object-contain"}`}
|
||
style={{
|
||
height: height ? `${height}px` : "100%",
|
||
borderRadius:
|
||
borderRadius !== undefined
|
||
? `${borderRadius}px`
|
||
: "10px",
|
||
}}
|
||
/>
|
||
)}
|
||
</div>
|
||
</CarouselItem>
|
||
))}
|
||
</CarouselContent>
|
||
</Carousel>
|
||
|
||
{images.length > 1 && (
|
||
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex gap-2">
|
||
{images.map((_, index) => (
|
||
<button
|
||
key={index}
|
||
onClick={() => api?.scrollTo(index)}
|
||
className={`w-2 h-2 rounded-full transition-colors ${
|
||
index === currentIndex ? "bg-WHITE" : "bg-WHITE/50"
|
||
}`}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Settings Drawer */}
|
||
<Drawer open={isDrawerOpen} onOpenChange={setIsDrawerOpen}>
|
||
<DrawerContent>
|
||
<DrawerHeader className="flex items-center relative justify-center">
|
||
<X
|
||
size={24}
|
||
className="text-BLACK2 absolute left-4"
|
||
onClick={() => setIsDrawerOpen(false)}
|
||
/>
|
||
<DrawerTitle className="text-center text-B12 font-bold">
|
||
تنظیمات محصول
|
||
</DrawerTitle>
|
||
</DrawerHeader>
|
||
<div className="flex gap-10 p-4 pb-6 items-center justify-center">
|
||
<div
|
||
className="flex flex-col items-center justify-center gap-1"
|
||
onClick={handleEditProduct}
|
||
onKeyDown={(e) => {
|
||
if (e.key === "Enter" || e.key === " ") {
|
||
handleEditProduct();
|
||
}
|
||
}}
|
||
role="button"
|
||
tabIndex={0}
|
||
aria-label="edit product"
|
||
>
|
||
<div className="bg-BLACK h-12 w-12 shrink-0 flex flex-col items-center justify-center rounded-full">
|
||
<Edit3 size={24} className="text-WHITE" />
|
||
</div>
|
||
<p className="text-R12 text-center">ویرایش محصول</p>
|
||
</div>
|
||
<div
|
||
className="flex flex-col items-center justify-center gap-1"
|
||
onClick={handleDeactiveClick}
|
||
onKeyDown={(e) => {
|
||
if (e.key === "Enter" || e.key === " ") {
|
||
handleDeactiveClick();
|
||
}
|
||
}}
|
||
role="button"
|
||
tabIndex={0}
|
||
aria-label={
|
||
currentActiveStatus ? "deactive product" : "active product"
|
||
}
|
||
>
|
||
<div className="bg-BLACK h-12 w-12 shrink-0 flex flex-col items-center justify-center rounded-full">
|
||
{currentActiveStatus ? (
|
||
<EyeOff size={24} className="text-WHITE" />
|
||
) : (
|
||
<Eye size={24} className="text-WHITE" />
|
||
)}
|
||
</div>
|
||
<p className="text-R12 text-center">
|
||
{currentActiveStatus ? "عدم نمایش محصول" : "نمایش محصول"}
|
||
</p>
|
||
</div>
|
||
<div
|
||
className="flex flex-col items-center justify-center gap-1"
|
||
onClick={handleDeleteClick}
|
||
onKeyDown={(e) => {
|
||
if (e.key === "Enter" || e.key === " ") {
|
||
handleDeleteClick();
|
||
}
|
||
}}
|
||
role="button"
|
||
tabIndex={0}
|
||
aria-label="Delete product"
|
||
>
|
||
<div className="bg-red-500 h-12 w-12 shrink-0 flex flex-col items-center justify-center rounded-full">
|
||
<Trash2 size={24} className="text-WHITE" />
|
||
</div>
|
||
<p className="text-R12 text-center">حذف محصول</p>
|
||
</div>
|
||
</div>
|
||
</DrawerContent>
|
||
</Drawer>
|
||
|
||
{/* Delete Confirmation Dialog */}
|
||
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||
<DialogOverlay className="bg-BLACK/50 z-[51]" />
|
||
<DialogContent className="p-0 bg-WHITE overflow-y-auto rounded-[15px] w-[calc(100%-40px)] z-[51]">
|
||
<div className="flex flex-col gap-6 items-center w-full p-6 overflow-hidden">
|
||
<div className="text-center flex flex-col gap-4 items-center justify-center">
|
||
<Trash2 className="text-BLACK" size={32} />
|
||
<h3 className="text-B16 font-bold">حذف محصول</h3>
|
||
<p className="text-R14 text-BLACK2">
|
||
آیا میخواهید این محصول را حذف کنید؟
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex gap-3 w-full">
|
||
<Button
|
||
onClick={handleConfirmDelete}
|
||
variant="dark"
|
||
size="xl"
|
||
className="w-full"
|
||
disabled={deleteProductMutation.isPending}
|
||
>
|
||
{deleteProductMutation.isPending ? "در حال حذف..." : "حذف پست"}
|
||
</Button>
|
||
|
||
<Button
|
||
onClick={() => setIsDeleteDialogOpen(false)}
|
||
variant="outline"
|
||
size="xl"
|
||
className="w-full"
|
||
disabled={deleteProductMutation.isPending}
|
||
>
|
||
انصراف
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* Status Change Confirmation Dialog */}
|
||
<Dialog
|
||
open={isChangeStatusDialogOpen}
|
||
onOpenChange={setIsChangeStatusDialogOpen}
|
||
>
|
||
<DialogOverlay className="bg-BLACK/50 z-[51]" />
|
||
<DialogContent className="p-0 bg-WHITE overflow-y-auto rounded-[15px] w-[calc(100%-40px)] z-[51]">
|
||
<div className="flex flex-col gap-6 items-center w-full p-6 overflow-hidden">
|
||
<div className="text-center flex flex-col gap-4 items-center justify-center">
|
||
{currentActiveStatus ? (
|
||
<EyeOff className="text-BLACK" size={32} />
|
||
) : (
|
||
<Eye className="text-BLACK" size={32} />
|
||
)}
|
||
<h3 className="text-B16 font-bold">
|
||
{currentActiveStatus ? "عدم نمایش محصول" : "نمایش محصول"}
|
||
</h3>
|
||
<p className="text-R14 text-BLACK2">
|
||
{currentActiveStatus
|
||
? "آیا میخواهید این محصول را غیرفعال کنید؟"
|
||
: "آیا میخواهید این محصول را فعال کنید؟"}
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex gap-3 w-full">
|
||
<Button
|
||
onClick={handleConfirmStatusChange}
|
||
variant="dark"
|
||
size="xl"
|
||
className="w-full"
|
||
disabled={toggleProductStatusMutation.isPending}
|
||
>
|
||
{toggleProductStatusMutation.isPending
|
||
? "در حال تغییر..."
|
||
: currentActiveStatus
|
||
? "غیرفعال کردن"
|
||
: "فعال کردن"}
|
||
</Button>
|
||
|
||
<Button
|
||
onClick={() => {
|
||
if (setIsChangeStatusDialogOpen)
|
||
setIsChangeStatusDialogOpen(false);
|
||
}}
|
||
variant="outline"
|
||
size="xl"
|
||
className="w-full"
|
||
disabled={toggleProductStatusMutation.isPending}
|
||
>
|
||
انصراف
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>
|
||
);
|
||
};
|