mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-23 01:17:56 +00:00
d282f609aa
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.
260 lines
8.0 KiB
TypeScript
260 lines
8.0 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { AlertCircle, Search, CheckCircle, Loader2 } from 'lucide-react';
|
|
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
|
|
|
interface TokenInfo {
|
|
assetId: number;
|
|
symbol: string;
|
|
name: string;
|
|
decimals: number;
|
|
exists: boolean;
|
|
}
|
|
|
|
interface AddTokenModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onAddToken: (assetId: number) => Promise<void>;
|
|
}
|
|
|
|
export const AddTokenModal: React.FC<AddTokenModalProps> = ({
|
|
isOpen,
|
|
onClose,
|
|
onAddToken,
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
const { assetHubApi, isAssetHubReady } = usePezkuwi();
|
|
const [assetId, setAssetId] = useState('');
|
|
const [error, setError] = useState('');
|
|
const [isSearching, setIsSearching] = useState(false);
|
|
const [isAdding, setIsAdding] = useState(false);
|
|
const [tokenInfo, setTokenInfo] = useState<TokenInfo | null>(null);
|
|
|
|
// Helper to decode hex string to UTF-8
|
|
const hexToString = (hex: string): string => {
|
|
if (!hex || hex === '0x') return '';
|
|
try {
|
|
const hexStr = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
const bytes = new Uint8Array(hexStr.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || []);
|
|
return new TextDecoder('utf-8').decode(bytes).replace(/\0/g, '');
|
|
} catch {
|
|
return '';
|
|
}
|
|
};
|
|
|
|
const handleSearch = async () => {
|
|
setError('');
|
|
setTokenInfo(null);
|
|
|
|
const id = parseInt(assetId);
|
|
if (isNaN(id) || id < 0) {
|
|
setError(t('addToken.invalidId'));
|
|
return;
|
|
}
|
|
|
|
if (!assetHubApi || !isAssetHubReady) {
|
|
setError(t('addToken.notReady'));
|
|
return;
|
|
}
|
|
|
|
setIsSearching(true);
|
|
try {
|
|
// Check if asset exists
|
|
const assetInfo = await assetHubApi.query.assets.asset(id);
|
|
|
|
if (!assetInfo || assetInfo.isNone) {
|
|
setError(t('addToken.notFound', { id }));
|
|
setIsSearching(false);
|
|
return;
|
|
}
|
|
|
|
// Get asset metadata
|
|
const metadata = await assetHubApi.query.assets.metadata(id);
|
|
const metaJson = metadata.toJSON() as { symbol?: string; name?: string; decimals?: number };
|
|
|
|
// Decode hex strings
|
|
let symbol = metaJson.symbol || '';
|
|
let name = metaJson.name || '';
|
|
|
|
if (typeof symbol === 'string' && symbol.startsWith('0x')) {
|
|
symbol = hexToString(symbol);
|
|
}
|
|
if (typeof name === 'string' && name.startsWith('0x')) {
|
|
name = hexToString(name);
|
|
}
|
|
|
|
// Fallback if no metadata
|
|
if (!symbol) symbol = `Asset #${id}`;
|
|
if (!name) name = `Unknown Asset`;
|
|
|
|
setTokenInfo({
|
|
assetId: id,
|
|
symbol: symbol.trim(),
|
|
name: name.trim(),
|
|
decimals: metaJson.decimals || 12,
|
|
exists: true,
|
|
});
|
|
} catch (err) {
|
|
console.error('Failed to fetch asset:', err);
|
|
setError(t('addToken.fetchFailed'));
|
|
} finally {
|
|
setIsSearching(false);
|
|
}
|
|
};
|
|
|
|
const handleAdd = async () => {
|
|
if (!tokenInfo) return;
|
|
|
|
setIsAdding(true);
|
|
try {
|
|
await onAddToken(tokenInfo.assetId);
|
|
handleClose();
|
|
} catch {
|
|
setError(t('addToken.addFailed'));
|
|
} finally {
|
|
setIsAdding(false);
|
|
}
|
|
};
|
|
|
|
const handleClose = () => {
|
|
setAssetId('');
|
|
setError('');
|
|
setTokenInfo(null);
|
|
onClose();
|
|
};
|
|
|
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
handleSearch();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={handleClose}>
|
|
<DialogContent className="bg-gray-900 border-gray-800 text-white sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-xl">{t('addToken.title')}</DialogTitle>
|
|
<DialogDescription className="text-gray-400">
|
|
{t('addToken.description')}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4 mt-4">
|
|
{/* Search Input */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="assetId" className="text-sm text-gray-300">
|
|
{t('addToken.assetIdLabel')}
|
|
</Label>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
id="assetId"
|
|
type="number"
|
|
value={assetId}
|
|
onChange={(e) => {
|
|
setAssetId(e.target.value);
|
|
setTokenInfo(null);
|
|
setError('');
|
|
}}
|
|
onKeyPress={handleKeyPress}
|
|
placeholder={t('addToken.assetIdPlaceholder')}
|
|
className="bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 flex-1"
|
|
min="0"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
onClick={handleSearch}
|
|
disabled={isSearching || !assetId}
|
|
className="bg-cyan-600 hover:bg-cyan-700"
|
|
>
|
|
{isSearching ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<Search className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
<p className="text-xs text-gray-500">
|
|
{t('addToken.assetIdHelp')}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Token Info Display */}
|
|
{tokenInfo && (
|
|
<div className="p-4 bg-gray-800/50 border border-green-500/30 rounded-lg">
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<CheckCircle className="h-5 w-5 text-green-400" />
|
|
<span className="text-green-400 font-medium">{t('addToken.found')}</span>
|
|
</div>
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-400">{t('addToken.symbol')}:</span>
|
|
<span className="text-white font-semibold">{tokenInfo.symbol}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-400">{t('addToken.name')}:</span>
|
|
<span className="text-white">{tokenInfo.name}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-400">{t('addToken.decimals')}:</span>
|
|
<span className="text-white">{tokenInfo.decimals}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-400">{t('addToken.assetId')}:</span>
|
|
<span className="text-white font-mono">#{tokenInfo.assetId}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Error Display */}
|
|
{error && (
|
|
<div className="flex items-center gap-2 p-3 bg-red-500/10 border border-red-500/20 rounded-md">
|
|
<AlertCircle className="h-4 w-4 text-red-400 flex-shrink-0" />
|
|
<p className="text-sm text-red-400">{error}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex justify-end gap-3 pt-2">
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
onClick={handleClose}
|
|
disabled={isAdding}
|
|
className="border border-gray-700 hover:bg-gray-800"
|
|
>
|
|
{t('addToken.cancel')}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
onClick={handleAdd}
|
|
disabled={!tokenInfo || isAdding}
|
|
className="bg-green-600 hover:bg-green-700"
|
|
>
|
|
{isAdding ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
{t('addToken.adding')}
|
|
</>
|
|
) : (
|
|
t('addToken.add')
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|