diff --git a/src/app/page.tsx b/src/app/page.tsx index d370c81..4249939 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { SwapForm } from '@/components/swap-form'; -import { TokenCountDisplay } from '@/components/token-count-display'; export default function HomePage() { const { t } = useTranslation(); @@ -17,10 +16,7 @@ export default function HomePage() { - - - - + diff --git a/src/components/swap-form.tsx b/src/components/swap-form.tsx index 97306fe..cf93e5d 100644 --- a/src/components/swap-form.tsx +++ b/src/components/swap-form.tsx @@ -1,18 +1,51 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; -import { ArrowDownUp } from 'lucide-react'; +import { ArrowDownUp, ChevronDown } from 'lucide-react'; import { useAccount } from 'wagmi'; +import { useTokenDetails, type TokenDetails } from '@/hooks/usePartyPlanner'; +import { formatUnits } from 'viem'; export function SwapForm() { const { t } = useTranslation(); - const { isConnected } = useAccount(); + const { isConnected, address } = useAccount(); const [fromAmount, setFromAmount] = useState(''); const [toAmount, setToAmount] = useState(''); + const [selectedFromToken, setSelectedFromToken] = useState(null); + const [selectedToToken, setSelectedToToken] = useState(null); + const [isFromDropdownOpen, setIsFromDropdownOpen] = useState(false); + const [isToDropdownOpen, setIsToDropdownOpen] = useState(false); + const fromDropdownRef = useRef(null); + const toDropdownRef = useRef(null); + + // Use the custom hook to get all token details with a single batched RPC call + const { tokenDetails, loading, error } = useTokenDetails(address); + + // Trigger the hook to fetch data when address is available + useEffect(() => { + if (tokenDetails) { + console.log('Token details loaded in swap-form'); + } + }, [tokenDetails]); + + // Close dropdowns when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (fromDropdownRef.current && !fromDropdownRef.current.contains(event.target as Node)) { + setIsFromDropdownOpen(false); + } + if (toDropdownRef.current && !toDropdownRef.current.contains(event.target as Node)) { + setIsToDropdownOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); const handleSwap = () => { // Swap logic will be implemented later @@ -35,7 +68,9 @@ export function SwapForm() { {t('swap.youPay')} - {t('swap.balance')}: 0.00 + + {t('swap.balance')}: {selectedFromToken ? formatUnits(selectedFromToken.balance, selectedFromToken.decimals) : '0.00'} + setFromAmount(e.target.value)} className="text-2xl h-16" /> - - {t('swap.selectToken')} - + + setIsFromDropdownOpen(!isFromDropdownOpen)} + > + {selectedFromToken ? ( + {selectedFromToken.symbol} + ) : ( + t('swap.selectToken') + )} + + + {isFromDropdownOpen && ( + + {tokenDetails && tokenDetails.length > 0 ? ( + tokenDetails.map((token) => ( + { + setSelectedFromToken(token); + setIsFromDropdownOpen(false); + }} + > + {token.symbol} + + {formatUnits(token.balance, token.decimals)} + + + )) + ) : ( + + {loading ? 'Loading tokens...' : 'No tokens available'} + + )} + + )} + @@ -67,7 +138,9 @@ export function SwapForm() { {t('swap.youReceive')} - {t('swap.balance')}: 0.00 + + {t('swap.balance')}: {selectedToToken ? formatUnits(selectedToToken.balance, selectedToToken.decimals) : '0.00'} + setToAmount(e.target.value)} className="text-2xl h-16" /> - - {t('swap.selectToken')} - + + setIsToDropdownOpen(!isToDropdownOpen)} + > + {selectedToToken ? ( + {selectedToToken.symbol} + ) : ( + t('swap.selectToken') + )} + + + {isToDropdownOpen && ( + + {tokenDetails && tokenDetails.length > 0 ? ( + tokenDetails.map((token) => ( + { + setSelectedToToken(token); + setIsToDropdownOpen(false); + }} + > + {token.symbol} + + {formatUnits(token.balance, token.decimals)} + + + )) + ) : ( + + {loading ? 'Loading tokens...' : 'No tokens available'} + + )} + + )} + diff --git a/src/components/token-count-display.tsx b/src/components/token-count-display.tsx deleted file mode 100644 index 42296a8..0000000 --- a/src/components/token-count-display.tsx +++ /dev/null @@ -1,95 +0,0 @@ -'use client'; - -import {useEffect, useState} from 'react'; -import {usePublicClient} from 'wagmi'; -import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'; -import chainInfo from '@/app/liquidity-party.json'; -import IPartyPlannerABI from '@/contracts/IPartyPlannerABI'; - - -export function TokenCountDisplay() { - const publicClient = usePublicClient(); - const [mounted, setMounted] = useState(false); - const [tokenCount, setTokenCount] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - // Handle hydration for Next.js static export - useEffect(() => { - setMounted(true); - }, []); - - useEffect(() => { - if (!mounted) return; - - const fetchTokenCount = async () => { - if (!publicClient) { - setLoading(false); - return; - } - - try { - setLoading(true); - setError(null); - - // In viem 2.x, readContract is a method on the client - const chainId = await publicClient.getChainId() - const address = chainInfo[chainId.toString()]?.IPartyPlanner; - const count = !address ? BigInt(0) : await publicClient.readContract({ - address, - abi: IPartyPlannerABI, - functionName: 'tokenCount', // Fully typed from ABI - }); - - setTokenCount(count); - } catch (err) { - console.error('Error reading token count:', err); - setError(err instanceof Error ? err.message : 'Failed to read token count'); - } finally { - setLoading(false); - } - }; - - fetchTokenCount(); - }, [publicClient, mounted]); - - // Don't render until client-side hydration is complete - if (!mounted) { - return ( - - - Party Planner Token Count - - - Loading... - - - ); - } - - return ( - - - Party Planner Token Count - - - {loading && ( - Loading token count... - )} - - {error && ( - Error: {error} - )} - - {tokenCount !== null && !loading && ( - - {tokenCount.toString()} - - Total tokens in Party Planner - - - )} - - - ); -} diff --git a/src/hooks/usePartyPlanner.ts b/src/hooks/usePartyPlanner.ts new file mode 100644 index 0000000..e3e58ec --- /dev/null +++ b/src/hooks/usePartyPlanner.ts @@ -0,0 +1,166 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { usePublicClient } from 'wagmi'; +import chainInfo from '@/app/liquidity-party.json'; +import IPartyPlannerABI from '@/contracts/IPartyPlannerABI'; +import { ERC20ABI } from '@/contracts/ERC20ABI'; + +export function useGetAllTokens(offset: number = 0, limit: number = 100) { + const publicClient = usePublicClient(); + const [mounted, setMounted] = useState(false); + const [tokens, setTokens] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Handle hydration for Next.js static export + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + if (!mounted) return; + + const fetchTokens = async () => { + if (!publicClient) { + setLoading(false); + return; + } + + try { + setLoading(true); + setError(null); + + // Get chain ID and contract address + const chainId = await publicClient.getChainId(); + const address = chainInfo[chainId.toString()]?.IPartyPlanner; + + if (!address) { + setError('IPartyPlanner contract not found for current chain'); + setTokens([]); + return; + } + + // Call getAllTokens function + const result = await publicClient.readContract({ + address, + abi: IPartyPlannerABI, + functionName: 'getAllTokens', + args: [BigInt(offset), BigInt(limit)], + }); + + setTokens(result); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch tokens'); + } finally { + setLoading(false); + } + }; + + fetchTokens(); + }, [publicClient, mounted, offset, limit]); + + return { + tokens, + loading, + error, + isReady: mounted, + }; +} + +export interface TokenDetails { + address: `0x${string}`; + name: string; + symbol: string; + decimals: number; + balance: bigint;P +} + +export function useTokenDetails(userAddress: `0x${string}` | undefined) { + const publicClient = usePublicClient(); + const { tokens, loading: tokensLoading, isReady } = useGetAllTokens(); + const [tokenDetails, setTokenDetails] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!isReady || tokensLoading || !tokens || !publicClient || !userAddress) { + setLoading(tokensLoading || !isReady); + return; + } + + const fetchTokenDetails = async () => { + try { + setLoading(true); + setError(null); + + if (tokens.length === 0) { + setTokenDetails([]); + return; + } + + const details: TokenDetails[] = []; + + // Make individual calls for each token + for (const tokenAddress of tokens) { + try { + const [name, symbol, decimals, balance] = await Promise.all([ + publicClient.readContract({ + address: tokenAddress, + abi: ERC20ABI, + functionName: 'name', + }).catch(() => 'Unknown'), + publicClient.readContract({ + address: tokenAddress, + abi: ERC20ABI, + functionName: 'symbol', + }).catch(() => '???'), + publicClient.readContract({ + address: tokenAddress, + abi: ERC20ABI, + functionName: 'decimals', + }).catch(() => 18), + publicClient.readContract({ + address: tokenAddress, + abi: ERC20ABI, + functionName: 'balanceOf', + args: [userAddress], + }).catch(() => BigInt(0)), + ]); + + details.push({ + address: tokenAddress, + name: name as string, + symbol: symbol as string, + decimals: Number(decimals), + balance: balance as bigint, + }); + } catch (err) { + // Add token with fallback values if individual call fails + details.push({ + address: tokenAddress, + name: 'Unknown', + symbol: '???', + decimals: 18, + balance: BigInt(0), + }); + } + } + + setTokenDetails(details); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch token details'); + } finally { + setLoading(false); + } + }; + + fetchTokenDetails(); + }, [tokens, tokensLoading, publicClient, isReady, userAddress]); + + return { + tokenDetails, + loading, + error, + }; +} \ No newline at end of file
Loading...
Loading token count...
Error: {error}
{tokenCount.toString()}
- Total tokens in Party Planner -