summaryrefslogtreecommitdiff
path: root/src-migrate/pages
diff options
context:
space:
mode:
authorMiqdad <ahmadmiqdad27@gmail.com>2025-05-19 11:02:19 +0700
committerMiqdad <ahmadmiqdad27@gmail.com>2025-05-19 11:02:19 +0700
commit7d4445bb9bad3d6c945503086a07bd882536e5f6 (patch)
treea24e5b110887fd96ee7803c7857a254c3aeb9590 /src-migrate/pages
parent746a11b810ae9e8a974a76d0548297cd0faff9b5 (diff)
<miqdad> fix unresponsive cart select
Diffstat (limited to 'src-migrate/pages')
-rw-r--r--src-migrate/pages/shop/cart/index.tsx395
1 files changed, 289 insertions, 106 deletions
diff --git a/src-migrate/pages/shop/cart/index.tsx b/src-migrate/pages/shop/cart/index.tsx
index 24baa933..475a4259 100644
--- a/src-migrate/pages/shop/cart/index.tsx
+++ b/src-migrate/pages/shop/cart/index.tsx
@@ -1,8 +1,16 @@
import style from './cart.module.css';
-import React, { useEffect, useMemo, useRef, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import Link from 'next/link';
-import { Button, Checkbox, Spinner, Tooltip } from '@chakra-ui/react';
+import {
+ Button,
+ Checkbox,
+ Spinner,
+ Tooltip,
+ Text,
+ Box,
+ Flex,
+} from '@chakra-ui/react';
import { toast } from 'react-hot-toast';
import { useRouter } from 'next/router';
import { getAuth } from '~/libs/auth';
@@ -14,26 +22,125 @@ import clsxm from '~/libs/clsxm';
import useDevice from '@/core/hooks/useDevice';
import CartSummaryMobile from '~/modules/cart/components/CartSummaryMobile';
import Image from '~/components/ui/image';
-import { CartItem } from '~/types/cart';
import { deleteUserCart, upsertUserCart } from '~/services/cart';
import { Trash2Icon } from 'lucide-react';
import { useProductCartContext } from '@/contexts/ProductCartContext';
+import {
+ getSelectedItemsFromCookie,
+ setSelectedItemsToCookie,
+ syncSelectedItemsWithCookie,
+ setAllSelectedInCookie,
+ removeSelectedItemsFromCookie,
+ forceResetAllSelectedItems,
+} from '~/utils/cart';
const CartPage = () => {
const router = useRouter();
const auth = getAuth();
const [isStepApproval, setIsStepApproval] = useState(false);
- const [isSelectedAll, setIsSelectedAll] = useState(false);
- const [isButtonChek, setIsButtonChek] = useState(false);
- const [buttonSelectNow, setButtonSelectNow] = useState(true);
const [isLoad, setIsLoad] = useState<boolean>(false);
const [isLoadDelete, setIsLoadDelete] = useState<boolean>(false);
const { loadCart, cart, summary, updateCartItem } = useCartStore();
- const useDivvice = useDevice();
+ const device = useDevice();
const { setRefreshCart } = useProductCartContext();
const [isTop, setIsTop] = useState(true);
- const [hasChanged, setHasChanged] = useState(false);
- const prevCartRef = useRef<CartItem[] | null>(null);
+ const [isUpdating, setIsUpdating] = useState(false);
+ const [isStateMismatch, setIsStateMismatch] = useState(false);
+
+ // Function to check if cart state is inconsistent
+ const checkCartStateMismatch = () => {
+ if (!cart || !cart.products || isUpdating) return false;
+
+ try {
+ // Ambil status selected dari cookie
+ const selectedItems = getSelectedItemsFromCookie();
+
+ // Periksa ketidaksesuaian antara UI dan cookie
+ // 1. Periksa item yang selected di UI tapi tidak di cookie
+ for (const product of cart.products) {
+ const cookieState = selectedItems[product.id];
+
+ // Jika ada di cookie tapi tidak sama dengan UI
+ if (cookieState !== undefined && cookieState !== product.selected) {
+ return true;
+ }
+
+ // Jika tidak ada di cookie tapi selected di UI
+ if (cookieState === undefined && product.selected) {
+ return true;
+ }
+ }
+
+ // 2. Periksa item yang selected di cookie tapi tidak ada di cart
+ for (const productId in selectedItems) {
+ const isSelected = selectedItems[productId];
+ if (isSelected) {
+ // Cek apakah product id ini ada di cart
+ const productExists = cart.products.some(
+ (p) => p.id.toString() === productId.toString()
+ );
+ if (!productExists) {
+ // Ada item selected di cookie yang tidak ada di cart
+ return true;
+ }
+ }
+ }
+
+ return false;
+ } catch (error) {
+ console.error('Error checking cart state mismatch:', error);
+ return false;
+ }
+ }; // Function to reset all selected items when state is inconsistent
+ const handleResetSelections = () => {
+ if (!cart) return;
+
+ setIsUpdating(true);
+ try {
+ // Use the forceResetSelection function from the store
+ useCartStore.getState().forceResetSelection();
+
+ // Set state back to normal
+ setIsStateMismatch(false);
+
+ // Give visual feedback
+ toast.success('Semua pilihan telah direset');
+
+ // Optional: Sync with server if needed
+ if (typeof auth === 'object') {
+ const updatePromises = cart.products.map((item) =>
+ upsertUserCart({
+ userId: auth.id,
+ type: item.cart_type,
+ id: item.id,
+ qty: item.quantity,
+ selected: false,
+ })
+ );
+
+ Promise.all(updatePromises)
+ .then(() => loadCart(auth.id))
+ .catch((error) => {
+ console.error('Error updating selections to server:', error);
+ })
+ .finally(() => setIsUpdating(false));
+ } else {
+ setIsUpdating(false);
+ }
+ } catch (error) {
+ console.error('Error resetting selections:', error);
+ setIsUpdating(false);
+ toast.error('Gagal mereset pilihan');
+ }
+ };
+
+ // Check for state inconsistency
+ useEffect(() => {
+ if (!cart || !cart.products || isUpdating) return;
+
+ const hasMismatch = checkCartStateMismatch();
+ setIsStateMismatch(hasMismatch);
+ }, [cart, isUpdating]);
useEffect(() => {
const handleScroll = () => {
@@ -47,40 +154,35 @@ const CartPage = () => {
}, []);
useEffect(() => {
- if (typeof auth === 'object' && !cart) {
- loadCart(auth.id);
- setIsStepApproval(auth?.feature?.soApproval);
- }
- }, [auth, loadCart, cart, isButtonChek]);
+ const loadCartWithStorage = async () => {
+ if (typeof auth === 'object' && !cart) {
+ await loadCart(auth.id);
+ setIsStepApproval(auth?.feature?.soApproval);
- useEffect(() => {
- if (typeof auth === 'object' && !cart) {
- loadCart(auth.id);
- setIsStepApproval(auth?.feature?.soApproval);
- }
- }, [auth, loadCart, cart, isButtonChek]);
+ // Sync selected items with server data using cookies
+ if (cart?.products) {
+ const { items, needsUpdate } = syncSelectedItemsWithCookie(
+ cart.products
+ );
- useEffect(() => {
- const hasSelectedChanged = () => {
- if (prevCartRef.current && cart) {
- const prevCart = prevCartRef.current;
- return cart.products.some(
- (item, index) =>
- prevCart[index] && prevCart[index].selected !== item.selected
- );
+ // If there's a mismatch between cookie and server data, update the UI
+ if (needsUpdate) {
+ const updatedCart = {
+ ...cart,
+ products: cart.products.map((item) => ({
+ ...item,
+ selected:
+ items[item.id] !== undefined ? items[item.id] : item.selected,
+ })),
+ };
+ updateCartItem(updatedCart);
+ }
+ }
}
- return false;
};
- if (hasSelectedChanged()) {
- setHasChanged(true);
- // Perform necessary actions here if selection has changed
- } else {
- setHasChanged(false);
- }
-
- prevCartRef.current = cart ? [...cart.products] : null;
- }, [cart]);
+ loadCartWithStorage();
+ }, [auth, cart]);
const hasSelectedPromo = useMemo(() => {
if (!cart) return false;
@@ -103,38 +205,24 @@ const CartPage = () => {
const hasSelectedAll = useMemo(() => {
if (!cart || !Array.isArray(cart.products)) return false;
- return cart.products.every((item) => item.selected);
+ return (
+ cart.products.length > 0 && cart.products.every((item) => item.selected)
+ );
}, [cart]);
- useEffect(() => {
- const updateCartItems = async () => {
- if (typeof auth === 'object' && cart) {
- const upsertPromises = cart.products.map((item) =>
- upsertUserCart({
- userId: auth.id,
- type: item.cart_type,
- id: item.id,
- qty: item.quantity,
- selected: item.selected,
- })
- );
- try {
- await Promise.all(upsertPromises);
- await loadCart(auth.id);
- } catch (error) {
- console.error('Failed to update cart items:', error);
- }
- }
- };
-
- updateCartItems();
- }, [hasChanged]);
-
const handleCheckout = () => {
+ if (isUpdating || isLoadDelete) {
+ toast.error('Harap tunggu pembaruan selesai');
+ return;
+ }
router.push('/shop/checkout');
};
const handleQuotation = () => {
+ if (isUpdating || isLoadDelete) {
+ toast.error('Harap tunggu pembaruan selesai');
+ return;
+ }
if (hasSelectedPromo || !hasSelected) {
toast.error('Maaf, Barang promo tidak dapat dibuat quotation');
} else {
@@ -143,20 +231,62 @@ const CartPage = () => {
};
const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
- if (cart) {
+ if (cart && !isUpdating && typeof auth === 'object') {
+ const newSelectedState = !hasSelectedAll;
+
+ // Update UI immediately
const updatedCart = {
...cart,
products: cart.products.map((item) => ({
...item,
- selected: !hasSelectedAll,
+ selected: newSelectedState,
})),
};
updateCartItem(updatedCart);
- if (hasSelectedAll) {
- setIsSelectedAll(false);
- } else {
- setIsSelectedAll(true);
+
+ // Get all product IDs in cart
+ const productIds = cart.products.map((item) => item.id);
+
+ // Update cookies immediately for responsive UI
+ setAllSelectedInCookie(productIds, newSelectedState);
+
+ setIsUpdating(true);
+
+ try {
+ // Update all items on server in background
+ const updatePromises = cart.products.map((item) =>
+ upsertUserCart({
+ userId: auth.id,
+ type: item.cart_type,
+ id: item.id,
+ qty: item.quantity,
+ selected: newSelectedState,
+ })
+ );
+
+ await Promise.all(updatePromises);
+ await loadCart(auth.id);
+ } catch (error) {
+ console.error('Error updating select all:', error);
+
+ // Revert changes on error
+ const revertedCart = {
+ ...cart,
+ products: cart.products.map((item) => ({
+ ...item,
+ selected: !newSelectedState,
+ })),
+ };
+
+ updateCartItem(revertedCart);
+
+ // Revert cookies
+ setAllSelectedInCookie(productIds, !newSelectedState);
+
+ toast.error('Gagal memperbarui pilihan');
+ } finally {
+ setIsUpdating(false);
}
}
};
@@ -165,14 +295,25 @@ const CartPage = () => {
if (typeof auth !== 'object' || !cart) return;
setIsLoadDelete(true);
- for (const item of cart.products) {
- if (item.selected === true) {
+ try {
+ const itemsToDelete = cart.products.filter((item) => item.selected);
+ const itemIdsToDelete = itemsToDelete.map((item) => item.id);
+
+ for (const item of itemsToDelete) {
await deleteUserCart(auth.id, [item.cart_id]);
- await loadCart(auth.id);
}
+
+ // Remove deleted items from cookie
+ removeSelectedItemsFromCookie(itemIdsToDelete);
+
+ await loadCart(auth.id);
+ setRefreshCart(true);
+ } catch (error) {
+ console.error('Failed to delete cart items:', error);
+ toast.error('Gagal menghapus item');
+ } finally {
+ setIsLoadDelete(false);
}
- setIsLoadDelete(false);
- setRefreshCart(true);
};
return (
@@ -180,27 +321,45 @@ const CartPage = () => {
<div
className={`${
isTop ? 'border-b-[0px]' : 'border-b-[1px]'
- } sticky md:top-[157px] flex-col bg-white py-4 border-gray-300 z-50 sm:w-full md:w-3/4`}
+ } sticky md:top-[157px] flex-col bg-white py-4 border-gray-300 z-50 sm:w-full md:w-3/4`}
>
- <h1 className={`${style['title']}`}>Keranjang Belanja</h1>
+ <div className='flex items-center justify-between mb-2'>
+ <h1 className={style.title}>Keranjang Belanja</h1>
+ {isStateMismatch && (
+ <Button
+ colorScheme='red'
+ size='sm'
+ isLoading={isUpdating}
+ onClick={handleResetSelections}
+ >
+ Reset Pilihan
+ </Button>
+ )}
+ </div>
+
<div className='h-2' />
- <div className={`flex items-center object-center justify-between `}>
+ <div className='flex items-center object-center justify-between flex-wrap gap-2'>
<div className='flex items-center object-center'>
- {isLoad && <Spinner className='my-auto' size='sm' />}
- {!isLoad && (
- <Checkbox
- borderColor='gray.600'
- colorScheme='red'
- size='lg'
- isChecked={hasSelectedAll}
- onChange={handleChange}
- />
- )}
+ <Checkbox
+ borderColor='gray.600'
+ colorScheme='red'
+ size='lg'
+ isChecked={hasSelectedAll}
+ onChange={handleChange}
+ isDisabled={isUpdating || isLoadDelete}
+ opacity={isUpdating ? 0.5 : 1}
+ cursor={isUpdating ? 'not-allowed' : 'pointer'}
+ _disabled={{
+ opacity: 0.5,
+ cursor: 'not-allowed',
+ backgroundColor: 'gray.100',
+ }}
+ />
<p className='p-2 text-caption-2'>
{hasSelectedAll ? 'Uncheck all' : 'Select all'}
</p>
</div>
- <div className='delate all flex items-center object-center'>
+ <div className='flex items-center object-center'>
<Tooltip
label={clsxm({
'Tidak ada item yang dipilih': !hasSelected,
@@ -210,8 +369,9 @@ const CartPage = () => {
bg='#fadede'
variant='outline'
colorScheme='red'
- w='full'
- isDisabled={!hasSelected}
+ w='auto'
+ size={device.isMobile ? 'sm' : 'md'}
+ isDisabled={!hasSelected || isUpdating}
onClick={handleDelete}
>
{isLoadDelete && <Spinner size='xs' />}
@@ -223,19 +383,19 @@ const CartPage = () => {
</div>
</div>
- <div className={style['content']}>
+ <div className={style.content}>
<div className={style['item-wrapper']}>
<div className={style['item-skeleton']}>
{!cart && <CartItemModule.Skeleton count={5} height='120px' />}
</div>
- <div className={style['items']}>
+ <div className={style.items}>
{cart?.products?.map((item) => (
<CartItemModule key={item.id} item={item} />
))}
{cart?.products?.length === 0 && (
- <div className='flex flex-col items-center'>
+ <div className='flex flex-col items-center p-4'>
<Image
src='/images/empty_cart.svg'
alt='Empty Cart'
@@ -263,15 +423,24 @@ const CartPage = () => {
</div>
<div
className={`${style['summary-wrapper']} ${
- useDivvice.isMobile && cart?.product_total === 0 ? 'hidden' : ''
+ device.isMobile && (!cart || cart?.product_total === 0)
+ ? 'hidden'
+ : ''
}`}
>
- <div className={style['summary']}>
- {useDivvice.isMobile && (
- <CartSummaryMobile {...summary} isLoaded={!!cart} />
- )}
- {!useDivvice.isMobile && (
- <CartSummary {...summary} isLoaded={!!cart} />
+ <div className={style.summary}>
+ {device.isMobile ? (
+ <CartSummaryMobile
+ {...summary}
+ isLoaded={!!cart}
+ products={cart?.products}
+ />
+ ) : (
+ <CartSummary
+ {...summary}
+ isLoaded={!!cart}
+ products={cart?.products}
+ />
)}
<div
@@ -282,17 +451,24 @@ const CartPage = () => {
}
>
<Tooltip
- label={
- hasSelectedPromo &&
- 'Barang promo tidak dapat dibuat quotation'
- }
+ label={clsxm({
+ 'Barang promo tidak dapat dibuat quotation': hasSelectedPromo,
+ 'Harap tunggu pembaruan selesai': isUpdating,
+ 'Tidak ada item yang dipilih': !hasSelected,
+ })}
>
<Button
colorScheme='yellow'
w='full'
- isDisabled={hasSelectedPromo || !hasSelected}
+ isDisabled={
+ hasSelectedPromo ||
+ !hasSelected ||
+ isUpdating ||
+ isLoadDelete
+ }
onClick={handleQuotation}
>
+ {isUpdating ? <Spinner size='sm' mr={2} /> : null}
Quotation
</Button>
</Tooltip>
@@ -301,14 +477,21 @@ const CartPage = () => {
label={clsxm({
'Tidak ada item yang dipilih': !hasSelected,
'Terdapat item yang tidak ada harga': hasSelectNoPrice,
+ 'Harap tunggu pembaruan selesai': isUpdating,
})}
>
<Button
colorScheme='red'
w='full'
- isDisabled={!hasSelected || hasSelectNoPrice}
+ isDisabled={
+ !hasSelected ||
+ hasSelectNoPrice ||
+ isUpdating ||
+ isLoadDelete
+ }
onClick={handleCheckout}
>
+ {isUpdating ? <Spinner size='sm' mr={2} /> : null}
Checkout
</Button>
</Tooltip>