Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 45 additions & 10 deletions src/components/wallet/wallet-card-carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import { useWalletTheme } from '@/hooks/useWalletTheme';
import { useChainIconUrls } from '@/hooks/useChainIconUrls';
import { cn } from '@/lib/utils';
import type { Wallet, ChainType } from '@/stores';
import { IconWallet, IconPlus } from '@tabler/icons-react';
import { IconWallet, IconPlus, IconDotsVertical, IconSearch, IconReceipt } from '@tabler/icons-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';

import 'swiper/css';
import 'swiper/css/effect-cards';
Expand All @@ -26,6 +32,8 @@ interface WalletCardCarouselProps {
onOpenSettings?: (walletId: string) => void;
onOpenWalletList?: () => void;
onAddWallet?: () => void;
onOpenAddressBalance?: () => void;
onOpenAddressTransactions?: () => void;
className?: string;
}

Expand All @@ -45,6 +53,8 @@ export function WalletCardCarousel({
onOpenSettings,
onOpenWalletList,
onAddWallet,
onOpenAddressBalance,
onOpenAddressTransactions,
className,
}: WalletCardCarouselProps) {
const swiperRef = useRef<SwiperType | null>(null);
Expand Down Expand Up @@ -111,15 +121,40 @@ export function WalletCardCarousel({
</button>
)}

{/* 右上角:添加钱包 */}
{onAddWallet && (
<button
onClick={onAddWallet}
className="bg-primary text-primary-foreground hover:bg-primary/90 absolute top-0 right-4 z-10 flex items-center justify-center rounded-full p-1.5 backdrop-blur-sm transition-colors"
>
<IconPlus className="size-4" />
</button>
)}
{/* 右上角:添加钱包 + 更多菜单 */}
<div className="absolute top-0 right-4 z-10 flex items-center gap-1.5">
{onAddWallet && (
<button
onClick={onAddWallet}
className="bg-primary text-primary-foreground hover:bg-primary/90 flex items-center justify-center rounded-full p-1.5 backdrop-blur-sm transition-colors"
>
<IconPlus className="size-4" />
</button>
)}
{(onOpenAddressBalance || onOpenAddressTransactions) && (
<DropdownMenu>
<DropdownMenuTrigger
className="bg-primary text-primary-foreground hover:bg-primary/90 flex items-center justify-center rounded-full p-1.5 backdrop-blur-sm transition-colors"
>
<IconDotsVertical className="size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={8}>
{onOpenAddressBalance && (
<DropdownMenuItem onClick={onOpenAddressBalance}>
<IconSearch className="size-4" />
地址余额查询
</DropdownMenuItem>
)}
{onOpenAddressTransactions && (
<DropdownMenuItem onClick={onOpenAddressTransactions}>
<IconReceipt className="size-4" />
地址交易查询
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
)}
</div>

<Swiper
modules={[EffectCards]}
Expand Down
15 changes: 15 additions & 0 deletions src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -352,5 +352,20 @@
"daysLater": "In {{count}} days",
"today": "Today",
"yesterday": "Yesterday"
},
"addressLookup": {
"balanceTitle": "Address Balance",
"transactionsTitle": "Address Transactions",
"chain": "Chain",
"address": "Address",
"addressPlaceholder": "Enter wallet address",
"addressOrHash": "Address or Transaction Hash",
"addressOrHashPlaceholder": "Enter address or tx hash",
"otherChains": "Other Chains",
"error": "Query Failed",
"onChain": "on {{chain}}",
"explorerHint": "Transaction history requires block explorer",
"openExplorer": "Open {{name}} Explorer",
"viewOnExplorer": "View on {{name}}"
}
}
15 changes: 15 additions & 0 deletions src/i18n/locales/zh-CN/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -330,5 +330,20 @@
"daysLater": "{{count}} 天后",
"today": "今天",
"yesterday": "昨天"
},
"addressLookup": {
"balanceTitle": "地址余额查询",
"transactionsTitle": "地址交易查询",
"chain": "链",
"address": "地址",
"addressPlaceholder": "输入钱包地址",
"addressOrHash": "地址或交易哈希",
"addressOrHashPlaceholder": "输入地址或交易哈希",
"otherChains": "其他链",
"error": "查询失败",
"onChain": "在 {{chain}} 上",
"explorerHint": "交易记录需要通过区块浏览器查询",
"openExplorer": "打开 {{name}} 浏览器",
"viewOnExplorer": "在 {{name}} 浏览器中查看"
}
}
154 changes: 154 additions & 0 deletions src/pages/address-balance/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { useState, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigation } from '@/stackflow'
import { PageHeader } from '@/components/layout/page-header'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Card, CardContent } from '@/components/ui/card'
import { LoadingSpinner } from '@/components/common/loading-spinner'
import { useAddressBalanceQuery } from '@/queries/use-address-balance-query'
import { useEnabledChains } from '@/stores'
import { IconSearch, IconAlertCircle, IconCurrencyEthereum } from '@tabler/icons-react'
import { cn } from '@/lib/utils'

export function AddressBalancePage() {
const { t } = useTranslation(['common', 'wallet'])
const { goBack } = useNavigation()
const enabledChains = useEnabledChains()

const [selectedChain, setSelectedChain] = useState('ethereum')
const [address, setAddress] = useState('')
const [queryAddress, setQueryAddress] = useState('')
const [queryChain, setQueryChain] = useState('')

const { data, isLoading, isFetching } = useAddressBalanceQuery(
queryChain,
queryAddress,
!!queryChain && !!queryAddress
)

const handleSearch = useCallback(() => {
if (address.trim()) {
setQueryAddress(address.trim())
setQueryChain(selectedChain)
}
}, [address, selectedChain])

const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleSearch()
}
},
[handleSearch]
)

const evmChains = enabledChains.filter((c) => c.type === 'evm')
const otherChains = enabledChains.filter((c) => c.type !== 'evm')

return (
<div className="bg-background flex min-h-screen flex-col">
<PageHeader title={t('common:addressLookup.balanceTitle')} onBack={goBack} />

<div className="flex-1 space-y-4 p-4">
{/* Chain Selector */}
<div className="space-y-2">
<Label>{t('common:addressLookup.chain')}</Label>
<Select value={selectedChain} onValueChange={setSelectedChain}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{evmChains.length > 0 && (
<>
<div className="text-muted-foreground px-2 py-1.5 text-xs font-medium">EVM</div>
{evmChains.map((chain) => (
<SelectItem key={chain.id} value={chain.id}>
{chain.name}
</SelectItem>
))}
</>
)}
{otherChains.length > 0 && (
<>
<div className="text-muted-foreground px-2 py-1.5 text-xs font-medium">
{t('common:addressLookup.otherChains')}
</div>
{otherChains.map((chain) => (
<SelectItem key={chain.id} value={chain.id}>
{chain.name}
</SelectItem>
))}
</>
)}
</SelectContent>
</Select>
</div>

{/* Address Input */}
<div className="space-y-2">
<Label>{t('common:addressLookup.address')}</Label>
<div className="flex gap-2">
<Input
placeholder={t('common:addressLookup.addressPlaceholder')}
value={address}
onChange={(e) => setAddress(e.target.value)}
onKeyDown={handleKeyDown}
className="flex-1 font-mono text-sm"
/>
<Button onClick={handleSearch} disabled={!address.trim() || isFetching}>
{isFetching ? <LoadingSpinner size="sm" /> : <IconSearch className="size-4" />}
</Button>
</div>
</div>

{/* Result */}
{queryAddress && (
<Card className={cn('transition-all', isLoading && 'opacity-50')}>
<CardContent className="pt-6">
{data?.error ? (
<div className="flex items-center gap-3 text-destructive">
<IconAlertCircle className="size-5 shrink-0" />
<div>
<div className="font-medium">{t('common:addressLookup.error')}</div>
<div className="text-sm opacity-80">{data.error}</div>
</div>
</div>
) : data?.balance ? (
<div className="flex items-center gap-3">
<div className="bg-primary/10 flex size-12 items-center justify-center rounded-full">
<IconCurrencyEthereum className="text-primary size-6" />
</div>
<div>
<div className="text-2xl font-bold">
{data.balance.amount.toFormatted()} {data.balance.symbol}
</div>
<div className="text-muted-foreground text-sm">
{t('common:addressLookup.onChain', {
chain: enabledChains.find((c) => c.id === queryChain)?.name ?? queryChain,
})}
</div>
</div>
</div>
) : isLoading ? (
<div className="flex items-center justify-center py-4">
<LoadingSpinner size="lg" />
</div>
) : null}
</CardContent>
</Card>
)}

{/* Debug Info (DEV only) */}
{import.meta.env.DEV && queryAddress && (
<div className="text-muted-foreground rounded-lg bg-muted/50 p-3 font-mono text-xs">
<div>Chain: {queryChain}</div>
<div className="truncate">Address: {queryAddress}</div>
</div>
)}
</div>
</div>
)
}
Loading