summaryrefslogtreecommitdiff
path: root/src-migrate/pages
diff options
context:
space:
mode:
authorIT Fixcomart <it@fixcomart.co.id>2025-05-31 02:33:14 +0000
committerIT Fixcomart <it@fixcomart.co.id>2025-05-31 02:33:14 +0000
commit2a1dea70b8f0062fe8eebeb7139a7b77a24e220b (patch)
treee7a5db13b2655cbdfb1c81859e240652fdc87bbb /src-migrate/pages
parent0b2e31247d4fe7eb1432079979478a0cfc38d049 (diff)
parent2732c04b36f98a25895826b28003b1e2c56ad952 (diff)
Merged in fix_responsive_cart (pull request #413)
<miqdad> Fix unresponsive cart
Diffstat (limited to 'src-migrate/pages')
-rw-r--r--src-migrate/pages/shop/cart/index.tsx399
1 files changed, 265 insertions, 134 deletions
diff --git a/src-migrate/pages/shop/cart/index.tsx b/src-migrate/pages/shop/cart/index.tsx
index 24baa933..03854d79 100644
--- a/src-migrate/pages/shop/cart/index.tsx
+++ b/src-migrate/pages/shop/cart/index.tsx
@@ -1,6 +1,6 @@
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 { toast } from 'react-hot-toast';
@@ -14,127 +14,137 @@ 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,
+ syncSelectedItemsWithCookie,
+ setAllSelectedInCookie,
+ removeSelectedItemsFromCookie,
+ removeCartItemsFromCookie,
+ checkboxUpdateState,
+ quantityUpdateState,
+} from '~/utils/cart';
+
+const SELECT_ALL_ID = 'select_all_checkbox';
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 [isAnyCheckboxUpdating, setIsAnyCheckboxUpdating] = useState(false);
+ const [isAnyQuantityUpdating, setIsAnyQuantityUpdating] = useState(false);
+ // Subscribe to update state changes
useEffect(() => {
- const handleScroll = () => {
- setIsTop(window.scrollY < 200);
- };
+ const handleCheckboxUpdate = (isUpdating) =>
+ setIsAnyCheckboxUpdating(isUpdating);
+ const handleQuantityUpdate = (isUpdating) =>
+ setIsAnyQuantityUpdating(isUpdating);
+
+ checkboxUpdateState.addListener(handleCheckboxUpdate);
+ quantityUpdateState.addListener(handleQuantityUpdate);
- window.addEventListener('scroll', handleScroll);
return () => {
- window.removeEventListener('scroll', handleScroll);
+ checkboxUpdateState.removeListener(handleCheckboxUpdate);
+ quantityUpdateState.removeListener(handleQuantityUpdate);
};
}, []);
+ // Handle scroll for sticky header styling
useEffect(() => {
- if (typeof auth === 'object' && !cart) {
- loadCart(auth.id);
- setIsStepApproval(auth?.feature?.soApproval);
- }
- }, [auth, loadCart, cart, isButtonChek]);
+ const handleScroll = () => setIsTop(window.scrollY < 200);
- useEffect(() => {
- if (typeof auth === 'object' && !cart) {
- loadCart(auth.id);
- setIsStepApproval(auth?.feature?.soApproval);
- }
- }, [auth, loadCart, cart, isButtonChek]);
+ window.addEventListener('scroll', handleScroll);
+ return () => window.removeEventListener('scroll', handleScroll);
+ }, []);
+ // Initialize cart and sync with cookies
useEffect(() => {
- const hasSelectedChanged = () => {
- if (prevCartRef.current && cart) {
- const prevCart = prevCartRef.current;
- return cart.products.some(
- (item, index) =>
- prevCart[index] && prevCart[index].selected !== item.selected
- );
+ const initializeCart = async () => {
+ if (typeof auth === 'object' && !cart) {
+ await loadCart(auth.id);
+ setIsStepApproval(auth?.feature?.soApproval);
+
+ if (cart?.products) {
+ const { items, needsUpdate } = syncSelectedItemsWithCookie(
+ cart.products
+ );
+
+ 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]);
+ initializeCart();
+ }, [auth, cart, loadCart, updateCartItem]);
+ // Computed values
const hasSelectedPromo = useMemo(() => {
- if (!cart) return false;
- return cart?.products?.some(
- (item) => item.cart_type === 'promotion' && item.selected
+ return (
+ cart?.products?.some(
+ (item) => item.cart_type === 'promotion' && item.selected
+ ) || false
);
}, [cart]);
const hasSelected = useMemo(() => {
- if (!cart) return false;
- return cart?.products?.some((item) => item.selected);
+ return cart?.products?.some((item) => item.selected) || false;
}, [cart]);
const hasSelectNoPrice = useMemo(() => {
- if (!cart) return false;
- return cart?.products?.some(
- (item) => item.selected && item.price.price_discount === 0
+ return (
+ cart?.products?.some(
+ (item) => item.selected && item.price.price_discount === 0
+ ) || false
);
}, [cart]);
const hasSelectedAll = useMemo(() => {
- if (!cart || !Array.isArray(cart.products)) return false;
+ if (!cart?.products?.length) return false;
return 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]);
+ // Button states
+ const areButtonsDisabled =
+ isUpdating ||
+ isLoadDelete ||
+ isAnyCheckboxUpdating ||
+ isAnyQuantityUpdating;
+ const isSelectAllDisabled =
+ isUpdating || checkboxUpdateState.isCheckboxUpdating(SELECT_ALL_ID);
+ // Handlers
const handleCheckout = () => {
+ if (areButtonsDisabled) {
+ toast.error('Harap tunggu pembaruan selesai');
+ return;
+ }
router.push('/shop/checkout');
};
const handleQuotation = () => {
+ if (areButtonsDisabled) {
+ toast.error('Harap tunggu pembaruan selesai');
+ return;
+ }
if (hasSelectedPromo || !hasSelected) {
toast.error('Maaf, Barang promo tidak dapat dibuat quotation');
} else {
@@ -142,22 +152,63 @@ const CartPage = () => {
}
};
- const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
- if (cart) {
+ const handleSelectAll = async (e: React.ChangeEvent<HTMLInputElement>) => {
+ if (!cart || isUpdating || typeof auth !== 'object') return;
+
+ const newSelectedState = !hasSelectedAll;
+ setIsUpdating(true);
+ checkboxUpdateState.startUpdate(SELECT_ALL_ID);
+
+ try {
+ // 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);
- }
+
+ // Update cookies
+ const productIds = cart.products.map((item) => item.id);
+ setAllSelectedInCookie(productIds, newSelectedState, false);
+
+ // Update server
+ const updatePromises = cart.products.map((item) =>
+ upsertUserCart({
+ userId: auth.id,
+ type: item.cart_type,
+ id: item.id,
+ qty: item.quantity,
+ selected: newSelectedState,
+ purchase_tax_id: item.purchase_tax_id || null,
+ })
+ );
+
+ await Promise.all(updatePromises);
+ await loadCart(auth.id);
+ } catch (error) {
+ console.error('Error updating select all:', error);
+ toast.error('Gagal memperbarui pilihan');
+
+ // Revert on error
+ const revertedCart = {
+ ...cart,
+ products: cart.products.map((item) => ({
+ ...item,
+ selected: !newSelectedState,
+ })),
+ };
+ updateCartItem(revertedCart);
+ setAllSelectedInCookie(
+ cart.products.map((item) => item.id),
+ !newSelectedState,
+ false
+ );
+ } finally {
+ setIsUpdating(false);
+ checkboxUpdateState.endUpdate(SELECT_ALL_ID);
}
};
@@ -165,53 +216,124 @@ const CartPage = () => {
if (typeof auth !== 'object' || !cart) return;
setIsLoadDelete(true);
- for (const item of cart.products) {
- if (item.selected === true) {
+ checkboxUpdateState.startUpdate('delete_operation');
+
+ try {
+ const itemsToDelete = cart.products.filter((item) => item.selected);
+ const itemIdsToDelete = itemsToDelete.map((item) => item.id);
+ const cartIdsToDelete = itemsToDelete.map((item) => item.cart_id);
+
+ // Delete from server
+ for (const item of itemsToDelete) {
await deleteUserCart(auth.id, [item.cart_id]);
- await loadCart(auth.id);
}
+
+ // Update local state optimistically
+ const updatedProducts = cart.products.filter((item) => !item.selected);
+ const updatedCart = {
+ ...cart,
+ products: updatedProducts,
+ product_total: updatedProducts.length,
+ };
+ updateCartItem(updatedCart);
+
+ // Clean up cookies
+ removeSelectedItemsFromCookie(itemIdsToDelete);
+ removeCartItemsFromCookie(cartIdsToDelete);
+
+ // Reload from server
+ loadCart(auth.id).catch((error) =>
+ console.error('Error reloading cart:', error)
+ );
+
+ setRefreshCart(true);
+ toast.success('Item berhasil dihapus');
+ } catch (error) {
+ console.error('Failed to delete cart items:', error);
+ toast.error('Gagal menghapus item');
+ loadCart(auth.id);
+ } finally {
+ setIsLoadDelete(false);
+ checkboxUpdateState.endUpdate('delete_operation');
}
- setIsLoadDelete(false);
- setRefreshCart(true);
+ };
+
+ // Tooltip messages
+ const getTooltipMessage = () => {
+ if (isAnyQuantityUpdating) return 'Harap tunggu update quantity selesai';
+ if (isAnyCheckboxUpdating) return 'Harap tunggu pembaruan checkbox selesai';
+ if (isLoadDelete) return 'Harap tunggu penghapusan selesai';
+ if (isUpdating) return 'Harap tunggu pembaruan selesai';
+ return '';
+ };
+
+ const getQuotationTooltip = () => {
+ const baseMessage = getTooltipMessage();
+ if (baseMessage) return baseMessage;
+ if (hasSelectedPromo) return 'Barang promo tidak dapat dibuat quotation';
+ if (!hasSelected) return 'Tidak ada item yang dipilih';
+ return '';
+ };
+
+ const getCheckoutTooltip = () => {
+ const baseMessage = getTooltipMessage();
+ if (baseMessage) return baseMessage;
+ if (!hasSelected) return 'Tidak ada item yang dipilih';
+ if (hasSelectNoPrice) return 'Terdapat item yang tidak ada harga';
+ return '';
+ };
+
+ const getDeleteTooltip = () => {
+ const baseMessage = getTooltipMessage();
+ if (baseMessage) return baseMessage;
+ if (!hasSelected) return 'Tidak ada item yang dipilih';
+ return '';
};
return (
<>
+ {/* Sticky Header */}
<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>
+ </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={handleSelectAll}
+ isDisabled={isSelectAllDisabled}
+ opacity={isSelectAllDisabled ? 0.5 : 1}
+ cursor={isSelectAllDisabled ? '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'>
- <Tooltip
- label={clsxm({
- 'Tidak ada item yang dipilih': !hasSelected,
- })}
- >
+
+ <div className='flex items-center object-center'>
+ <Tooltip label={getDeleteTooltip()}>
<Button
bg='#fadede'
variant='outline'
colorScheme='red'
- w='full'
- isDisabled={!hasSelected}
+ w='auto'
+ size={device.isMobile ? 'sm' : 'md'}
+ isDisabled={!hasSelected || areButtonsDisabled}
onClick={handleDelete}
>
{isLoadDelete && <Spinner size='xs' />}
@@ -223,19 +345,20 @@ const CartPage = () => {
</div>
</div>
- <div className={style['content']}>
+ {/* Main 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'
@@ -261,17 +384,28 @@ const CartPage = () => {
)}
</div>
</div>
+
+ {/* Cart Summary */}
<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
@@ -281,34 +415,31 @@ const CartPage = () => {
: style['summary-buttons']
}
>
- <Tooltip
- label={
- hasSelectedPromo &&
- 'Barang promo tidak dapat dibuat quotation'
- }
- >
+ <Tooltip label={getQuotationTooltip()}>
<Button
colorScheme='yellow'
w='full'
- isDisabled={hasSelectedPromo || !hasSelected}
+ isDisabled={
+ hasSelectedPromo || !hasSelected || areButtonsDisabled
+ }
onClick={handleQuotation}
>
+ {areButtonsDisabled && <Spinner size='sm' mr={2} />}
Quotation
</Button>
</Tooltip>
+
{!isStepApproval && (
- <Tooltip
- label={clsxm({
- 'Tidak ada item yang dipilih': !hasSelected,
- 'Terdapat item yang tidak ada harga': hasSelectNoPrice,
- })}
- >
+ <Tooltip label={getCheckoutTooltip()}>
<Button
colorScheme='red'
w='full'
- isDisabled={!hasSelected || hasSelectNoPrice}
+ isDisabled={
+ !hasSelected || hasSelectNoPrice || areButtonsDisabled
+ }
onClick={handleCheckout}
>
+ {areButtonsDisabled && <Spinner size='sm' mr={2} />}
Checkout
</Button>
</Tooltip>
@@ -321,4 +452,4 @@ const CartPage = () => {
);
};
-export default CartPage;
+export default CartPage; \ No newline at end of file