diff options
| author | it-fixcomart <it@fixcomart.co.id> | 2025-07-29 09:46:05 +0700 |
|---|---|---|
| committer | it-fixcomart <it@fixcomart.co.id> | 2025-07-29 09:46:05 +0700 |
| commit | 077467cf53b46d8049df8b812577cd1a03011eba (patch) | |
| tree | 0dc641a9acb1237a3caca3f7f8a157a3e938c0b8 /src-migrate/modules/cart/components/ItemAction.tsx | |
| parent | 0d28dc8ff5fb8c5399e356ed6ecae4fce2019ca6 (diff) | |
| parent | dc31efb2fec4c7b79917324d922ae820c4b5bb50 (diff) | |
<hafid> merging new release
Diffstat (limited to 'src-migrate/modules/cart/components/ItemAction.tsx')
| -rw-r--r-- | src-migrate/modules/cart/components/ItemAction.tsx | 265 |
1 files changed, 209 insertions, 56 deletions
diff --git a/src-migrate/modules/cart/components/ItemAction.tsx b/src-migrate/modules/cart/components/ItemAction.tsx index 7220e362..b06e8e75 100644 --- a/src-migrate/modules/cart/components/ItemAction.tsx +++ b/src-migrate/modules/cart/components/ItemAction.tsx @@ -1,74 +1,227 @@ -import style from '../styles/item-action.module.css' +import style from '../styles/item-action.module.css'; -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react'; -import { Spinner, Tooltip } from '@chakra-ui/react' -import { MinusIcon, PlusIcon, Trash2Icon } from 'lucide-react' +import { Spinner, Tooltip } from '@chakra-ui/react'; +import { MinusIcon, PlusIcon, Trash2Icon } from 'lucide-react'; -import { CartItem } from '~/types/cart' -import { getAuth } from '~/libs/auth' -import { deleteUserCart, upsertUserCart } from '~/services/cart' +import { CartItem } from '~/types/cart'; +import { getAuth } from '~/libs/auth'; +import { deleteUserCart, upsertUserCart } from '~/services/cart'; -import { useDebounce } from 'usehooks-ts' -import { useCartStore } from '../stores/useCartStore' -import { useProductCartContext } from '@/contexts/ProductCartContext' +import { useDebounce } from 'usehooks-ts'; +import { useCartStore } from '../stores/useCartStore'; +import { useProductCartContext } from '@/contexts/ProductCartContext'; +import { + removeSelectedItemsFromCookie, + removeCartItemsFromCookie, + quantityUpdateState, + getCartDataFromCookie, + setCartDataToCookie, +} from '~/utils/cart'; -type Props = { - item: CartItem +import { toast } from 'react-hot-toast'; + +interface Props { + item: CartItem; } -const CartItemAction = ({ item }: Props) => { - const auth = getAuth() - const { setRefreshCart } = useProductCartContext() - const [isLoadDelete, setIsLoadDelete] = useState<boolean>(false) - const [isLoadQuantity, setIsLoadQuantity] = useState<boolean>(false) +const CartItemAction: React.FC<Props> = ({ item }) => { + const auth = getAuth(); + const { setRefreshCart } = useProductCartContext(); + const [isLoadDelete, setIsLoadDelete] = useState<boolean>(false); + const [isLoadQuantity, setIsLoadQuantity] = useState<boolean>(false); + + const [quantity, setQuantity] = useState<number>(item.quantity); + + const { loadCart, cart, updateCartItem } = useCartStore(); + + const limitQty: number = item.limit_qty?.transaction || 0; + + const handleDelete = async (): Promise<void> => { + if (typeof auth !== 'object') return; + + setIsLoadDelete(true); + + try { + // Delete from server + await deleteUserCart(auth.id, [item.cart_id]); + + // Clean up cookies immediately + removeSelectedItemsFromCookie([item.id]); + removeCartItemsFromCookie([item.cart_id]); - const [quantity, setQuantity] = useState<number>(item.quantity) + // Update local cart state optimistically + if (cart) { + const updatedProducts = cart.products.filter( + (product) => product.id !== item.id + ); + const updatedCart = { + ...cart, + products: updatedProducts, + product_total: updatedProducts.length, + }; + updateCartItem(updatedCart); + } - const { loadCart } = useCartStore() + // Reload from server and refresh context + await loadCart(auth.id); + setRefreshCart(true); - const limitQty = item.limit_qty?.transaction || 0 + toast.success('Item berhasil dihapus'); + } catch (error) { + console.error('Failed to delete cart item:', error); + toast.error('Gagal menghapus item'); + + // Reload on error + await loadCart(auth.id); + } finally { + setIsLoadDelete(false); + } + }; + + const updateQuantityInCookie = ( + productId: number, + cartId: string | number, + newQuantity: number + ): boolean => { + try { + const cartData = getCartDataFromCookie() as Record<string, any>; + let itemFound = false; + const cartIdString = String(cartId); + + // Find item by cart_id key or search within objects + if (cartData[cartIdString]) { + cartData[cartIdString].quantity = newQuantity; + itemFound = true; + } else { + // Search by product id or cart_id within objects + Object.keys(cartData).forEach((key) => { + const item = cartData[key]; + if (item.id === productId || String(item.cart_id) === cartIdString) { + item.quantity = newQuantity; + itemFound = true; + } + }); + } + + if (itemFound) { + setCartDataToCookie(cartData); + return true; + } + + return false; + } catch (error) { + console.error('Error updating quantity in cookie:', error); + return false; + } + }; - const handleDelete = async () => { - if (typeof auth !== 'object') return + const decreaseQty = (): void => { + setQuantity((prevQuantity) => prevQuantity - 1); + }; - setIsLoadDelete(true) - await deleteUserCart(auth.id, [item.cart_id]) - await loadCart(auth.id) - setIsLoadDelete(false) - setRefreshCart(true) - } + const increaseQty = (): void => { + setQuantity((prevQuantity) => prevQuantity + 1); + }; + + const debounceQty = useDebounce(quantity, 1000); - const decreaseQty = () => { setQuantity((quantity) => quantity -= 1) } - const increaseQty = () => { setQuantity((quantity) => quantity += 1) } - const debounceQty = useDebounce(quantity, 1000) useEffect(() => { - if (isNaN(debounceQty)) setQuantity(1) - if (limitQty > 0 && debounceQty > limitQty) setQuantity(limitQty) - }, [debounceQty, limitQty]) + if (isNaN(debounceQty)) setQuantity(1); + if (limitQty > 0 && debounceQty > limitQty) setQuantity(limitQty); + }, [debounceQty, limitQty]); useEffect(() => { - const updateCart = async () => { - if (typeof auth !== 'object' || isNaN(debounceQty)) return - - setIsLoadQuantity(true) - await upsertUserCart({ - userId: auth.id, - type: item.cart_type, - id: item.id, - qty: debounceQty, - selected: item.selected, - }) - await loadCart(auth.id) - setIsLoadQuantity(false) - } - updateCart() + const updateCart = async (): Promise<void> => { + if (typeof auth !== 'object' || isNaN(debounceQty)) return; + if (debounceQty === item.quantity) return; + + quantityUpdateState.startUpdate(item.id); + setIsLoadQuantity(true); + + try { + // Update cookie immediately for responsive UI + updateQuantityInCookie(item.id, item.cart_id, debounceQty); + + // Update local cart state optimistically + if (cart) { + const updatedProducts = cart.products.map((product) => + product.id === item.id + ? { ...product, quantity: debounceQty } + : product + ); + const updatedCart = { + ...cart, + products: updatedProducts, + }; + updateCartItem(updatedCart); + } + + // Send update to server + await upsertUserCart({ + userId: auth.id, + type: item.cart_type, + id: item.id, + qty: debounceQty, + selected: item.selected, + }); + + // Reload from server to ensure consistency + await loadCart(auth.id); + + // Re-update cookie if server reload overwrote it + const currentCookieData = getCartDataFromCookie() as Record< + string, + any + >; + let needsReUpdate = false; + + Object.keys(currentCookieData).forEach((key) => { + const cookieItem = currentCookieData[key]; + if ( + (cookieItem.id === item.id || + String(cookieItem.cart_id) === String(item.cart_id)) && + cookieItem.quantity !== debounceQty + ) { + needsReUpdate = true; + } + }); + + if (needsReUpdate) { + updateQuantityInCookie(item.id, item.cart_id, debounceQty); + } + } catch (error) { + console.error('Error updating quantity:', error); + toast.error('Gagal mengupdate quantity'); + + // Revert changes on error + updateQuantityInCookie(item.id, item.cart_id, item.quantity); + loadCart(auth.id); + } finally { + setIsLoadQuantity(false); + quantityUpdateState.endUpdate(item.id); + } + }; + + updateCart(); //eslint-disable-next-line react-hooks/exhaustive-deps - }, [debounceQty]) + }, [debounceQty]); + + const handleQuantityInputChange = ( + e: React.ChangeEvent<HTMLInputElement> + ): void => { + const value = parseInt(e.target.value); + setQuantity(isNaN(value) ? 1 : value); + }; return ( <div className={style.actionSection}> - <button className={style.deleteButton} onClick={handleDelete} disabled={isLoadDelete}> + <button + className={style.deleteButton} + onClick={handleDelete} + disabled={isLoadDelete} + > {isLoadDelete && <Spinner size='xs' />} {!isLoadDelete && <Trash2Icon size={16} />} </button> @@ -90,8 +243,8 @@ const CartItemAction = ({ item }: Props) => { <input type='number' - className={style.quantity.toString()} - onChange={(e) => setQuantity(parseInt(e.target.value))} + className={style.quantity} + onChange={handleQuantityInputChange} value={quantity} /> @@ -106,7 +259,7 @@ const CartItemAction = ({ item }: Props) => { </Tooltip> </div> </div> - ) -} + ); +}; -export default CartItemAction
\ No newline at end of file +export default CartItemAction; |
