Files
pwap/web/src/components/dex/PoolBrowser.tsx
T
pezkuwichain d282f609aa feat: complete i18n support for all components (6 languages)
Add full internationalization across 127+ components and pages.
790+ translation keys in en, tr, kmr, ckb, ar, fa locales.
Remove duplicate keys and delete unused .json locale files.
2026-02-22 04:48:20 +03:00

254 lines
8.8 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { useWallet } from '@/contexts/WalletContext';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { TrendingUp, Droplet, Search, Plus } from 'lucide-react';
import { PoolInfo } from '@/types/dex';
import { fetchPools, formatTokenBalance } from '@pezkuwi/utils/dex';
import { isFounderWallet } from '@pezkuwi/utils/auth';
import { LoadingState } from '@pezkuwi/components/AsyncComponent';
interface PoolBrowserProps {
onAddLiquidity?: (pool: PoolInfo) => void;
onRemoveLiquidity?: (pool: PoolInfo) => void;
onSwap?: (pool: PoolInfo) => void;
onCreatePool?: () => void;
}
export const PoolBrowser: React.FC<PoolBrowserProps> = ({
onAddLiquidity,
onRemoveLiquidity,
onSwap,
onCreatePool,
}) => {
const { t } = useTranslation();
// Use Asset Hub API for DEX operations
const { assetHubApi, isAssetHubReady, sudoKey } = usePezkuwi();
const { account } = useWallet();
const [pools, setPools] = useState<PoolInfo[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const isFounder = account ? isFounderWallet(account.address, sudoKey) : false;
useEffect(() => {
const loadPools = async () => {
if (!assetHubApi || !isAssetHubReady) return;
try {
setLoading(true);
const poolsData = await fetchPools(assetHubApi);
setPools(poolsData);
} catch (error) {
if (import.meta.env.DEV) console.error('Failed to load pools:', error);
} finally {
setLoading(false);
}
};
loadPools();
// Refresh pools every 10 seconds
const interval = setInterval(loadPools, 10000);
return () => clearInterval(interval);
}, [assetHubApi, isAssetHubReady]);
const filteredPools = pools.filter((pool) => {
if (!searchTerm) return true;
const search = searchTerm.toLowerCase();
return (
pool.asset1Symbol.toLowerCase().includes(search) ||
pool.asset2Symbol.toLowerCase().includes(search) ||
pool.id.toLowerCase().includes(search)
);
});
if (loading && pools.length === 0) {
return <LoadingState message={t('pools.loading')} />;
}
return (
<div className="space-y-6">
{/* Header with search and create */}
<div className="flex flex-col sm:flex-row gap-4 items-center justify-between">
<div className="relative flex-1 w-full">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
placeholder={t('pools.searchPlaceholder')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-gray-900 border border-gray-800 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500"
/>
</div>
{isFounder && onCreatePool && (
<button
onClick={onCreatePool}
className="flex items-center gap-2 px-6 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors"
>
<Plus className="w-4 h-4" />
{t('pools.createPool')}
</button>
)}
</div>
{/* Pools grid */}
{filteredPools.length === 0 ? (
<Card className="bg-gray-900/50 border-gray-800">
<CardContent className="py-12">
<div className="text-center text-gray-400">
{searchTerm
? t('pools.noSearchResults')
: t('pools.noPools')}
</div>
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4">
{filteredPools.map((pool) => (
<PoolCard
key={pool.id}
pool={pool}
onAddLiquidity={onAddLiquidity}
onRemoveLiquidity={onRemoveLiquidity}
onSwap={onSwap}
/>
))}
</div>
)}
</div>
);
};
interface PoolCardProps {
pool: PoolInfo;
onAddLiquidity?: (pool: PoolInfo) => void;
onRemoveLiquidity?: (pool: PoolInfo) => void;
onSwap?: (pool: PoolInfo) => void;
}
const PoolCard: React.FC<PoolCardProps> = ({
pool,
onAddLiquidity,
onRemoveLiquidity,
onSwap,
}) => {
const { t } = useTranslation();
const reserve1Display = formatTokenBalance(
pool.reserve1,
pool.asset1Decimals,
2
);
const reserve2Display = formatTokenBalance(
pool.reserve2,
pool.asset2Decimals,
2
);
// Calculate exchange rate
const rate =
BigInt(pool.reserve1) > BigInt(0)
? (Number(pool.reserve2) / Number(pool.reserve1)).toFixed(4)
: '0';
return (
<Card className="bg-gray-900/50 border-gray-800 hover:border-gray-700 transition-colors">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-lg font-bold text-white flex items-center gap-2">
<span className="text-green-400">{pool.asset1Symbol}</span>
<span className="text-gray-500">/</span>
<span className="text-yellow-400">{pool.asset2Symbol}</span>
</CardTitle>
<Badge className="bg-green-500/10 text-green-400 border-green-500/20">
{t('pools.active')}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Reserves */}
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">{t('pools.reserve', { symbol: pool.asset1Symbol })}</span>
<span className="text-white font-mono">
{reserve1Display} {pool.asset1Symbol}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">{t('pools.reserve', { symbol: pool.asset2Symbol })}</span>
<span className="text-white font-mono">
{reserve2Display} {pool.asset2Symbol}
</span>
</div>
</div>
{/* Exchange rate */}
<div className="p-3 bg-gray-800/50 rounded-lg">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">{t('common.exchangeRate')}</span>
<span className="text-cyan-400 font-mono">
1 {pool.asset1Symbol} = {rate} {pool.asset2Symbol}
</span>
</div>
</div>
{/* Stats row */}
<div className="grid grid-cols-3 gap-2 pt-2 border-t border-gray-800">
<div className="text-center">
<div className="text-xs text-gray-500">{t('pools.fee')}</div>
<div className="text-sm font-semibold text-white">
{pool.feeRate || '0.3'}%
</div>
</div>
<div className="text-center">
<div className="text-xs text-gray-500">{t('pools.volume24h')}</div>
<div className="text-sm font-semibold text-white">
{pool.volume24h || t('pools.na')}
</div>
</div>
<div className="text-center">
<div className="text-xs text-gray-500">{t('pools.apr')}</div>
<div className="text-sm font-semibold text-green-400">
{pool.apr7d || t('pools.na')}
</div>
</div>
</div>
{/* Action buttons */}
<div className="grid grid-cols-3 gap-2 pt-2">
{onAddLiquidity && (
<button
onClick={() => onAddLiquidity(pool)}
className="px-3 py-2 bg-green-600/20 hover:bg-green-600/30 text-green-400 border border-green-600/30 rounded-lg text-xs font-medium transition-colors flex items-center justify-center gap-1"
>
<Droplet className="w-3 h-3" />
{t('pools.add')}
</button>
)}
{onRemoveLiquidity && (
<button
onClick={() => onRemoveLiquidity(pool)}
className="px-3 py-2 bg-red-600/20 hover:bg-red-600/30 text-red-400 border border-red-600/30 rounded-lg text-xs font-medium transition-colors"
>
{t('pools.remove')}
</button>
)}
{onSwap && (
<button
onClick={() => onSwap(pool)}
className="px-3 py-2 bg-blue-600/20 hover:bg-blue-600/30 text-blue-400 border border-blue-600/30 rounded-lg text-xs font-medium transition-colors flex items-center justify-center gap-1"
>
<TrendingUp className="w-3 h-3" />
{t('pools.swap')}
</button>
)}
</div>
</CardContent>
</Card>
);
};