Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User dashboard product page action modal #708

Merged
merged 6 commits into from
Jul 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading