import { ReactNode, useEffect, useMemo, useRef, useState } from 'react'; import { __ } from '@wordpress/i18n'; import { Button, Card, CardBody, Flex, FlexItem, Icon, } from '@wordpress/components'; import { check } from '@wordpress/icons'; import { Spinner } from '@woocommerce/components'; import { Quote } from '../../types/quote'; import { useWindowSize } from '../../shared/hooks'; import { Order } from '../../types/order'; import ordersToCheckoutValidatePayload from './utils/ordersToCheckoutValidatePayload'; import { PluginSettings } from 'types/settings'; import { useBulkQuotes } from './hooks/useBulkQuotes'; import { formatCurrency } from '../../shared/utils'; export default function BulkShipOverview({ orders, onShipNow, defaultQuotes, settings, }: { orders: Order[]; onShipNow: () => void; defaultQuotes: Quote[]; settings: PluginSettings; }) { const {fetchBulkQuotes, loading: quotesLoading, error, quotes} = useBulkQuotes(); const [isVisible, setIsVisible] = useState(false); const size = useWindowSize(); const ordersPayload = ordersToCheckoutValidatePayload({ orders, settings }); const Quotes = useMemo(() => quotes ?? defaultQuotes, [quotes, defaultQuotes]); const quoteRequests = useMemo( () => ordersPayload.items.map((item) => ({ ref: item.ref, origin: { country: item.origin.useAddress.country, postcode: item.origin.useAddress.postcode, }, destination: { country: item.destination.useAddress.country, postcode: item.destination.useAddress.postcode, }, serviceSlugs: item.serviceSlug ? [item.serviceSlug] : undefined, parcels: item.parcels.map((x) => ({ ref: x.ref, weight: x.weight, height: x.height, length: x.length, width: x.width, value: x.value, })), })), // eslint-disable-next-line react-hooks/exhaustive-deps [orders, settings] ); useEffect(() => { fetchBulkQuotes(quoteRequests); }, [quoteRequests, fetchBulkQuotes]); useEffect(() => { // Trigger animation on mount const timer = setTimeout(() => setIsVisible(true), 100); return () => clearTimeout(timer); }, []); const totalValue = useMemo(() => { const shipping = Quotes?.reduce((acc, quote) => acc + quote.price.gross, 0) ?? 0; const protection = Quotes?.reduce( (acc, quote) => acc + (quote.availableExtras.find( (extra) => extra.key === 'protection:base' )?.price.gross ?? 0), 0 ) ?? 0; return (shipping + protection) / 100; }, [Quotes]); const { displayPrice, isAnimating } = useAnimatedPrice( quotesLoading ? 0 : totalValue, 800, quotesLoading ); const orderLabel = orders.length === 1 ? __('1 order', 'parcel2go-shipping') : `${orders.length} ${__('orders', 'parcel2go-shipping')}`; const BulkShipContent = useMemo( (): ReactNode => ( {!size.isXs && ( )} {(!quotesLoading && !Quotes) || Quotes?.length === 0 ? (

{__('Ship', 'parcel2go-shipping')} {orderLabel}

) : (

{__('Ship', 'parcel2go-shipping')} {orderLabel}{' '} {__('for', 'parcel2go-shipping')} {displayPrice === 0 && quotesLoading ? ( ) : ( {' '}{formatCurrency(displayPrice)} )}{' '} {__('inc VAT', 'parcel2go-shipping')}

)}
), [ displayPrice, isAnimating, onShipNow, orders.length, quotesLoading, Quotes, size.isXs, orderLabel, ] ); if (size.isXs) { // Mobile bottom banner with slide-in animation return (
{BulkShipContent}
); } return
{BulkShipContent}
; } // Custom hook for animated price counter function useAnimatedPrice( targetPrice: number, duration: number = 800, isLoading: boolean = false ) { const [displayPrice, setDisplayPrice] = useState(targetPrice); const [isAnimating, setIsAnimating] = useState(false); const prevPriceRef = useRef(targetPrice); const animationRef = useRef(); useEffect(() => { // Don't animate if data is loading if (isLoading) { return; } if (prevPriceRef.current !== targetPrice && targetPrice > 0) { setIsAnimating(true); const startPrice = prevPriceRef.current; const priceDiff = targetPrice - startPrice; const startTime = Date.now(); const animate = () => { const elapsed = Date.now() - startTime; const progress = Math.min(elapsed / duration, 1); // Easing function for smooth animation const easeProgress = 1 - Math.pow(1 - progress, 3); const currentPrice = startPrice + priceDiff * easeProgress; setDisplayPrice(currentPrice); if (progress < 1) { animationRef.current = requestAnimationFrame(animate); } else { setIsAnimating(false); setDisplayPrice(targetPrice); prevPriceRef.current = targetPrice; } }; animationRef.current = requestAnimationFrame(animate); } else { prevPriceRef.current = targetPrice; } return () => { if (animationRef.current) { cancelAnimationFrame(animationRef.current); } }; }, [targetPrice, duration, isLoading]); return { displayPrice, isAnimating }; }