import React, { useState, useRef, useEffect } from 'react' import { isNull, isString } from 'lodash' import { useTranslations } from 'next-intl' import { useAppDispatch } from '@/store/hooks' import { isValidAddress } from '@/utils/isValidAddress' import { shortenAddress } from '@/utils/shortenAddress' import { useBridgeState, useBridgeDisplayState } from '@/slices/bridge/hooks' import { setDestinationAddress, clearDestinationAddress, } from '@/slices/bridge/reducer' import { setShowDestinationWarning, setIsDestinationWarningAccepted, } from '@/slices/bridgeDisplaySlice' import { Address } from 'viem' import { isEmptyString } from '@/utils/isEmptyString' import { CloseButton } from '@/components/ui/CloseButton' import { useTransactionsState } from '@/slices/transactions/hooks' import { TransactionsState } from '@/slices/transactions/reducer' import { BridgeTransaction } from '@/slices/api/generated' import { getValidAddress } from '@/utils/isValidAddress' import { useKeyPress } from '@/utils/hooks/useKeyPress' import useCloseOnOutsideClick from '@/utils/hooks/useCloseOnOutsideClick' export const DestinationAddressInput = ({ connectedAddress, }: { connectedAddress: string }) => { const dispatch = useAppDispatch() const t = useTranslations('Bridge') const { destinationAddress } = useBridgeState() const { showDestinationWarning } = useBridgeDisplayState() const { userHistoricalTransactions }: TransactionsState = useTransactionsState() const recipientList = filterTxsByRecipient( userHistoricalTransactions, connectedAddress ) const filteredRecipientList = filterNewestTxByRecipient(recipientList) const isInputValidAddress: boolean = destinationAddress ? isValidAddress(destinationAddress) : false const isInputInvalid: boolean = (destinationAddress && isString(destinationAddress) && isEmptyString(destinationAddress)) || (destinationAddress && !isInputValidAddress) const isSameAddress = connectedAddress && isInputValidAddress && getValidAddress(destinationAddress) === getValidAddress(connectedAddress) useEffect(() => { const showWarning = isInputValidAddress && !isSameAddress if (showWarning && !showDestinationWarning) { dispatch(setShowDestinationWarning(true)) } if (!isInputValidAddress && showDestinationWarning) { dispatch(setShowDestinationWarning(false)) dispatch(setIsDestinationWarningAccepted(false)) } }, [ dispatch, connectedAddress, destinationAddress, showDestinationWarning, isInputValidAddress, ]) const inputRef = useRef(null) const [isInputFocused, setIsInputFocused] = useState(false) const handleInputFocus = () => { setIsInputFocused(true) setShowRecipientList(true) if (inputRef.current) { inputRef.current.focus() } } const handleInputBlur = () => { setIsInputFocused(false) if (inputRef.current) { inputRef.current.blur() } if (destinationAddress && isInputInvalid) { handleClearInput() } } const handleClearInput = () => { dispatch(clearDestinationAddress()) if (inputRef.current) { inputRef.current.value = '' } } const onClearUserInput = () => { handleClearInput() handleInputBlur() } useEffect(() => { dispatch(clearDestinationAddress()) handleClearInput() }, [connectedAddress]) useEffect(() => { if (!isInputFocused && isSameAddress) { handleClearInput() } }, [isSameAddress, destinationAddress, connectedAddress, isInputFocused]) let placeholder if (isInputFocused) { placeholder = '' } else { placeholder = connectedAddress ? shortenAddress(connectedAddress) : '0x...' } let inputValue if (!destinationAddress) { inputValue = '' } else { inputValue = isInputValidAddress && !isInputFocused ? shortenAddress(destinationAddress) : destinationAddress } const listRef = useRef(null) const [showRecipientList, setShowRecipientList] = useState(false) const [currentIdx, setCurrentIdx] = useState(null) const handleCloseList = () => { if (showRecipientList) { setShowRecipientList(false) } setCurrentIdx(null) } const handlePaste = async () => { const pastedValue = await navigator.clipboard.readText() dispatch(setDestinationAddress(pastedValue as Address)) } const onSelectRecipient = (address) => { dispatch(setDestinationAddress(address)) handleInputBlur() handleCloseList() } useCloseOnOutsideClick(listRef, handleCloseList) const escPressed = useKeyPress('Escape', true) const arrowUp = useKeyPress('ArrowUp', true) const arrowDown = useKeyPress('ArrowDown', true) const enterPressed = useKeyPress('Enter', true) function escFunc() { if (!showRecipientList) return if (escPressed) { handleCloseList() handleClearInput() handleInputBlur() } } function arrowDownFunc() { if (filteredRecipientList.length === 0) return if (!showRecipientList) return const nextIdx = currentIdx + 1 if (currentIdx === null) { setCurrentIdx(0) } else if (arrowDown && nextIdx < filteredRecipientList.length) { setCurrentIdx(nextIdx) } } function arrowUpFunc() { if (filteredRecipientList.length === 0) return if (!showRecipientList) return const nextIdx = currentIdx - 1 if (arrowUp && -1 < nextIdx) { setCurrentIdx(nextIdx) } } function enterPressedFunc() { if (enterPressed && isNull(currentIdx)) { onSelectRecipient(destinationAddress) } if (enterPressed && !isNull(currentIdx) && currentIdx > -1) { onSelectRecipient(filteredRecipientList[currentIdx]?.toAddress) } } useEffect(enterPressedFunc, [enterPressed]) useEffect(escFunc, [escPressed]) useEffect(arrowDownFunc, [arrowDown]) useEffect(arrowUpFunc, [arrowUp]) const adjustInputSize = () => { const addressInput: HTMLElement = document.getElementById('address-input') if (!addressInput) return if (isInputFocused || isInputInvalid) { addressInput.style.width = '12rem' } else if (inputValue.length > 0) { addressInput.style.width = inputValue.length + 2 + 'ch' } else { addressInput.style.width = placeholder.length + 'ch' } } useEffect(() => { adjustInputSize() }, [inputValue, placeholder, isInputFocused, showRecipientList]) return (
{t('To')}:
dispatch(setDestinationAddress(e.target.value as Address)) } onFocus={handleInputFocus} onBlur={handleInputBlur} placeholder={placeholder} value={inputValue} className={` transform-gpu transition-all duration-175 cursor-pointer max-w-36 md:max-w-48 text-sm rounded-sm text-strong py-0.5 pl-1.5 z-0 border-0 bg-transparent focus:text-white focus:border-transparent focus:outline-none focus:ring-0 ${isInputFocused || isInputInvalid ? 'text-left ' : 'text-center'} ${destinationAddress ? 'pr-6' : 'pr-1.5'} `} /> {destinationAddress ? ( ) : isInputFocused ? ( ) : null}
{showRecipientList && (
    {filteredRecipientList?.map((recipient, index) => { return ( ) })}
)}
) } const ListRecipient = ({ index, address, daysAgo, selectedListItem, onSelectRecipient, }: { index: number address: string daysAgo: number selectedListItem: number | null onSelectRecipient?: (destinationAddress: Address) => void }) => { const handleMouseDown = (event: React.MouseEvent) => { event.preventDefault() onSelectRecipient && onSelectRecipient(address as Address) } const t = useTranslations('Time') return (
{shortenAddress(address)}
{daysAgo} {t('d')}
) } const filterTxsByRecipient = ( transactions: BridgeTransaction[], connectedAddress?: string ): { toAddress: string | undefined time: string | undefined daysAgo: number }[] => { const checkAddress = getValidAddress(connectedAddress) return transactions ?.filter( (transaction) => getValidAddress(transaction.toInfo?.address) !== checkAddress ) .map((transaction) => ({ toAddress: getValidAddress(transaction.toInfo?.address), time: transaction.toInfo?.formattedTime, daysAgo: calculateDaysAgo(transaction.toInfo?.formattedTime), })) } const filterNewestTxByRecipient = ( transactions: { toAddress: string | undefined time: string | undefined daysAgo: number }[] ) => { const newestTxMap = new Map() transactions?.forEach((tx) => { const existingTx = newestTxMap.get(tx.toAddress) if (!existingTx || tx.daysAgo < existingTx.daysAgo) { newestTxMap.set(tx.toAddress, tx) } }) return Array.from(newestTxMap.values()) } const calculateDaysAgo = (time?: string) => { if (time) { // Assuming timeString is in "YYYY-MM-DD HH:MM:SS +0000 UTC" format const formattedTimeString = time.replace(' +0000 UTC', 'Z') const timeDate = new Date(formattedTimeString) const now = new Date() return calculateDaysBetween(timeDate, now) } else { return null } } const calculateDaysBetween = (startDate: Date, endDate: Date) => { const msPerDay = 1000 * 60 * 60 * 24 const utc1 = Date.UTC( startDate.getFullYear(), startDate.getMonth(), startDate.getDate() ) const utc2 = Date.UTC( endDate.getFullYear(), endDate.getMonth(), endDate.getDate() ) return Math.floor((utc2 - utc1) / msPerDay) } const PasteButton = ({ onPaste }: { onPaste: () => Promise }) => { return ( e.preventDefault()} className={` absolute border-transparent cursor-pointer right-0.5 mt-0.5 justify-self-end fill-zinc-100 stroke-zinc-100 hover:opacity-70 `} > ) }