138 lines
4.9 KiB
TypeScript
138 lines
4.9 KiB
TypeScript
|
|
"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>
|
|||
|
|
);
|
|||
|
|
}
|