From a1b0742f460b8ab00ab1e584eaae791e9a0ee3e6 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Tue, 11 Feb 2025 18:30:14 +0800 Subject: [PATCH 1/5] feat: add toast --- app/markets/components/markets.tsx | 10 ++++---- docs/Styling.md | 13 +++++++++++ src/components/common/StyledToast.tsx | 22 ++++++++++++++++++ .../layout/header/AccountDropdown.tsx | 5 +++- .../providers/ConnectRedirectProvider.tsx | 6 ++++- src/hooks/useStyledToast.tsx | 17 ++++++++++++++ src/hooks/useTransactionWithToast.tsx | 23 ++++--------------- 7 files changed, 72 insertions(+), 24 deletions(-) create mode 100644 src/components/common/StyledToast.tsx create mode 100644 src/hooks/useStyledToast.tsx diff --git a/app/markets/components/markets.tsx b/app/markets/components/markets.tsx index 2b9ad3c2..0652c6f7 100644 --- a/app/markets/components/markets.tsx +++ b/app/markets/components/markets.tsx @@ -6,7 +6,7 @@ import { Chain } from '@rainbow-me/rainbowkit'; import storage from 'local-storage-fallback'; import { useRouter, useSearchParams } from 'next/navigation'; import { FaSync } from 'react-icons/fa'; -import { toast } from 'react-toastify'; +import { useStyledToast } from '@/hooks/useStyledToast'; import { Button } from '@/components/common'; import Header from '@/components/layout/header/Header'; import EmptyScreen from '@/components/Status/EmptyScreen'; @@ -42,6 +42,8 @@ export default function Markets() { const router = useRouter(); const searchParams = useSearchParams(); + const toast = useStyledToast() + const { loading, markets: rawMarkets, refetch, isRefetching } = useMarkets(); const defaultNetwork = (() => { @@ -108,7 +110,7 @@ export default function Markets() { (id: string) => { setStaredIds([...staredIds, id]); storage.setItem(keys.MarketFavoritesKey, JSON.stringify([...staredIds, id])); - toast.success('Market starred', { icon: 🌟 }); + toast.success('Market starred', 'Market added to favorites', { icon: 🌟 }); }, [staredIds], ); @@ -117,7 +119,7 @@ export default function Markets() { (id: string) => { setStaredIds(staredIds.filter((i) => i !== id)); storage.setItem(keys.MarketFavoritesKey, JSON.stringify(staredIds.filter((i) => i !== id))); - toast.success('Market unstarred', { icon: 🌟 }); + toast.success('Market unstarred', 'Market removed from favorites', { icon: 🌟 }); }, [staredIds], ); @@ -317,7 +319,7 @@ export default function Markets() { }; const handleRefresh = () => { - refetch(() => toast.success('Markets refreshed')); + refetch(() => toast.success('Markets refreshed', 'Markets refreshed successfully')); }; return ( diff --git a/docs/Styling.md b/docs/Styling.md index bfd4f4ec..97835694 100644 --- a/docs/Styling.md +++ b/docs/Styling.md @@ -97,3 +97,16 @@ Use the nextui tooltip with component for consistnet styling > ``` + +## Toast + +Use `useStyledToast` hook to create toasts. + +```typescript +import { useStyledToast } from '@/hooks/useStyledToast'; + +const { success, error } = useStyledToast(); + +success('Success', 'Detail of the success'); +error('Error', 'Detail of the error'); +``` diff --git a/src/components/common/StyledToast.tsx b/src/components/common/StyledToast.tsx new file mode 100644 index 00000000..14310b4d --- /dev/null +++ b/src/components/common/StyledToast.tsx @@ -0,0 +1,22 @@ +import { TxHashDisplay } from "../TxHashDisplay"; + +export function StyledToast({ title, message }: { title: string, message?: string }) { + return ( +
+
{title}
+ {message &&
+ {message} +
} +
+ ); +} + +export function TransactionToast({ title, description, hash }: { title: string, hash?: string, description?: string }) { + return ( +
+
{title}
+ {description &&
{description}
} + +
+ ); +} diff --git a/src/components/layout/header/AccountDropdown.tsx b/src/components/layout/header/AccountDropdown.tsx index 3750007c..02a82d30 100644 --- a/src/components/layout/header/AccountDropdown.tsx +++ b/src/components/layout/header/AccountDropdown.tsx @@ -10,11 +10,14 @@ import { Avatar } from '@/components/Avatar/Avatar'; import { Name } from '@/components/common/Name'; import { getSlicedAddress } from '@/utils/address'; import { getExplorerURL } from '@/utils/external'; +import { useStyledToast } from '@/hooks/useStyledToast'; export function AccountDropdown() { const { address, chainId } = useAccount(); const { disconnect } = useDisconnect(); + const toast = useStyledToast(); + const handleDisconnectWallet = useCallback(() => { disconnect(); }, [disconnect]); @@ -22,7 +25,7 @@ export function AccountDropdown() { const handleCopyAddress = useCallback(() => { if (address) { void navigator.clipboard.writeText(address).then(() => { - toast.success('Address copied to clipboard!', { toastId: 'address-copied' }); + toast.success('Address copied', 'Address copied to clipboard'); }); } }, [address]); diff --git a/src/components/providers/ConnectRedirectProvider.tsx b/src/components/providers/ConnectRedirectProvider.tsx index a5565d34..45a0aa94 100644 --- a/src/components/providers/ConnectRedirectProvider.tsx +++ b/src/components/providers/ConnectRedirectProvider.tsx @@ -3,6 +3,8 @@ import { useRouter } from 'next/navigation'; import { toast } from 'react-toastify'; import { useAccountEffect } from 'wagmi'; +import { useStyledToast } from '@/hooks/useStyledToast'; + type ConnectRedirectContextType = { setRedirectPath: (path: string | undefined) => void; }; @@ -13,11 +15,13 @@ export function ConnectRedirectProvider({ children }: { children: ReactNode }) { const router = useRouter(); const [redirectPath, setRedirectPath] = useState(); + const toast = useStyledToast(); + useAccountEffect({ onConnect: ({ address, isReconnected }) => { if (redirectPath && !isReconnected) { router.push(`/${redirectPath}/${address}`); - toast.success('Address connected, redirecting...', { toastId: 'address-connected' }); + toast.success('Address connected', 'Redirecting to portfolio...', { toastId: 'address-connected' }); // Reset the path after redirect setRedirectPath(undefined); } diff --git a/src/hooks/useStyledToast.tsx b/src/hooks/useStyledToast.tsx new file mode 100644 index 00000000..9f610d02 --- /dev/null +++ b/src/hooks/useStyledToast.tsx @@ -0,0 +1,17 @@ +import React, { useCallback } from 'react'; +import { toast, ToastOptions } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; +import { StyledToast } from '../components/common/StyledToast'; + +export function useStyledToast() { + + const success = useCallback((title: string, message?: string, options?: ToastOptions) => { + toast.success(, options); + }, []); + + const error = useCallback((title: string, message?: string, options?: ToastOptions) => { + toast.error(, options); + }, []); + + return { success, error }; +} diff --git a/src/hooks/useTransactionWithToast.tsx b/src/hooks/useTransactionWithToast.tsx index c6e9937a..16681b77 100644 --- a/src/hooks/useTransactionWithToast.tsx +++ b/src/hooks/useTransactionWithToast.tsx @@ -5,6 +5,7 @@ import { useSendTransaction, useWaitForTransactionReceipt } from 'wagmi'; import { TxHashDisplay } from '../components/TxHashDisplay'; import { getExplorerTxURL } from '../utils/external'; import { SupportedNetworks } from '../utils/networks'; +import { StyledToast, TransactionToast } from '@/components/common/StyledToast'; type UseTransactionWithToastProps = { toastId: string; @@ -45,31 +46,21 @@ export function useTransactionWithToast({ } }, [hash, chainId]); - const renderToastContent = useCallback( - (title: string, description?: string) => ( -
-
{title}
- {description &&
{description}
} - -
- ), - [hash], - ); useEffect(() => { if (isConfirming) { - toast.loading(renderToastContent(pendingText, pendingDescription), { + toast.loading(, { toastId, onClick, closeButton: true, }); } - }, [isConfirming, pendingText, pendingDescription, toastId, onClick, renderToastContent]); + }, [isConfirming, pendingText, pendingDescription, toastId, onClick]); useEffect(() => { if (isConfirmed) { toast.update(toastId, { - render: renderToastContent(`${successText} 🎉`, successDescription), + render: , type: 'success', isLoading: false, autoClose: 5000, @@ -83,10 +74,7 @@ export function useTransactionWithToast({ if (txError) { toast.update(toastId, { render: ( -
-
{errorText}
-
{txError.message}
-
+ ), type: 'error', isLoading: false, @@ -103,7 +91,6 @@ export function useTransactionWithToast({ errorText, toastId, onClick, - renderToastContent, onSuccess, ]); From 6745ec6bc37ef519f901ff3e57c61b898f40f0f9 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Tue, 11 Feb 2025 19:01:37 +0800 Subject: [PATCH 2/5] chore: update all files to use useStyledToast --- .../components/PositionsSummaryTable.tsx | 8 +++-- app/positions/components/RebalanceModal.tsx | 30 +++++++++-------- .../components/onboarding/SetupPositions.tsx | 14 ++++---- app/rewards/components/RewardTable.tsx | 17 ++++------ .../SearchOrConnect/SearchOrConnect.tsx | 7 ++-- src/components/common/StyledToast.tsx | 2 +- src/components/supplyModal.tsx | 33 +++++++++---------- src/components/withdrawModal.tsx | 5 +-- src/hooks/useAuthorizeAgent.ts | 8 ++--- src/hooks/useMultiMarketSupply.ts | 11 ++++--- src/hooks/useRebalance.ts | 7 ++-- src/hooks/useStyledToast.tsx | 6 +++- 12 files changed, 76 insertions(+), 72 deletions(-) diff --git a/app/positions/components/PositionsSummaryTable.tsx b/app/positions/components/PositionsSummaryTable.tsx index 5d7fe82f..e69a5097 100644 --- a/app/positions/components/PositionsSummaryTable.tsx +++ b/app/positions/components/PositionsSummaryTable.tsx @@ -6,7 +6,7 @@ import Image from 'next/image'; import { BsQuestionCircle } from 'react-icons/bs'; import { IoRefreshOutline, IoChevronDownOutline } from 'react-icons/io5'; import { PiHandCoins } from 'react-icons/pi'; -import { toast } from 'react-toastify'; +import { useStyledToast } from '@/hooks/useStyledToast'; import { useAccount } from 'wagmi'; import { Button } from '@/components/common/Button'; import { TokenIcon } from '@/components/TokenIcon'; @@ -65,6 +65,8 @@ export function PositionsSummaryTable({ const [earningsPeriod, setEarningsPeriod] = useState(EarningsPeriod.Day); const { address } = useAccount(); + const toast = useStyledToast(); + const isOwner = useMemo(() => { if (!account) return false; return account === address; @@ -253,7 +255,7 @@ export function PositionsSummaryTable({ }; const handleManualRefresh = () => { - refetch(() => toast.info('Data refreshed', { icon: 🚀 })); + refetch(() => toast.info('Data updated', 'Position data updated', { icon: 🚀 })); }; return ( @@ -428,7 +430,7 @@ export function PositionsSummaryTable({ className="text-xs" onClick={() => { if (!isOwner) { - toast.error('You can only rebalance your own positions'); + toast.error('No authorization', 'You can only rebalance your own positions'); return; } setSelectedGroupedPosition(groupedPosition); diff --git a/app/positions/components/RebalanceModal.tsx b/app/positions/components/RebalanceModal.tsx index f679bf7a..073c6e40 100644 --- a/app/positions/components/RebalanceModal.tsx +++ b/app/positions/components/RebalanceModal.tsx @@ -1,7 +1,6 @@ import React, { useState, useMemo, useCallback } from 'react'; import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@nextui-org/react'; import { GrRefresh } from 'react-icons/gr'; -import { toast } from 'react-toastify'; import { parseUnits, formatUnits } from 'viem'; import { useAccount, useSwitchChain } from 'wagmi'; import { Button } from '@/components/common'; @@ -16,7 +15,7 @@ import { FromAndToMarkets } from './FromAndToMarkets'; import { RebalanceActionInput } from './RebalanceActionInput'; import { RebalanceCart } from './RebalanceCart'; import { RebalanceProcessModal } from './RebalanceProcessModal'; - +import { useStyledToast } from '@/hooks/useStyledToast'; type RebalanceModalProps = { groupedPosition: GroupedPosition; isOpen: boolean; @@ -40,6 +39,7 @@ export function RebalanceModal({ const [selectedToMarketUniqueKey, setSelectedToMarketUniqueKey] = useState(''); const [amount, setAmount] = useState('0'); const [showProcessModal, setShowProcessModal] = useState(false); + const toast = useStyledToast(); const { markets: allMarkets } = useMarkets(); const { @@ -77,12 +77,19 @@ export function RebalanceModal({ const validateInputs = () => { if (!selectedFromMarketUniqueKey || !selectedToMarketUniqueKey || !amount) { - toast.error('Please fill in all fields'); + const missingFields = []; + if (!selectedFromMarketUniqueKey) missingFields.push('"From Market"'); + if (!selectedToMarketUniqueKey) missingFields.push('"To Market"'); + if (!amount) missingFields.push('"Amount"'); + + const errorMessage = `Missing fields: ${missingFields.join(', ')}`; + + toast.error('Missing fields', errorMessage); return false; } const scaledAmount = parseUnits(amount, groupedPosition.loanAssetDecimals); if (scaledAmount <= 0) { - toast.error('Amount must be greater than zero'); + toast.error('Invalid amount', 'Amount must be greater than zero'); return false; } return true; @@ -94,7 +101,9 @@ export function RebalanceModal({ const toMarket = eligibleMarkets.find((m) => m.uniqueKey === selectedToMarketUniqueKey); if (!fromMarket || !toMarket) { - toast.error('Invalid market selection'); + const errorMessage = `Invalid ${!fromMarket ? '"From" Market' : ''}${!toMarket ? '"To" Market' : ''}`; + + toast.error('Invalid market selection', errorMessage); return null; } @@ -111,7 +120,7 @@ export function RebalanceModal({ const scaledAmount = parseUnits(amount, groupedPosition.loanAssetDecimals); if (scaledAmount > pendingBalance) { - toast.error('Insufficient balance for this action'); + toast.error('Insufficient balance', 'You don\'t have enough balance to perform this action'); return false; } return true; @@ -171,11 +180,8 @@ export function RebalanceModal({ const handleAddAction = useCallback(() => { if (!validateInputs()) return; - console.log('handleAddAction'); - const markets = getMarkets(); if (!markets) { - toast.error('Invalid markets selected'); return; } @@ -195,8 +201,6 @@ export function RebalanceModal({ selectedPosition !== undefined && BigInt(selectedPosition.supplyAssets) + BigInt(pendingDelta) === scaledAmount; - console.log('isMaxAmount', isMaxAmount); - console.log('pendingDelta', pendingDelta.toString()); // Create the action using the helper function const action = createAction(fromMarket, toMarket, scaledAmount, isMaxAmount); @@ -232,7 +236,7 @@ export function RebalanceModal({ return; } catch (error) { console.error('Failed to switch network:', error); - toast.error('Failed to switch network'); + toast.error('Something went wrong', 'Failed to switch network. Please try again'); return; } } @@ -250,7 +254,7 @@ export function RebalanceModal({ const handleManualRefresh = () => { refetch(() => { - toast.info('Data refreshed', { icon: 🚀 }); + toast.info('Data refreshed', 'Position data updated', { icon: 🚀 }); }); }; diff --git a/app/positions/components/onboarding/SetupPositions.tsx b/app/positions/components/onboarding/SetupPositions.tsx index 7cadbd24..2bc3c8bd 100644 --- a/app/positions/components/onboarding/SetupPositions.tsx +++ b/app/positions/components/onboarding/SetupPositions.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useMemo, useCallback } from 'react'; import { Slider } from '@nextui-org/react'; import { LockClosedIcon, LockOpen1Icon } from '@radix-ui/react-icons'; import Image from 'next/image'; -import { toast } from 'react-toastify'; +import { useStyledToast } from '@/hooks/useStyledToast'; import { formatUnits, parseUnits } from 'viem'; import { useChainId, useSwitchChain } from 'wagmi'; import { Button } from '@/components/common'; @@ -16,6 +16,7 @@ import { findToken } from '@/utils/tokens'; import { useOnboarding } from './OnboardingContext'; export function SetupPositions() { + const toast = useStyledToast(); const chainId = useChainId(); const { selectedToken, selectedMarkets, goToNextStep, goToPrevStep } = useOnboarding(); const { balances } = useUserBalances(); @@ -33,7 +34,7 @@ export function SetupPositions() { [chainId, selectedToken], ); - const { switchChain } = useSwitchChain(); + const { switchChainAsync } = useSwitchChain(); // Compute token balance and decimals const tokenBalance = useMemo(() => { @@ -220,18 +221,15 @@ export function SetupPositions() { const handleSupply = async () => { if (isSupplying) { - toast.info('Supplying in progress'); + toast.info('Loading', 'Supplying in progress'); return; } if (needSwitchChain && selectedToken) { try { - switchChain({ chainId: selectedToken.network }); - toast.info('Network changed, please click again to execute'); - return; + await switchChainAsync({ chainId: selectedToken.network }); } catch (switchError) { - console.error('Failed to switch network:', switchError); - toast.error('Failed to switch network'); + toast.error('Failed to switch network', 'Please try again'); return; } } diff --git a/app/rewards/components/RewardTable.tsx b/app/rewards/components/RewardTable.tsx index 9d089acb..47d43c6a 100644 --- a/app/rewards/components/RewardTable.tsx +++ b/app/rewards/components/RewardTable.tsx @@ -4,7 +4,6 @@ import { useMemo } from 'react'; import { Table, TableHeader, TableBody, TableColumn, TableRow, TableCell } from '@nextui-org/table'; import Image from 'next/image'; import Link from 'next/link'; -import { toast } from 'react-toastify'; import { Address } from 'viem'; import { useAccount, useSwitchChain } from 'wagmi'; import { Button } from '@/components/common/Button'; @@ -16,7 +15,7 @@ import { getAssetURL } from '@/utils/external'; import { getNetworkImg } from '@/utils/networks'; import { findToken } from '@/utils/tokens'; import { AggregatedRewardType } from '@/utils/types'; - +import { useStyledToast } from '@/hooks/useStyledToast'; type RewardTableProps = { account: string; rewards: AggregatedRewardType[]; @@ -31,8 +30,8 @@ export default function RewardTable({ showClaimed, }: RewardTableProps) { const { chainId } = useAccount(); - const { switchChain } = useSwitchChain(); - + const { switchChainAsync } = useSwitchChain(); + const toast = useStyledToast(); const { sendTransaction } = useTransactionWithToast({ toastId: 'claim', pendingText: 'Claiming Reward...', @@ -194,20 +193,18 @@ export default function RewardTable({ isDisabled={ tokenReward.total.claimable === BigInt(0) || distribution === undefined } - onClick={(e) => { + onClick={async(e) => { e.stopPropagation(); if (!account) { - toast.error('Connect wallet'); + toast.error('No account connected', 'Please connect your wallet to continue.'); return; } if (!distribution) { - toast.error('No claim data'); + toast.error('No claim data', 'No claim data found for this reward please try again later.'); return; } if (chainId !== distribution.distributor.chain_id) { - switchChain({ chainId: distribution.distributor.chain_id }); - toast('Click on claim again after switching network'); - return; + await switchChainAsync({ chainId: distribution.distributor.chain_id }); } sendTransaction({ account: account as Address, diff --git a/src/components/SearchOrConnect/SearchOrConnect.tsx b/src/components/SearchOrConnect/SearchOrConnect.tsx index 41a25d68..bc214ee5 100644 --- a/src/components/SearchOrConnect/SearchOrConnect.tsx +++ b/src/components/SearchOrConnect/SearchOrConnect.tsx @@ -3,15 +3,15 @@ import { useState } from 'react'; import { ArrowRightIcon } from '@radix-ui/react-icons'; import Link from 'next/link'; -import { toast } from 'react-toastify'; import { isAddress } from 'viem'; import { useAccount } from 'wagmi'; import AccountConnect from '@/components/layout/header/AccountConnect'; import Header from '@/components/layout/header/Header'; +import { useStyledToast } from '@/hooks/useStyledToast'; export default function SearchOrConnect({ path }: { path: string }) { const { address } = useAccount(); - + const toast = useStyledToast(); const [inputAddress, setInputAddress] = useState(''); return ( @@ -66,8 +66,7 @@ export default function SearchOrConnect({ path }: { path: string }) { if (isAddress(inputAddress.toLowerCase(), { strict: false })) { window.location.href = `/${path}/${inputAddress}`; } else { - console.log('inputAddress', inputAddress); - toast.error('Invalid address'); + toast.error('Invalid address', `The address you enter ${inputAddress} is not valid.`); } }} className="bg-monarch-orange justify-center p-6 text-center text-sm duration-100 ease-in-out hover:opacity-100" diff --git a/src/components/common/StyledToast.tsx b/src/components/common/StyledToast.tsx index 14310b4d..ba5de018 100644 --- a/src/components/common/StyledToast.tsx +++ b/src/components/common/StyledToast.tsx @@ -14,7 +14,7 @@ export function StyledToast({ title, message }: { title: string, message?: strin export function TransactionToast({ title, description, hash }: { title: string, hash?: string, description?: string }) { return (
-
{title}
+
{title}
{description &&
{description}
}
diff --git a/src/components/supplyModal.tsx b/src/components/supplyModal.tsx index d3c52d98..81076d1d 100644 --- a/src/components/supplyModal.tsx +++ b/src/components/supplyModal.tsx @@ -2,7 +2,6 @@ import React, { useCallback, useMemo, useState } from 'react'; import { Switch } from '@nextui-org/react'; import { Cross1Icon, ExternalLinkIcon } from '@radix-ui/react-icons'; import Image from 'next/image'; -import { toast } from 'react-toastify'; import { Address, encodeFunctionData } from 'viem'; import { useAccount, useBalance, useSwitchChain } from 'wagmi'; import morphoBundlerAbi from '@/abis/bundlerV2'; @@ -21,7 +20,7 @@ import { Button } from './common'; import { MarketInfoBlock } from './common/MarketInfoBlock'; import OracleVendorBadge from './OracleVendorBadge'; import { SupplyProcessModal } from './SupplyProcessModal'; - +import { useStyledToast } from '@/hooks/useStyledToast'; type SupplyModalProps = { market: Market; onClose: () => void; @@ -42,6 +41,8 @@ export function SupplyModal({ market, onClose }: SupplyModalProps): JSX.Element const { switchChain } = useSwitchChain(); + const toast = useStyledToast(); + const { data: tokenBalance } = useBalance({ token: market.loanAsset.address as `0x${string}`, address: account, @@ -176,11 +177,7 @@ export function SupplyModal({ market, onClose }: SupplyModalProps): JSX.Element setShowProcessModal(false); } catch (error: unknown) { setShowProcessModal(false); - if (error instanceof Error) { - toast.error('An error occurred. Please try again.'); - } else { - toast.error('An unexpected error occurred'); - } + toast.error('Supply failed', 'Supply to market failed or cancelled'); } }, [ account, @@ -194,7 +191,7 @@ export function SupplyModal({ market, onClose }: SupplyModalProps): JSX.Element const approveAndSupply = useCallback(async () => { if (!account) { - toast.error('Please connect your wallet'); + toast.info('No account connected', 'Please connect your wallet to continue.'); return; } @@ -222,12 +219,12 @@ export function SupplyModal({ market, onClose }: SupplyModalProps): JSX.Element console.error('Error in Permit2 flow:', error); if (error instanceof Error) { if (error.message.includes('User rejected')) { - toast.error('Transaction rejected by user'); + toast.error('Transaction rejected', 'Transaction rejected by user'); } else { - toast.error('Failed to process Permit2 transaction'); + toast.error('Error', 'Failed to process Permit2 transaction'); } } else { - toast.error('An unexpected error occurred'); + toast.error('Error', 'An unexpected error occurred'); } throw error; } @@ -246,12 +243,12 @@ export function SupplyModal({ market, onClose }: SupplyModalProps): JSX.Element console.error('Error in approval:', error); if (error instanceof Error) { if (error.message.includes('User rejected')) { - toast.error('Approval rejected by user'); + toast.error('Transaction rejected', 'Approval rejected by user'); } else { - toast.error('Failed to approve token'); + toast.error('Transaction Error', 'Failed to approve token'); } } else { - toast.error('An unexpected error occurred during approval'); + toast.error('Transaction Error', 'An unexpected error occurred during approval'); } throw error; } @@ -276,7 +273,7 @@ export function SupplyModal({ market, onClose }: SupplyModalProps): JSX.Element const signAndSupply = useCallback(async () => { if (!account) { - toast.error('Please connect your wallet'); + toast.info('No account connected', 'Please connect your wallet to continue.'); return; } @@ -289,12 +286,12 @@ export function SupplyModal({ market, onClose }: SupplyModalProps): JSX.Element setShowProcessModal(false); if (error instanceof Error) { if (error.message.includes('User rejected')) { - toast.error('Transaction rejected by user'); + toast.error('Transaction rejected', 'Transaction rejected by user'); } else { - toast.error('Failed to process transaction'); + toast.error('Transaction Error', 'Failed to process transaction'); } } else { - toast.error('An unexpected error occurred'); + toast.error('Transaction Error', 'An unexpected error occurred'); } } }, [account, executeSupplyTransaction]); diff --git a/src/components/withdrawModal.tsx b/src/components/withdrawModal.tsx index b009a513..85d6783e 100644 --- a/src/components/withdrawModal.tsx +++ b/src/components/withdrawModal.tsx @@ -3,7 +3,6 @@ import { useCallback, useMemo, useState } from 'react'; import { Cross1Icon } from '@radix-ui/react-icons'; import Image from 'next/image'; -import { toast } from 'react-toastify'; import { Address, encodeFunctionData } from 'viem'; import { useAccount, useSwitchChain } from 'wagmi'; import morphoAbi from '@/abis/morpho'; @@ -15,6 +14,7 @@ import { formatBalance, formatReadable, min } from '@/utils/balance'; import { MORPHO } from '@/utils/morpho'; import { findToken } from '@/utils/tokens'; import { MarketPosition } from '@/utils/types'; +import { useStyledToast } from '@/hooks/useStyledToast'; type ModalProps = { position: MarketPosition; @@ -24,6 +24,7 @@ type ModalProps = { export function WithdrawModal({ position, onClose, refetch }: ModalProps): JSX.Element { // Add state for the supply amount + const toast = useStyledToast(); const [inputError, setInputError] = useState(null); const [withdrawAmount, setWithdrawAmount] = useState(BigInt(0)); @@ -63,7 +64,7 @@ export function WithdrawModal({ position, onClose, refetch }: ModalProps): JSX.E const withdraw = useCallback(async () => { if (!account) { - toast.error('Please connect your wallet'); + toast.info('No account connected', 'Please connect your wallet to continue.'); return; } diff --git a/src/hooks/useAuthorizeAgent.ts b/src/hooks/useAuthorizeAgent.ts index 73b62fda..17a0bb50 100644 --- a/src/hooks/useAuthorizeAgent.ts +++ b/src/hooks/useAuthorizeAgent.ts @@ -1,5 +1,4 @@ import { useState, useCallback } from 'react'; -import { toast } from 'react-toastify'; import { Address, encodeFunctionData, parseSignature } from 'viem'; import { useAccount, useReadContract, useSignTypedData, useSwitchChain } from 'wagmi'; import monarchAgentAbi from '@/abis/monarch-agent-v1'; @@ -9,7 +8,7 @@ import { AGENT_CONTRACT } from '@/utils/monarch-agent'; import { MONARCH_TX_IDENTIFIER, MORPHO } from '@/utils/morpho'; import { SupportedNetworks } from '@/utils/networks'; import { Market } from '@/utils/types'; - +import { useStyledToast } from '@/hooks/useStyledToast'; export enum AuthorizeAgentStep { Idle = 'idle', Authorize = 'authorize', @@ -33,6 +32,7 @@ export const useAuthorizeAgent = ( marketCaps: MarketCap[], onSuccess?: () => void, ) => { + const toast = useStyledToast(); const [isConfirming, setIsConfirming] = useState(false); const [currentStep, setCurrentStep] = useState(AuthorizeAgentStep.Idle); @@ -128,7 +128,7 @@ export const useAuthorizeAgent = ( }); } catch (error) { console.log('Failed to sign authorization:', error); - toast.error('Signature request was rejected or failed. Please try again.'); + toast.error('Signature Request Failed', 'Signature request was rejected or failed. Please try again.'); return; } const signature = parseSignature(signatureRaw); @@ -197,7 +197,7 @@ export const useAuthorizeAgent = ( } catch (error) { console.error('Error during agent setup:', error); onError?.(); - toast.error('An error occurred during agent setup. Please try again.'); + toast.error('Agent Setup Failed', 'An error occurred during agent setup. Please try again.'); throw error; } finally { setIsConfirming(false); diff --git a/src/hooks/useMultiMarketSupply.ts b/src/hooks/useMultiMarketSupply.ts index 0747bce8..108e5947 100644 --- a/src/hooks/useMultiMarketSupply.ts +++ b/src/hooks/useMultiMarketSupply.ts @@ -1,5 +1,4 @@ import { useCallback, useState } from 'react'; -import { toast } from 'react-toastify'; import { Address, encodeFunctionData } from 'viem'; import { useAccount } from 'wagmi'; import morphoBundlerAbi from '@/abis/bundlerV2'; @@ -11,6 +10,7 @@ import { getBundlerV2, MONARCH_TX_IDENTIFIER } from '@/utils/morpho'; import { SupportedNetworks } from '@/utils/networks'; import { Market } from '@/utils/types'; import { useERC20Approval } from './useERC20Approval'; +import { useStyledToast } from './useStyledToast'; export type MarketSupply = { market: Market; @@ -26,6 +26,7 @@ export function useMultiMarketSupply( ) { const [currentStep, setCurrentStep] = useState<'approve' | 'signing' | 'supplying'>('approve'); const [showProcessModal, setShowProcessModal] = useState(false); + const toast = useStyledToast() const { address: account } = useAccount(); const chainId = loanAsset?.network; @@ -164,9 +165,9 @@ export function useMultiMarketSupply( console.error('Error in executeSupplyTransaction:', error); setShowProcessModal(false); if (error instanceof Error) { - toast.error('Transaction failed or cancelled'); + toast.error('Transaction failed', error.message); } else { - toast.error('Transaction failed'); + toast.error('Transaction failed', 'Transaction failed or cancled'); } throw error; // Re-throw to be caught by approveAndSupply } @@ -184,7 +185,7 @@ export function useMultiMarketSupply( const approveAndSupply = useCallback(async () => { if (!account) { - toast.error('Please connect your wallet'); + toast.error('No account connected', 'Please connect your wallet to continue.'); return false; } @@ -223,7 +224,7 @@ export function useMultiMarketSupply( setShowProcessModal(false); if (error instanceof Error) { if (error.message.includes('User rejected')) { - toast.error('Approval rejected by user'); + toast.error('Approval rejected', 'Token approval rejected by the user'); } else { toast.error('Failed to approve token'); } diff --git a/src/hooks/useRebalance.ts b/src/hooks/useRebalance.ts index deea1b36..913c0e0e 100644 --- a/src/hooks/useRebalance.ts +++ b/src/hooks/useRebalance.ts @@ -1,5 +1,4 @@ import { useState, useCallback } from 'react'; -import { toast } from 'react-toastify'; import { Address, encodeFunctionData, maxUint256, parseSignature } from 'viem'; import { useAccount, useReadContract, useSignTypedData } from 'wagmi'; import morphoBundlerAbi from '@/abis/bundlerV2'; @@ -8,6 +7,7 @@ import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; import { getBundlerV2, MONARCH_TX_IDENTIFIER, MORPHO } from '@/utils/morpho'; import { GroupedPosition, RebalanceAction } from '@/utils/types'; import { usePermit2 } from './usePermit2'; +import { useStyledToast } from './useStyledToast'; export const useRebalance = (groupedPosition: GroupedPosition, onRebalance?: () => void) => { const [rebalanceActions, setRebalanceActions] = useState([]); @@ -19,7 +19,7 @@ export const useRebalance = (groupedPosition: GroupedPosition, onRebalance?: () const { address: account } = useAccount(); const { signTypedDataAsync } = useSignTypedData(); const bundlerAddress = getBundlerV2(groupedPosition.chainId); - + const toast = useStyledToast(); const { data: isAuthorized } = useReadContract({ address: MORPHO, abi: morphoAbi, @@ -121,7 +121,8 @@ export const useRebalance = (groupedPosition: GroupedPosition, onRebalance?: () message: value, }); } catch (error) { - toast.error('Signature request was rejected or failed. Please try again.'); + const errorMessage = 'Signature request was rejected or failed. Please try again.'; + toast.error('Signature request failed', errorMessage); return; } const signature = parseSignature(signatureRaw); diff --git a/src/hooks/useStyledToast.tsx b/src/hooks/useStyledToast.tsx index 9f610d02..53621b88 100644 --- a/src/hooks/useStyledToast.tsx +++ b/src/hooks/useStyledToast.tsx @@ -13,5 +13,9 @@ export function useStyledToast() { toast.error(, options); }, []); - return { success, error }; + const info = useCallback((title: string, message?: string, options?: ToastOptions) => { + toast.info(, options); + }, []); + + return { success, error, info }; } From ca3e9511c002c2b62390f97d4fdab6cc638516e1 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Tue, 11 Feb 2025 19:12:59 +0800 Subject: [PATCH 3/5] chore: lint and dependencies --- app/markets/components/markets.tsx | 8 +-- .../components/PositionsSummaryTable.tsx | 9 ++-- app/positions/components/RebalanceModal.tsx | 37 +++++++------- .../components/onboarding/SetupPositions.tsx | 2 +- app/rewards/components/RewardTable.tsx | 50 +++++++++++-------- .../SearchOrConnect/SearchOrConnect.tsx | 5 +- src/components/common/StyledToast.tsx | 26 ++++++---- .../layout/header/AccountDropdown.tsx | 5 +- .../providers/ConnectRedirectProvider.tsx | 5 +- src/components/supplyModal.tsx | 6 ++- src/components/withdrawModal.tsx | 3 +- src/hooks/useAuthorizeAgent.ts | 12 +++-- src/hooks/useMultiMarketSupply.ts | 4 +- src/hooks/useRebalance.ts | 1 + src/hooks/useStyledToast.tsx | 3 +- src/hooks/useTransactionWithToast.tsx | 31 +++++++----- 16 files changed, 124 insertions(+), 83 deletions(-) diff --git a/app/markets/components/markets.tsx b/app/markets/components/markets.tsx index 0652c6f7..fd82421b 100644 --- a/app/markets/components/markets.tsx +++ b/app/markets/components/markets.tsx @@ -6,7 +6,6 @@ import { Chain } from '@rainbow-me/rainbowkit'; import storage from 'local-storage-fallback'; import { useRouter, useSearchParams } from 'next/navigation'; import { FaSync } from 'react-icons/fa'; -import { useStyledToast } from '@/hooks/useStyledToast'; import { Button } from '@/components/common'; import Header from '@/components/layout/header/Header'; import EmptyScreen from '@/components/Status/EmptyScreen'; @@ -16,6 +15,7 @@ import { TooltipContent } from '@/components/TooltipContent'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useMarkets } from '@/hooks/useMarkets'; import { usePagination } from '@/hooks/usePagination'; +import { useStyledToast } from '@/hooks/useStyledToast'; import { SupportedNetworks } from '@/utils/networks'; import { OracleVendors, parseOracleVendors } from '@/utils/oracle'; import * as keys from '@/utils/storageKeys'; @@ -42,7 +42,7 @@ export default function Markets() { const router = useRouter(); const searchParams = useSearchParams(); - const toast = useStyledToast() + const toast = useStyledToast(); const { loading, markets: rawMarkets, refetch, isRefetching } = useMarkets(); @@ -112,7 +112,7 @@ export default function Markets() { storage.setItem(keys.MarketFavoritesKey, JSON.stringify([...staredIds, id])); toast.success('Market starred', 'Market added to favorites', { icon: 🌟 }); }, - [staredIds], + [staredIds, toast], ); const unstarMarket = useCallback( @@ -121,7 +121,7 @@ export default function Markets() { storage.setItem(keys.MarketFavoritesKey, JSON.stringify(staredIds.filter((i) => i !== id))); toast.success('Market unstarred', 'Market removed from favorites', { icon: 🌟 }); }, - [staredIds], + [staredIds, toast], ); useEffect(() => { diff --git a/app/positions/components/PositionsSummaryTable.tsx b/app/positions/components/PositionsSummaryTable.tsx index e69a5097..3ced05c4 100644 --- a/app/positions/components/PositionsSummaryTable.tsx +++ b/app/positions/components/PositionsSummaryTable.tsx @@ -6,11 +6,11 @@ import Image from 'next/image'; import { BsQuestionCircle } from 'react-icons/bs'; import { IoRefreshOutline, IoChevronDownOutline } from 'react-icons/io5'; import { PiHandCoins } from 'react-icons/pi'; -import { useStyledToast } from '@/hooks/useStyledToast'; import { useAccount } from 'wagmi'; import { Button } from '@/components/common/Button'; import { TokenIcon } from '@/components/TokenIcon'; import { TooltipContent } from '@/components/TooltipContent'; +import { useStyledToast } from '@/hooks/useStyledToast'; import { formatReadable, formatBalance } from '@/utils/balance'; import { getNetworkImg } from '@/utils/networks'; import { @@ -70,7 +70,7 @@ export function PositionsSummaryTable({ const isOwner = useMemo(() => { if (!account) return false; return account === address; - }, [marketPositions, address]); + }, [account, address]); const getEarningsForPeriod = (position: MarketPositionWithEarnings) => { if (!position.earned) return '0'; @@ -430,7 +430,10 @@ export function PositionsSummaryTable({ className="text-xs" onClick={() => { if (!isOwner) { - toast.error('No authorization', 'You can only rebalance your own positions'); + toast.error( + 'No authorization', + 'You can only rebalance your own positions', + ); return; } setSelectedGroupedPosition(groupedPosition); diff --git a/app/positions/components/RebalanceModal.tsx b/app/positions/components/RebalanceModal.tsx index 073c6e40..97a4e759 100644 --- a/app/positions/components/RebalanceModal.tsx +++ b/app/positions/components/RebalanceModal.tsx @@ -2,12 +2,13 @@ import React, { useState, useMemo, useCallback } from 'react'; import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@nextui-org/react'; import { GrRefresh } from 'react-icons/gr'; import { parseUnits, formatUnits } from 'viem'; -import { useAccount, useSwitchChain } from 'wagmi'; +import { useAccount, useCall, useSwitchChain } from 'wagmi'; import { Button } from '@/components/common'; import { Spinner } from '@/components/common/Spinner'; import { useMarkets } from '@/hooks/useMarkets'; import { usePagination } from '@/hooks/usePagination'; import { useRebalance } from '@/hooks/useRebalance'; +import { useStyledToast } from '@/hooks/useStyledToast'; import { findToken } from '@/utils/tokens'; import { Market } from '@/utils/types'; import { GroupedPosition, RebalanceAction } from '@/utils/types'; @@ -15,7 +16,6 @@ import { FromAndToMarkets } from './FromAndToMarkets'; import { RebalanceActionInput } from './RebalanceActionInput'; import { RebalanceCart } from './RebalanceCart'; import { RebalanceProcessModal } from './RebalanceProcessModal'; -import { useStyledToast } from '@/hooks/useStyledToast'; type RebalanceModalProps = { groupedPosition: GroupedPosition; isOpen: boolean; @@ -63,7 +63,7 @@ export function RebalanceModal({ ); }, [allMarkets, groupedPosition.loanAssetAddress, groupedPosition.chainId]); - const getPendingDelta = (marketUniqueKey: string) => { + const getPendingDelta = useCallback((marketUniqueKey: string) => { return rebalanceActions.reduce((acc: number, action: RebalanceAction) => { if (action.fromMarket.uniqueKey === marketUniqueKey) { return acc - Number(action.amount); @@ -73,9 +73,9 @@ export function RebalanceModal({ } return acc; }, 0); - }; + }, [rebalanceActions]); - const validateInputs = () => { + const validateInputs = useCallback(() => { if (!selectedFromMarketUniqueKey || !selectedToMarketUniqueKey || !amount) { const missingFields = []; if (!selectedFromMarketUniqueKey) missingFields.push('"From Market"'); @@ -93,24 +93,26 @@ export function RebalanceModal({ return false; } return true; - }; + }, [selectedFromMarketUniqueKey, selectedToMarketUniqueKey, amount, groupedPosition.loanAssetDecimals, toast]); - const getMarkets = () => { + const getMarkets = useCallback(() => { const fromMarket = eligibleMarkets.find((m) => m.uniqueKey === selectedFromMarketUniqueKey); const toMarket = eligibleMarkets.find((m) => m.uniqueKey === selectedToMarketUniqueKey); if (!fromMarket || !toMarket) { - const errorMessage = `Invalid ${!fromMarket ? '"From" Market' : ''}${!toMarket ? '"To" Market' : ''}`; + const errorMessage = `Invalid ${!fromMarket ? '"From" Market' : ''}${ + !toMarket ? '"To" Market' : '' + }`; toast.error('Invalid market selection', errorMessage); return null; } return { fromMarket, toMarket }; - }; + }, [eligibleMarkets, selectedFromMarketUniqueKey, selectedToMarketUniqueKey, toast]); - const checkBalance = () => { + const checkBalance = useCallback(() => { const oldBalance = groupedPosition.markets.find( (m) => m.market.uniqueKey === selectedFromMarketUniqueKey, )?.supplyAssets; @@ -120,13 +122,13 @@ export function RebalanceModal({ const scaledAmount = parseUnits(amount, groupedPosition.loanAssetDecimals); if (scaledAmount > pendingBalance) { - toast.error('Insufficient balance', 'You don\'t have enough balance to perform this action'); + toast.error('Insufficient balance', "You don't have enough balance to perform this action"); return false; } return true; - }; + }, [selectedFromMarketUniqueKey, amount, groupedPosition.loanAssetDecimals, getPendingDelta, toast]); - const createAction = ( + const createAction = useCallback(( fromMarket: Market, toMarket: Market, actionAmount: bigint, @@ -152,13 +154,13 @@ export function RebalanceModal({ amount: actionAmount, isMax, }; - }; + }, []); - const resetSelections = () => { + const resetSelections = useCallback(() => { setSelectedFromMarketUniqueKey(''); setSelectedToMarketUniqueKey(''); setAmount('0'); - }; + }, []); const handleMaxSelect = useCallback( (marketUniqueKey: string, maxAmount: number) => { @@ -201,7 +203,6 @@ export function RebalanceModal({ selectedPosition !== undefined && BigInt(selectedPosition.supplyAssets) + BigInt(pendingDelta) === scaledAmount; - // Create the action using the helper function const action = createAction(fromMarket, toMarket, scaledAmount, isMaxAmount); addRebalanceAction(action); @@ -250,7 +251,7 @@ export function RebalanceModal({ } finally { setShowProcessModal(false); } - }, [executeRebalance, needSwitchChain, switchChain, groupedPosition.chainId]); + }, [executeRebalance, needSwitchChain, switchChain, groupedPosition.chainId, toast]); const handleManualRefresh = () => { refetch(() => { diff --git a/app/positions/components/onboarding/SetupPositions.tsx b/app/positions/components/onboarding/SetupPositions.tsx index 2bc3c8bd..d120fffd 100644 --- a/app/positions/components/onboarding/SetupPositions.tsx +++ b/app/positions/components/onboarding/SetupPositions.tsx @@ -2,7 +2,6 @@ import { useState, useEffect, useMemo, useCallback } from 'react'; import { Slider } from '@nextui-org/react'; import { LockClosedIcon, LockOpen1Icon } from '@radix-ui/react-icons'; import Image from 'next/image'; -import { useStyledToast } from '@/hooks/useStyledToast'; import { formatUnits, parseUnits } from 'viem'; import { useChainId, useSwitchChain } from 'wagmi'; import { Button } from '@/components/common'; @@ -10,6 +9,7 @@ import { MarketInfoBlock } from '@/components/common/MarketInfoBlock'; import { SupplyProcessModal } from '@/components/SupplyProcessModal'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useMultiMarketSupply } from '@/hooks/useMultiMarketSupply'; +import { useStyledToast } from '@/hooks/useStyledToast'; import { useUserBalances } from '@/hooks/useUserBalances'; import { formatBalance } from '@/utils/balance'; import { findToken } from '@/utils/tokens'; diff --git a/app/rewards/components/RewardTable.tsx b/app/rewards/components/RewardTable.tsx index 47d43c6a..fefc771e 100644 --- a/app/rewards/components/RewardTable.tsx +++ b/app/rewards/components/RewardTable.tsx @@ -9,13 +9,13 @@ import { useAccount, useSwitchChain } from 'wagmi'; import { Button } from '@/components/common/Button'; import { TokenIcon } from '@/components/TokenIcon'; import { DistributionResponseType } from '@/hooks/useRewards'; +import { useStyledToast } from '@/hooks/useStyledToast'; import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; import { formatBalance, formatSimple } from '@/utils/balance'; import { getAssetURL } from '@/utils/external'; import { getNetworkImg } from '@/utils/networks'; import { findToken } from '@/utils/tokens'; import { AggregatedRewardType } from '@/utils/types'; -import { useStyledToast } from '@/hooks/useStyledToast'; type RewardTableProps = { account: string; rewards: AggregatedRewardType[]; @@ -193,25 +193,35 @@ export default function RewardTable({ isDisabled={ tokenReward.total.claimable === BigInt(0) || distribution === undefined } - onClick={async(e) => { - e.stopPropagation(); - if (!account) { - toast.error('No account connected', 'Please connect your wallet to continue.'); - return; - } - if (!distribution) { - toast.error('No claim data', 'No claim data found for this reward please try again later.'); - return; - } - if (chainId !== distribution.distributor.chain_id) { - await switchChainAsync({ chainId: distribution.distributor.chain_id }); - } - sendTransaction({ - account: account as Address, - to: distribution.distributor.address as Address, - data: distribution.tx_data as `0x${string}`, - chainId: distribution.distributor.chain_id, - }); + onClick={(e) => { + void (async () => { + e.stopPropagation(); + if (!account) { + toast.error( + 'No account connected', + 'Please connect your wallet to continue.', + ); + return; + } + if (!distribution) { + toast.error( + 'No claim data', + 'No claim data found for this reward please try again later.', + ); + return; + } + if (chainId !== distribution.distributor.chain_id) { + await switchChainAsync({ + chainId: distribution.distributor.chain_id, + }); + } + sendTransaction({ + account: account as Address, + to: distribution.distributor.address as Address, + data: distribution.tx_data as `0x${string}`, + chainId: distribution.distributor.chain_id, + }); + })(); }} > Claim diff --git a/src/components/SearchOrConnect/SearchOrConnect.tsx b/src/components/SearchOrConnect/SearchOrConnect.tsx index bc214ee5..2202de33 100644 --- a/src/components/SearchOrConnect/SearchOrConnect.tsx +++ b/src/components/SearchOrConnect/SearchOrConnect.tsx @@ -66,7 +66,10 @@ export default function SearchOrConnect({ path }: { path: string }) { if (isAddress(inputAddress.toLowerCase(), { strict: false })) { window.location.href = `/${path}/${inputAddress}`; } else { - toast.error('Invalid address', `The address you enter ${inputAddress} is not valid.`); + toast.error( + 'Invalid address', + `The address you enter ${inputAddress} is not valid.`, + ); } }} className="bg-monarch-orange justify-center p-6 text-center text-sm duration-100 ease-in-out hover:opacity-100" diff --git a/src/components/common/StyledToast.tsx b/src/components/common/StyledToast.tsx index ba5de018..bcf635a6 100644 --- a/src/components/common/StyledToast.tsx +++ b/src/components/common/StyledToast.tsx @@ -1,22 +1,28 @@ -import { TxHashDisplay } from "../TxHashDisplay"; +import { TxHashDisplay } from '../TxHashDisplay'; -export function StyledToast({ title, message }: { title: string, message?: string }) { +export function StyledToast({ title, message }: { title: string; message?: string }) { return (
{title}
- {message &&
- {message} -
} + {message &&
{message}
}
); } -export function TransactionToast({ title, description, hash }: { title: string, hash?: string, description?: string }) { +export function TransactionToast({ + title, + description, + hash, +}: { + title: string; + hash?: string; + description?: string; +}) { return (
-
{title}
- {description &&
{description}
} - -
+
{title}
+ {description &&
{description}
} + + ); } diff --git a/src/components/layout/header/AccountDropdown.tsx b/src/components/layout/header/AccountDropdown.tsx index 02a82d30..df0c0a34 100644 --- a/src/components/layout/header/AccountDropdown.tsx +++ b/src/components/layout/header/AccountDropdown.tsx @@ -4,13 +4,12 @@ import { useCallback } from 'react'; import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from '@nextui-org/react'; import { ExitIcon, ExternalLinkIcon, CopyIcon } from '@radix-ui/react-icons'; import { clsx } from 'clsx'; -import { toast } from 'react-toastify'; import { useAccount, useDisconnect } from 'wagmi'; import { Avatar } from '@/components/Avatar/Avatar'; import { Name } from '@/components/common/Name'; +import { useStyledToast } from '@/hooks/useStyledToast'; import { getSlicedAddress } from '@/utils/address'; import { getExplorerURL } from '@/utils/external'; -import { useStyledToast } from '@/hooks/useStyledToast'; export function AccountDropdown() { const { address, chainId } = useAccount(); @@ -28,7 +27,7 @@ export function AccountDropdown() { toast.success('Address copied', 'Address copied to clipboard'); }); } - }, [address]); + }, [address, toast]); if (!address) return null; diff --git a/src/components/providers/ConnectRedirectProvider.tsx b/src/components/providers/ConnectRedirectProvider.tsx index 45a0aa94..63330804 100644 --- a/src/components/providers/ConnectRedirectProvider.tsx +++ b/src/components/providers/ConnectRedirectProvider.tsx @@ -1,6 +1,5 @@ import { ReactNode, createContext, useCallback, useContext, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; -import { toast } from 'react-toastify'; import { useAccountEffect } from 'wagmi'; import { useStyledToast } from '@/hooks/useStyledToast'; @@ -21,7 +20,9 @@ export function ConnectRedirectProvider({ children }: { children: ReactNode }) { onConnect: ({ address, isReconnected }) => { if (redirectPath && !isReconnected) { router.push(`/${redirectPath}/${address}`); - toast.success('Address connected', 'Redirecting to portfolio...', { toastId: 'address-connected' }); + toast.success('Address connected', 'Redirecting to portfolio...', { + toastId: 'address-connected', + }); // Reset the path after redirect setRedirectPath(undefined); } diff --git a/src/components/supplyModal.tsx b/src/components/supplyModal.tsx index 81076d1d..39ddacc0 100644 --- a/src/components/supplyModal.tsx +++ b/src/components/supplyModal.tsx @@ -10,6 +10,7 @@ import AccountConnect from '@/components/layout/header/AccountConnect'; import { useERC20Approval } from '@/hooks/useERC20Approval'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { usePermit2 } from '@/hooks/usePermit2'; +import { useStyledToast } from '@/hooks/useStyledToast'; import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; import { formatBalance, formatReadable } from '@/utils/balance'; import { getExplorerURL } from '@/utils/external'; @@ -20,7 +21,6 @@ import { Button } from './common'; import { MarketInfoBlock } from './common/MarketInfoBlock'; import OracleVendorBadge from './OracleVendorBadge'; import { SupplyProcessModal } from './SupplyProcessModal'; -import { useStyledToast } from '@/hooks/useStyledToast'; type SupplyModalProps = { market: Market; onClose: () => void; @@ -187,6 +187,7 @@ export function SupplyModal({ market, onClose }: SupplyModalProps): JSX.Element useEth, signForBundlers, usePermit2Setting, + toast, ]); const approveAndSupply = useCallback(async () => { @@ -269,6 +270,7 @@ export function SupplyModal({ market, onClose }: SupplyModalProps): JSX.Element usePermit2Setting, isApproved, approve, + toast, ]); const signAndSupply = useCallback(async () => { @@ -294,7 +296,7 @@ export function SupplyModal({ market, onClose }: SupplyModalProps): JSX.Element toast.error('Transaction Error', 'An unexpected error occurred'); } } - }, [account, executeSupplyTransaction]); + }, [account, executeSupplyTransaction, toast]); return (
('approve'); const [showProcessModal, setShowProcessModal] = useState(false); - const toast = useStyledToast() + const toast = useStyledToast(); const { address: account } = useAccount(); const chainId = loanAsset?.network; @@ -181,6 +181,7 @@ export function useMultiMarketSupply( usePermit2Setting, chainId, loanAsset, + toast ]); const approveAndSupply = useCallback(async () => { @@ -251,6 +252,7 @@ export function useMultiMarketSupply( approve, useEth, executeSupplyTransaction, + toast, ]); return { diff --git a/src/hooks/useRebalance.ts b/src/hooks/useRebalance.ts index 913c0e0e..b59eff91 100644 --- a/src/hooks/useRebalance.ts +++ b/src/hooks/useRebalance.ts @@ -307,6 +307,7 @@ export const useRebalance = (groupedPosition: GroupedPosition, onRebalance?: () sendTransactionAsync, groupedPosition.loanAssetAddress, totalAmount, + toast, ]); return { diff --git a/src/hooks/useStyledToast.tsx b/src/hooks/useStyledToast.tsx index 53621b88..383a8ce7 100644 --- a/src/hooks/useStyledToast.tsx +++ b/src/hooks/useStyledToast.tsx @@ -4,9 +4,8 @@ import 'react-toastify/dist/ReactToastify.css'; import { StyledToast } from '../components/common/StyledToast'; export function useStyledToast() { - const success = useCallback((title: string, message?: string, options?: ToastOptions) => { - toast.success(, options); + toast.success(, options); }, []); const error = useCallback((title: string, message?: string, options?: ToastOptions) => { diff --git a/src/hooks/useTransactionWithToast.tsx b/src/hooks/useTransactionWithToast.tsx index 16681b77..bdd7906f 100644 --- a/src/hooks/useTransactionWithToast.tsx +++ b/src/hooks/useTransactionWithToast.tsx @@ -2,10 +2,9 @@ import React, { useCallback, useEffect } from 'react'; import { toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import { useSendTransaction, useWaitForTransactionReceipt } from 'wagmi'; -import { TxHashDisplay } from '../components/TxHashDisplay'; +import { StyledToast, TransactionToast } from '@/components/common/StyledToast'; import { getExplorerTxURL } from '../utils/external'; import { SupportedNetworks } from '../utils/networks'; -import { StyledToast, TransactionToast } from '@/components/common/StyledToast'; type UseTransactionWithToastProps = { toastId: string; @@ -46,21 +45,29 @@ export function useTransactionWithToast({ } }, [hash, chainId]); - useEffect(() => { if (isConfirming) { - toast.loading(, { - toastId, - onClick, - closeButton: true, - }); + toast.loading( + , + { + toastId, + onClick, + closeButton: true, + }, + ); } }, [isConfirming, pendingText, pendingDescription, toastId, onClick]); useEffect(() => { if (isConfirmed) { toast.update(toastId, { - render: , + render: ( + + ), type: 'success', isLoading: false, autoClose: 5000, @@ -73,9 +80,7 @@ export function useTransactionWithToast({ } if (txError) { toast.update(toastId, { - render: ( - - ), + render: , type: 'error', isLoading: false, autoClose: 5000, @@ -92,6 +97,8 @@ export function useTransactionWithToast({ toastId, onClick, onSuccess, + toast, + hash, ]); return { sendTransactionAsync, sendTransaction, isConfirming, isConfirmed }; From 9c6f6f22637e92af0b123b54ea2b53958abeafcf Mon Sep 17 00:00:00 2001 From: antoncoding Date: Tue, 11 Feb 2025 19:16:37 +0800 Subject: [PATCH 4/5] chore: test hover underline --- src/components/common/StyledToast.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/common/StyledToast.tsx b/src/components/common/StyledToast.tsx index bcf635a6..0e9c02e9 100644 --- a/src/components/common/StyledToast.tsx +++ b/src/components/common/StyledToast.tsx @@ -4,7 +4,7 @@ export function StyledToast({ title, message }: { title: string; message?: strin return (
{title}
- {message &&
{message}
} + {message &&
{message}
}
); } From 05f56bba6f7b5c7bf3a0faa2415243aa10c773e2 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Tue, 11 Feb 2025 19:19:32 +0800 Subject: [PATCH 5/5] chore: lint --- app/positions/components/RebalanceModal.tsx | 102 ++++++++++-------- .../SearchOrConnect/SearchOrConnect.tsx | 5 +- src/hooks/useMultiMarketSupply.ts | 2 +- 3 files changed, 62 insertions(+), 47 deletions(-) diff --git a/app/positions/components/RebalanceModal.tsx b/app/positions/components/RebalanceModal.tsx index 97a4e759..2bbc75de 100644 --- a/app/positions/components/RebalanceModal.tsx +++ b/app/positions/components/RebalanceModal.tsx @@ -2,7 +2,7 @@ import React, { useState, useMemo, useCallback } from 'react'; import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@nextui-org/react'; import { GrRefresh } from 'react-icons/gr'; import { parseUnits, formatUnits } from 'viem'; -import { useAccount, useCall, useSwitchChain } from 'wagmi'; +import { useAccount, useSwitchChain } from 'wagmi'; import { Button } from '@/components/common'; import { Spinner } from '@/components/common/Spinner'; import { useMarkets } from '@/hooks/useMarkets'; @@ -63,17 +63,20 @@ export function RebalanceModal({ ); }, [allMarkets, groupedPosition.loanAssetAddress, groupedPosition.chainId]); - const getPendingDelta = useCallback((marketUniqueKey: string) => { - return rebalanceActions.reduce((acc: number, action: RebalanceAction) => { - if (action.fromMarket.uniqueKey === marketUniqueKey) { - return acc - Number(action.amount); - } - if (action.toMarket.uniqueKey === marketUniqueKey) { - return acc + Number(action.amount); - } - return acc; - }, 0); - }, [rebalanceActions]); + const getPendingDelta = useCallback( + (marketUniqueKey: string) => { + return rebalanceActions.reduce((acc: number, action: RebalanceAction) => { + if (action.fromMarket.uniqueKey === marketUniqueKey) { + return acc - Number(action.amount); + } + if (action.toMarket.uniqueKey === marketUniqueKey) { + return acc + Number(action.amount); + } + return acc; + }, 0); + }, + [rebalanceActions], + ); const validateInputs = useCallback(() => { if (!selectedFromMarketUniqueKey || !selectedToMarketUniqueKey || !amount) { @@ -93,7 +96,13 @@ export function RebalanceModal({ return false; } return true; - }, [selectedFromMarketUniqueKey, selectedToMarketUniqueKey, amount, groupedPosition.loanAssetDecimals, toast]); + }, [ + selectedFromMarketUniqueKey, + selectedToMarketUniqueKey, + amount, + groupedPosition.loanAssetDecimals, + toast, + ]); const getMarkets = useCallback(() => { const fromMarket = eligibleMarkets.find((m) => m.uniqueKey === selectedFromMarketUniqueKey); @@ -126,35 +135,44 @@ export function RebalanceModal({ return false; } return true; - }, [selectedFromMarketUniqueKey, amount, groupedPosition.loanAssetDecimals, getPendingDelta, toast]); - - const createAction = useCallback(( - fromMarket: Market, - toMarket: Market, - actionAmount: bigint, - isMax: boolean, - ): RebalanceAction => { - return { - fromMarket: { - loanToken: fromMarket.loanAsset.address, - collateralToken: fromMarket.collateralAsset.address, - oracle: fromMarket.oracleAddress, - irm: fromMarket.irmAddress, - lltv: fromMarket.lltv, - uniqueKey: fromMarket.uniqueKey, - }, - toMarket: { - loanToken: toMarket.loanAsset.address, - collateralToken: toMarket.collateralAsset.address, - oracle: toMarket.oracleAddress, - irm: toMarket.irmAddress, - lltv: toMarket.lltv, - uniqueKey: toMarket.uniqueKey, - }, - amount: actionAmount, - isMax, - }; - }, []); + }, [ + selectedFromMarketUniqueKey, + amount, + groupedPosition.loanAssetDecimals, + getPendingDelta, + toast, + ]); + + const createAction = useCallback( + ( + fromMarket: Market, + toMarket: Market, + actionAmount: bigint, + isMax: boolean, + ): RebalanceAction => { + return { + fromMarket: { + loanToken: fromMarket.loanAsset.address, + collateralToken: fromMarket.collateralAsset.address, + oracle: fromMarket.oracleAddress, + irm: fromMarket.irmAddress, + lltv: fromMarket.lltv, + uniqueKey: fromMarket.uniqueKey, + }, + toMarket: { + loanToken: toMarket.loanAsset.address, + collateralToken: toMarket.collateralAsset.address, + oracle: toMarket.oracleAddress, + irm: toMarket.irmAddress, + lltv: toMarket.lltv, + uniqueKey: toMarket.uniqueKey, + }, + amount: actionAmount, + isMax, + }; + }, + [], + ); const resetSelections = useCallback(() => { setSelectedFromMarketUniqueKey(''); diff --git a/src/components/SearchOrConnect/SearchOrConnect.tsx b/src/components/SearchOrConnect/SearchOrConnect.tsx index 2202de33..611c8d27 100644 --- a/src/components/SearchOrConnect/SearchOrConnect.tsx +++ b/src/components/SearchOrConnect/SearchOrConnect.tsx @@ -66,10 +66,7 @@ export default function SearchOrConnect({ path }: { path: string }) { if (isAddress(inputAddress.toLowerCase(), { strict: false })) { window.location.href = `/${path}/${inputAddress}`; } else { - toast.error( - 'Invalid address', - `The address you enter ${inputAddress} is not valid.`, - ); + toast.error('Invalid address', `The address ${inputAddress} is not valid.`); } }} className="bg-monarch-orange justify-center p-6 text-center text-sm duration-100 ease-in-out hover:opacity-100" diff --git a/src/hooks/useMultiMarketSupply.ts b/src/hooks/useMultiMarketSupply.ts index bd2b14ff..85090089 100644 --- a/src/hooks/useMultiMarketSupply.ts +++ b/src/hooks/useMultiMarketSupply.ts @@ -181,7 +181,7 @@ export function useMultiMarketSupply( usePermit2Setting, chainId, loanAsset, - toast + toast, ]); const approveAndSupply = useCallback(async () => {