Files
assetx/webapp/components/common/TokenSelector.tsx

138 lines
4.9 KiB
TypeScript
Raw Permalink Normal View History

"use client";
import { useEffect, useRef } from "react";
import Image from "next/image";
import {
Dropdown,
DropdownTrigger,
DropdownMenu,
DropdownItem,
} from "@heroui/react";
import { useTokenList } from "@/hooks/useTokenList";
import { Token } from "@/lib/api/tokens";
interface TokenSelectorProps {
selectedToken?: Token;
onSelect: (token: Token) => void;
filterTypes?: ('stablecoin' | 'yield-token')[];
disabled?: boolean;
defaultSymbol?: string;
}
export default function TokenSelector({
selectedToken,
onSelect,
filterTypes = ['stablecoin', 'yield-token'],
disabled = false,
defaultSymbol,
}: TokenSelectorProps) {
const hasInitialized = useRef(false);
const { tokens: allTokens, isLoading } = useTokenList();
const tokens = allTokens.filter(t => filterTypes.includes(t.tokenType));
// 数据加载完成后自动选中默认 token仅初始化一次
useEffect(() => {
if (hasInitialized.current || selectedToken || tokens.length === 0) return;
hasInitialized.current = true;
const target = defaultSymbol
? (tokens.find(t => t.symbol === defaultSymbol) ?? tokens[0])
: tokens[0];
onSelect(target);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tokens]);
if (isLoading) {
return (
<button className="bg-white dark:bg-gray-700 rounded-full border border-gray-400 dark:border-gray-500 px-4 h-[46px] flex items-center gap-2 opacity-50 cursor-not-allowed">
<div className="w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-600 animate-pulse" />
<span className="text-body-default font-bold text-text-primary dark:text-white">
Loading...
</span>
</button>
);
}
return (
<Dropdown shouldBlockScroll={false}>
<DropdownTrigger>
<button
disabled={disabled}
className={`bg-white dark:bg-gray-700 rounded-full border border-gray-400 dark:border-gray-500 px-4 h-[46px] flex items-center justify-between gap-2 outline-none focus:outline-none transition-all duration-200 ease-in-out ${
disabled ? 'opacity-50 cursor-not-allowed' : 'hover:border-primary dark:hover:border-primary-dark hover:shadow-md'
}`}
>
{selectedToken ? (
<>
<div className="flex items-center gap-2">
<Image
src={selectedToken.iconUrl || '/assets/tokens/default.svg'}
alt={selectedToken.symbol}
width={32}
height={32}
className="rounded-full transition-opacity duration-150"
priority
onError={(e) => {
(e.target as HTMLImageElement).src = '/assets/tokens/default.svg';
}}
/>
<span className="text-body-default font-bold text-text-primary dark:text-white transition-opacity duration-150">
{selectedToken.symbol}
</span>
</div>
<svg
className="w-4 h-4 text-text-tertiary dark:text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</>
) : (
<span className="text-body-default text-text-tertiary dark:text-gray-400">
No tokens available
</span>
)}
</button>
</DropdownTrigger>
<DropdownMenu
aria-label="Token selection"
onAction={(key) => {
const token = tokens.find(t => t.symbol === key);
if (token) onSelect(token);
}}
>
{tokens.map((token) => (
<DropdownItem
key={token.symbol}
className="hover:bg-bg-subtle dark:hover:bg-gray-700 rounded-lg"
textValue={token.symbol}
>
<div className="flex items-center gap-3 py-1">
<Image
src={token.iconUrl || '/assets/tokens/default.svg'}
alt={token.symbol || 'token'}
width={32}
height={32}
className="rounded-full"
onError={(e) => {
(e.target as HTMLImageElement).src = '/assets/tokens/default.svg';
}}
/>
<div className="flex flex-col">
<span className="text-body-default font-bold text-text-primary dark:text-white">
{token.symbol}
</span>
<span className="text-caption-tiny text-text-tertiary dark:text-gray-400">
{token.name}
</span>
</div>
</div>
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
);
}