From 09cebc9020c4f1995a73305187bc1576e339d183 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 22 May 2025 10:05:09 +0700 Subject: disable button when updating checkboxes and change summary design --- src-migrate/modules/cart/components/ItemSelect.tsx | 43 +++++++-- src-migrate/modules/cart/components/Summary.tsx | 28 ++---- src-migrate/pages/shop/cart/index.tsx | 98 +++++++++++++------- src-migrate/utils/cart.js | 102 ++++++++++++++++++++- src-migrate/utils/checkBoxState.js | 89 ++++++++++++++++++ 5 files changed, 296 insertions(+), 64 deletions(-) create mode 100644 src-migrate/utils/checkBoxState.js (limited to 'src-migrate') diff --git a/src-migrate/modules/cart/components/ItemSelect.tsx b/src-migrate/modules/cart/components/ItemSelect.tsx index 733ee64d..70b656ec 100644 --- a/src-migrate/modules/cart/components/ItemSelect.tsx +++ b/src-migrate/modules/cart/components/ItemSelect.tsx @@ -8,6 +8,7 @@ import { toast } from 'react-hot-toast'; import { getSelectedItemsFromCookie, updateSelectedItemInCookie, + checkboxUpdateState, } from '~/utils/cart'; type Props = { @@ -19,6 +20,20 @@ const CartItemSelect = ({ item }: Props) => { const { updateCartItem, cart, loadCart } = useCartStore(); const [isUpdating, setIsUpdating] = useState(false); const [localSelected, setLocalSelected] = useState(item.selected); + const [isGlobalUpdating, setIsGlobalUpdating] = useState(false); + + // Subscribe to global checkbox update state + useEffect(() => { + const handleUpdateStateChange = (isUpdating) => { + setIsGlobalUpdating(isUpdating); + }; + + checkboxUpdateState.addListener(handleUpdateStateChange); + + return () => { + checkboxUpdateState.removeListener(handleUpdateStateChange); + }; + }, []); // Initialize local state from cookie or server useEffect(() => { @@ -54,7 +69,7 @@ const CartItemSelect = ({ item }: Props) => { setLocalSelected(item.selected); // Save this state to cookie for future use - updateSelectedItemInCookie(item.id, item.selected); + updateSelectedItemInCookie(item.id, item.selected, false); // don't notify for initial sync } }, [item.id, item.selected, localSelected, cart, updateCartItem, isUpdating]); @@ -70,9 +85,12 @@ const CartItemSelect = ({ item }: Props) => { setLocalSelected(newSelectedState); setIsUpdating(true); + // Start the update - notify global state with this checkbox's ID + checkboxUpdateState.startUpdate(item.id); + try { - // Update cookie immediately - updateSelectedItemInCookie(item.id, newSelectedState); + // The cookie update is now handled inside the function with notification + updateSelectedItemInCookie(item.id, newSelectedState, false); // We already started above // Update cart state immediately for UI responsiveness const updatedCartItems = cart.products.map((cartItem) => @@ -91,6 +109,7 @@ const CartItemSelect = ({ item }: Props) => { id: item.id, qty: item.quantity, selected: newSelectedState, + purchase_tax_id: item.purchase_tax_id || null, // Ensure null for numeric fields }); // Reload cart to ensure consistency @@ -102,18 +121,26 @@ const CartItemSelect = ({ item }: Props) => { // Revert local state on error setLocalSelected(!newSelectedState); - // Update cookie back - updateSelectedItemInCookie(item.id, !newSelectedState); + // Revert cookie change + updateSelectedItemInCookie(item.id, !newSelectedState, false); // Reload cart to get server state loadCart(auth.id); } finally { setIsUpdating(false); + + // End the update - notify global state with this checkbox's ID + checkboxUpdateState.endUpdate(item.id); } }, [auth, cart, item, isUpdating, updateCartItem, loadCart] ); + // Determine if THIS specific checkbox should be disabled - only disable + // if this specific checkbox is updating + const isDisabled = + isUpdating || checkboxUpdateState.isCheckboxUpdating(item.id); + return (
{ size='lg' isChecked={localSelected} onChange={handleChange} - isDisabled={isUpdating} - opacity={isUpdating ? 0.5 : 1} - cursor={isUpdating ? 'not-allowed' : 'pointer'} + isDisabled={isDisabled} + opacity={isDisabled ? 0.5 : 1} + cursor={isDisabled ? 'not-allowed' : 'pointer'} _disabled={{ opacity: 0.5, cursor: 'not-allowed', diff --git a/src-migrate/modules/cart/components/Summary.tsx b/src-migrate/modules/cart/components/Summary.tsx index b4fbab6b..68db6323 100644 --- a/src-migrate/modules/cart/components/Summary.tsx +++ b/src-migrate/modules/cart/components/Summary.tsx @@ -40,8 +40,6 @@ const CartSummary = ({ grandTotal: 0, }); - const bgHighlight = useColorModeValue('red.50', 'red.900'); - // This fixes hydration issues by ensuring the component only renders fully after mounting useEffect(() => { setIsMounted(true); @@ -156,15 +154,7 @@ const CartSummary = ({ } = summaryValues; return ( - +
Ringkasan Pesanan @@ -208,17 +198,15 @@ const CartSummary = ({
- - - Grand Total - - - Rp {formatCurrency(displayGrandTotal)} - - + + Grand Total + + + Rp {formatCurrency(displayGrandTotal)} +
- +
); }; diff --git a/src-migrate/pages/shop/cart/index.tsx b/src-migrate/pages/shop/cart/index.tsx index 475a4259..eefe8d09 100644 --- a/src-migrate/pages/shop/cart/index.tsx +++ b/src-migrate/pages/shop/cart/index.tsx @@ -32,8 +32,12 @@ import { setAllSelectedInCookie, removeSelectedItemsFromCookie, forceResetAllSelectedItems, + checkboxUpdateState, } from '~/utils/cart'; +// Special ID for the "select all" checkbox +const SELECT_ALL_ID = 'select_all_checkbox'; + const CartPage = () => { const router = useRouter(); const auth = getAuth(); @@ -46,6 +50,22 @@ const CartPage = () => { const [isTop, setIsTop] = useState(true); const [isUpdating, setIsUpdating] = useState(false); const [isStateMismatch, setIsStateMismatch] = useState(false); + const [isAnyCheckboxUpdating, setIsAnyCheckboxUpdating] = useState(false); + + // Subscribe to checkbox update state changes + useEffect(() => { + const handleUpdateStateChange = (isUpdating) => { + setIsAnyCheckboxUpdating(isUpdating); + }; + + // Add listener for checkbox update state changes + checkboxUpdateState.addListener(handleUpdateStateChange); + + // Cleanup listener on component unmount + return () => { + checkboxUpdateState.removeListener(handleUpdateStateChange); + }; + }, []); // Function to check if cart state is inconsistent const checkCartStateMismatch = () => { @@ -91,11 +111,14 @@ const CartPage = () => { console.error('Error checking cart state mismatch:', error); return false; } - }; // Function to reset all selected items when state is inconsistent + }; + + // 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(); @@ -115,6 +138,7 @@ const CartPage = () => { id: item.id, qty: item.quantity, selected: false, + purchase_tax_id: item.purchase_tax_id || null, // Fix integer field issue }) ); @@ -123,7 +147,9 @@ const CartPage = () => { .catch((error) => { console.error('Error updating selections to server:', error); }) - .finally(() => setIsUpdating(false)); + .finally(() => { + setIsUpdating(false); + }); } else { setIsUpdating(false); } @@ -211,7 +237,7 @@ const CartPage = () => { }, [cart]); const handleCheckout = () => { - if (isUpdating || isLoadDelete) { + if (isUpdating || isLoadDelete || isAnyCheckboxUpdating) { toast.error('Harap tunggu pembaruan selesai'); return; } @@ -219,7 +245,7 @@ const CartPage = () => { }; const handleQuotation = () => { - if (isUpdating || isLoadDelete) { + if (isUpdating || isLoadDelete || isAnyCheckboxUpdating) { toast.error('Harap tunggu pembaruan selesai'); return; } @@ -234,6 +260,12 @@ const CartPage = () => { if (cart && !isUpdating && typeof auth === 'object') { const newSelectedState = !hasSelectedAll; + // Set updating flag + setIsUpdating(true); + + // Notify checkbox update state system with the special select all ID + checkboxUpdateState.startUpdate(SELECT_ALL_ID); + // Update UI immediately const updatedCart = { ...cart, @@ -249,9 +281,7 @@ const CartPage = () => { const productIds = cart.products.map((item) => item.id); // Update cookies immediately for responsive UI - setAllSelectedInCookie(productIds, newSelectedState); - - setIsUpdating(true); + setAllSelectedInCookie(productIds, newSelectedState, false); // We're already notifying try { // Update all items on server in background @@ -262,6 +292,7 @@ const CartPage = () => { id: item.id, qty: item.quantity, selected: newSelectedState, + purchase_tax_id: item.purchase_tax_id || null, // Fix integer field issue }) ); @@ -282,11 +313,14 @@ const CartPage = () => { updateCartItem(revertedCart); // Revert cookies - setAllSelectedInCookie(productIds, !newSelectedState); + setAllSelectedInCookie(productIds, !newSelectedState, false); toast.error('Gagal memperbarui pilihan'); } finally { setIsUpdating(false); + + // End update notification + checkboxUpdateState.endUpdate(SELECT_ALL_ID); } } }; @@ -295,6 +329,8 @@ const CartPage = () => { if (typeof auth !== 'object' || !cart) return; setIsLoadDelete(true); + checkboxUpdateState.startUpdate('delete_operation'); // Use special ID for delete + try { const itemsToDelete = cart.products.filter((item) => item.selected); const itemIdsToDelete = itemsToDelete.map((item) => item.id); @@ -313,9 +349,18 @@ const CartPage = () => { toast.error('Gagal menghapus item'); } finally { setIsLoadDelete(false); + checkboxUpdateState.endUpdate('delete_operation'); } }; + // Check if buttons should be disabled + const areButtonsDisabled = + isUpdating || isLoadDelete || isAnyCheckboxUpdating; + + // Only disable the select all checkbox if it specifically is updating + const isSelectAllDisabled = + isUpdating || checkboxUpdateState.isCheckboxUpdating(SELECT_ALL_ID); + return ( <>
{ >

Keranjang Belanja

- {isStateMismatch && ( - - )}
@@ -346,9 +381,9 @@ const CartPage = () => { size='lg' isChecked={hasSelectedAll} onChange={handleChange} - isDisabled={isUpdating || isLoadDelete} - opacity={isUpdating ? 0.5 : 1} - cursor={isUpdating ? 'not-allowed' : 'pointer'} + isDisabled={isSelectAllDisabled} + opacity={isSelectAllDisabled ? 0.5 : 1} + cursor={isSelectAllDisabled ? 'not-allowed' : 'pointer'} _disabled={{ opacity: 0.5, cursor: 'not-allowed', @@ -363,6 +398,7 @@ const CartPage = () => { @@ -477,21 +510,18 @@ const CartPage = () => { label={clsxm({ 'Tidak ada item yang dipilih': !hasSelected, 'Terdapat item yang tidak ada harga': hasSelectNoPrice, - 'Harap tunggu pembaruan selesai': isUpdating, + 'Harap tunggu pembaruan selesai': areButtonsDisabled, })} > diff --git a/src-migrate/utils/cart.js b/src-migrate/utils/cart.js index 431ff530..f474cbde 100644 --- a/src-migrate/utils/cart.js +++ b/src-migrate/utils/cart.js @@ -1,5 +1,6 @@ // cart-cookie-utils.js import Cookies from 'js-cookie'; +import checkboxUpdateState from './checkBoxState'; // Constants const CART_ITEMS_COOKIE = 'cart_data'; @@ -165,9 +166,19 @@ export const syncCartWithCookie = (cart) => { * Update selected status item di cookie * @param {number} productId ID produk * @param {boolean} isSelected Status selected baru + * @param {boolean} notifyUpdate Whether to notify checkbox update state (default: true) */ -export const updateSelectedItemInCookie = (productId, isSelected) => { +export const updateSelectedItemInCookie = ( + productId, + isSelected, + notifyUpdate = true +) => { try { + // Notify checkbox update state if requested + if (notifyUpdate) { + checkboxUpdateState.startUpdate(); + } + const selectedItems = getSelectedItemsFromCookie(); selectedItems[productId] = isSelected; setSelectedItemsToCookie(selectedItems); @@ -188,6 +199,11 @@ export const updateSelectedItemInCookie = (productId, isSelected) => { } catch (error) { console.error('Error updating selected item in cookie:', error); return {}; + } finally { + // End update notification if requested + if (notifyUpdate) { + checkboxUpdateState.endUpdate(); + } } }; @@ -195,9 +211,19 @@ export const updateSelectedItemInCookie = (productId, isSelected) => { * Set semua item menjadi selected atau unselected di cookie * @param {Array} productIds Array product IDs * @param {boolean} isSelected Status selected baru + * @param {boolean} notifyUpdate Whether to notify checkbox update state (default: true) */ -export const setAllSelectedInCookie = (productIds, isSelected) => { +export const setAllSelectedInCookie = ( + productIds, + isSelected, + notifyUpdate = true +) => { try { + // Notify checkbox update state if requested + if (notifyUpdate) { + checkboxUpdateState.startUpdate(); + } + const selectedItems = getSelectedItemsFromCookie(); productIds.forEach((id) => { @@ -221,6 +247,11 @@ export const setAllSelectedInCookie = (productIds, isSelected) => { } catch (error) { console.error('Error setting all selected in cookie:', error); return {}; + } finally { + // End update notification if requested + if (notifyUpdate) { + checkboxUpdateState.endUpdate(); + } } }; @@ -262,11 +293,38 @@ export const removeCartItemsFromCookie = (cartIds) => { } }; +/** + * Hapus item selected dari cookie berdasarkan product IDs + * @param {Array} productIds Array product IDs untuk dihapus + */ +export const removeSelectedItemsFromCookie = (productIds) => { + try { + const selectedItems = getSelectedItemsFromCookie(); + + // Hapus dari selectedItems + productIds.forEach((productId) => { + if (selectedItems[productId] !== undefined) { + delete selectedItems[productId]; + } + }); + + // Simpan kembali ke cookie + setSelectedItemsToCookie(selectedItems); + + return { selectedItems }; + } catch (error) { + console.error('Error removing selected items from cookie:', error); + return {}; + } +}; + /** * Force reset semua selected items ke unselected state */ export const forceResetAllSelectedItems = () => { try { + checkboxUpdateState.startUpdate(); + const cartData = getCartDataFromCookie(); const selectedItems = {}; @@ -286,5 +344,45 @@ export const forceResetAllSelectedItems = () => { } catch (error) { console.error('Error resetting all selected items:', error); return {}; + } finally { + checkboxUpdateState.endUpdate(); } }; + +/** + * Sync selected items between cookie and cart data + * @param {Array} cartProducts Products array from cart + */ +export const syncSelectedItemsWithCookie = (cartProducts) => { + try { + if (!cartProducts || !Array.isArray(cartProducts)) { + return { items: {}, needsUpdate: false }; + } + + const selectedItems = getSelectedItemsFromCookie(); + let needsUpdate = false; + + // Check if we need to update any items based on cookie values + cartProducts.forEach((product) => { + if (product.id && selectedItems[product.id] !== undefined) { + if (product.selected !== selectedItems[product.id]) { + needsUpdate = true; + } + } else if (product.id) { + // If not in cookie, add with current value + selectedItems[product.id] = product.selected; + } + }); + + // Update the cookie with the latest values + setSelectedItemsToCookie(selectedItems); + + return { items: selectedItems, needsUpdate }; + } catch (error) { + console.error('Error syncing selected items with cookie:', error); + return { items: {}, needsUpdate: false }; + } +}; + +// Export the checkbox update state for use in components +export { checkboxUpdateState }; diff --git a/src-migrate/utils/checkBoxState.js b/src-migrate/utils/checkBoxState.js new file mode 100644 index 00000000..0c58321f --- /dev/null +++ b/src-migrate/utils/checkBoxState.js @@ -0,0 +1,89 @@ +// ~/modules/cart/utils/checkboxUpdateState.js + +/** + * Enhanced state manager for checkbox updates + * Tracks both global update state and individual checkbox update states + */ + +// Track the number of ongoing updates +let updateCount = 0; +let listeners = []; + +// Track which checkboxes are currently updating by ID +let updatingCheckboxIds = new Set(); + +const checkboxUpdateState = { + // Check if any checkboxes are currently updating (for buttons) + isUpdating: () => updateCount > 0, + + // Check if a specific checkbox is updating (for disabling just that checkbox) + isCheckboxUpdating: (itemId) => updatingCheckboxIds.has(itemId.toString()), + + // Start an update for a specific checkbox + startUpdate: (itemId = null) => { + updateCount++; + + // If an item ID is provided, mark it as updating + if (itemId !== null) { + updatingCheckboxIds.add(itemId.toString()); + } + + notifyListeners(); + return updateCount; + }, + + // End an update for a specific checkbox + endUpdate: (itemId = null) => { + updateCount = Math.max(0, updateCount - 1); + + // If an item ID is provided, remove it from updating set + if (itemId !== null) { + updatingCheckboxIds.delete(itemId.toString()); + } + + notifyListeners(); + return updateCount; + }, + + // Reset the update counter and clear all updating checkboxes + reset: () => { + updateCount = 0; + updatingCheckboxIds.clear(); + notifyListeners(); + }, + + // Get IDs of all checkboxes currently updating (for debugging) + getUpdatingCheckboxIds: () => [...updatingCheckboxIds], + + // Add a listener function to be called when update state changes + addListener: (callback) => { + if (typeof callback === 'function') { + listeners.push(callback); + + // Immediately call with current state + callback(updateCount > 0); + } + }, + + // Remove a listener + removeListener: (callback) => { + listeners = listeners.filter((listener) => listener !== callback); + }, + + // Get current counter (for debugging) + getUpdateCount: () => updateCount, +}; + +// Private function to notify all listeners of state changes +function notifyListeners() { + const isUpdating = updateCount > 0; + listeners.forEach((listener) => { + try { + listener(isUpdating); + } catch (error) { + console.error('Error in checkbox update state listener:', error); + } + }); +} + +export default checkboxUpdateState; -- cgit v1.2.3