From 5e45aee2bba30c58313f12164aa228860174ac3d Mon Sep 17 00:00:00 2001 From: fdhhhdjd Date: Sun, 22 Oct 2023 12:29:57 +0700 Subject: [PATCH] #12 [ Frontend ] Cart Products --- .eslintrc.json | 2 +- src/app/carts/page.tsx | 16 ++++++ src/components/ProductCart.tsx | 3 +- src/components/buttons/CheckoutBtn.tsx | 14 ++++- src/components/cards/CartCard.tsx | 68 ++++++++++++++++++++++ src/components/sections/CartSection.tsx | 69 +++++++++++++++++++++++ src/helpers/validations/cartItemSchema.ts | 1 + src/hooks/useLocalStorage.tsx | 16 +++++- src/providers/CartContextProvider.tsx | 5 +- 9 files changed, 188 insertions(+), 6 deletions(-) create mode 100644 src/app/carts/page.tsx create mode 100644 src/components/cards/CartCard.tsx create mode 100644 src/components/sections/CartSection.tsx diff --git a/.eslintrc.json b/.eslintrc.json index b97170a..d1ef7b3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -59,7 +59,7 @@ "prefer-const": "warn", "max-len": ["error", 200], "array-bracket-newline": "warn", - "consistent-return": "warn", + "consistent-return": "error", "eqeqeq": "error", "no-fallthrough": "off", "no-unused-expressions": "warn", diff --git a/src/app/carts/page.tsx b/src/app/carts/page.tsx new file mode 100644 index 0000000..e746912 --- /dev/null +++ b/src/app/carts/page.tsx @@ -0,0 +1,16 @@ +//* IMPORT +import Border from '@/src/components/Border'; +import BackButton from '@/src/components/buttons/BackButton'; +import CartSection from '@/src/components/sections/CartSection'; + +const CartsPage = () => { + return ( + <> + + + + + ); +}; + +export default CartsPage; diff --git a/src/components/ProductCart.tsx b/src/components/ProductCart.tsx index 964415f..42ab3ed 100644 --- a/src/components/ProductCart.tsx +++ b/src/components/ProductCart.tsx @@ -19,7 +19,7 @@ interface ProductCartProps { } const ProductCart = ({ product }: ProductCartProps) => { - const { id, name, image, sizes, quantity } = product; + const { id, name, image, sizes, quantity, price } = product; const [size, setSize] = useState(); @@ -41,6 +41,7 @@ const ProductCart = ({ product }: ProductCartProps) => { name, image, size, + price, }, ]); setSize(undefined); diff --git a/src/components/buttons/CheckoutBtn.tsx b/src/components/buttons/CheckoutBtn.tsx index 6fde41a..14e8e7f 100644 --- a/src/components/buttons/CheckoutBtn.tsx +++ b/src/components/buttons/CheckoutBtn.tsx @@ -7,6 +7,7 @@ import { usePathname, useRouter } from 'next/navigation'; import { useState } from 'react'; import toast from 'react-hot-toast'; import { AiOutlineShoppingCart } from 'react-icons/ai'; +import { BsFillTrashFill } from 'react-icons/bs'; //* IMPORT import Button from './Button'; @@ -22,7 +23,7 @@ const CheckoutBtn = () => { const [isLoading, setIsLoading] = useState(false); - const { cartItems, setCartItems } = useCartContext(); + const { cartItems, setCartItems, clearStorage } = useCartContext(); const toHide = path && (['/carts', '/orders/status'].includes(path) || path.startsWith('/checkout')); @@ -59,6 +60,17 @@ const CheckoutBtn = () => { {cartItems.length ? (

Total ({cartItems.length})

+ {cartItems.length > 2 && ( + + )} + Checkout
) : ( diff --git a/src/components/cards/CartCard.tsx b/src/components/cards/CartCard.tsx new file mode 100644 index 0000000..001f9a3 --- /dev/null +++ b/src/components/cards/CartCard.tsx @@ -0,0 +1,68 @@ +'use client'; + +//* LIB +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-hot-toast'; + +//* IMPORT +import cn from '../../helpers/cn'; +import { displayNumbers } from '../../helpers/numbers'; +import { useCartContext } from '../../providers/CartContextProvider'; +import { CartItem } from '../../types/types'; +import Button from '../buttons/Button'; + +export interface CartCardProps { + item: CartItem; + disableAction?: boolean; +} + +interface Props extends CartCardProps { + index: number; +} +const CartCard = ({ item, index, disableAction = false }: Props) => { + const { cartItems, setCartItems } = useCartContext(); + const router = useRouter(); + + const [isDeleting, setIsDeleting] = useState(false); + + const handleRemove = () => { + setIsDeleting(true); + setCartItems([...cartItems.filter((_, idx) => idx !== index)]); + setTimeout(() => { + setIsDeleting(false); + toast.success('Item successfully removed!'); + }, 200); + }; + + return ( +
+
+ {item.id} +
+
+

!disableAction && router.push(`/products/${item.id}`)} + className={cn( + 'sm:text-lg text-[16px] font-black ease-in duration-75', + !disableAction && 'hover:underline hover:text-red-800 cursor-pointer' + )} + > + {item.name} +

+

US M{item.size}

+

${displayNumbers(item.price)}

+ {!disableAction && ( +
+ +
+ )} +
+
+ ); +}; + +export default CartCard; diff --git a/src/components/sections/CartSection.tsx b/src/components/sections/CartSection.tsx new file mode 100644 index 0000000..578564d --- /dev/null +++ b/src/components/sections/CartSection.tsx @@ -0,0 +1,69 @@ +'use client'; + +//* LIB +import { useRouter } from 'next/navigation'; +import { useMemo, useState } from 'react'; +import { toast } from 'react-hot-toast'; +import { IoArrowForwardOutline } from 'react-icons/io5'; + +//* IMPORT +import { displayNumbers } from '../../helpers/numbers'; +import { useCartContext } from '../../providers/CartContextProvider'; +import { useUserContext } from '../../providers/UserProvider'; +import Button from '../buttons/Button'; +import CartCard from '../cards/CartCard'; +import Loader from '../loaders/Loader'; +import NotFoundText from '../NotFoundText'; + +const CartSection = () => { + const { user } = useUserContext(); + const { cartItems } = useCartContext(); + const [isRedirecting] = useState(false); + + const router = useRouter(); + + const totalPrice = useMemo(() => cartItems.reduce((acc, curr) => acc + (curr?.price ?? 0), 0), [cartItems]); + + const handleCheckout = () => { + if (!user || !user.email) toast.error('Please sign-in before checking out'); + }; + + if (isRedirecting) return ; + + return ( +
+

Your Cart ({cartItems.length}):

+ {cartItems.length > 0 ? ( + <> + {cartItems.map((item, idx) => ( + + ))} + +
+

Your Total

+

${displayNumbers(totalPrice)}

+
+ + + +
+
+ + ) : ( + No Items. + )} +
+ ); +}; + +export default CartSection; diff --git a/src/helpers/validations/cartItemSchema.ts b/src/helpers/validations/cartItemSchema.ts index 9e3c61c..288bb4f 100644 --- a/src/helpers/validations/cartItemSchema.ts +++ b/src/helpers/validations/cartItemSchema.ts @@ -6,6 +6,7 @@ export const cartItemSchema = z.object({ name: z.string(), image: z.string(), size: z.string(), + price: z.number(), }); export const cartsSchema = z.array(cartItemSchema); diff --git a/src/hooks/useLocalStorage.tsx b/src/hooks/useLocalStorage.tsx index 8890e65..abe838a 100644 --- a/src/hooks/useLocalStorage.tsx +++ b/src/hooks/useLocalStorage.tsx @@ -3,7 +3,7 @@ //* LIB import { useEffect, useState } from 'react'; -const useLocalStorage = (key: string, initialValue: T): [T, (value: T | ((prop: T) => T)) => void] => { +const useLocalStorage = (key: string, initialValue: T): [T, (value: T | ((prop: T) => T)) => void, () => void] => { const [storedValue, setStoredValue] = useState(initialValue); useEffect(() => { @@ -25,7 +25,19 @@ const useLocalStorage = (key: string, initialValue: T): [T, (value: T | ((pr console.error(error); } }; - return [storedValue, setValue]; + + const clearStorage = () => { + try { + setStoredValue(initialValue); + if (typeof window !== 'undefined') { + window.localStorage.removeItem(key); + } + } catch (error) { + console.error(error); + } + }; + + return [storedValue, setValue, clearStorage]; }; export default useLocalStorage; diff --git a/src/providers/CartContextProvider.tsx b/src/providers/CartContextProvider.tsx index e9b3532..9dd03ee 100644 --- a/src/providers/CartContextProvider.tsx +++ b/src/providers/CartContextProvider.tsx @@ -11,16 +11,18 @@ import { CartItem } from '@/src/types/types'; interface CartContextValues { cartItems: CartItem[]; setCartItems: (cartItems: CartItem[]) => void; + clearStorage: () => void; } export const CartContext = React.createContext({ cartItems: [], setCartItems: () => {}, + clearStorage: () => {}, }); export const useCartContext = () => React.useContext(CartContext); const CartContextProvider = ({ children }: { children: React.ReactNode }) => { - const [cartItems, setCartItems] = useLocalStorage('carts', []); + const [cartItems, setCartItems, clearStorage] = useLocalStorage('carts', []); const parsedCartItems = (cartItems: CartItem[]) => { try { @@ -34,6 +36,7 @@ const CartContextProvider = ({ children }: { children: React.ReactNode }) => { const data = { cartItems: parsedCartItems(cartItems), setCartItems, + clearStorage, }; return {children};