Skip to content

Commit

Permalink
Merge pull request #708 from hngprojects/Vxrcel-clean
Browse files Browse the repository at this point in the history
User dashboard product page action modal
  • Loading branch information
Prudent Bird authored Jul 28, 2024
2 parents 894fb2f + 2c35dd3 commit b94eb06
Show file tree
Hide file tree
Showing 6 changed files with 262 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { MoreVertical } from "lucide-react";
/* eslint-disable react-hooks/exhaustive-deps */
import { AnimatePresence, motion } from "framer-motion";
import { Edit2, MoreVertical, Trash } from "lucide-react";
import { useEffect, useRef } from "react";

import BlurImage from "~/components/miscellaneous/blur-image";
import { Button } from "~/components/ui/button";
Expand All @@ -21,14 +24,49 @@ const ProductBodyShadcn = ({
searchTerm,
}: Properties) => {
const { products } = useProducts();
const { updateOpen, updateProductId, product_id } = useProductModal();
const {
updateOpen,
updateProductId,
product_id,
isActionModal,
setIsActionModal,
setIsDelete,
} = useProductModal();
const modalReference = useRef<HTMLDivElement>(null);

useEffect(() => {
if (!isActionModal || !modalReference.current) return;
modalReference.current.scrollIntoView({ behavior: "smooth" });
// handle click outside modal
const handleOutsideClick = (event: MouseEvent) => {
if (
modalReference.current &&
!modalReference.current.contains(event.target as Node)
) {
setIsActionModal(false);
}
};
document.addEventListener("click", handleOutsideClick);
return () => {
document.removeEventListener("click", handleOutsideClick);
};
}, [isActionModal]);
if (!products) return;
const handleOpenDetail = (product_id: string) => {
setIsActionModal(false);
updateProductId(product_id);
updateOpen(true);
};
const handleDeleteAction = (id: string) => {
setIsActionModal(false);
updateProductId(id);
setIsDelete(true);
};

return (
filteredProducts.length > 0 &&
subset.length > 0 &&
subset.map((product) => (
subset.map((product, index) => (
<TableRow
key={product.product_id}
className={cn(
Expand All @@ -49,10 +87,7 @@ const ProductBodyShadcn = ({
</div>{" "}
<span
role="button"
onClick={() => {
updateProductId(product.product_id);
updateOpen(true);
}}
onClick={() => handleOpenDetail(product.product_id)}
className="hide_scrollbar overflow-x-auto text-neutral-dark-2 md:w-[200px] lg:w-[200px]"
>
{searchTerm.length > 1 ? (
Expand All @@ -78,31 +113,28 @@ const ProductBodyShadcn = ({
)}
</span>
</TableCell>
<TableCell className="uppercase">{product.product_id}</TableCell>
<TableCell
role="button"
onClick={() => {
updateProductId(product.product_id);
updateOpen(true);
}}
onClick={() => handleOpenDetail(product.product_id)}
className="uppercase"
>
{product.product_id}
</TableCell>
<TableCell
role="button"
onClick={() => handleOpenDetail(product.product_id)}
>
{product.category}
</TableCell>
<TableCell
role="button"
onClick={() => {
updateProductId(product.product_id);
updateOpen(true);
}}
onClick={() => handleOpenDetail(product.product_id)}
>
{formatPrice(product.price)}
</TableCell>
<TableCell
role="button"
onClick={() => {
updateProductId(product.product_id);
updateOpen(true);
}}
onClick={() => handleOpenDetail(product.product_id)}
>
<span
className={cn(
Expand All @@ -121,10 +153,62 @@ const ProductBodyShadcn = ({
{product.status === "out_of_stock" && "Out of Stock"}
</span>
</TableCell>
<TableCell className="whitespace-nowrap px-2 py-4 md:gap-x-4 min-[1440px]:px-6">
<Button variant={"ghost"} size={"icon"}>
<TableCell className="relative whitespace-nowrap px-2 py-4 md:gap-x-4 min-[1440px]:px-6">
<Button
onClick={() => {
updateProductId(product.product_id);
setIsActionModal(!isActionModal);
}}
variant={"ghost"}
size={"icon"}
>
<MoreVertical />
</Button>
<AnimatePresence>
{isActionModal && product_id === product.product_id && (
<motion.div
ref={modalReference}
initial={{ opacity: 0, y: -20, x: 20 }}
animate={{ opacity: 1, y: 0, x: 0 }}
exit={{ opacity: 0, y: -20, x: 20 }}
className={cn(
"absolute right-16 z-30 flex w-[121px] flex-col justify-between gap-y-1 rounded-[6px] border border-gray-300 bg-white/80 shadow-[0px_1px_18px_0px_rgba(10,_57,_176,_0.12)] backdrop-blur-sm sm:w-full sm:max-w-[121px]",
index === subset.length - 1 || index === subset.length - 2
? "bottom-[3.7rem]"
: "-bottom-[5.5rem]",
)}
>
<span className="border-b border-gray-200 px-2 py-2 text-sm font-semibold text-neutral-dark-2 md:px-4">
Actions
</span>
<div className="flex flex-col">
<Button
variant="ghost"
size={"sm"}
className={cn(
"flex h-8 cursor-pointer items-center justify-start gap-x-2 px-2 py-1 text-xs min-[500px]:text-sm",
)}
>
<Edit2 className={cn("size-4")} />

<span>Edit</span>
</Button>
<Button
onClick={() => handleDeleteAction(product.product_id)}
variant="ghost"
size={"sm"}
className={cn(
"flex h-8 cursor-pointer items-center justify-start gap-x-2 px-2 py-1 text-xs text-red-500 min-[500px]:text-sm",
)}
>
<Trash className={cn("size-4")} />

<span>Delete</span>
</Button>
</div>
</motion.div>
)}
</AnimatePresence>
</TableCell>
</TableRow>
))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { AnimatePresence, motion } from "framer-motion";

import { Button } from "~/components/ui/button";
import { toast } from "~/components/ui/use-toast";
import { useProductModal } from "~/hooks/admin-product/use-product.modal";
import { useProducts } from "~/hooks/admin-product/use-products.persistence";
import { cn } from "~/lib/utils";

const variantProperties = {
left: "50%",
top: "50%",
translateX: "-50%",
translateY: "-50%",
};
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const ProductDeleteModal = () => {
const { products, deleteProduct } = useProducts();

const { product_id, updateProductId, updateOpen, isDelete, setIsDelete } =
useProductModal();

const product = products?.find(
(product) => product.product_id === product_id,
);

const handleDelete = async (id: string) => {
toast({
title: "Deleting product",
description: "Please wait...",
variant: "destructive",
});
setIsDelete(false);

await delay(3000);
deleteProduct(id);
toast({
title: `Product deleted`,
description: (
<span>
<b>{product?.name}</b> has been deleted.
</span>
),
variant: "default",
className: "z-[99999]",
});
updateOpen(false);
updateProductId("null");
};

return (
<>
<div
onClick={() => {
updateOpen(false);
updateProductId("null");

setIsDelete(false);
}}
className={cn(
"fixed left-0 top-0 z-[99999] min-h-screen w-full overflow-hidden bg-neutral-700/10 transition-all duration-300 lg:hidden",
isDelete
? "pointer-events-auto opacity-100"
: "pointer-events-none opacity-0",
)}
/>

<AnimatePresence>
{isDelete && (
<motion.div
initial={{
...variantProperties,
opacity: 0,
scale: 0.5,
}}
animate={{
...variantProperties,
opacity: 1,
scale: 1,
}}
exit={{
...variantProperties,
opacity: 0,
scale: 0.5,
}}
transition={{ duration: 0.2 }}
className={cn(
"fixed left-1/2 top-1/2 z-[99999] grid w-full min-w-[350px] max-w-[349px] -translate-x-1/2 -translate-y-1/2 transform-gpu flex-col place-items-center items-center min-[360px]:max-w-[480px] sm:max-w-[403px]",
)}
>
<div
className={cn(
"absolute left-1/2 top-1/2 flex w-full max-w-[90%] -translate-x-1/2 -translate-y-1/2 flex-col gap-y-5 border bg-white/80 px-2 py-5 shadow-[0px_1px_18px_0px_rgba(10,_57,_176,_0.12)] backdrop-blur transition-all duration-300",
isDelete
? "pointer-events-auto scale-100 opacity-100"
: "pointer-events-none scale-50 opacity-0",
)}
>
<p className="text-center text-sm">
Are you sure you want to delete <b>{product?.name}</b>?
</p>
<div className="flex w-full items-center justify-center gap-x-2">
<Button
onClick={() => handleDelete(product!.product_id!)}
variant="outline"
className="bg-white font-medium text-error"
>
Yes
</Button>
<Button
onClick={() => setIsDelete(false)}
variant="outline"
className="bg-white font-medium"
>
No
</Button>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</>
);
};

export default ProductDeleteModal;
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ const ProductDetailModal = () => {
document.body.style.overflow =
isOpen && winWidth < 1024 ? "hidden" : "unset";
}, [isOpen, winWidth]);

useEffect(() => {
document.title = isOpen
? `Product - ${product?.name}`
: "Products - HNG Boilerplate";
}, [isOpen, product?.name]);
return (
<>
<div
Expand Down Expand Up @@ -96,7 +100,7 @@ const ProductDetailModal = () => {
exit={{
...variantProperties,
opacity: 0,
scale: 2,
scale: 0.5,
}}
transition={{ duration: 0.2 }}
className={cn(
Expand All @@ -112,7 +116,7 @@ const ProductDetailModal = () => {
)}
>
<p className="text-center text-sm">
Are you sure you want to delete this <b>{product?.name}</b>?
Are you sure you want to delete <b>{product?.name}</b>?
</p>
<div className="flex w-full items-center justify-center gap-x-2">
<Button
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AnimatePresence, motion } from "framer-motion";
import { X } from "lucide-react";
import { useTransition } from "react";
import { useEffect, useTransition } from "react";

import BlurImage from "~/components/miscellaneous/blur-image";
import LoadingSpinner from "~/components/miscellaneous/loading-spinner";
Expand Down Expand Up @@ -46,6 +46,12 @@ const ProductDetailView = () => {
setIsDelete(false);
});
};
useEffect(() => {
document.title = isOpen
? `Product - ${product?.name}`
: "Products - HNG Boilerplate";
}, [isOpen, product?.name]);

return (
<AnimatePresence>
{isOpen && (
Expand All @@ -65,7 +71,7 @@ const ProductDetailView = () => {
)}
>
<p className="text-center text-sm">
Are you sure you want to delete this <b>{product?.name}</b>?
Are you sure you want to delete <b>{product?.name}</b>?
</p>
<div className="flex w-full items-center justify-center gap-x-2">
<Button
Expand Down
Loading

0 comments on commit b94eb06

Please sign in to comment.