mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 09:07:55 +00:00
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.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Wallet, TrendingUp, RefreshCw, Award, Plus, Coins, Send, Shield, Users, Fuel, Lock } from 'lucide-react';
|
||||
@@ -21,6 +22,7 @@ interface TokenBalance {
|
||||
}
|
||||
|
||||
export const AccountBalance: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { api, assetHubApi, peopleApi, isApiReady, isAssetHubReady, isPeopleReady, selectedAccount } = usePezkuwi();
|
||||
const [balance, setBalance] = useState<{
|
||||
free: string;
|
||||
@@ -526,7 +528,7 @@ export const AccountBalance: React.FC = () => {
|
||||
// Add custom token handler
|
||||
const handleAddToken = async (assetId: number) => {
|
||||
if (customTokenIds.includes(assetId)) {
|
||||
alert('Token already added!');
|
||||
alert(t('balance.tokenAlreadyAdded'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -683,7 +685,7 @@ export const AccountBalance: React.FC = () => {
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center text-gray-400">
|
||||
<Wallet className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||
<p>Connect your wallet to view balance</p>
|
||||
<p>{t('balance.connectWallet')}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -700,9 +702,9 @@ export const AccountBalance: React.FC = () => {
|
||||
<img src="/tokens/HEZ.png" alt="HEZ" className="w-10 h-10 rounded-full" />
|
||||
<div>
|
||||
<CardTitle className="text-lg font-medium text-gray-300">
|
||||
HEZ Balance
|
||||
{t('balance.hezBalance')}
|
||||
</CardTitle>
|
||||
<div className="text-xs text-gray-500">Multi-Chain Overview</div>
|
||||
<div className="text-xs text-gray-500">{t('balance.multiChain')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -714,9 +716,9 @@ export const AccountBalance: React.FC = () => {
|
||||
title="Send HEZ to teyrcahins for transaction fees"
|
||||
>
|
||||
<Fuel className="w-4 h-4 mr-1" />
|
||||
Add Fee
|
||||
{t('balance.addFee')}
|
||||
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 text-xs bg-gray-800 text-gray-200 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10">
|
||||
Send HEZ to Asset Hub / People Chain
|
||||
{t('balance.sendHezToTeyrcahins')}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
@@ -741,8 +743,8 @@ export const AccountBalance: React.FC = () => {
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
{hezUsdPrice > 0
|
||||
? `≈ $${((parseFloat(balance.total) + parseFloat(assetHubHezBalance) + parseFloat(peopleHezBalance)) * hezUsdPrice).toFixed(2)} USD (Toplam)`
|
||||
: 'Price loading...'}
|
||||
? t('balance.usdTotal', { amount: ((parseFloat(balance.total) + parseFloat(assetHubHezBalance) + parseFloat(peopleHezBalance)) * hezUsdPrice).toFixed(2) })
|
||||
: t('balance.priceLoading')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -753,11 +755,11 @@ export const AccountBalance: React.FC = () => {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500"></div>
|
||||
<span className="text-sm text-gray-300">Pezkuwi (Relay Chain)</span>
|
||||
<span className="text-sm text-gray-300">{t('balance.relayChain')}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-semibold text-white">{balance.free} HEZ</div>
|
||||
<div className="text-xs text-gray-500">Reserved: {balance.reserved}</div>
|
||||
<div className="text-xs text-gray-500">{t('balance.reserved', { amount: balance.reserved })}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -767,13 +769,13 @@ export const AccountBalance: React.FC = () => {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-500"></div>
|
||||
<span className="text-sm text-gray-300">Pezkuwi Asset Hub</span>
|
||||
<span className="text-xs text-gray-500">(PEZ fees)</span>
|
||||
<span className="text-sm text-gray-300">{t('balance.assetHub')}</span>
|
||||
<span className="text-xs text-gray-500">{t('balance.pezFees')}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-semibold text-white">{assetHubHezBalance} HEZ</div>
|
||||
{parseFloat(assetHubHezBalance) < 0.1 && (
|
||||
<div className="text-xs text-yellow-400">⚠️ Low for fees</div>
|
||||
<div className="text-xs text-yellow-400">{`⚠️ ${t('balance.lowForFees')}`}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -784,13 +786,13 @@ export const AccountBalance: React.FC = () => {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-purple-500"></div>
|
||||
<span className="text-sm text-gray-300">Pezkuwi People</span>
|
||||
<span className="text-xs text-gray-500">(Identity fees)</span>
|
||||
<span className="text-sm text-gray-300">{t('balance.peopleChain')}</span>
|
||||
<span className="text-xs text-gray-500">{t('balance.identityFees')}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-semibold text-white">{peopleHezBalance} HEZ</div>
|
||||
{parseFloat(peopleHezBalance) < 0.1 && (
|
||||
<div className="text-xs text-yellow-400">⚠️ Low for fees</div>
|
||||
<div className="text-xs text-yellow-400">{`⚠️ ${t('balance.lowForFees')}`}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -807,7 +809,7 @@ export const AccountBalance: React.FC = () => {
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<img src="/tokens/PEZ.png" alt="PEZ" className="w-10 h-10 rounded-full flex-shrink-0" />
|
||||
<CardTitle className="text-lg font-medium text-gray-300 whitespace-nowrap">
|
||||
PEZ Balance
|
||||
{t('balance.pezBalance')}
|
||||
</CardTitle>
|
||||
</div>
|
||||
<Button
|
||||
@@ -818,9 +820,9 @@ export const AccountBalance: React.FC = () => {
|
||||
title="Send HEZ to Asset Hub for transaction fees"
|
||||
>
|
||||
<Fuel className="w-4 h-4 mr-1" />
|
||||
Add Fees
|
||||
{t('balance.addFees')}
|
||||
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 text-xs bg-gray-800 text-gray-200 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none">
|
||||
Send HEZ for PEZ transfer fees
|
||||
{t('balance.sendHezForPezFees')}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -834,10 +836,10 @@ export const AccountBalance: React.FC = () => {
|
||||
<div className="text-sm text-gray-400">
|
||||
{pezUsdPrice > 0
|
||||
? `≈ $${(parseFloat(pezBalance) * pezUsdPrice).toFixed(2)} USD`
|
||||
: 'Price loading...'}
|
||||
: t('balance.priceLoading')}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
Governance & Rewards Token (on Asset Hub)
|
||||
{t('balance.govRewardsToken')}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -849,7 +851,7 @@ export const AccountBalance: React.FC = () => {
|
||||
<div className="flex items-center gap-3">
|
||||
<img src="/tokens/USDT.png" alt="USDT" className="w-10 h-10 rounded-full" />
|
||||
<CardTitle className="text-lg font-medium text-gray-300">
|
||||
USDT Balance
|
||||
{t('balance.usdtBalance')}
|
||||
</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -860,7 +862,7 @@ export const AccountBalance: React.FC = () => {
|
||||
<span className="text-2xl text-gray-400 ml-2">USDT</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
≈ ${usdtBalance} USD • Stablecoin (on Asset Hub)
|
||||
{t('balance.stablecoinOnAssetHub', { amount: usdtBalance })}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -873,7 +875,7 @@ export const AccountBalance: React.FC = () => {
|
||||
<div className="flex items-center gap-3">
|
||||
<img src="/tokens/HEZ.png" alt="wHEZ" className="w-10 h-10 rounded-full" />
|
||||
<CardTitle className="text-lg font-medium text-gray-300">
|
||||
wHEZ Balance
|
||||
{t('balance.whezBalance')}
|
||||
</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -886,7 +888,7 @@ export const AccountBalance: React.FC = () => {
|
||||
<div className="text-sm text-gray-400">
|
||||
{hezUsdPrice > 0
|
||||
? `≈ $${(parseFloat(whezBalance) * hezUsdPrice).toFixed(2)} USD`
|
||||
: 'Price loading...'} • Wrapped HEZ (on Asset Hub)
|
||||
: t('balance.priceLoading')} • {t('balance.wrappedHezOnAssetHub')}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -900,7 +902,7 @@ export const AccountBalance: React.FC = () => {
|
||||
<div className="flex items-center gap-3">
|
||||
<img src="/tokens/LP.png" alt="LP" className="w-10 h-10 rounded-full" />
|
||||
<CardTitle className="text-lg font-medium text-gray-300">
|
||||
LP Token Positions
|
||||
{t('balance.lpTokenPositions')}
|
||||
</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -918,7 +920,7 @@ export const AccountBalance: React.FC = () => {
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-semibold text-white">{lp.balance}</div>
|
||||
<div className="text-xs text-gray-500">Pool Share</div>
|
||||
<div className="text-xs text-gray-500">{t('balance.poolShare')}</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -929,7 +931,7 @@ export const AccountBalance: React.FC = () => {
|
||||
className="bg-gradient-to-r from-purple-600 to-pink-600 text-white font-medium hover:from-purple-500 hover:to-pink-500 border-0"
|
||||
>
|
||||
<Lock className="w-3 h-3 mr-1" />
|
||||
Stake
|
||||
{t('balance.stake')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -943,7 +945,7 @@ export const AccountBalance: React.FC = () => {
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg font-medium text-gray-300">
|
||||
Account Information
|
||||
{t('balance.accountInfo')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -951,13 +953,13 @@ export const AccountBalance: React.FC = () => {
|
||||
{/* Account Details */}
|
||||
<div className="space-y-2 pb-4 border-b border-gray-800">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-400">Account</span>
|
||||
<span className="text-gray-400">{t('balance.account')}</span>
|
||||
<span className="text-white font-mono">
|
||||
{selectedAccount.meta.name || 'Unnamed'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-400">Address</span>
|
||||
<span className="text-gray-400">{t('balance.address')}</span>
|
||||
<span className="text-white font-mono text-xs">
|
||||
{selectedAccount.address.slice(0, 8)}...{selectedAccount.address.slice(-8)}
|
||||
</span>
|
||||
@@ -966,9 +968,9 @@ export const AccountBalance: React.FC = () => {
|
||||
|
||||
{/* Scores from Blockchain */}
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 mb-3">Scores from Blockchain</div>
|
||||
<div className="text-xs text-gray-400 mb-3">{t('balance.scoresFromBlockchain')}</div>
|
||||
{loadingScores ? (
|
||||
<div className="text-sm text-gray-400">Loading scores...</div>
|
||||
<div className="text-sm text-gray-400">{t('balance.loadingScores')}</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{/* Score Grid */}
|
||||
@@ -976,28 +978,28 @@ export const AccountBalance: React.FC = () => {
|
||||
<div className="bg-gray-800/50 rounded-lg p-3">
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<Shield className="h-3 w-3 text-purple-400" />
|
||||
<span className="text-xs text-gray-400">Trust</span>
|
||||
<span className="text-xs text-gray-400">{t('balance.trust')}</span>
|
||||
</div>
|
||||
<span className="text-base font-bold text-purple-400">{scores.trustScore}</span>
|
||||
</div>
|
||||
<div className="bg-gray-800/50 rounded-lg p-3">
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<Users className="h-3 w-3 text-cyan-400" />
|
||||
<span className="text-xs text-gray-400">Referral</span>
|
||||
<span className="text-xs text-gray-400">{t('balance.referral')}</span>
|
||||
</div>
|
||||
<span className="text-base font-bold text-cyan-400">{scores.referralScore}</span>
|
||||
</div>
|
||||
<div className="bg-gray-800/50 rounded-lg p-3">
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<TrendingUp className="h-3 w-3 text-green-400" />
|
||||
<span className="text-xs text-gray-400">Staking</span>
|
||||
<span className="text-xs text-gray-400">{t('balance.staking')}</span>
|
||||
</div>
|
||||
<span className="text-base font-bold text-green-400">{scores.stakingScore}</span>
|
||||
</div>
|
||||
<div className="bg-gray-800/50 rounded-lg p-3">
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<Award className="h-3 w-3 text-pink-400" />
|
||||
<span className="text-xs text-gray-400">Tiki</span>
|
||||
<span className="text-xs text-gray-400">{t('balance.tiki')}</span>
|
||||
</div>
|
||||
<span className="text-base font-bold text-pink-400">{scores.tikiScore}</span>
|
||||
</div>
|
||||
@@ -1006,7 +1008,7 @@ export const AccountBalance: React.FC = () => {
|
||||
{/* Total Score */}
|
||||
<div className="pt-3 border-t border-gray-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-400">Total Score</span>
|
||||
<span className="text-sm text-gray-400">{t('balance.totalScore')}</span>
|
||||
<span className="text-xl font-bold bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
|
||||
{scores.totalScore}
|
||||
</span>
|
||||
@@ -1026,7 +1028,7 @@ export const AccountBalance: React.FC = () => {
|
||||
<div className="flex items-center gap-2">
|
||||
<Coins className="h-5 w-5 text-cyan-400" />
|
||||
<CardTitle className="text-lg font-medium text-gray-300">
|
||||
Other Assets
|
||||
{t('balance.otherAssets')}
|
||||
</CardTitle>
|
||||
</div>
|
||||
<Button
|
||||
@@ -1036,7 +1038,7 @@ export const AccountBalance: React.FC = () => {
|
||||
className="text-cyan-400 hover:text-cyan-300 hover:bg-cyan-400/10"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Add Token
|
||||
{t('balance.addToken')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -1044,9 +1046,9 @@ export const AccountBalance: React.FC = () => {
|
||||
{otherTokens.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<Coins className="w-12 h-12 text-gray-600 mx-auto mb-3 opacity-50" />
|
||||
<p className="text-gray-500 text-sm">No custom tokens yet</p>
|
||||
<p className="text-gray-500 text-sm">{t('balance.noCustomTokens')}</p>
|
||||
<p className="text-gray-600 text-xs mt-1">
|
||||
Add custom tokens to track additional assets
|
||||
{t('balance.addCustomTokensDesc')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -31,6 +32,7 @@ export const AddTokenModal: React.FC<AddTokenModalProps> = ({
|
||||
onClose,
|
||||
onAddToken,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { assetHubApi, isAssetHubReady } = usePezkuwi();
|
||||
const [assetId, setAssetId] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
@@ -56,12 +58,12 @@ export const AddTokenModal: React.FC<AddTokenModalProps> = ({
|
||||
|
||||
const id = parseInt(assetId);
|
||||
if (isNaN(id) || id < 0) {
|
||||
setError('Please enter a valid asset ID (positive number)');
|
||||
setError(t('addToken.invalidId'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!assetHubApi || !isAssetHubReady) {
|
||||
setError('Asset Hub connection not ready. Please wait...');
|
||||
setError(t('addToken.notReady'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -71,7 +73,7 @@ export const AddTokenModal: React.FC<AddTokenModalProps> = ({
|
||||
const assetInfo = await assetHubApi.query.assets.asset(id);
|
||||
|
||||
if (!assetInfo || assetInfo.isNone) {
|
||||
setError(`Asset #${id} not found on blockchain`);
|
||||
setError(t('addToken.notFound', { id }));
|
||||
setIsSearching(false);
|
||||
return;
|
||||
}
|
||||
@@ -104,7 +106,7 @@ export const AddTokenModal: React.FC<AddTokenModalProps> = ({
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch asset:', err);
|
||||
setError('Failed to fetch asset from blockchain');
|
||||
setError(t('addToken.fetchFailed'));
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
@@ -118,7 +120,7 @@ export const AddTokenModal: React.FC<AddTokenModalProps> = ({
|
||||
await onAddToken(tokenInfo.assetId);
|
||||
handleClose();
|
||||
} catch {
|
||||
setError('Failed to add token');
|
||||
setError(t('addToken.addFailed'));
|
||||
} finally {
|
||||
setIsAdding(false);
|
||||
}
|
||||
@@ -142,9 +144,9 @@ export const AddTokenModal: React.FC<AddTokenModalProps> = ({
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="bg-gray-900 border-gray-800 text-white sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">Add Custom Token</DialogTitle>
|
||||
<DialogTitle className="text-xl">{t('addToken.title')}</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Enter the asset ID to fetch token details from blockchain.
|
||||
{t('addToken.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -152,7 +154,7 @@ export const AddTokenModal: React.FC<AddTokenModalProps> = ({
|
||||
{/* Search Input */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="assetId" className="text-sm text-gray-300">
|
||||
Asset ID
|
||||
{t('addToken.assetIdLabel')}
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
@@ -165,7 +167,7 @@ export const AddTokenModal: React.FC<AddTokenModalProps> = ({
|
||||
setError('');
|
||||
}}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="e.g., 1001"
|
||||
placeholder={t('addToken.assetIdPlaceholder')}
|
||||
className="bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 flex-1"
|
||||
min="0"
|
||||
/>
|
||||
@@ -183,7 +185,7 @@ export const AddTokenModal: React.FC<AddTokenModalProps> = ({
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
Known assets: 1001 (DOT), 1002 (ETH), 1003 (BTC)
|
||||
{t('addToken.assetIdHelp')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -192,23 +194,23 @@ export const AddTokenModal: React.FC<AddTokenModalProps> = ({
|
||||
<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">Token Found!</span>
|
||||
<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">Symbol:</span>
|
||||
<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">Name:</span>
|
||||
<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">Decimals:</span>
|
||||
<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">Asset ID:</span>
|
||||
<span className="text-gray-400">{t('addToken.assetId')}:</span>
|
||||
<span className="text-white font-mono">#{tokenInfo.assetId}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -232,7 +234,7 @@ export const AddTokenModal: React.FC<AddTokenModalProps> = ({
|
||||
disabled={isAdding}
|
||||
className="border border-gray-700 hover:bg-gray-800"
|
||||
>
|
||||
Cancel
|
||||
{t('addToken.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -243,10 +245,10 @@ export const AddTokenModal: React.FC<AddTokenModalProps> = ({
|
||||
{isAdding ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Adding...
|
||||
{t('addToken.adding')}
|
||||
</>
|
||||
) : (
|
||||
'Add Token'
|
||||
t('addToken.add')
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -110,14 +110,14 @@ const AppLayout: React.FC = () => {
|
||||
className="text-cyan-300 hover:text-cyan-100 transition-colors flex items-center gap-1 text-sm font-semibold whitespace-nowrap"
|
||||
>
|
||||
<Users className="w-4 h-4" />
|
||||
Be Citizen
|
||||
{t('nav.beCitizen')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate('/login')}
|
||||
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition-colors flex items-center gap-2 text-sm whitespace-nowrap"
|
||||
>
|
||||
<LogIn className="w-4 h-4" />
|
||||
Login
|
||||
{t('nav.login')}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
@@ -129,7 +129,7 @@ const AppLayout: React.FC = () => {
|
||||
href="/docs"
|
||||
className="text-gray-300 hover:text-white transition-colors text-sm"
|
||||
>
|
||||
Docs
|
||||
{t('nav.docs')}
|
||||
</a>
|
||||
|
||||
<div className="w-px h-6 bg-gray-700"></div>
|
||||
@@ -161,7 +161,7 @@ const AppLayout: React.FC = () => {
|
||||
className="flex flex-col items-center gap-0.5 sm:gap-1 p-1.5 sm:p-2 rounded-xl bg-gray-900/70 border border-green-500/40 text-[10px] sm:text-xs font-medium transition-all hover:scale-[1.03] active:scale-95 cursor-pointer text-gray-300 hover:text-white"
|
||||
>
|
||||
<LayoutDashboard className="w-4 h-4 sm:w-5 sm:h-5 text-green-400" />
|
||||
Dashboard
|
||||
{t('nav.dashboard')}
|
||||
</button>
|
||||
{/* Wallet */}
|
||||
<button
|
||||
@@ -169,7 +169,7 @@ const AppLayout: React.FC = () => {
|
||||
className="flex flex-col items-center gap-0.5 sm:gap-1 p-1.5 sm:p-2 rounded-xl bg-gray-900/70 border border-yellow-400/40 text-[10px] sm:text-xs font-medium transition-all hover:scale-[1.03] active:scale-95 cursor-pointer text-gray-300 hover:text-white"
|
||||
>
|
||||
<Wallet className="w-4 h-4 sm:w-5 sm:h-5 text-yellow-400" />
|
||||
Wallet
|
||||
{t('nav.wallet')}
|
||||
</button>
|
||||
{/* Be Citizen */}
|
||||
<button
|
||||
@@ -177,7 +177,7 @@ const AppLayout: React.FC = () => {
|
||||
className="flex flex-col items-center gap-0.5 sm:gap-1 p-1.5 sm:p-2 rounded-xl bg-gray-900/70 border border-cyan-400/40 text-[10px] sm:text-xs font-medium transition-all hover:scale-[1.03] active:scale-95 cursor-pointer text-gray-300 hover:text-white"
|
||||
>
|
||||
<Users className="w-4 h-4 sm:w-5 sm:h-5 text-cyan-400" />
|
||||
Be Citizen
|
||||
{t('nav.beCitizen')}
|
||||
</button>
|
||||
{/* Governance (dropdown) */}
|
||||
<div className="relative">
|
||||
@@ -186,24 +186,24 @@ const AppLayout: React.FC = () => {
|
||||
className="w-full flex flex-col items-center gap-0.5 sm:gap-1 p-1.5 sm:p-2 rounded-xl bg-gray-900/70 border border-green-500/40 text-[10px] sm:text-xs font-medium transition-all hover:scale-[1.03] active:scale-95 cursor-pointer text-gray-300 hover:text-white"
|
||||
>
|
||||
<FileEdit className="w-4 h-4 sm:w-5 sm:h-5 text-green-400" />
|
||||
<span className="flex items-center gap-0.5">Governance <ChevronDown className="w-3 h-3" /></span>
|
||||
<span className="flex items-center gap-0.5">{t('nav.governance')} <ChevronDown className="w-3 h-3" /></span>
|
||||
</button>
|
||||
{openMenu === 'governance' && (
|
||||
<div className="absolute left-0 top-full mt-1 w-48 bg-gray-900 border border-gray-700 rounded-lg shadow-lg z-50">
|
||||
<button onClick={() => { setShowProposalWizard(true); setOpenMenu(null); }} className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2 rounded-t-lg">
|
||||
<FileEdit className="w-4 h-4" /> Proposals
|
||||
<FileEdit className="w-4 h-4" /> {t('governance.proposals')}
|
||||
</button>
|
||||
<button onClick={() => { setShowDelegation(true); setOpenMenu(null); }} className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2">
|
||||
<Users2 className="w-4 h-4" /> Delegation
|
||||
<Users2 className="w-4 h-4" /> {t('governance.delegation')}
|
||||
</button>
|
||||
<button onClick={() => { setShowForum(true); setOpenMenu(null); }} className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2">
|
||||
<MessageSquare className="w-4 h-4" /> Forum
|
||||
<MessageSquare className="w-4 h-4" /> {t('nav.forum')}
|
||||
</button>
|
||||
<button onClick={() => { setShowTreasury(true); setTreasuryTab('overview'); setOpenMenu(null); }} className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2">
|
||||
<PiggyBank className="w-4 h-4" /> Treasury
|
||||
<PiggyBank className="w-4 h-4" /> {t('nav.treasury')}
|
||||
</button>
|
||||
<button onClick={() => { setShowModeration(true); setOpenMenu(null); }} className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2 rounded-b-lg">
|
||||
<ShieldCheck className="w-4 h-4" /> Moderation
|
||||
<ShieldCheck className="w-4 h-4" /> {t('nav.moderation')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -215,24 +215,24 @@ const AppLayout: React.FC = () => {
|
||||
className="w-full flex flex-col items-center gap-0.5 sm:gap-1 p-1.5 sm:p-2 rounded-xl bg-gray-900/70 border border-red-500/40 text-[10px] sm:text-xs font-medium transition-all hover:scale-[1.03] active:scale-95 cursor-pointer text-gray-300 hover:text-white"
|
||||
>
|
||||
<ArrowRightLeft className="w-4 h-4 sm:w-5 sm:h-5 text-red-400" />
|
||||
<span className="flex items-center gap-0.5">Trading <ChevronDown className="w-3 h-3" /></span>
|
||||
<span className="flex items-center gap-0.5">{t('nav.trading')} <ChevronDown className="w-3 h-3" /></span>
|
||||
</button>
|
||||
{openMenu === 'trading' && (
|
||||
<div className="absolute left-0 top-full mt-1 w-48 bg-gray-900 border border-gray-700 rounded-lg shadow-lg z-50">
|
||||
<button onClick={() => { setShowDEX(true); setOpenMenu(null); }} className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2 rounded-t-lg">
|
||||
<Droplet className="w-4 h-4" /> DEX Pools
|
||||
<Droplet className="w-4 h-4" /> {t('trading.dexPools')}
|
||||
</button>
|
||||
<button onClick={() => { setShowP2P(true); navigate('/p2p'); setOpenMenu(null); }} className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2">
|
||||
<Users className="w-4 h-4" /> P2P
|
||||
<Users className="w-4 h-4" /> {t('trading.p2p')}
|
||||
</button>
|
||||
<button onClick={() => { navigate('/presale'); setOpenMenu(null); }} className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2">
|
||||
<Coins className="w-4 h-4" /> Presale
|
||||
<Coins className="w-4 h-4" /> {t('trading.presale')}
|
||||
</button>
|
||||
<button onClick={() => { setShowStaking(true); setOpenMenu(null); }} className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2">
|
||||
<TrendingUp className="w-4 h-4" /> Staking
|
||||
<TrendingUp className="w-4 h-4" /> {t('trading.staking')}
|
||||
</button>
|
||||
<button onClick={() => { setShowMultiSig(true); setOpenMenu(null); }} className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2 rounded-b-lg">
|
||||
<Lock className="w-4 h-4" /> MultiSig
|
||||
<Lock className="w-4 h-4" /> {t('trading.multiSig')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -243,7 +243,7 @@ const AppLayout: React.FC = () => {
|
||||
className="flex flex-col items-center gap-0.5 sm:gap-1 p-1.5 sm:p-2 rounded-xl bg-gray-900/70 border border-yellow-400/40 text-[10px] sm:text-xs font-medium transition-all hover:scale-[1.03] active:scale-95 cursor-pointer text-gray-300 hover:text-white"
|
||||
>
|
||||
<Award className="w-4 h-4 sm:w-5 sm:h-5 text-yellow-400" />
|
||||
Education
|
||||
{t('nav.education')}
|
||||
</button>
|
||||
{/* Settings */}
|
||||
<button
|
||||
@@ -251,7 +251,7 @@ const AppLayout: React.FC = () => {
|
||||
className="flex flex-col items-center gap-0.5 sm:gap-1 p-1.5 sm:p-2 rounded-xl bg-gray-900/70 border border-gray-500/40 text-[10px] sm:text-xs font-medium transition-all hover:scale-[1.03] active:scale-95 cursor-pointer text-gray-300 hover:text-white"
|
||||
>
|
||||
<Settings className="w-4 h-4 sm:w-5 sm:h-5 text-gray-400" />
|
||||
Settings
|
||||
{t('nav.settings')}
|
||||
</button>
|
||||
{/* Logout */}
|
||||
<button
|
||||
@@ -259,7 +259,7 @@ const AppLayout: React.FC = () => {
|
||||
className="flex flex-col items-center gap-0.5 sm:gap-1 p-1.5 sm:p-2 rounded-xl bg-gray-900/70 border border-red-500/40 text-[10px] sm:text-xs font-medium transition-all hover:scale-[1.03] active:scale-95 cursor-pointer text-gray-300 hover:text-white"
|
||||
>
|
||||
<LogIn className="w-4 h-4 sm:w-5 sm:h-5 text-red-400 rotate-180" />
|
||||
Logout
|
||||
{t('nav.logout')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -281,81 +281,81 @@ const AppLayout: React.FC = () => {
|
||||
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
|
||||
>
|
||||
<LayoutDashboard className="w-5 h-5" />
|
||||
Dashboard
|
||||
{t('nav.dashboard')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { navigate('/wallet'); setMobileMenuOpen(false); }}
|
||||
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
|
||||
>
|
||||
<Wallet className="w-5 h-5" />
|
||||
Wallet
|
||||
{t('nav.wallet')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { navigate('/citizens'); setMobileMenuOpen(false); }}
|
||||
className="w-full text-left px-4 py-3 rounded-lg text-cyan-400 hover:bg-gray-800 flex items-center gap-3"
|
||||
>
|
||||
<Users className="w-5 h-5" />
|
||||
Citizens Portal
|
||||
{t('nav.citizensPortal')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { navigate('/be-citizen'); setMobileMenuOpen(false); }}
|
||||
className="w-full text-left px-4 py-3 rounded-lg text-cyan-300 hover:bg-gray-800 flex items-center gap-3"
|
||||
>
|
||||
<Users className="w-5 h-5" />
|
||||
Be Citizen
|
||||
{t('nav.beCitizen')}
|
||||
</button>
|
||||
<div className="border-t border-gray-800 my-2" />
|
||||
<div className="px-4 py-2 text-xs text-gray-500 uppercase tracking-wider">Governance</div>
|
||||
<div className="px-4 py-2 text-xs text-gray-500 uppercase tracking-wider">{t('nav.governance')}</div>
|
||||
<button
|
||||
onClick={() => { setShowProposalWizard(true); setMobileMenuOpen(false); }}
|
||||
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
|
||||
>
|
||||
<FileEdit className="w-5 h-5" />
|
||||
Proposals
|
||||
{t('governance.proposals')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setShowDelegation(true); setMobileMenuOpen(false); }}
|
||||
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
|
||||
>
|
||||
<Users2 className="w-5 h-5" />
|
||||
Delegation
|
||||
{t('governance.delegation')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setShowTreasury(true); setMobileMenuOpen(false); }}
|
||||
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
|
||||
>
|
||||
<PiggyBank className="w-5 h-5" />
|
||||
Treasury
|
||||
{t('nav.treasury')}
|
||||
</button>
|
||||
<div className="border-t border-gray-800 my-2" />
|
||||
<div className="px-4 py-2 text-xs text-gray-500 uppercase tracking-wider">Trading</div>
|
||||
<div className="px-4 py-2 text-xs text-gray-500 uppercase tracking-wider">{t('nav.trading')}</div>
|
||||
<button
|
||||
onClick={() => { setShowDEX(true); setMobileMenuOpen(false); }}
|
||||
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
|
||||
>
|
||||
<Droplet className="w-5 h-5" />
|
||||
DEX Pools
|
||||
{t('trading.dexPools')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { navigate('/p2p'); setMobileMenuOpen(false); }}
|
||||
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
|
||||
>
|
||||
<ArrowRightLeft className="w-5 h-5" />
|
||||
P2P
|
||||
{t('trading.p2p')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { navigate('/presale'); setMobileMenuOpen(false); }}
|
||||
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
|
||||
>
|
||||
<Coins className="w-5 h-5" />
|
||||
Presale
|
||||
{t('trading.presale')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setShowStaking(true); setMobileMenuOpen(false); }}
|
||||
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
|
||||
>
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
Staking
|
||||
{t('trading.staking')}
|
||||
</button>
|
||||
<div className="border-t border-gray-800 my-2" />
|
||||
<button
|
||||
@@ -363,14 +363,14 @@ const AppLayout: React.FC = () => {
|
||||
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
|
||||
>
|
||||
<Settings className="w-5 h-5" />
|
||||
Settings
|
||||
{t('nav.settings')}
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => { await signOut(); navigate('/login'); setMobileMenuOpen(false); }}
|
||||
className="w-full text-left px-4 py-3 rounded-lg text-red-400 hover:bg-gray-800 flex items-center gap-3"
|
||||
>
|
||||
<LogIn className="w-5 h-5 rotate-180" />
|
||||
Logout
|
||||
{t('nav.logout')}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
@@ -380,14 +380,14 @@ const AppLayout: React.FC = () => {
|
||||
className="w-full text-left px-4 py-3 rounded-lg text-cyan-300 hover:bg-gray-800 flex items-center gap-3"
|
||||
>
|
||||
<Users className="w-5 h-5" />
|
||||
Be Citizen
|
||||
{t('nav.beCitizen')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { navigate('/login'); setMobileMenuOpen(false); }}
|
||||
className="w-full text-left px-4 py-3 rounded-lg bg-green-600 hover:bg-green-700 text-white flex items-center gap-3"
|
||||
>
|
||||
<LogIn className="w-5 h-5" />
|
||||
Login
|
||||
{t('nav.login')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
@@ -398,7 +398,7 @@ const AppLayout: React.FC = () => {
|
||||
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
|
||||
>
|
||||
<ExternalLink className="w-5 h-5" />
|
||||
Docs
|
||||
{t('nav.docs')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -499,10 +499,10 @@ const AppLayout: React.FC = () => {
|
||||
<div className="max-w-full mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
|
||||
Staking Rewards
|
||||
{t('staking.title')}
|
||||
</h2>
|
||||
<p className="text-gray-400 text-lg max-w-3xl mx-auto">
|
||||
Stake your tokens and earn rewards
|
||||
{t('staking.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<StakingDashboard />
|
||||
@@ -513,10 +513,10 @@ const AppLayout: React.FC = () => {
|
||||
<div className="max-w-full mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
|
||||
Multi-Signature Wallet
|
||||
{t('multiSig.title')}
|
||||
</h2>
|
||||
<p className="text-gray-400 text-lg max-w-3xl mx-auto">
|
||||
Secure your funds with multi-signature protection
|
||||
{t('multiSig.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<MultiSigWallet />
|
||||
@@ -571,7 +571,7 @@ const AppLayout: React.FC = () => {
|
||||
}}
|
||||
className="bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded-full shadow-lg flex items-center gap-2 transition-all"
|
||||
>
|
||||
← Back to Home
|
||||
{`← ${t('common.backToHome')}`}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -600,12 +600,12 @@ const AppLayout: React.FC = () => {
|
||||
PezkuwiChain
|
||||
</h3>
|
||||
<p className="text-gray-400 text-sm">
|
||||
Decentralized governance platform
|
||||
{t('footer.platform')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-white font-semibold mb-4 text-left">About</h4>
|
||||
<h4 className="text-white font-semibold mb-4 text-left">{t('footer.about')}</h4>
|
||||
<ul className="space-y-2 text-left">
|
||||
<li>
|
||||
<a
|
||||
@@ -613,13 +613,13 @@ const AppLayout: React.FC = () => {
|
||||
download="Pezkuwi_Whitepaper.pdf"
|
||||
className="text-gray-400 hover:text-white text-sm inline-flex items-center"
|
||||
>
|
||||
Whitepaper
|
||||
{t('footer.whitepaper')}
|
||||
<ExternalLink className="w-3 h-3 ml-1" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/pezkuwichain" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white text-sm inline-flex items-center">
|
||||
GitHub
|
||||
{t('footer.github')}
|
||||
<ExternalLink className="w-3 h-3 ml-1" />
|
||||
</a>
|
||||
</li>
|
||||
@@ -627,17 +627,17 @@ const AppLayout: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-white font-semibold mb-4 text-left">Developers</h4>
|
||||
<h4 className="text-white font-semibold mb-4 text-left">{t('footer.developers')}</h4>
|
||||
<ul className="space-y-2 text-left">
|
||||
<li>
|
||||
<a href="/api" className="text-gray-400 hover:text-white text-sm inline-flex items-center">
|
||||
API
|
||||
{t('footer.api')}
|
||||
<ExternalLink className="w-3 h-3 ml-1" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/developers" className="text-gray-400 hover:text-white text-sm inline-flex items-center">
|
||||
SDK
|
||||
{t('footer.sdk')}
|
||||
<ExternalLink className="w-3 h-3 ml-1" />
|
||||
</a>
|
||||
</li>
|
||||
@@ -645,45 +645,45 @@ const AppLayout: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-white font-semibold mb-4 text-left">Community</h4>
|
||||
<h4 className="text-white font-semibold mb-4 text-left">{t('footer.community')}</h4>
|
||||
<ul className="space-y-2 text-left">
|
||||
<li>
|
||||
<a href="https://discord.gg/pezkuwichain" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white text-sm inline-flex items-center">
|
||||
Discord
|
||||
{t('footer.discord')}
|
||||
<ExternalLink className="w-3 h-3 ml-1" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://x.com/PezkuwiChain" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white text-sm inline-flex items-center">
|
||||
Twitter/X
|
||||
{t('footer.twitter')}
|
||||
<ExternalLink className="w-3 h-3 ml-1" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://t.me/PezkuwiApp" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white text-sm inline-flex items-center">
|
||||
Telegram
|
||||
{t('footer.telegram')}
|
||||
<ExternalLink className="w-3 h-3 ml-1" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.youtube.com/@SatoshiQazi" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white text-sm inline-flex items-center">
|
||||
YouTube
|
||||
{t('footer.youtube')}
|
||||
<ExternalLink className="w-3 h-3 ml-1" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://facebook.com/profile.php?id=61582484611719" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white text-sm inline-flex items-center">
|
||||
Facebook
|
||||
{t('footer.facebook')}
|
||||
<ExternalLink className="w-3 h-3 ml-1" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mt-8 pt-8 border-t border-gray-800 text-center">
|
||||
<p className="text-gray-400 text-sm">
|
||||
© 2024 PezkuwiChain. All rights reserved.
|
||||
{t('footer.copyright')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// Catches React errors and displays fallback UI
|
||||
|
||||
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||
import { useTranslation, Translation } from 'react-i18next';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -92,31 +93,33 @@ export class ErrorBoundary extends Component<Props, State> {
|
||||
|
||||
// Default error UI
|
||||
return (
|
||||
<Translation>
|
||||
{(t) => (
|
||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center p-4">
|
||||
<Card className="bg-gray-900 border-gray-800 max-w-2xl w-full">
|
||||
<CardContent className="p-8">
|
||||
<Alert className="bg-red-900/20 border-red-500 mb-6">
|
||||
<AlertTriangle className="h-6 w-6 text-red-400" />
|
||||
<AlertDescription className="text-gray-300">
|
||||
<h2 className="text-xl font-bold mb-2 text-white">Something Went Wrong</h2>
|
||||
<h2 className="text-xl font-bold mb-2 text-white">{t('errors.somethingWentWrong')}</h2>
|
||||
<p className="mb-4">
|
||||
An unexpected error occurred. We apologize for the inconvenience.
|
||||
{t('errors.unexpectedError')}
|
||||
</p>
|
||||
{this.state.error && (
|
||||
<details className="mt-4 p-4 bg-gray-950 rounded border border-gray-700">
|
||||
<summary className="cursor-pointer text-sm font-semibold text-gray-400 hover:text-gray-300">
|
||||
Error Details (for developers)
|
||||
{t('errors.errorDetails')}
|
||||
</summary>
|
||||
<div className="mt-3 text-xs font-mono space-y-2">
|
||||
<div>
|
||||
<strong className="text-red-400">Error:</strong>
|
||||
<strong className="text-red-400">{t('errors.error')}</strong>
|
||||
<pre className="mt-1 text-gray-400 whitespace-pre-wrap">
|
||||
{this.state.error.toString()}
|
||||
</pre>
|
||||
</div>
|
||||
{this.state.errorInfo && (
|
||||
<div>
|
||||
<strong className="text-red-400">Component Stack:</strong>
|
||||
<strong className="text-red-400">{t('errors.componentStack')}</strong>
|
||||
<pre className="mt-1 text-gray-400 whitespace-pre-wrap">
|
||||
{this.state.errorInfo.componentStack}
|
||||
</pre>
|
||||
@@ -134,7 +137,7 @@ export class ErrorBoundary extends Component<Props, State> {
|
||||
className="bg-green-600 hover:bg-green-700 flex items-center gap-2"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Try Again
|
||||
{t('common.tryAgain')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={this.handleReload}
|
||||
@@ -142,7 +145,7 @@ export class ErrorBoundary extends Component<Props, State> {
|
||||
className="border-gray-700 hover:bg-gray-800 flex items-center gap-2"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Reload Page
|
||||
{t('common.reloadPage')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={this.handleGoHome}
|
||||
@@ -150,12 +153,12 @@ export class ErrorBoundary extends Component<Props, State> {
|
||||
className="border-gray-700 hover:bg-gray-800 flex items-center gap-2"
|
||||
>
|
||||
<Home className="w-4 h-4" />
|
||||
Go Home
|
||||
{t('common.goHome')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="mt-6 text-sm text-gray-500">
|
||||
If this problem persists, please contact support at{' '}
|
||||
{t('errors.contactSupport')}{' '}
|
||||
<a
|
||||
href="mailto:info@pezkuwichain.io"
|
||||
className="text-green-400 hover:underline"
|
||||
@@ -166,6 +169,8 @@ export class ErrorBoundary extends Component<Props, State> {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</Translation>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -187,6 +192,7 @@ export const RouteErrorBoundary: React.FC<{
|
||||
routeName?: string;
|
||||
}> = ({ children, routeName = 'this page' }) => {
|
||||
const [hasError, setHasError] = React.useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleReset = () => {
|
||||
setHasError(false);
|
||||
@@ -198,12 +204,12 @@ export const RouteErrorBoundary: React.FC<{
|
||||
<Alert className="bg-red-900/20 border-red-500">
|
||||
<AlertTriangle className="h-5 w-5 text-red-400" />
|
||||
<AlertDescription className="text-gray-300">
|
||||
<strong className="block mb-2">Error loading {routeName}</strong>
|
||||
An error occurred while rendering this component.
|
||||
<strong className="block mb-2">{t('errors.errorLoading', { routeName })}</strong>
|
||||
{t('errors.renderError')}
|
||||
<div className="mt-4">
|
||||
<Button onClick={handleReset} size="sm" className="bg-green-600 hover:bg-green-700">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Try Again
|
||||
{t('common.tryAgain')}
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
@@ -223,17 +229,18 @@ const RouteErrorFallback: React.FC<{ routeName: string; onReset: () => void }> =
|
||||
routeName,
|
||||
onReset,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="p-8">
|
||||
<Alert className="bg-red-900/20 border-red-500">
|
||||
<AlertTriangle className="h-5 w-5 text-red-400" />
|
||||
<AlertDescription className="text-gray-300">
|
||||
<strong className="block mb-2">Error loading {routeName}</strong>
|
||||
An unexpected error occurred.
|
||||
<strong className="block mb-2">{t('errors.errorLoading', { routeName })}</strong>
|
||||
{t('errors.unexpectedError')}
|
||||
<div className="mt-4">
|
||||
<Button onClick={onReset} size="sm" className="bg-green-600 hover:bg-green-700">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Try Again
|
||||
{t('common.tryAgain')}
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TrendingUp, FileText, Users, Shield, Vote, History } from 'lucide-react';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||
import GovernanceOverview from './governance/GovernanceOverview';
|
||||
@@ -9,6 +10,7 @@ import MyVotes from './governance/MyVotes';
|
||||
import GovernanceHistory from './governance/GovernanceHistory';
|
||||
|
||||
const GovernanceInterface: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState('overview');
|
||||
|
||||
return (
|
||||
@@ -17,11 +19,11 @@ const GovernanceInterface: React.FC = () => {
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl font-bold mb-4">
|
||||
<span className="bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
|
||||
On-Chain Governance
|
||||
{t('governance.title')}
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-gray-400 text-lg max-w-3xl mx-auto">
|
||||
Participate in PezkuwiChain's decentralized governance. Vote on proposals, elect representatives, and shape the future of the network.
|
||||
{t('governance.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -29,27 +31,27 @@ const GovernanceInterface: React.FC = () => {
|
||||
<TabsList className="grid grid-cols-3 lg:grid-cols-6 gap-2 bg-gray-900/50 p-1 rounded-lg overflow-x-auto scrollbar-hide">
|
||||
<TabsTrigger value="overview" className="flex items-center space-x-2">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
<span>Overview</span>
|
||||
<span>{t('governance.overview')}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="proposals" className="flex items-center space-x-2">
|
||||
<FileText className="w-4 h-4" />
|
||||
<span>Proposals</span>
|
||||
<span>{t('governance.proposals')}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="elections" className="flex items-center space-x-2">
|
||||
<Users className="w-4 h-4" />
|
||||
<span>Elections</span>
|
||||
<span>{t('governance.elections')}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="delegation" className="flex items-center space-x-2">
|
||||
<Shield className="w-4 h-4" />
|
||||
<span>Delegation</span>
|
||||
<span>{t('governance.delegation')}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="voting" className="flex items-center space-x-2">
|
||||
<Vote className="w-4 h-4" />
|
||||
<span>My Votes</span>
|
||||
<span>{t('governance.myVotes')}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="history" className="flex items-center space-x-2">
|
||||
<History className="w-4 h-4" />
|
||||
<span>History</span>
|
||||
<span>{t('governance.history')}</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { X, Lock, AlertCircle, Loader2, Clock } from 'lucide-react';
|
||||
import { web3FromAddress } from '@pezkuwi/extension-dapp';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
@@ -36,10 +37,10 @@ interface DurationOption {
|
||||
}
|
||||
|
||||
const DURATION_OPTIONS: DurationOption[] = [
|
||||
{ label: '1 Ay', months: 1, multiplier: 1 },
|
||||
{ label: '3 Ay', months: 3, multiplier: 1.5 },
|
||||
{ label: '6 Ay', months: 6, multiplier: 2 },
|
||||
{ label: '1 Yıl', months: 12, multiplier: 3 },
|
||||
{ label: 'lpStake.month1', months: 1, multiplier: 1 },
|
||||
{ label: 'lpStake.month3', months: 3, multiplier: 1.5 },
|
||||
{ label: 'lpStake.month6', months: 6, multiplier: 2 },
|
||||
{ label: 'lpStake.year1', months: 12, multiplier: 3 },
|
||||
];
|
||||
|
||||
export const LPStakeModal: React.FC<LPStakeModalProps> = ({
|
||||
@@ -48,6 +49,7 @@ export const LPStakeModal: React.FC<LPStakeModalProps> = ({
|
||||
lpToken,
|
||||
onStakeSuccess,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { assetHubApi, selectedAccount, isAssetHubReady } = usePezkuwi();
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -62,18 +64,18 @@ export const LPStakeModal: React.FC<LPStakeModalProps> = ({
|
||||
|
||||
const handleStake = async () => {
|
||||
if (!assetHubApi || !isAssetHubReady || !selectedAccount || poolId === undefined) {
|
||||
setError('API bağlantısı hazır değil');
|
||||
setError(t('lpStake.apiNotReady'));
|
||||
return;
|
||||
}
|
||||
|
||||
const amount = parseFloat(stakeAmount);
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
setError('Geçerli bir miktar girin');
|
||||
setError(t('lpStake.invalidAmount'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (amount > maxBalance) {
|
||||
setError('Yetersiz LP token bakiyesi');
|
||||
setError(t('lpStake.insufficientBalance'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -108,8 +110,9 @@ export const LPStakeModal: React.FC<LPStakeModalProps> = ({
|
||||
);
|
||||
});
|
||||
|
||||
const durationLabel = DURATION_OPTIONS.find(d => d.months === selectedDuration)?.label || `${selectedDuration} ay`;
|
||||
setSuccess(`${stakeAmount} ${lpToken.symbol} başarıyla ${durationLabel} süreyle stake edildi!`);
|
||||
const durationOption = DURATION_OPTIONS.find(d => d.months === selectedDuration);
|
||||
const durationLabel = durationOption ? t(durationOption.label) : `${selectedDuration}`;
|
||||
setSuccess(t('lpStake.success', { amount: stakeAmount, symbol: lpToken.symbol, duration: durationLabel }));
|
||||
setStakeAmount('');
|
||||
|
||||
if (onStakeSuccess) {
|
||||
@@ -121,7 +124,7 @@ export const LPStakeModal: React.FC<LPStakeModalProps> = ({
|
||||
onClose();
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Stake işlemi başarısız oldu');
|
||||
setError(err instanceof Error ? err.message : t('lpStake.failed'));
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
@@ -142,7 +145,7 @@ export const LPStakeModal: React.FC<LPStakeModalProps> = ({
|
||||
<Lock className="w-5 h-5 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white">LP Token Stake</h2>
|
||||
<h2 className="text-xl font-bold text-white">{t('lpStake.title')}</h2>
|
||||
<p className="text-sm text-gray-400">{lpToken.symbol}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -169,7 +172,7 @@ export const LPStakeModal: React.FC<LPStakeModalProps> = ({
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-3">
|
||||
<Clock className="w-4 h-4 inline mr-2" />
|
||||
Stake Süresi
|
||||
{t('lpStake.duration')}
|
||||
</label>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{DURATION_OPTIONS.map((option) => (
|
||||
@@ -183,7 +186,7 @@ export const LPStakeModal: React.FC<LPStakeModalProps> = ({
|
||||
}`}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
<div className="text-sm font-medium">{option.label}</div>
|
||||
<div className="text-sm font-medium">{t(option.label)}</div>
|
||||
<div className="text-xs mt-1 text-gray-500">{option.multiplier}x</div>
|
||||
</button>
|
||||
))}
|
||||
@@ -193,12 +196,12 @@ export const LPStakeModal: React.FC<LPStakeModalProps> = ({
|
||||
{/* Balance Info */}
|
||||
<div className="bg-gray-800/50 rounded-lg p-4">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Mevcut Bakiye:</span>
|
||||
<span className="text-gray-400">{t('lpStake.currentBalance')}</span>
|
||||
<span className="text-white font-medium">{lpToken.balance} {lpToken.symbol}</span>
|
||||
</div>
|
||||
{selectedDurationOption && (
|
||||
<div className="flex justify-between text-sm mt-2">
|
||||
<span className="text-gray-400">Ödül Çarpanı:</span>
|
||||
<span className="text-gray-400">{t('lpStake.rewardMultiplier')}</span>
|
||||
<span className="text-purple-400 font-medium">{selectedDurationOption.multiplier}x</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -207,7 +210,7 @@ export const LPStakeModal: React.FC<LPStakeModalProps> = ({
|
||||
{/* Amount Input */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Stake Miktarı
|
||||
{t('lpStake.stakeAmount')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
@@ -230,7 +233,7 @@ export const LPStakeModal: React.FC<LPStakeModalProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
Pool ID: {poolId}
|
||||
{t('lpStake.poolId', { id: poolId })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -239,8 +242,7 @@ export const LPStakeModal: React.FC<LPStakeModalProps> = ({
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-yellow-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-xs text-yellow-300">
|
||||
LP tokenlarınız seçilen süre boyunca kilitlenecektir. Bu süre içinde unstake yapamazsınız.
|
||||
Ödüller her blokta otomatik olarak birikir.
|
||||
{t('lpStake.lockWarning')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -254,12 +256,12 @@ export const LPStakeModal: React.FC<LPStakeModalProps> = ({
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Stake Ediliyor...
|
||||
{t('lpStake.staking')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Lock className="w-4 h-4 mr-2" />
|
||||
{selectedDurationOption?.label} Stake Et
|
||||
{t('lpStake.stakeBtn', { duration: selectedDurationOption ? t(selectedDurationOption.label) : '' })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { X, Lock, Unlock, Gift, AlertCircle, Loader2, Info } from 'lucide-react';
|
||||
import { web3FromAddress } from '@pezkuwi/extension-dapp';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
@@ -31,6 +32,7 @@ const LP_TOKEN_NAMES: Record<number, string> = {
|
||||
|
||||
export const LPStakingModal: React.FC<LPStakingModalProps> = ({ isOpen, onClose }) => {
|
||||
const { assetHubApi, selectedAccount, isAssetHubReady } = usePezkuwi();
|
||||
const { t } = useTranslation();
|
||||
const [pools, setPools] = useState<StakingPool[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
@@ -114,7 +116,7 @@ export const LPStakingModal: React.FC<LPStakingModalProps> = ({ isOpen, onClose
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching staking pools:', err);
|
||||
setError('Failed to fetch staking pools');
|
||||
setError(t('lpStaking.fetchError'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -165,10 +167,10 @@ export const LPStakingModal: React.FC<LPStakingModalProps> = ({ isOpen, onClose
|
||||
);
|
||||
});
|
||||
|
||||
setSuccess(`Successfully staked ${stakeAmount} ${pool.stakedAsset}!`);
|
||||
setSuccess(t('lpStaking.stakeSuccess', { amount: stakeAmount, asset: pool.stakedAsset }));
|
||||
setStakeAmount('');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to stake');
|
||||
setError(err instanceof Error ? err.message : t('lpStaking.stakeFailed'));
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
@@ -211,10 +213,10 @@ export const LPStakingModal: React.FC<LPStakingModalProps> = ({ isOpen, onClose
|
||||
);
|
||||
});
|
||||
|
||||
setSuccess(`Successfully unstaked ${unstakeAmount} ${pool.stakedAsset}!`);
|
||||
setSuccess(t('lpStaking.unstakeSuccess', { amount: unstakeAmount, asset: pool.stakedAsset }));
|
||||
setUnstakeAmount('');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to unstake');
|
||||
setError(err instanceof Error ? err.message : t('lpStaking.unstakeFailed'));
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
@@ -252,9 +254,9 @@ export const LPStakingModal: React.FC<LPStakingModalProps> = ({ isOpen, onClose
|
||||
);
|
||||
});
|
||||
|
||||
setSuccess('Successfully harvested rewards!');
|
||||
setSuccess(t('lpStaking.harvestSuccess'));
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to harvest rewards');
|
||||
setError(err instanceof Error ? err.message : t('lpStaking.harvestFailed'));
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
@@ -268,7 +270,7 @@ export const LPStakingModal: React.FC<LPStakingModalProps> = ({ isOpen, onClose
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-gray-900 rounded-lg max-w-lg w-full p-6 border border-gray-700 max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-bold text-white">LP Staking</h2>
|
||||
<h2 className="text-2xl font-bold text-white">{t('lpStaking.title')}</h2>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-white">
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
@@ -290,17 +292,17 @@ export const LPStakingModal: React.FC<LPStakingModalProps> = ({ isOpen, onClose
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8">
|
||||
<Loader2 className="w-8 h-8 animate-spin mx-auto text-cyan-400" />
|
||||
<p className="text-gray-400 mt-2">Loading staking pools...</p>
|
||||
<p className="text-gray-400 mt-2">{t('lpStaking.loading')}</p>
|
||||
</div>
|
||||
) : pools.length === 0 ? (
|
||||
<Alert className="bg-yellow-900/20 border-yellow-500">
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription>No staking pools available.</AlertDescription>
|
||||
<AlertDescription>{t('lpStaking.noPools')}</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Select Pool</label>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">{t('lpStaking.selectPool')}</label>
|
||||
<select
|
||||
value={selectedPool ?? ''}
|
||||
onChange={(e) => setSelectedPool(parseInt(e.target.value))}
|
||||
@@ -317,19 +319,19 @@ export const LPStakingModal: React.FC<LPStakingModalProps> = ({ isOpen, onClose
|
||||
{currentPool && (
|
||||
<div className="bg-gray-800/50 rounded-lg p-4 mb-6 space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Total Staked:</span>
|
||||
<span className="text-gray-400">{t('lpStaking.totalStaked')}</span>
|
||||
<span className="text-white">{formatAmount(currentPool.totalStaked)} LP</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Your Staked:</span>
|
||||
<span className="text-gray-400">{t('lpStaking.yourStaked')}</span>
|
||||
<span className="text-white">{formatAmount(currentPool.userStaked)} LP</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Your LP Balance:</span>
|
||||
<span className="text-gray-400">{t('lpStaking.yourLpBalance')}</span>
|
||||
<span className="text-white">{formatAmount(currentPool.lpBalance)} LP</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Reward Rate:</span>
|
||||
<span className="text-gray-400">{t('lpStaking.rewardRate')}</span>
|
||||
<span className="text-cyan-400">{formatAmount(currentPool.rewardRatePerBlock)} PEZ/block</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -339,22 +341,22 @@ export const LPStakingModal: React.FC<LPStakingModalProps> = ({ isOpen, onClose
|
||||
<TabsList className="w-full mb-4">
|
||||
<TabsTrigger value="stake" className="flex-1">
|
||||
<Lock className="w-4 h-4 mr-2" />
|
||||
Stake
|
||||
{t('lpStaking.tabStake')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="unstake" className="flex-1">
|
||||
<Unlock className="w-4 h-4 mr-2" />
|
||||
Unstake
|
||||
{t('lpStaking.tabUnstake')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="harvest" className="flex-1">
|
||||
<Gift className="w-4 h-4 mr-2" />
|
||||
Harvest
|
||||
{t('lpStaking.tabHarvest')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="stake">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Amount to Stake</label>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">{t('lpStaking.amountToStake')}</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="number"
|
||||
@@ -378,7 +380,7 @@ export const LPStakingModal: React.FC<LPStakingModalProps> = ({ isOpen, onClose
|
||||
className="w-full bg-gradient-to-r from-green-600 to-cyan-600 h-12"
|
||||
>
|
||||
{isProcessing ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Lock className="w-4 h-4 mr-2" />}
|
||||
{isProcessing ? 'Staking...' : 'Stake LP Tokens'}
|
||||
{isProcessing ? t('lpStaking.staking') : t('lpStaking.stakeLp')}
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
@@ -386,7 +388,7 @@ export const LPStakingModal: React.FC<LPStakingModalProps> = ({ isOpen, onClose
|
||||
<TabsContent value="unstake">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Amount to Unstake</label>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">{t('lpStaking.amountToUnstake')}</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="number"
|
||||
@@ -410,7 +412,7 @@ export const LPStakingModal: React.FC<LPStakingModalProps> = ({ isOpen, onClose
|
||||
className="w-full bg-gradient-to-r from-orange-600 to-red-600 h-12"
|
||||
>
|
||||
{isProcessing ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Unlock className="w-4 h-4 mr-2" />}
|
||||
{isProcessing ? 'Unstaking...' : 'Unstake LP Tokens'}
|
||||
{isProcessing ? t('lpStaking.unstaking') : t('lpStaking.unstakeLp')}
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
@@ -418,7 +420,7 @@ export const LPStakingModal: React.FC<LPStakingModalProps> = ({ isOpen, onClose
|
||||
<TabsContent value="harvest">
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gray-800/50 rounded-lg p-4 text-center">
|
||||
<p className="text-gray-400 mb-2">Pending Rewards</p>
|
||||
<p className="text-gray-400 mb-2">{t('lpStaking.pendingRewards')}</p>
|
||||
<p className="text-3xl font-bold text-cyan-400">
|
||||
{currentPool ? formatAmount(currentPool.pendingRewards) : '0'} PEZ
|
||||
</p>
|
||||
@@ -429,7 +431,7 @@ export const LPStakingModal: React.FC<LPStakingModalProps> = ({ isOpen, onClose
|
||||
className="w-full bg-gradient-to-r from-purple-600 to-pink-600 h-12"
|
||||
>
|
||||
{isProcessing ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Gift className="w-4 h-4 mr-2" />}
|
||||
{isProcessing ? 'Harvesting...' : 'Harvest Rewards'}
|
||||
{isProcessing ? t('lpStaking.harvesting') : t('lpStaking.harvestRewards')}
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const PezkuwiChainLogo: React.FC = () => {
|
||||
return (
|
||||
<img src="/PezkuwiChain_Logo_Horizontal_Green_White.png" alt="PezkuwiChain Logo" className="h-8" />
|
||||
<img src="/explorer/PezkuwiExplorer.png" alt="PezkuwiChain" className="h-8" />
|
||||
);
|
||||
};
|
||||
|
||||
const Header: React.FC = () => {
|
||||
const linkStyle = "text-white hover:text-green-400 transition-colors";
|
||||
const activeLinkStyle = { color: '#34D399' }; // green-400
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<header className="bg-gray-900 text-white p-4 fixed top-0 left-0 right-0 z-[1000]">
|
||||
@@ -19,16 +21,16 @@ const Header: React.FC = () => {
|
||||
</Link>
|
||||
<nav>
|
||||
<ul className="flex space-x-4">
|
||||
<li><NavLink to="/explorer" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>Explorer</NavLink></li>
|
||||
<li><NavLink to="/docs" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>Docs</NavLink></li>
|
||||
<li><NavLink to="/wallet" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>Wallet</NavLink></li>
|
||||
<li><NavLink to="/api" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>API</NavLink></li>
|
||||
<li><NavLink to="/faucet" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>Faucet</NavLink></li>
|
||||
<li><NavLink to="/developers" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>Developers</NavLink></li>
|
||||
<li><NavLink to="/grants" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>Grants</NavLink></li>
|
||||
<li><NavLink to="/wiki" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>Wiki</NavLink></li>
|
||||
<li><NavLink to="/forum" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>Forum</NavLink></li>
|
||||
<li><NavLink to="/telemetry" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>Telemetry</NavLink></li>
|
||||
<li><NavLink to="/explorer" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>{t('nav.explorer')}</NavLink></li>
|
||||
<li><NavLink to="/docs" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>{t('nav.docs')}</NavLink></li>
|
||||
<li><NavLink to="/wallet" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>{t('nav.wallet')}</NavLink></li>
|
||||
<li><NavLink to="/api" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>{t('nav.api')}</NavLink></li>
|
||||
<li><NavLink to="/faucet" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>{t('nav.faucet')}</NavLink></li>
|
||||
<li><NavLink to="/developers" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>{t('nav.developers')}</NavLink></li>
|
||||
<li><NavLink to="/grants" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>{t('nav.grants')}</NavLink></li>
|
||||
<li><NavLink to="/wiki" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>{t('nav.wiki')}</NavLink></li>
|
||||
<li><NavLink to="/forum" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>{t('nav.forum')}</NavLink></li>
|
||||
<li><NavLink to="/telemetry" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>{t('nav.telemetry')}</NavLink></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -37,10 +39,11 @@ const Header: React.FC = () => {
|
||||
};
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<footer className="bg-gray-900 text-white p-4">
|
||||
<div className="container mx-auto text-center">
|
||||
<p>© {new Date().getFullYear()} PezkuwiChain. All rights reserved.</p>
|
||||
<p>© {new Date().getFullYear()} PezkuwiChain. {t('footer.rights')}</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Shield, Users, CheckCircle, XCircle, ExternalLink } from 'lucide-react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -31,6 +32,7 @@ export const MultisigMembers: React.FC<MultisigMembersProps> = ({
|
||||
specificAddresses = {},
|
||||
showMultisigAddress = true,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { api, isApiReady } = usePezkuwi();
|
||||
const [members, setMembers] = useState<MultisigMember[]>([]);
|
||||
const [multisigAddress, setMultisigAddress] = useState('');
|
||||
@@ -78,29 +80,29 @@ export const MultisigMembers: React.FC<MultisigMembersProps> = ({
|
||||
<div className="flex items-center gap-3">
|
||||
<Shield className="h-6 w-6 text-blue-400" />
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-white">USDT Treasury Multisig</h3>
|
||||
<h3 className="text-lg font-bold text-white">{t('multisigMembers.title')}</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
{USDT_MULTISIG_CONFIG.threshold}/{members.length} Signatures Required
|
||||
{t('multisigMembers.threshold', { threshold: USDT_MULTISIG_CONFIG.threshold, total: members.length })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="flex items-center gap-1">
|
||||
<Users className="h-3 w-3" />
|
||||
{members.length} Members
|
||||
{t('multisigMembers.members', { count: members.length })}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Multisig Address */}
|
||||
{showMultisigAddress && multisigAddress && (
|
||||
<div className="mb-6 p-4 bg-gray-900/50 rounded-lg">
|
||||
<p className="text-xs text-gray-400 mb-2">Multisig Account</p>
|
||||
<p className="text-xs text-gray-400 mb-2">{t('multisigMembers.account')}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<code className="text-sm text-green-400 font-mono">{formatMultisigAddress(multisigAddress)}</code>
|
||||
<button
|
||||
onClick={() => navigator.clipboard.writeText(multisigAddress)}
|
||||
className="text-blue-400 hover:text-blue-300 text-xs"
|
||||
>
|
||||
Copy Full
|
||||
{t('multisigMembers.copyFull')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -126,7 +128,7 @@ export const MultisigMembers: React.FC<MultisigMembersProps> = ({
|
||||
{member.isUnique && (
|
||||
<Badge variant="secondary" className="text-xs flex items-center gap-1">
|
||||
<CheckCircle className="h-3 w-3" />
|
||||
On-Chain
|
||||
{t('multisigMembers.onChain')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -139,9 +141,9 @@ export const MultisigMembers: React.FC<MultisigMembersProps> = ({
|
||||
</code>
|
||||
<div className="flex items-center gap-2 mt-1 justify-end">
|
||||
{member.isUnique ? (
|
||||
<CheckCircle className="h-4 w-4 text-green-500" title="Verified on-chain" />
|
||||
<CheckCircle className="h-4 w-4 text-green-500" title={t('multisigMembers.verified')} />
|
||||
) : (
|
||||
<XCircle className="h-4 w-4 text-yellow-500" title="Specified address" />
|
||||
<XCircle className="h-4 w-4 text-yellow-500" title={t('multisigMembers.specified')} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -153,12 +155,12 @@ export const MultisigMembers: React.FC<MultisigMembersProps> = ({
|
||||
<Alert className="mt-6 bg-blue-900/20 border-blue-500">
|
||||
<Shield className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<p className="font-semibold mb-1">Security Features</p>
|
||||
<p className="font-semibold mb-1">{t('multisigMembers.securityTitle')}</p>
|
||||
<ul className="text-sm space-y-1">
|
||||
<li>• {USDT_MULTISIG_CONFIG.threshold} out of {members.length} signatures required</li>
|
||||
<li>• {members.filter(m => m.isUnique).length} members verified on-chain via Tiki</li>
|
||||
<li>• No single person can control funds</li>
|
||||
<li>• All transactions visible on blockchain</li>
|
||||
<li>• {t('multisigMembers.sigRequired', { threshold: USDT_MULTISIG_CONFIG.threshold, total: members.length })}</li>
|
||||
<li>• {t('multisigMembers.verifiedOnChain', { count: members.filter(m => m.isUnique).length })}</li>
|
||||
<li>• {t('multisigMembers.noSingleControl')}</li>
|
||||
<li>• {t('multisigMembers.allVisible')}</li>
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
@@ -172,7 +174,7 @@ export const MultisigMembers: React.FC<MultisigMembersProps> = ({
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 text-sm text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
View on Explorer
|
||||
{t('multisigMembers.viewExplorer')}
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Activity, Wifi, WifiOff, Users, Box, TrendingUp } from 'lucide-react';
|
||||
|
||||
export const NetworkStats: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { api, assetHubApi, peopleApi, isApiReady, isAssetHubReady, isPeopleReady, error } = usePezkuwi();
|
||||
const [blockNumber, setBlockNumber] = useState<number>(0);
|
||||
const [blockHash, setBlockHash] = useState<string>('');
|
||||
@@ -125,13 +127,13 @@ export const NetworkStats: React.FC = () => {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-red-400">
|
||||
<WifiOff className="w-5 h-5" />
|
||||
Network Disconnected
|
||||
{t('networkStats.disconnected')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-red-300 text-sm">{error}</p>
|
||||
<p className="text-red-400 text-xs mt-2">
|
||||
Make sure your validator node is running at ws://127.0.0.1:9944
|
||||
{t('networkStats.disconnectedDesc')}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -144,7 +146,7 @@ export const NetworkStats: React.FC = () => {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Activity className="w-5 h-5 animate-pulse" />
|
||||
Connecting to Network...
|
||||
{t('networkStats.connecting')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
@@ -158,15 +160,15 @@ export const NetworkStats: React.FC = () => {
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-gray-400 flex items-center gap-2">
|
||||
<Wifi className="w-4 h-4 text-green-500" />
|
||||
Network Status
|
||||
{t('networkStats.title')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge className="bg-green-500/20 text-green-400 border-green-500/50">
|
||||
Connected
|
||||
{t('networkStats.connected')}
|
||||
</Badge>
|
||||
<span className="text-xs text-gray-500">{peers} peers</span>
|
||||
<span className="text-xs text-gray-500">{peers} {t('networkStats.peers')}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -176,7 +178,7 @@ export const NetworkStats: React.FC = () => {
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-gray-400 flex items-center gap-2">
|
||||
<Box className="w-4 h-4 text-blue-500" />
|
||||
Latest Block
|
||||
{t('networkStats.latestBlock')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -196,7 +198,7 @@ export const NetworkStats: React.FC = () => {
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-gray-400 flex items-center gap-2">
|
||||
<TrendingUp className="w-4 h-4 text-purple-500" />
|
||||
Finalized Block
|
||||
{t('networkStats.finalizedBlock')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -204,7 +206,7 @@ export const NetworkStats: React.FC = () => {
|
||||
#{finalizedBlock.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
{blockNumber - finalizedBlock} blocks behind
|
||||
{blockNumber - finalizedBlock} {t('networkStats.blocksBehind')}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -214,7 +216,7 @@ export const NetworkStats: React.FC = () => {
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-gray-400 flex items-center gap-2">
|
||||
<Users className="w-4 h-4 text-yellow-500" />
|
||||
Active Validators
|
||||
{t('networkStats.activeValidators')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -222,7 +224,7 @@ export const NetworkStats: React.FC = () => {
|
||||
{validatorCount}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
Validating blocks
|
||||
{t('networkStats.validating')}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -232,7 +234,7 @@ export const NetworkStats: React.FC = () => {
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-gray-400 flex items-center gap-2">
|
||||
<Users className="w-4 h-4 text-orange-500" />
|
||||
Active Collators
|
||||
{t('networkStats.activeCollators')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -240,7 +242,7 @@ export const NetworkStats: React.FC = () => {
|
||||
{collatorCount}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
Producing blocks
|
||||
{t('networkStats.producing')}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -250,7 +252,7 @@ export const NetworkStats: React.FC = () => {
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-gray-400 flex items-center gap-2">
|
||||
<Users className="w-4 h-4 text-cyan-500" />
|
||||
Active Nominators
|
||||
{t('networkStats.activeNominators')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -258,7 +260,7 @@ export const NetworkStats: React.FC = () => {
|
||||
{nominatorCount}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
Staking to validators
|
||||
{t('networkStats.staking')}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Loader2, Award, Crown, Shield, Users } from 'lucide-react';
|
||||
@@ -39,6 +40,7 @@ const getRoleBadgeColor = (role: string) => {
|
||||
};
|
||||
|
||||
export const NftList: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { api, isApiReady, selectedAccount } = usePezkuwi();
|
||||
const [tikis, setTikis] = useState<TikiInfo[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -72,8 +74,8 @@ export const NftList: React.FC = () => {
|
||||
return (
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Your NFTs (Tikis)</CardTitle>
|
||||
<CardDescription>Your Tiki collection</CardDescription>
|
||||
<CardTitle className="text-white">{t('nftList.title')}</CardTitle>
|
||||
<CardDescription>{t('nftList.description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-center py-12">
|
||||
@@ -88,12 +90,12 @@ export const NftList: React.FC = () => {
|
||||
return (
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Your NFTs (Tikis)</CardTitle>
|
||||
<CardDescription>Your Tiki collection</CardDescription>
|
||||
<CardTitle className="text-white">{t('nftList.title')}</CardTitle>
|
||||
<CardDescription>{t('nftList.description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-center py-12">
|
||||
<p className="text-red-500">{error}</p>
|
||||
<p className="text-red-500">{t('nftList.error')}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -104,15 +106,15 @@ export const NftList: React.FC = () => {
|
||||
return (
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Your NFTs (Tikis)</CardTitle>
|
||||
<CardDescription>Your Tiki collection</CardDescription>
|
||||
<CardTitle className="text-white">{t('nftList.title')}</CardTitle>
|
||||
<CardDescription>{t('nftList.description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-center py-12">
|
||||
<Award className="w-12 h-12 text-gray-600 mx-auto mb-3" />
|
||||
<p className="text-gray-500 mb-2">No NFTs yet</p>
|
||||
<p className="text-gray-500 mb-2">{t('nftList.empty')}</p>
|
||||
<p className="text-gray-600 text-sm">
|
||||
Complete your citizenship application to receive your Welati Tiki NFT
|
||||
{t('nftList.emptyHelp')}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -125,9 +127,9 @@ export const NftList: React.FC = () => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Award className="w-5 h-5" />
|
||||
Your NFTs (Tikiler)
|
||||
{t('nftList.title')}
|
||||
</CardTitle>
|
||||
<CardDescription>Your Tiki collection ({tikis.length} total)</CardDescription>
|
||||
<CardDescription>{t('nftList.descriptionCount', { count: tikis.length })}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
@@ -146,7 +148,7 @@ export const NftList: React.FC = () => {
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2 mb-2">
|
||||
<h3 className="font-semibold text-white text-sm">
|
||||
Tiki #{tiki.id}
|
||||
{t('nftList.cardTitle', { id: tiki.id })}
|
||||
</h3>
|
||||
<Badge className={getRoleBadgeColor(tiki.role)}>
|
||||
{tiki.role}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Code, Database, TrendingUp, Gift, Award } from 'lucide-react';
|
||||
|
||||
interface Pallet {
|
||||
@@ -51,6 +52,7 @@ const pallets: Pallet[] = [
|
||||
];
|
||||
|
||||
const PalletsGrid: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedPallet, setSelectedPallet] = useState<Pallet | null>(null);
|
||||
|
||||
return (
|
||||
@@ -58,10 +60,10 @@ const PalletsGrid: React.FC = () => {
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
|
||||
Core Runtime Pallets
|
||||
{t('palletsGrid.title')}
|
||||
</h2>
|
||||
<p className="text-gray-400 text-lg max-w-2xl mx-auto">
|
||||
Modular blockchain components powering PezkuwiChain's advanced features
|
||||
{t('palletsGrid.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -93,10 +95,10 @@ const PalletsGrid: React.FC = () => {
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="px-2 py-1 bg-kurdish-yellow/30 text-kurdish-yellow text-xs rounded-full">
|
||||
{pallet.extrinsics.length} Extrinsics
|
||||
{t('palletsGrid.extrinsics', { count: pallet.extrinsics.length })}
|
||||
</span>
|
||||
<span className="px-2 py-1 bg-cyan-900/30 text-cyan-400 text-xs rounded-full">
|
||||
{pallet.storage.length} Storage Items
|
||||
{t('palletsGrid.storageItems', { count: pallet.storage.length })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -130,7 +132,7 @@ const PalletsGrid: React.FC = () => {
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold text-purple-400 mb-3">Extrinsics</h4>
|
||||
<h4 className="text-lg font-semibold text-purple-400 mb-3">{t('palletsGrid.extrinsicsTitle')}</h4>
|
||||
<div className="space-y-2">
|
||||
{selectedPallet.extrinsics.map((ext) => (
|
||||
<div key={ext} className="flex items-center p-3 bg-gray-800/50 rounded-lg">
|
||||
@@ -142,7 +144,7 @@ const PalletsGrid: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold text-cyan-400 mb-3">Storage Items</h4>
|
||||
<h4 className="text-lg font-semibold text-cyan-400 mb-3">{t('palletsGrid.storageTitle')}</h4>
|
||||
<div className="space-y-2">
|
||||
{selectedPallet.storage.map((item) => (
|
||||
<div key={item} className="flex items-center p-3 bg-gray-800/50 rounded-lg">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -23,6 +24,7 @@ export const PezkuwiWalletButton: React.FC = () => {
|
||||
error
|
||||
} = usePezkuwi();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const isMobile = useIsMobile();
|
||||
@@ -38,7 +40,7 @@ export const PezkuwiWalletButton: React.FC = () => {
|
||||
setSelectedAccount(account);
|
||||
setIsOpen(false);
|
||||
toast({
|
||||
title: "Account Connected",
|
||||
title: t('pezWallet.connected'),
|
||||
description: `${account.meta.name} - ${formatAddress(account.address)}`,
|
||||
});
|
||||
};
|
||||
@@ -46,8 +48,8 @@ export const PezkuwiWalletButton: React.FC = () => {
|
||||
const handleDisconnect = () => {
|
||||
disconnectWallet();
|
||||
toast({
|
||||
title: "Wallet Disconnected",
|
||||
description: "Your wallet has been disconnected",
|
||||
title: t('pezWallet.disconnected'),
|
||||
description: t('pezWallet.disconnectedDesc'),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -59,8 +61,8 @@ export const PezkuwiWalletButton: React.FC = () => {
|
||||
if (selectedAccount) {
|
||||
navigator.clipboard.writeText(selectedAccount.address);
|
||||
toast({
|
||||
title: "Address Copied",
|
||||
description: "Address copied to clipboard",
|
||||
title: t('pezWallet.addressCopied'),
|
||||
description: t('pezWallet.addressCopiedDesc'),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -96,22 +98,22 @@ export const PezkuwiWalletButton: React.FC = () => {
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent className="bg-gray-900 border-gray-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Account Details</DialogTitle>
|
||||
<DialogTitle className="text-white">{t('pezWallet.accountDetails')}</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Your connected Pezkuwi account
|
||||
{t('pezWallet.accountDetailsDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gray-800/50 rounded-lg p-4">
|
||||
<div className="text-sm text-gray-400 mb-1">Account Name</div>
|
||||
<div className="text-sm text-gray-400 mb-1">{t('pezWallet.accountName')}</div>
|
||||
<div className="text-white font-medium">
|
||||
{selectedAccount.meta.name || 'Unnamed Account'}
|
||||
{selectedAccount.meta.name || t('pezWallet.unnamed')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800/50 rounded-lg p-4">
|
||||
<div className="text-sm text-gray-400 mb-1">Address</div>
|
||||
<div className="text-sm text-gray-400 mb-1">{t('pezWallet.address')}</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<code className="text-white text-sm font-mono">
|
||||
{selectedAccount.address}
|
||||
@@ -128,7 +130,7 @@ export const PezkuwiWalletButton: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800/50 rounded-lg p-4">
|
||||
<div className="text-sm text-gray-400 mb-1">Source</div>
|
||||
<div className="text-sm text-gray-400 mb-1">{t('pezWallet.source')}</div>
|
||||
<div className="text-white">
|
||||
{selectedAccount.meta.source || 'pezkuwi'}
|
||||
</div>
|
||||
@@ -136,7 +138,7 @@ export const PezkuwiWalletButton: React.FC = () => {
|
||||
|
||||
{accounts.length > 1 && (
|
||||
<div>
|
||||
<div className="text-sm text-gray-400 mb-2">Switch Account</div>
|
||||
<div className="text-sm text-gray-400 mb-2">{t('pezWallet.switchAccount')}</div>
|
||||
<div className="space-y-2">
|
||||
{accounts.map((account) => (
|
||||
<button
|
||||
@@ -150,7 +152,7 @@ export const PezkuwiWalletButton: React.FC = () => {
|
||||
>
|
||||
<div className="text-left">
|
||||
<div className="text-white font-medium">
|
||||
{account.meta.name || 'Unnamed'}
|
||||
{account.meta.name || t('pezWallet.unnamed')}
|
||||
</div>
|
||||
<div className="text-gray-400 text-xs font-mono">
|
||||
{formatAddress(account.address)}
|
||||
@@ -179,22 +181,22 @@ export const PezkuwiWalletButton: React.FC = () => {
|
||||
size={isMobile ? "icon" : "default"}
|
||||
>
|
||||
<Wallet className={isMobile ? "w-4 h-4" : "w-4 h-4 mr-2"} />
|
||||
{!isMobile && "Connect Wallet"}
|
||||
{!isMobile && t('pezWallet.connect')}
|
||||
</Button>
|
||||
|
||||
{error && error.includes('not found') && (
|
||||
<Dialog open={!!error} onOpenChange={() => {}}>
|
||||
<DialogContent className="bg-gray-900 border-gray-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Install Pezkuwi Wallet Extension</DialogTitle>
|
||||
<DialogTitle className="text-white">{t('pezWallet.installTitle')}</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
You need the Pezkuwi Wallet browser extension to connect
|
||||
{t('pezWallet.installDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-300">
|
||||
The Pezkuwi Wallet extension allows you to manage your accounts and sign transactions securely.
|
||||
{t('pezWallet.installText')}
|
||||
</p>
|
||||
|
||||
<div className="flex gap-3">
|
||||
@@ -206,13 +208,13 @@ export const PezkuwiWalletButton: React.FC = () => {
|
||||
>
|
||||
<Button className="w-full bg-green-600 hover:bg-green-700">
|
||||
<ExternalLink className="w-4 h-4 mr-2" />
|
||||
Install from Chrome Web Store
|
||||
{t('pezWallet.installChrome')}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-500">
|
||||
After installing, refresh this page and click "Connect Wallet" again.
|
||||
{t('pezWallet.installRefresh')}
|
||||
</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
@@ -222,9 +224,9 @@ export const PezkuwiWalletButton: React.FC = () => {
|
||||
<Dialog open={isOpen && accounts.length > 0} onOpenChange={setIsOpen}>
|
||||
<DialogContent className="bg-gray-900 border-gray-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Select Account</DialogTitle>
|
||||
<DialogTitle className="text-white">{t('pezWallet.selectTitle')}</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Choose an account to connect
|
||||
{t('pezWallet.selectDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -236,7 +238,7 @@ export const PezkuwiWalletButton: React.FC = () => {
|
||||
className="w-full p-4 rounded-lg border border-gray-700 bg-gray-800/50 hover:border-green-500/50 hover:bg-gray-800 transition-all text-left"
|
||||
>
|
||||
<div className="text-white font-medium mb-1">
|
||||
{account.meta.name || 'Unnamed Account'}
|
||||
{account.meta.name || t('pezWallet.unnamed')}
|
||||
</div>
|
||||
<div className="text-gray-400 text-sm font-mono">
|
||||
{account.address}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { NATIVE_TOKEN_ID } from '@/types/dex';
|
||||
import { AddLiquidityModal } from '@/components/AddLiquidityModal';
|
||||
import { RemoveLiquidityModal } from '@/components/RemoveLiquidityModal';
|
||||
import { LPStakingModal } from '@/components/LPStakingModal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// Helper function to convert asset IDs to user-friendly display names
|
||||
// Users should only see HEZ, PEZ, USDT - wrapped tokens are backend details
|
||||
@@ -45,6 +46,7 @@ interface LPPosition {
|
||||
const PoolDashboard = () => {
|
||||
// Use Asset Hub API for DEX operations (assetConversion pallet is on Asset Hub)
|
||||
const { assetHubApi, isAssetHubReady, selectedAccount } = usePezkuwi();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [poolData, setPoolData] = useState<PoolData | null>(null);
|
||||
const [lpPosition, setLPPosition] = useState<LPPosition | null>(null);
|
||||
@@ -331,7 +333,7 @@ const PoolDashboard = () => {
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
|
||||
<p className="text-gray-400">Loading pool data...</p>
|
||||
<p className="text-gray-400">{t('poolDash.loadingPoolData')}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -350,7 +352,7 @@ const PoolDashboard = () => {
|
||||
return (
|
||||
<Alert className="bg-yellow-900/20 border-yellow-500">
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription>No pool data available</AlertDescription>
|
||||
<AlertDescription>{t('poolDash.noPoolData')}</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -365,10 +367,10 @@ const PoolDashboard = () => {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-1">Pool Dashboards</h3>
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-1">{t('poolDash.poolDashboards')}</h3>
|
||||
<Select value={selectedPool} onValueChange={setSelectedPool}>
|
||||
<SelectTrigger className="w-[240px] bg-gray-800/50 border-gray-700">
|
||||
<SelectValue placeholder="Select pool" />
|
||||
<SelectValue placeholder={t('poolDash.selectPool')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availablePools.map(([asset0, asset1]) => {
|
||||
@@ -386,7 +388,7 @@ const PoolDashboard = () => {
|
||||
</div>
|
||||
<Badge variant="outline" className="flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
Live
|
||||
{t('poolDash.live')}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -395,9 +397,9 @@ const PoolDashboard = () => {
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white flex items-center gap-2">
|
||||
<Droplet className="h-6 w-6 text-blue-400" />
|
||||
{asset0Symbol}/{asset1Symbol} Pool Dashboard
|
||||
{t('poolDash.poolDashboard', { asset0: asset0Symbol, asset1: asset1Symbol })}
|
||||
</h2>
|
||||
<p className="text-gray-400 mt-1">Monitor liquidity pool metrics and your position</p>
|
||||
<p className="text-gray-400 mt-1">{t('poolDash.monitorDesc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -407,7 +409,7 @@ const PoolDashboard = () => {
|
||||
<Card className="p-4 bg-gray-800/50 border-gray-700">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Total Liquidity</p>
|
||||
<p className="text-sm text-gray-400">{t('poolDash.totalLiquidity')}</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">
|
||||
${totalLiquidityUSD.toLocaleString('en-US', { maximumFractionDigits: 0 })}
|
||||
</p>
|
||||
@@ -423,7 +425,7 @@ const PoolDashboard = () => {
|
||||
<Card className="p-4 bg-gray-800/50 border-gray-700">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">{asset0Symbol} Price</p>
|
||||
<p className="text-sm text-gray-400">{t('poolDash.price', { symbol: asset0Symbol })}</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">
|
||||
${currentPrice.toFixed(4)}
|
||||
</p>
|
||||
@@ -439,12 +441,12 @@ const PoolDashboard = () => {
|
||||
<Card className="p-4 bg-gray-800/50 border-gray-700">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Estimated APR</p>
|
||||
<p className="text-sm text-gray-400">{t('poolDash.estimatedApr')}</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">
|
||||
{estimateAPR().toFixed(2)}%
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
From swap fees
|
||||
{t('poolDash.fromSwapFees')}
|
||||
</p>
|
||||
</div>
|
||||
<Percent className="h-8 w-8 text-yellow-400" />
|
||||
@@ -455,7 +457,7 @@ const PoolDashboard = () => {
|
||||
<Card className="p-4 bg-gray-800/50 border-gray-700">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Constant (k)</p>
|
||||
<p className="text-sm text-gray-400">{t('poolDash.constant')}</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">
|
||||
{(constantProduct / 1e9).toFixed(1)}B
|
||||
</p>
|
||||
@@ -470,20 +472,20 @@ const PoolDashboard = () => {
|
||||
|
||||
<Tabs defaultValue="reserves" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3 bg-gray-800">
|
||||
<TabsTrigger value="reserves">Reserves</TabsTrigger>
|
||||
<TabsTrigger value="position">Your Position</TabsTrigger>
|
||||
<TabsTrigger value="calculator">IL Calculator</TabsTrigger>
|
||||
<TabsTrigger value="reserves">{t('poolDash.reserves')}</TabsTrigger>
|
||||
<TabsTrigger value="position">{t('poolDash.yourPosition')}</TabsTrigger>
|
||||
<TabsTrigger value="calculator">{t('poolDash.ilCalculator')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Reserves Tab */}
|
||||
<TabsContent value="reserves" className="space-y-4">
|
||||
<Card className="p-6 bg-gray-800/50 border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Pool Reserves</h3>
|
||||
<h3 className="text-lg font-semibold text-white mb-4">{t('poolDash.poolReserves')}</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 bg-gray-900/50 rounded-lg">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">{asset0Symbol} Reserve</p>
|
||||
<p className="text-sm text-gray-400">{t('poolDash.reserve', { symbol: asset0Symbol })}</p>
|
||||
<p className="text-2xl font-bold text-white">{poolData.reserve0.toLocaleString('en-US', { maximumFractionDigits: 2 })}</p>
|
||||
</div>
|
||||
<Badge variant="outline">Asset 1</Badge>
|
||||
@@ -491,7 +493,7 @@ const PoolDashboard = () => {
|
||||
|
||||
<div className="flex items-center justify-between p-4 bg-gray-900/50 rounded-lg">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">{asset1Symbol} Reserve</p>
|
||||
<p className="text-sm text-gray-400">{t('poolDash.reserve', { symbol: asset1Symbol })}</p>
|
||||
<p className="text-2xl font-bold text-white">{poolData.reserve1.toLocaleString('en-US', { maximumFractionDigits: 2 })}</p>
|
||||
</div>
|
||||
<Badge variant="outline">Asset 2</Badge>
|
||||
@@ -502,8 +504,8 @@ const PoolDashboard = () => {
|
||||
<div className="flex items-start gap-2">
|
||||
<Info className="h-4 w-4 text-blue-400 mt-0.5" />
|
||||
<div className="text-sm text-gray-300">
|
||||
<p className="font-semibold text-blue-400 mb-1">AMM Formula</p>
|
||||
<p>Pool maintains constant product: x × y = k</p>
|
||||
<p className="font-semibold text-blue-400 mb-1">{t('poolDash.ammFormula')}</p>
|
||||
<p>{t('poolDash.poolMaintains')}</p>
|
||||
<p className="mt-2 font-mono text-xs">
|
||||
{poolData.reserve0.toFixed(2)} × {poolData.reserve1.toFixed(2)} = {constantProduct.toLocaleString()}
|
||||
</p>
|
||||
@@ -516,39 +518,39 @@ const PoolDashboard = () => {
|
||||
{/* Your Position Tab */}
|
||||
<TabsContent value="position" className="space-y-4">
|
||||
<Card className="p-6 bg-gray-800/50 border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Your Liquidity Position</h3>
|
||||
<h3 className="text-lg font-semibold text-white mb-4">{t('poolDash.yourLpPosition')}</h3>
|
||||
|
||||
{!selectedAccount ? (
|
||||
<Alert className="bg-yellow-900/20 border-yellow-500">
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription>Connect wallet to view your position</AlertDescription>
|
||||
<AlertDescription>{t('poolDash.connectWallet')}</AlertDescription>
|
||||
</Alert>
|
||||
) : !lpPosition ? (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
<Droplet className="h-12 w-12 mx-auto mb-2 opacity-50" />
|
||||
<p>No liquidity position found</p>
|
||||
<p>{t('poolDash.noPosition')}</p>
|
||||
<Button
|
||||
onClick={() => setIsAddLiquidityModalOpen(true)}
|
||||
className="mt-4 bg-gradient-to-r from-green-600 to-blue-600 hover:from-green-700 hover:to-blue-700"
|
||||
>
|
||||
Add Liquidity
|
||||
{t('poolDash.addLiquidity')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-gray-900/50 rounded-lg">
|
||||
<p className="text-sm text-gray-400">LP Tokens</p>
|
||||
<p className="text-sm text-gray-400">{t('poolDash.lpTokens')}</p>
|
||||
<p className="text-xl font-bold text-white">{lpPosition.lpTokenBalance.toFixed(4)}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-gray-900/50 rounded-lg">
|
||||
<p className="text-sm text-gray-400">Pool Share</p>
|
||||
<p className="text-sm text-gray-400">{t('poolDash.poolShare')}</p>
|
||||
<p className="text-xl font-bold text-white">{lpPosition.share.toFixed(4)}%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-gray-900/50 rounded-lg">
|
||||
<p className="text-sm text-gray-400 mb-2">Your Position Value</p>
|
||||
<p className="text-sm text-gray-400 mb-2">{t('poolDash.positionValue')}</p>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-300">{asset0Symbol}:</span>
|
||||
@@ -562,18 +564,18 @@ const PoolDashboard = () => {
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-green-900/20 border border-green-500/30 rounded-lg">
|
||||
<p className="text-sm text-gray-400 mb-2">Estimated Earnings (APR {estimateAPR().toFixed(2)}%)</p>
|
||||
<p className="text-sm text-gray-400 mb-2">{t('poolDash.estimatedEarnings', { apr: estimateAPR().toFixed(2) })}</p>
|
||||
<div className="space-y-1 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-300">Daily:</span>
|
||||
<span className="text-gray-300">{t('poolDash.daily')}</span>
|
||||
<span className="text-green-400">~{((lpPosition.asset0Amount * 2 * estimateAPR()) / 365 / 100).toFixed(4)} {asset0Symbol}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-300">Monthly:</span>
|
||||
<span className="text-gray-300">{t('poolDash.monthly')}</span>
|
||||
<span className="text-green-400">~{((lpPosition.asset0Amount * 2 * estimateAPR()) / 12 / 100).toFixed(4)} {asset0Symbol}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-300">Yearly:</span>
|
||||
<span className="text-gray-300">{t('poolDash.yearly')}</span>
|
||||
<span className="text-green-400">~{((lpPosition.asset0Amount * 2 * estimateAPR()) / 100).toFixed(4)} {asset0Symbol}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -584,21 +586,21 @@ const PoolDashboard = () => {
|
||||
onClick={() => setIsAddLiquidityModalOpen(true)}
|
||||
className="bg-gradient-to-r from-green-600 to-blue-600 hover:from-green-700 hover:to-blue-700"
|
||||
>
|
||||
Add More
|
||||
{t('poolDash.addMore')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setIsStakingModalOpen(true)}
|
||||
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700"
|
||||
>
|
||||
<Lock className="w-4 h-4 mr-1" />
|
||||
Stake LP
|
||||
{t('poolDash.stakeLP')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setIsRemoveLiquidityModalOpen(true)}
|
||||
variant="outline"
|
||||
className="border-red-600 text-red-400 hover:bg-red-900/20"
|
||||
>
|
||||
Remove
|
||||
{t('poolDash.remove')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -609,11 +611,11 @@ const PoolDashboard = () => {
|
||||
{/* Impermanent Loss Calculator Tab */}
|
||||
<TabsContent value="calculator" className="space-y-4">
|
||||
<Card className="p-6 bg-gray-800/50 border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Impermanent Loss Calculator</h3>
|
||||
<h3 className="text-lg font-semibold text-white mb-4">{t('poolDash.ilCalcTitle')}</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-gray-900/50 rounded-lg">
|
||||
<p className="text-sm text-gray-400 mb-3">If {asset0Symbol} price changes by:</p>
|
||||
<p className="text-sm text-gray-400 mb-3">{t('poolDash.ifPriceChanges', { symbol: asset0Symbol })}</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
{[10, 25, 50, 100, 200].map((change) => {
|
||||
@@ -625,7 +627,7 @@ const PoolDashboard = () => {
|
||||
variant="outline"
|
||||
className={il < -1 ? 'border-red-500 text-red-400' : 'border-yellow-500 text-yellow-400'}
|
||||
>
|
||||
{il.toFixed(2)}% Loss
|
||||
{t('poolDash.loss', { percent: il.toFixed(2) })}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
@@ -636,11 +638,9 @@ const PoolDashboard = () => {
|
||||
<Alert className="bg-orange-900/20 border-orange-500">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<p className="font-semibold mb-1">What is Impermanent Loss?</p>
|
||||
<p className="font-semibold mb-1">{t('poolDash.whatIsIL')}</p>
|
||||
<p className="text-sm text-gray-300">
|
||||
Impermanent loss occurs when the price ratio of tokens in the pool changes.
|
||||
The larger the price change, the greater the loss compared to simply holding the tokens.
|
||||
Fees earned from swaps can offset this loss over time.
|
||||
{t('poolDash.ilExplanation')}
|
||||
</p>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -20,6 +21,7 @@ interface ReceiveModalProps {
|
||||
export const ReceiveModal: React.FC<ReceiveModalProps> = ({ isOpen, onClose }) => {
|
||||
const { selectedAccount } = usePezkuwi();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [qrCodeDataUrl, setQrCodeDataUrl] = useState<string>('');
|
||||
|
||||
@@ -46,15 +48,15 @@ export const ReceiveModal: React.FC<ReceiveModalProps> = ({ isOpen, onClose }) =
|
||||
await navigator.clipboard.writeText(selectedAccount.address);
|
||||
setCopied(true);
|
||||
toast({
|
||||
title: "Address Copied!",
|
||||
description: "Your wallet address has been copied to clipboard",
|
||||
title: t('receive.addressCopied'),
|
||||
description: t('receive.addressCopiedDesc'),
|
||||
});
|
||||
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
toast({
|
||||
title: "Copy Failed",
|
||||
description: "Failed to copy address to clipboard",
|
||||
title: t('receive.copyFailed'),
|
||||
description: t('receive.copyFailedDesc'),
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
@@ -68,9 +70,9 @@ export const ReceiveModal: React.FC<ReceiveModalProps> = ({ isOpen, onClose }) =
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="bg-gray-900 border-gray-800 max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Receive Tokens</DialogTitle>
|
||||
<DialogTitle className="text-white">{t('receive.title')}</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Share this address to receive HEZ, PEZ, and other tokens
|
||||
{t('receive.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -88,15 +90,15 @@ export const ReceiveModal: React.FC<ReceiveModalProps> = ({ isOpen, onClose }) =
|
||||
|
||||
{/* Account Name */}
|
||||
<div className="text-center">
|
||||
<div className="text-sm text-gray-400 mb-1">Account Name</div>
|
||||
<div className="text-sm text-gray-400 mb-1">{t('receive.accountName')}</div>
|
||||
<div className="text-xl font-semibold text-white">
|
||||
{selectedAccount.meta.name || 'Unnamed Account'}
|
||||
{selectedAccount.meta.name || t('receive.unnamed')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Address */}
|
||||
<div className="bg-gray-800/50 rounded-lg p-4">
|
||||
<div className="text-sm text-gray-400 mb-2">Wallet Address</div>
|
||||
<div className="text-sm text-gray-400 mb-2">{t('receive.walletAddress')}</div>
|
||||
<div className="bg-gray-900 rounded p-3 mb-3">
|
||||
<div className="text-white font-mono text-sm break-all">
|
||||
{selectedAccount.address}
|
||||
@@ -110,12 +112,12 @@ export const ReceiveModal: React.FC<ReceiveModalProps> = ({ isOpen, onClose }) =
|
||||
{copied ? (
|
||||
<>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
Copied!
|
||||
{t('receive.copied')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
Copy Address
|
||||
{t('receive.copyAddress')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -124,7 +126,7 @@ export const ReceiveModal: React.FC<ReceiveModalProps> = ({ isOpen, onClose }) =
|
||||
{/* Warning */}
|
||||
<div className="bg-yellow-500/10 border border-yellow-500/30 rounded-lg p-3">
|
||||
<p className="text-yellow-400 text-xs">
|
||||
<strong>Important:</strong> Only send PezkuwiChain compatible tokens to this address. Sending other tokens may result in permanent loss.
|
||||
{t('receive.warning')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DollarSign, TrendingUp, Shield, AlertTriangle, RefreshCw, ExternalLink } from 'lucide-react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -18,6 +19,7 @@ export const ReservesDashboard: React.FC<ReservesDashboardProps> = ({
|
||||
specificAddresses = {},
|
||||
offChainReserveAmount = 0,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { api, isApiReady } = usePezkuwi();
|
||||
|
||||
const [wusdtSupply, setWusdtSupply] = useState(0);
|
||||
@@ -64,9 +66,9 @@ export const ReservesDashboard: React.FC<ReservesDashboardProps> = ({
|
||||
};
|
||||
|
||||
const getHealthStatus = () => {
|
||||
if (collateralRatio >= 105) return 'Healthy';
|
||||
if (collateralRatio >= 100) return 'Warning';
|
||||
return 'Critical';
|
||||
if (collateralRatio >= 105) return t('reserves.healthy');
|
||||
if (collateralRatio >= 100) return t('reserves.warning');
|
||||
return t('reserves.critical');
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -76,9 +78,9 @@ export const ReservesDashboard: React.FC<ReservesDashboardProps> = ({
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white flex items-center gap-2">
|
||||
<Shield className="h-6 w-6 text-blue-400" />
|
||||
USDT Reserves Dashboard
|
||||
{t('reserves.title')}
|
||||
</h2>
|
||||
<p className="text-gray-400 mt-1">Real-time reserve status and multisig info</p>
|
||||
<p className="text-gray-400 mt-1">{t('reserves.subtitle')}</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={fetchReserveData}
|
||||
@@ -88,7 +90,7 @@ export const ReservesDashboard: React.FC<ReservesDashboardProps> = ({
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
{t('reserves.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -98,11 +100,11 @@ export const ReservesDashboard: React.FC<ReservesDashboardProps> = ({
|
||||
<Card className="p-4 bg-gray-800/50 border-gray-700">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Total wUSDT Supply</p>
|
||||
<p className="text-sm text-gray-400">{t('reserves.totalSupply')}</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">
|
||||
${formatWUSDT(wusdtSupply)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">On-chain (Assets pallet)</p>
|
||||
<p className="text-xs text-gray-500 mt-1">{t('reserves.onChainHint')}</p>
|
||||
</div>
|
||||
<DollarSign className="h-8 w-8 text-blue-400" />
|
||||
</div>
|
||||
@@ -112,7 +114,7 @@ export const ReservesDashboard: React.FC<ReservesDashboardProps> = ({
|
||||
<Card className="p-4 bg-gray-800/50 border-gray-700">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Off-chain USDT Reserve</p>
|
||||
<p className="text-sm text-gray-400">{t('reserves.offChainReserve')}</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">
|
||||
${formatWUSDT(offChainReserve)}
|
||||
</p>
|
||||
@@ -125,7 +127,7 @@ export const ReservesDashboard: React.FC<ReservesDashboardProps> = ({
|
||||
placeholder="Amount"
|
||||
/>
|
||||
<Button size="sm" variant="ghost" onClick={fetchReserveData} className="text-xs h-6">
|
||||
Update
|
||||
{t('reserves.update')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -137,7 +139,7 @@ export const ReservesDashboard: React.FC<ReservesDashboardProps> = ({
|
||||
<Card className="p-4 bg-gray-800/50 border-gray-700">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Collateral Ratio</p>
|
||||
<p className="text-sm text-gray-400">{t('reserves.collateralRatio')}</p>
|
||||
<p className={`text-2xl font-bold mt-1 ${getHealthColor()}`}>
|
||||
{collateralRatio.toFixed(2)}%
|
||||
</p>
|
||||
@@ -160,10 +162,9 @@ export const ReservesDashboard: React.FC<ReservesDashboardProps> = ({
|
||||
<Alert className="bg-red-900/20 border-red-500">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<p className="font-semibold">Under-collateralized!</p>
|
||||
<p className="font-semibold">{t('reserves.underCollateralized')}</p>
|
||||
<p className="text-sm">
|
||||
Reserve ratio is below 100%. Off-chain USDT reserves ({formatWUSDT(offChainReserve)})
|
||||
are less than on-chain wUSDT supply ({formatWUSDT(wusdtSupply)}).
|
||||
{t('reserves.underCollateralizedDesc')}
|
||||
</p>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
@@ -172,43 +173,43 @@ export const ReservesDashboard: React.FC<ReservesDashboardProps> = ({
|
||||
{/* Tabs */}
|
||||
<Tabs defaultValue="overview" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3 bg-gray-800">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="multisig">Multisig</TabsTrigger>
|
||||
<TabsTrigger value="proof">Proof of Reserves</TabsTrigger>
|
||||
<TabsTrigger value="overview">{t('reserves.tabOverview')}</TabsTrigger>
|
||||
<TabsTrigger value="multisig">{t('reserves.tabMultisig')}</TabsTrigger>
|
||||
<TabsTrigger value="proof">{t('reserves.tabProof')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Overview Tab */}
|
||||
<TabsContent value="overview" className="space-y-4">
|
||||
<Card className="p-6 bg-gray-800/50 border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Reserve Details</h3>
|
||||
<h3 className="text-lg font-semibold text-white mb-4">{t('reserves.detailsTitle')}</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between p-3 bg-gray-900/50 rounded">
|
||||
<span className="text-gray-300">On-chain wUSDT</span>
|
||||
<span className="text-gray-300">{t('reserves.onChainWusdt')}</span>
|
||||
<span className="text-white font-semibold">${formatWUSDT(wusdtSupply)}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between p-3 bg-gray-900/50 rounded">
|
||||
<span className="text-gray-300">Off-chain USDT</span>
|
||||
<span className="text-gray-300">{t('reserves.offChainUsdt')}</span>
|
||||
<span className="text-white font-semibold">${formatWUSDT(offChainReserve)}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between p-3 bg-gray-900/50 rounded">
|
||||
<span className="text-gray-300">Backing Ratio</span>
|
||||
<span className="text-gray-300">{t('reserves.backingRatio')}</span>
|
||||
<span className={`font-semibold ${getHealthColor()}`}>
|
||||
{collateralRatio.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between p-3 bg-gray-900/50 rounded">
|
||||
<span className="text-gray-300">Status</span>
|
||||
<span className="text-gray-300">{t('reserves.status')}</span>
|
||||
<Badge variant={isHealthy ? 'default' : 'destructive'}>
|
||||
{getHealthStatus()}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between p-3 bg-gray-900/50 rounded">
|
||||
<span className="text-gray-300">Last Updated</span>
|
||||
<span className="text-gray-300">{t('reserves.lastUpdated')}</span>
|
||||
<span className="text-gray-400 text-sm">{lastUpdate.toLocaleTimeString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -216,10 +217,9 @@ export const ReservesDashboard: React.FC<ReservesDashboardProps> = ({
|
||||
<Alert className="mt-4 bg-blue-900/20 border-blue-500">
|
||||
<Shield className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<p className="font-semibold mb-1">1:1 Backing</p>
|
||||
<p className="font-semibold mb-1">{t('reserves.backingTitle')}</p>
|
||||
<p className="text-sm">
|
||||
Every wUSDT is backed by real USDT held in the multisig treasury.
|
||||
Target ratio: ≥100% (ideally 105% for safety buffer).
|
||||
{t('reserves.backingDesc')}
|
||||
</p>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
@@ -237,24 +237,24 @@ export const ReservesDashboard: React.FC<ReservesDashboardProps> = ({
|
||||
{/* Proof of Reserves Tab */}
|
||||
<TabsContent value="proof" className="space-y-4">
|
||||
<Card className="p-6 bg-gray-800/50 border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Proof of Reserves</h3>
|
||||
<h3 className="text-lg font-semibold text-white mb-4">{t('reserves.proofTitle')}</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Alert className="bg-green-900/20 border-green-500">
|
||||
<Shield className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<p className="font-semibold mb-2">How to Verify Reserves:</p>
|
||||
<p className="font-semibold mb-2">{t('reserves.howToVerify')}</p>
|
||||
<ol className="list-decimal list-inside space-y-1 text-sm">
|
||||
<li>Check on-chain wUSDT supply via Pezkuwi Explorer</li>
|
||||
<li>Verify multisig account balance (if reserves on-chain)</li>
|
||||
<li>Compare with off-chain treasury (bank/exchange account)</li>
|
||||
<li>Ensure ratio ≥ 100%</li>
|
||||
<li>{t('reserves.verifyStep1')}</li>
|
||||
<li>{t('reserves.verifyStep2')}</li>
|
||||
<li>{t('reserves.verifyStep3')}</li>
|
||||
<li>{t('reserves.verifyStep4')}</li>
|
||||
</ol>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="p-4 bg-gray-900/50 rounded-lg">
|
||||
<p className="text-sm text-gray-400 mb-3">Quick Links:</p>
|
||||
<p className="text-sm text-gray-400 mb-3">{t('reserves.quickLinks')}</p>
|
||||
<div className="space-y-2">
|
||||
<a
|
||||
href="https://pezkuwichain.io/explorer/assets"
|
||||
@@ -263,7 +263,7 @@ export const ReservesDashboard: React.FC<ReservesDashboardProps> = ({
|
||||
className="flex items-center gap-2 text-blue-400 hover:text-blue-300 text-sm"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
View wUSDT Asset on Explorer
|
||||
{t('reserves.viewAsset')}
|
||||
</a>
|
||||
<a
|
||||
href="https://pezkuwichain.io/explorer/accounts"
|
||||
@@ -272,19 +272,17 @@ export const ReservesDashboard: React.FC<ReservesDashboardProps> = ({
|
||||
className="flex items-center gap-2 text-blue-400 hover:text-blue-300 text-sm"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
View Multisig Account
|
||||
{t('reserves.viewMultisig')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-orange-900/20 border border-orange-500/30 rounded-lg">
|
||||
<p className="text-sm font-semibold text-orange-400 mb-2">
|
||||
Note: Off-chain Reserves
|
||||
{t('reserves.noteTitle')}
|
||||
</p>
|
||||
<p className="text-sm text-gray-300">
|
||||
In this MVP implementation, off-chain USDT reserves are manually reported.
|
||||
For full decentralization, consider integrating with oracle services or
|
||||
using XCM bridge for on-chain verification.
|
||||
{t('reserves.noteDesc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Gift, Calendar, Users, Timer, DollarSign } from 'lucide-react';
|
||||
|
||||
const RewardDistribution: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [currentEpoch, setCurrentEpoch] = useState(1);
|
||||
const [trustScoreInput, setTrustScoreInput] = useState(500);
|
||||
const [totalParticipants, setTotalParticipants] = useState(1000);
|
||||
@@ -15,9 +17,9 @@ const RewardDistribution: React.FC = () => {
|
||||
const nftRewardPerHolder = parliamentaryAllocation / 201;
|
||||
|
||||
const epochPhases = [
|
||||
{ name: 'Active', duration: '30 days', blocks: 432000, status: 'current' },
|
||||
{ name: 'Claim Period', duration: '7 days', blocks: 100800, status: 'upcoming' },
|
||||
{ name: 'Closed', duration: 'Permanent', blocks: 0, status: 'final' }
|
||||
{ name: t('rewardDist.active'), duration: '30 days', blocks: 432000, status: 'current' },
|
||||
{ name: t('rewardDist.claimPeriod'), duration: '7 days', blocks: 100800, status: 'upcoming' },
|
||||
{ name: t('rewardDist.closed'), duration: t('rewardDist.permanent'), blocks: 0, status: 'final' }
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -25,10 +27,10 @@ const RewardDistribution: React.FC = () => {
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
|
||||
Reward Distribution System
|
||||
{t('rewardDist.title')}
|
||||
</h2>
|
||||
<p className="text-gray-400 text-lg max-w-2xl mx-auto">
|
||||
Monthly epoch-based rewards distributed by trust score and NFT holdings
|
||||
{t('rewardDist.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -45,12 +47,12 @@ const RewardDistribution: React.FC = () => {
|
||||
<div className="lg:col-span-2 bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
|
||||
<div className="flex items-center mb-6">
|
||||
<Calendar className="w-6 h-6 text-purple-400 mr-3" />
|
||||
<h3 className="text-xl font-semibold text-white">Epoch Timeline</h3>
|
||||
<h3 className="text-xl font-semibold text-white">{t('rewardDist.epochTimeline')}</h3>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-gray-400">Current Epoch</span>
|
||||
<span className="text-gray-400">{t('rewardDist.currentEpoch')}</span>
|
||||
<span className="text-2xl font-bold text-white">#{currentEpoch}</span>
|
||||
</div>
|
||||
<input
|
||||
@@ -97,13 +99,13 @@ const RewardDistribution: React.FC = () => {
|
||||
|
||||
<div className="mt-6 grid grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-gray-900/50 rounded-lg">
|
||||
<div className="text-gray-400 text-sm mb-1">Epoch Start Block</div>
|
||||
<div className="text-gray-400 text-sm mb-1">{t('rewardDist.epochStartBlock')}</div>
|
||||
<div className="text-white font-semibold">
|
||||
#{((currentEpoch - 1) * 432000).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 bg-gray-900/50 rounded-lg">
|
||||
<div className="text-gray-400 text-sm mb-1">Claim Deadline Block</div>
|
||||
<div className="text-gray-400 text-sm mb-1">{t('rewardDist.claimDeadline')}</div>
|
||||
<div className="text-cyan-400 font-semibold">
|
||||
#{((currentEpoch * 432000) + 100800).toLocaleString()}
|
||||
</div>
|
||||
@@ -116,7 +118,7 @@ const RewardDistribution: React.FC = () => {
|
||||
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<Gift className="w-6 h-6 text-cyan-400 mr-3" />
|
||||
<h3 className="text-lg font-semibold text-white">Epoch Pool</h3>
|
||||
<h3 className="text-lg font-semibold text-white">{t('rewardDist.epochPool')}</h3>
|
||||
</div>
|
||||
|
||||
<div className="text-3xl font-bold text-white mb-4">
|
||||
@@ -125,11 +127,11 @@ const RewardDistribution: React.FC = () => {
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between p-3 bg-gray-800/50 rounded-lg">
|
||||
<span className="text-gray-400">Trust Score Pool</span>
|
||||
<span className="text-gray-400">{t('rewardDist.trustScorePool')}</span>
|
||||
<span className="text-cyan-400 font-semibold">90%</span>
|
||||
</div>
|
||||
<div className="flex justify-between p-3 bg-gray-800/50 rounded-lg">
|
||||
<span className="text-gray-400">Parliamentary NFTs</span>
|
||||
<span className="text-gray-400">{t('rewardDist.parliamentaryNfts')}</span>
|
||||
<span className="text-purple-400 font-semibold">10%</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,23 +140,23 @@ const RewardDistribution: React.FC = () => {
|
||||
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<Users className="w-6 h-6 text-purple-400 mr-3" />
|
||||
<h3 className="text-lg font-semibold text-white">NFT Rewards</h3>
|
||||
<h3 className="text-lg font-semibold text-white">{t('rewardDist.nftRewards')}</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Total NFTs</span>
|
||||
<span className="text-gray-400">{t('rewardDist.totalNfts')}</span>
|
||||
<span className="text-white">201</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Per NFT Reward</span>
|
||||
<span className="text-gray-400">{t('rewardDist.perNftReward')}</span>
|
||||
<span className="text-purple-400 font-semibold">
|
||||
{Math.floor(nftRewardPerHolder).toLocaleString()} PEZ
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-3 bg-kurdish-red/20 rounded-lg border border-kurdish-red/30">
|
||||
<div className="text-xs text-purple-400 mb-1">Auto-distributed</div>
|
||||
<div className="text-sm text-gray-300">No claim required</div>
|
||||
<div className="text-xs text-purple-400 mb-1">{t('rewardDist.autoDistributed')}</div>
|
||||
<div className="text-sm text-gray-300">{t('rewardDist.noClaimRequired')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,12 +167,12 @@ const RewardDistribution: React.FC = () => {
|
||||
<div className="mt-8 bg-gradient-to-br from-purple-900/20 to-cyan-900/20 backdrop-blur-sm rounded-xl border border-purple-500/30 p-6">
|
||||
<h3 className="text-xl font-semibold text-white mb-6 flex items-center">
|
||||
<DollarSign className="w-6 h-6 text-cyan-400 mr-3" />
|
||||
Reward Calculator
|
||||
{t('rewardDist.rewardCalculator')}
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<label className="text-gray-400 text-sm block mb-2">Your Trust Score</label>
|
||||
<label className="text-gray-400 text-sm block mb-2">{t('rewardDist.yourTrustScore')}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={trustScoreInput}
|
||||
@@ -180,7 +182,7 @@ const RewardDistribution: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-gray-400 text-sm block mb-2">Total Participants</label>
|
||||
<label className="text-gray-400 text-sm block mb-2">{t('rewardDist.totalParticipants')}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={totalParticipants}
|
||||
@@ -190,7 +192,7 @@ const RewardDistribution: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-gray-400 text-sm block mb-2">Total Trust Score</label>
|
||||
<label className="text-gray-400 text-sm block mb-2">{t('rewardDist.totalTrustScore')}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={totalTrustScore}
|
||||
@@ -203,19 +205,19 @@ const RewardDistribution: React.FC = () => {
|
||||
<div className="mt-6 p-4 bg-gray-900/50 rounded-lg">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="text-center">
|
||||
<div className="text-gray-400 text-sm mb-1">Reward per Trust Point</div>
|
||||
<div className="text-gray-400 text-sm mb-1">{t('rewardDist.rewardPerPoint')}</div>
|
||||
<div className="text-xl font-semibold text-cyan-400">
|
||||
{rewardPerTrustPoint.toFixed(4)} PEZ
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-gray-400 text-sm mb-1">Your Share</div>
|
||||
<div className="text-gray-400 text-sm mb-1">{t('rewardDist.yourShare')}</div>
|
||||
<div className="text-xl font-semibold text-purple-400">
|
||||
{((trustScoreInput / totalTrustScore) * 100).toFixed(3)}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-gray-400 text-sm mb-1">Estimated Reward</div>
|
||||
<div className="text-gray-400 text-sm mb-1">{t('rewardDist.estimatedReward')}</div>
|
||||
<div className="text-2xl font-bold bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
|
||||
{Math.floor(userReward).toLocaleString()} PEZ
|
||||
</div>
|
||||
|
||||
+147
-120
@@ -5,6 +5,7 @@
|
||||
|
||||
import React, { useEffect, useState, ReactNode } from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import {
|
||||
@@ -28,12 +29,152 @@ interface RouteGuardProps {
|
||||
// ========================================
|
||||
|
||||
const LoadingGuard: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
|
||||
<Card className="bg-gray-900 border-gray-800 p-8">
|
||||
<CardContent className="flex flex-col items-center gap-4">
|
||||
<Loader2 className="w-12 h-12 text-green-500 animate-spin" />
|
||||
<p className="text-gray-400">Checking permissions...</p>
|
||||
<p className="text-gray-400">{t('guards.checkingPermissions')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const WalletNotConnectedGuard: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center p-4">
|
||||
<Card className="bg-gray-900 border-gray-800 max-w-md">
|
||||
<CardContent className="p-8">
|
||||
<div className="flex flex-col items-center gap-4 text-center">
|
||||
<Users className="w-16 h-16 text-yellow-500" />
|
||||
<h2 className="text-2xl font-bold text-white">{t('guards.walletNotConnected')}</h2>
|
||||
<p className="text-gray-400">
|
||||
{t('guards.connectWalletMessage')}
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => window.location.href = '/'}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
{t('common.goHome')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ValidatorRequiredGuard: React.FC<{ fallbackPath: string }> = ({ fallbackPath }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center p-4">
|
||||
<Card className="bg-gray-900 border-gray-800 max-w-md">
|
||||
<CardContent className="p-8">
|
||||
<Alert className="bg-red-900/20 border-red-500">
|
||||
<AlertCircle className="h-5 w-5 text-red-400" />
|
||||
<AlertDescription className="text-gray-300">
|
||||
<strong className="block mb-2">{t('guards.validatorRequired')}</strong>
|
||||
{t('guards.validatorMessage')}
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
onClick={() => window.location.href = fallbackPath}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
{t('common.goToStaking')}
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EducatorRequiredGuard: React.FC<{ fallbackPath: string }> = ({ fallbackPath }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center p-4">
|
||||
<Card className="bg-gray-900 border-gray-800 max-w-md">
|
||||
<CardContent className="p-8">
|
||||
<Alert className="bg-red-900/20 border-red-500">
|
||||
<GraduationCap className="h-5 w-5 text-red-400" />
|
||||
<AlertDescription className="text-gray-300">
|
||||
<strong className="block mb-2">{t('guards.educatorRequired')}</strong>
|
||||
{t('guards.educatorMessage')}
|
||||
<ul className="list-disc list-inside mt-2 text-sm">
|
||||
<li>{t('guards.roles.perwerdekar')}</li>
|
||||
<li>{t('guards.roles.mamoste')}</li>
|
||||
<li>{t('guards.roles.wezirecand')}</li>
|
||||
<li>{t('guards.roles.rewsenbir')}</li>
|
||||
</ul>
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
onClick={() => window.location.href = fallbackPath}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
{t('common.browseCourses')}
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ModeratorRequiredGuard: React.FC<{ fallbackPath: string }> = ({ fallbackPath }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center p-4">
|
||||
<Card className="bg-gray-900 border-gray-800 max-w-md">
|
||||
<CardContent className="p-8">
|
||||
<Alert className="bg-red-900/20 border-red-500">
|
||||
<Shield className="h-5 w-5 text-red-400" />
|
||||
<AlertDescription className="text-gray-300">
|
||||
<strong className="block mb-2">{t('guards.moderatorRequired')}</strong>
|
||||
{t('guards.moderatorMessage')}
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
onClick={() => window.location.href = fallbackPath}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
{t('common.goHome')}
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AdminRequiredGuard: React.FC<{ fallbackPath: string }> = ({ fallbackPath }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center p-4">
|
||||
<Card className="bg-gray-900 border-gray-800 max-w-md">
|
||||
<CardContent className="p-8">
|
||||
<Alert className="bg-red-900/20 border-red-500">
|
||||
<AlertCircle className="h-5 w-5 text-red-400" />
|
||||
<AlertDescription className="text-gray-300">
|
||||
<strong className="block mb-2">{t('guards.adminRequired')}</strong>
|
||||
{t('guards.adminMessage')}
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
onClick={() => window.location.href = fallbackPath}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
{t('common.goHome')}
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -98,27 +239,7 @@ export const CitizenRoute: React.FC<RouteGuardProps> = ({
|
||||
|
||||
// Not connected to wallet
|
||||
if (!selectedAccount) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center p-4">
|
||||
<Card className="bg-gray-900 border-gray-800 max-w-md">
|
||||
<CardContent className="p-8">
|
||||
<div className="flex flex-col items-center gap-4 text-center">
|
||||
<Users className="w-16 h-16 text-yellow-500" />
|
||||
<h2 className="text-2xl font-bold text-white">Wallet Not Connected</h2>
|
||||
<p className="text-gray-400">
|
||||
Please connect your Pezkuwi wallet to access this feature.
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => window.location.href = '/'}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
Go to Home
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
return <WalletNotConnectedGuard />;
|
||||
}
|
||||
|
||||
// Not a citizen
|
||||
@@ -192,29 +313,7 @@ export const ValidatorRoute: React.FC<RouteGuardProps> = ({
|
||||
|
||||
// Not in validator pool
|
||||
if (isValidator === false) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center p-4">
|
||||
<Card className="bg-gray-900 border-gray-800 max-w-md">
|
||||
<CardContent className="p-8">
|
||||
<Alert className="bg-red-900/20 border-red-500">
|
||||
<AlertCircle className="h-5 w-5 text-red-400" />
|
||||
<AlertDescription className="text-gray-300">
|
||||
<strong className="block mb-2">Validator Access Required</strong>
|
||||
You must be registered in the Validator Pool to access this feature.
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
onClick={() => window.location.href = fallbackPath}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
Go to Staking
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
return <ValidatorRequiredGuard fallbackPath={fallbackPath} />;
|
||||
}
|
||||
|
||||
// Authorized
|
||||
@@ -283,35 +382,7 @@ export const EducatorRoute: React.FC<RouteGuardProps> = ({
|
||||
|
||||
// Not an educator
|
||||
if (isEducator === false) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center p-4">
|
||||
<Card className="bg-gray-900 border-gray-800 max-w-md">
|
||||
<CardContent className="p-8">
|
||||
<Alert className="bg-red-900/20 border-red-500">
|
||||
<GraduationCap className="h-5 w-5 text-red-400" />
|
||||
<AlertDescription className="text-gray-300">
|
||||
<strong className="block mb-2">Educator Role Required</strong>
|
||||
You need one of these Tiki roles to create courses:
|
||||
<ul className="list-disc list-inside mt-2 text-sm">
|
||||
<li>Perwerdekar (Educator)</li>
|
||||
<li>Mamoste (Teacher)</li>
|
||||
<li>WezireCand (Education Minister)</li>
|
||||
<li>Rewsenbîr (Intellectual)</li>
|
||||
</ul>
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
onClick={() => window.location.href = fallbackPath}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
Browse Courses
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
return <EducatorRequiredGuard fallbackPath={fallbackPath} />;
|
||||
}
|
||||
|
||||
// Authorized
|
||||
@@ -380,29 +451,7 @@ export const ModeratorRoute: React.FC<RouteGuardProps> = ({
|
||||
|
||||
// Not a moderator
|
||||
if (isModerator === false) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center p-4">
|
||||
<Card className="bg-gray-900 border-gray-800 max-w-md">
|
||||
<CardContent className="p-8">
|
||||
<Alert className="bg-red-900/20 border-red-500">
|
||||
<Shield className="h-5 w-5 text-red-400" />
|
||||
<AlertDescription className="text-gray-300">
|
||||
<strong className="block mb-2">Moderator Access Required</strong>
|
||||
You need moderator privileges to access this feature.
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
onClick={() => window.location.href = fallbackPath}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
Go to Home
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
return <ModeratorRequiredGuard fallbackPath={fallbackPath} />;
|
||||
}
|
||||
|
||||
// Authorized
|
||||
@@ -436,29 +485,7 @@ export const AdminRoute: React.FC<RouteGuardProps> = ({
|
||||
|
||||
// Not admin
|
||||
if (!isAdmin) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center p-4">
|
||||
<Card className="bg-gray-900 border-gray-800 max-w-md">
|
||||
<CardContent className="p-8">
|
||||
<Alert className="bg-red-900/20 border-red-500">
|
||||
<AlertCircle className="h-5 w-5 text-red-400" />
|
||||
<AlertDescription className="text-gray-300">
|
||||
<strong className="block mb-2">Admin Access Required</strong>
|
||||
You do not have permission to access the admin panel.
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
onClick={() => window.location.href = fallbackPath}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
Go to Home
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
return <AdminRequiredGuard fallbackPath={fallbackPath} />;
|
||||
}
|
||||
|
||||
// Authorized
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Users } from 'lucide-react';
|
||||
@@ -11,53 +12,54 @@ interface TeamMember {
|
||||
}
|
||||
|
||||
const TeamSection: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const teamMembers: TeamMember[] = [
|
||||
{
|
||||
name: "Satoshi Qazi Muhammed",
|
||||
role: "Chief Architect",
|
||||
description: "Blockchain visionary and protocol designer",
|
||||
role: t('teamSection.chiefArchitect'),
|
||||
description: t('teamSection.chiefArchitectDesc'),
|
||||
image: "https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760358016604_9ae228b4.webp"
|
||||
},
|
||||
{
|
||||
name: "Abdurrahman Qasimlo",
|
||||
role: "Governance Lead",
|
||||
description: "Democratic systems and consensus mechanisms",
|
||||
role: t('teamSection.govLead'),
|
||||
description: t('teamSection.govLeadDesc'),
|
||||
image: "https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760358018357_f19e128d.webp"
|
||||
},
|
||||
{
|
||||
name: "Abdusselam Barzani",
|
||||
role: "Protocol Engineer",
|
||||
description: "Core protocol development and optimization",
|
||||
role: t('teamSection.protocolEngineer'),
|
||||
description: t('teamSection.protocolEngineerDesc'),
|
||||
image: "https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760358020150_1ea35457.webp"
|
||||
},
|
||||
{
|
||||
name: "Ihsan Nuri",
|
||||
role: "Security Advisor",
|
||||
description: "Cryptography and network security expert",
|
||||
role: t('teamSection.securityAdvisor'),
|
||||
description: t('teamSection.securityAdvisorDesc'),
|
||||
image: "https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760358021872_362f1214.webp"
|
||||
},
|
||||
{
|
||||
name: "Seyh Said",
|
||||
role: "Community Director",
|
||||
description: "Ecosystem growth and community relations",
|
||||
role: t('teamSection.communityDirector'),
|
||||
description: t('teamSection.communityDirectorDesc'),
|
||||
image: "https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760358023648_4bb8f4c7.webp"
|
||||
},
|
||||
{
|
||||
name: "Seyyid Riza",
|
||||
role: "Treasury Manager",
|
||||
description: "Economic models and treasury operations",
|
||||
role: t('teamSection.treasuryManager'),
|
||||
description: t('teamSection.treasuryManagerDesc'),
|
||||
image: "https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760358025533_d9df77a9.webp"
|
||||
},
|
||||
{
|
||||
name: "Beritan",
|
||||
role: "Developer Relations",
|
||||
description: "Technical documentation and developer support",
|
||||
role: t('teamSection.devRelations'),
|
||||
description: t('teamSection.devRelationsDesc'),
|
||||
image: "https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760358027281_9254657a.webp"
|
||||
},
|
||||
{
|
||||
name: "Mashuk Xaznevi",
|
||||
role: "Research Lead",
|
||||
description: "Blockchain research and innovation",
|
||||
role: t('teamSection.researchLead'),
|
||||
description: t('teamSection.researchLeadDesc'),
|
||||
image: "https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760358029000_3ffc04bc.webp"
|
||||
}
|
||||
];
|
||||
@@ -68,13 +70,13 @@ const TeamSection: React.FC = () => {
|
||||
<div className="text-center mb-12">
|
||||
<Badge className="mb-4 bg-kurdish-green/20 text-kurdish-green border-kurdish-green/30">
|
||||
<Users className="w-4 h-4 mr-2" />
|
||||
Our Team
|
||||
{t('teamSection.title')}
|
||||
</Badge>
|
||||
<h2 className="text-4xl font-bold text-white mb-4">
|
||||
Meet the Visionaries
|
||||
{t('teamSection.subtitle')}
|
||||
</h2>
|
||||
<p className="text-gray-400 max-w-2xl mx-auto">
|
||||
A dedicated team of blockchain experts and governance specialists building the future of decentralized democracy
|
||||
{t('teamSection.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { formatAssetLocation, NATIVE_TOKEN_ID } from '@pezkuwi/utils/dex';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { KurdistanSun } from './KurdistanSun';
|
||||
import { PriceChart } from './trading/PriceChart';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// Available tokens for swap
|
||||
const AVAILABLE_TOKENS = [
|
||||
@@ -27,6 +28,7 @@ const TokenSwap = () => {
|
||||
const { assetHubApi, isAssetHubReady, selectedAccount } = usePezkuwi();
|
||||
const { balances, refreshBalances } = useWallet();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [fromToken, setFromToken] = useState('PEZ');
|
||||
const [toToken, setToToken] = useState('HEZ');
|
||||
@@ -514,8 +516,8 @@ const TokenSwap = () => {
|
||||
const handleConfirmSwap = async () => {
|
||||
if (!assetHubApi || !selectedAccount) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Please connect your wallet',
|
||||
title: t('common.error'),
|
||||
description: t('common.connectWalletAlert'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -523,8 +525,8 @@ const TokenSwap = () => {
|
||||
|
||||
if (!isDexAvailable) {
|
||||
toast({
|
||||
title: 'DEX Not Available',
|
||||
description: 'AssetConversion pallet is not enabled in runtime',
|
||||
title: t('tokenSwap.dexNotAvailable'),
|
||||
description: t('tokenSwap.dexNotAvailableDesc'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -532,8 +534,8 @@ const TokenSwap = () => {
|
||||
|
||||
if (!exchangeRate || exchangeRate === 0) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'No liquidity pool available for this pair',
|
||||
title: t('common.error'),
|
||||
description: t('swap.noPool'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -545,8 +547,8 @@ const TokenSwap = () => {
|
||||
|
||||
if (fromAmountNum > fromBalanceNum) {
|
||||
toast({
|
||||
title: 'Insufficient Balance',
|
||||
description: `You only have ${fromBalanceNum.toFixed(4)} ${getTokenDisplayName(fromToken)}. Cannot swap ${fromAmountNum} ${getTokenDisplayName(fromToken)}.`,
|
||||
title: t('swap.insufficientBalanceBtn', { token: getTokenDisplayName(fromToken) }),
|
||||
description: t('tokenSwap.insufficientBalanceToast', { balance: fromBalanceNum.toFixed(4), token: getTokenDisplayName(fromToken), amount: fromAmountNum }),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -685,8 +687,8 @@ const TokenSwap = () => {
|
||||
if (import.meta.env.DEV) console.log('✅ Transaction in block:', status.asInBlock.toHex());
|
||||
|
||||
toast({
|
||||
title: 'Transaction Submitted',
|
||||
description: `Processing in block ${status.asInBlock.toHex().slice(0, 10)}...`,
|
||||
title: t('tokenSwap.txSubmitted'),
|
||||
description: t('tokenSwap.processingInBlock', { hash: status.asInBlock.toHex().slice(0, 10) }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -705,7 +707,7 @@ const TokenSwap = () => {
|
||||
}
|
||||
|
||||
toast({
|
||||
title: 'Error',
|
||||
title: t('common.error'),
|
||||
description: errorMessage,
|
||||
variant: 'destructive',
|
||||
});
|
||||
@@ -720,8 +722,8 @@ const TokenSwap = () => {
|
||||
|
||||
if (hasSwapEvent || fromToken === 'HEZ' || toToken === 'HEZ') {
|
||||
toast({
|
||||
title: 'Success!',
|
||||
description: `Swapped ${fromAmount} ${fromToken} for ~${toAmount} ${toToken}`,
|
||||
title: t('common.success'),
|
||||
description: t('swap.swapped', { fromAmount, fromToken, toAmount, toToken }),
|
||||
});
|
||||
|
||||
setFromAmount('');
|
||||
@@ -809,8 +811,8 @@ const TokenSwap = () => {
|
||||
}, 3000);
|
||||
} else {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Swap transaction failed',
|
||||
title: t('common.error'),
|
||||
description: t('swap.swapFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@@ -822,8 +824,8 @@ const TokenSwap = () => {
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Swap failed:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error instanceof Error ? error.message : 'Swap transaction failed',
|
||||
title: t('common.error'),
|
||||
description: error instanceof Error ? error.message : t('swap.swapFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
setIsSwapping(false);
|
||||
@@ -843,20 +845,19 @@ const TokenSwap = () => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-2">DEX Coming Soon</h2>
|
||||
<h2 className="text-2xl font-bold mb-2">{t('tokenSwap.dexComingSoon')}</h2>
|
||||
<p className="text-gray-300 max-w-md mx-auto">
|
||||
The AssetConversion pallet is not yet enabled in the runtime.
|
||||
Token swapping functionality will be available after the next runtime upgrade.
|
||||
{t('tokenSwap.dexComingDesc')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Badge variant="outline" className="text-yellow-500 border-yellow-500/30">
|
||||
Scheduled for Next Runtime Upgrade
|
||||
{t('tokenSwap.scheduledUpgrade')}
|
||||
</Badge>
|
||||
|
||||
<div className="pt-4">
|
||||
<Button variant="outline" onClick={() => window.location.href = '/'}>
|
||||
Back to Dashboard
|
||||
{t('tokenSwap.backToDashboard')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -873,7 +874,7 @@ const TokenSwap = () => {
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<KurdistanSun size={300} />
|
||||
<p className="text-white text-xl font-semibold animate-pulse">
|
||||
Processing your swap...
|
||||
{t('tokenSwap.processingSwap')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -891,7 +892,7 @@ const TokenSwap = () => {
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-bold">Token Swap</h2>
|
||||
<h2 className="text-2xl font-bold">{t('tokenSwap.tokenSwap')}</h2>
|
||||
<Button variant="ghost" size="icon" onClick={() => setShowSettings(true)}>
|
||||
<Settings className="h-5 w-5" />
|
||||
</Button>
|
||||
@@ -901,7 +902,7 @@ const TokenSwap = () => {
|
||||
<Alert className="mb-4 bg-yellow-500/10 border-yellow-500/30">
|
||||
<AlertCircle className="h-4 w-4 text-yellow-500" />
|
||||
<AlertDescription className="text-yellow-300">
|
||||
Please connect your wallet to swap tokens
|
||||
{t('tokenSwap.connectWalletAlert')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -909,9 +910,9 @@ const TokenSwap = () => {
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-lg p-4">
|
||||
<div className="flex justify-between mb-2">
|
||||
<span className="text-sm text-gray-400">From</span>
|
||||
<span className="text-sm text-gray-400">{t('tokenSwap.from')}</span>
|
||||
<span className="text-sm text-gray-400">
|
||||
Balance: {fromBalance} {getTokenDisplayName(fromToken)}
|
||||
{t('common.balance')}: {fromBalance} {getTokenDisplayName(fromToken)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
@@ -980,9 +981,9 @@ const TokenSwap = () => {
|
||||
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-lg p-4">
|
||||
<div className="flex justify-between mb-2">
|
||||
<span className="text-sm text-gray-400">To</span>
|
||||
<span className="text-sm text-gray-400">{t('tokenSwap.to')}</span>
|
||||
<span className="text-sm text-gray-400">
|
||||
Balance: {toBalance} {getTokenDisplayName(toToken)}
|
||||
{t('common.balance')}: {toBalance} {getTokenDisplayName(toToken)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
@@ -1041,15 +1042,15 @@ const TokenSwap = () => {
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400 flex items-center gap-1">
|
||||
<Info className="w-3 h-3" />
|
||||
Exchange Rate
|
||||
{t('common.exchangeRate')}
|
||||
</span>
|
||||
<span className="font-semibold text-white">
|
||||
{isLoadingRate ? (
|
||||
'Loading...'
|
||||
t('common.loading')
|
||||
) : exchangeRate > 0 ? (
|
||||
`1 ${getTokenDisplayName(fromToken)} = ${exchangeRate.toFixed(4)} ${getTokenDisplayName(toToken)}`
|
||||
) : (
|
||||
'No pool available'
|
||||
t('tokenSwap.noPoolAvailable')
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -1063,7 +1064,7 @@ const TokenSwap = () => {
|
||||
priceImpact < 5 ? 'text-yellow-500' :
|
||||
'text-red-500'
|
||||
}`} />
|
||||
Price Impact
|
||||
{t('tokenSwap.priceImpact')}
|
||||
</span>
|
||||
<span className={`font-semibold ${
|
||||
priceImpact < 1 ? 'text-green-400' :
|
||||
@@ -1078,7 +1079,7 @@ const TokenSwap = () => {
|
||||
{/* LP Fee */}
|
||||
{fromAmount && parseFloat(fromAmount) > 0 && lpFee && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Liquidity Provider Fee</span>
|
||||
<span className="text-gray-400">{t('tokenSwap.lpFee')}</span>
|
||||
<span className="text-gray-300">{lpFee} {getTokenDisplayName(fromToken)}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -1086,13 +1087,13 @@ const TokenSwap = () => {
|
||||
{/* Minimum Received */}
|
||||
{fromAmount && parseFloat(fromAmount) > 0 && minimumReceived && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Minimum Received</span>
|
||||
<span className="text-gray-400">{t('tokenSwap.minimumReceived')}</span>
|
||||
<span className="text-gray-300">{minimumReceived} {getTokenDisplayName(toToken)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between text-sm pt-2 border-t border-gray-700">
|
||||
<span className="text-gray-400">Slippage Tolerance</span>
|
||||
<span className="text-gray-400">{t('common.slippageTolerance')}</span>
|
||||
<span className="font-semibold text-blue-400">{slippage}%</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1102,7 +1103,7 @@ const TokenSwap = () => {
|
||||
<Alert className="bg-red-900/20 border-red-500/30">
|
||||
<AlertTriangle className="h-4 w-4 text-red-500" />
|
||||
<AlertDescription className="text-red-300 text-sm">
|
||||
Insufficient {getTokenDisplayName(fromToken)} balance. You have {fromBalance} {getTokenDisplayName(fromToken)} but trying to swap {fromAmount} {getTokenDisplayName(fromToken)}.
|
||||
{t('tokenSwap.insufficientWarning', { token: getTokenDisplayName(fromToken), balance: fromBalance, amount: fromAmount })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -1112,7 +1113,7 @@ const TokenSwap = () => {
|
||||
<Alert className="bg-red-900/20 border-red-500/30">
|
||||
<AlertTriangle className="h-4 w-4 text-red-500" />
|
||||
<AlertDescription className="text-red-300 text-sm">
|
||||
High price impact! Your trade will significantly affect the pool price. Consider a smaller amount or check if there's better liquidity.
|
||||
{t('tokenSwap.highPriceImpact')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -1123,12 +1124,12 @@ const TokenSwap = () => {
|
||||
disabled={!fromAmount || parseFloat(fromAmount) <= 0 || !selectedAccount || exchangeRate === 0 || hasInsufficientBalance}
|
||||
>
|
||||
{!selectedAccount
|
||||
? 'Connect Wallet'
|
||||
? t('tokenSwap.connectWallet')
|
||||
: hasInsufficientBalance
|
||||
? `Insufficient ${getTokenDisplayName(fromToken)} Balance`
|
||||
? t('tokenSwap.insufficientBalance', { token: getTokenDisplayName(fromToken) })
|
||||
: exchangeRate === 0
|
||||
? 'No Pool Available'
|
||||
: 'Swap Tokens'}
|
||||
? t('tokenSwap.noPool')
|
||||
: t('tokenSwap.swapTokens')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -1136,11 +1137,11 @@ const TokenSwap = () => {
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<TrendingUp className="h-5 w-5" />
|
||||
Liquidity Pools
|
||||
{t('tokenSwap.liquidityPools')}
|
||||
</h3>
|
||||
|
||||
{isLoadingPools ? (
|
||||
<div className="text-center text-gray-400 py-8">Loading pools...</div>
|
||||
<div className="text-center text-gray-400 py-8">{t('tokenSwap.loadingPools')}</div>
|
||||
) : liquidityPools.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{liquidityPools.map((pool, idx) => (
|
||||
@@ -1158,7 +1159,7 @@ const TokenSwap = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-gray-400 py-8">
|
||||
No liquidity pools available yet
|
||||
{t('tokenSwap.noPoolsAvailable')}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
@@ -1168,16 +1169,16 @@ const TokenSwap = () => {
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<Clock className="h-5 w-5" />
|
||||
Recent Swaps
|
||||
{t('tokenSwap.recentSwaps')}
|
||||
</h3>
|
||||
|
||||
{!selectedAccount ? (
|
||||
<div className="text-center text-gray-400 py-8">
|
||||
Connect wallet to view history
|
||||
{t('tokenSwap.connectForHistory')}
|
||||
</div>
|
||||
) : isLoadingHistory ? (
|
||||
<div className="text-center text-gray-400 py-8">
|
||||
Loading history...
|
||||
{t('tokenSwap.loadingHistory')}
|
||||
</div>
|
||||
) : swapHistory.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
@@ -1196,11 +1197,11 @@ const TokenSwap = () => {
|
||||
</div>
|
||||
<div className="text-sm text-gray-400 space-y-1">
|
||||
<div className="flex justify-between">
|
||||
<span>Sent:</span>
|
||||
<span>{t('tokenSwap.sent')}</span>
|
||||
<span className="text-red-400">-{tx.fromAmount} {getTokenDisplayName(tx.fromToken)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Received:</span>
|
||||
<span>{t('tokenSwap.received')}</span>
|
||||
<span className="text-green-400">+{tx.toAmount} {getTokenDisplayName(tx.toToken)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs pt-1 border-t border-gray-700">
|
||||
@@ -1213,7 +1214,7 @@ const TokenSwap = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-gray-400 py-8">
|
||||
No swap history yet
|
||||
{t('tokenSwap.noSwapHistory')}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
@@ -1222,11 +1223,11 @@ const TokenSwap = () => {
|
||||
<Dialog open={showSettings} onOpenChange={setShowSettings}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Swap Settings</DialogTitle>
|
||||
<DialogTitle>{t('tokenSwap.swapSettings')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium">Slippage Tolerance</label>
|
||||
<label className="text-sm font-medium">{t('common.slippageTolerance')}</label>
|
||||
<div className="flex gap-2 mt-2">
|
||||
{['0.1', '0.5', '1.0'].map(val => (
|
||||
<Button
|
||||
@@ -1253,24 +1254,24 @@ const TokenSwap = () => {
|
||||
<Dialog open={showConfirm} onOpenChange={setShowConfirm}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Confirm Swap</DialogTitle>
|
||||
<DialogTitle>{t('tokenSwap.confirmSwap')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-gray-800 border border-gray-700 rounded-lg">
|
||||
<div className="flex justify-between mb-2">
|
||||
<span className="text-gray-300">You Pay</span>
|
||||
<span className="text-gray-300">{t('tokenSwap.youPay')}</span>
|
||||
<span className="font-bold text-white">{fromAmount} {getTokenDisplayName(fromToken)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between mb-2">
|
||||
<span className="text-gray-300">You Receive</span>
|
||||
<span className="text-gray-300">{t('tokenSwap.youReceive')}</span>
|
||||
<span className="font-bold text-white">{toAmount} {getTokenDisplayName(toToken)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between mt-3 pt-3 border-t border-gray-700 text-sm">
|
||||
<span className="text-gray-400">Exchange Rate</span>
|
||||
<span className="text-gray-400">{t('common.exchangeRate')}</span>
|
||||
<span className="text-gray-400">1 {getTokenDisplayName(fromToken)} = {exchangeRate.toFixed(4)} {getTokenDisplayName(toToken)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Slippage</span>
|
||||
<span className="text-gray-400">{t('swap.slippage')}</span>
|
||||
<span className="text-gray-400">{slippage}%</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1279,7 +1280,7 @@ const TokenSwap = () => {
|
||||
onClick={handleConfirmSwap}
|
||||
disabled={isSwapping}
|
||||
>
|
||||
{isSwapping ? 'Swapping...' : 'Confirm Swap'}
|
||||
{isSwapping ? t('tokenSwap.swapping') : t('tokenSwap.confirmSwap')}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PieChart, ArrowRightLeft } from 'lucide-react';
|
||||
|
||||
const TokenomicsSection: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedToken, setSelectedToken] = useState<'PEZ' | 'HEZ'>('PEZ');
|
||||
const [monthsPassed] = useState(0);
|
||||
|
||||
@@ -16,16 +18,16 @@ const TokenomicsSection: React.FC = () => {
|
||||
}, [monthsPassed, halvingPeriod, selectedToken]);
|
||||
|
||||
const pezDistribution = [
|
||||
{ name: 'Treasury', percentage: 96.25, amount: 4812500000, color: 'from-purple-500 to-purple-600' },
|
||||
{ name: 'Presale', percentage: 1.875, amount: 93750000, color: 'from-cyan-500 to-cyan-600' },
|
||||
{ name: 'Founder', percentage: 1.875, amount: 93750000, color: 'from-teal-500 to-teal-600' }
|
||||
{ name: t('tokenomics.treasury'), percentage: 96.25, amount: 4812500000, color: 'from-purple-500 to-purple-600' },
|
||||
{ name: t('tokenomics.presale'), percentage: 1.875, amount: 93750000, color: 'from-cyan-500 to-cyan-600' },
|
||||
{ name: t('tokenomics.founder'), percentage: 1.875, amount: 93750000, color: 'from-teal-500 to-teal-600' }
|
||||
];
|
||||
|
||||
const hezDistribution = [
|
||||
{ name: 'Staking Rewards', percentage: 40, amount: 1000000000, color: 'from-yellow-500 to-orange-600' },
|
||||
{ name: 'Governance', percentage: 30, amount: 750000000, color: 'from-green-500 to-emerald-600' },
|
||||
{ name: 'Ecosystem', percentage: 20, amount: 500000000, color: 'from-blue-500 to-indigo-600' },
|
||||
{ name: 'Team', percentage: 10, amount: 250000000, color: 'from-red-500 to-pink-600' }
|
||||
{ name: t('tokenomics.stakingRewards'), percentage: 40, amount: 1000000000, color: 'from-yellow-500 to-orange-600' },
|
||||
{ name: t('tokenomics.governance'), percentage: 30, amount: 750000000, color: 'from-green-500 to-emerald-600' },
|
||||
{ name: t('tokenomics.ecosystem'), percentage: 20, amount: 500000000, color: 'from-blue-500 to-indigo-600' },
|
||||
{ name: t('tokenomics.team'), percentage: 10, amount: 250000000, color: 'from-red-500 to-pink-600' }
|
||||
];
|
||||
|
||||
const distribution = selectedToken === 'PEZ' ? pezDistribution : hezDistribution;
|
||||
@@ -37,10 +39,10 @@ const TokenomicsSection: React.FC = () => {
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-purple-400 to-yellow-400 bg-clip-text text-transparent">
|
||||
Dual Token Ecosystem
|
||||
{t('tokenomics.dualToken')}
|
||||
</h2>
|
||||
<p className="text-gray-400 text-lg max-w-2xl mx-auto mb-6">
|
||||
PEZ & HEZ tokens working together for governance and utility
|
||||
{t('tokenomics.dualTokenDesc')}
|
||||
</p>
|
||||
|
||||
{/* Token Selector */}
|
||||
@@ -53,17 +55,17 @@ const TokenomicsSection: React.FC = () => {
|
||||
: 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
PEZ Token
|
||||
{t('tokenomics.pezToken')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedToken('HEZ')}
|
||||
className={`px-6 py-2 rounded-md font-semibold transition-all ${
|
||||
selectedToken === 'HEZ'
|
||||
? 'bg-yellow-600 text-white'
|
||||
selectedToken === 'HEZ'
|
||||
? 'bg-yellow-600 text-white'
|
||||
: 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
HEZ Token
|
||||
{t('tokenomics.hezToken')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,7 +75,7 @@ const TokenomicsSection: React.FC = () => {
|
||||
<div className="bg-gray-950/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
|
||||
<div className="flex items-center mb-6">
|
||||
<PieChart className={`w-6 h-6 text-${tokenColor}-400 mr-3`} />
|
||||
<h3 className="text-xl font-semibold text-white">{selectedToken} Distribution</h3>
|
||||
<h3 className="text-xl font-semibold text-white">{t('tokenomics.pezDistribution')}</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mb-6">
|
||||
@@ -99,7 +101,7 @@ const TokenomicsSection: React.FC = () => {
|
||||
|
||||
<div className={`mt-6 p-4 bg-${tokenColor}-500/20 rounded-lg border border-${tokenColor}-500/30`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={`text-${tokenColor}-400`}>Total Supply</span>
|
||||
<span className={`text-${tokenColor}-400`}>{t('tokenomics.totalSupply')}</span>
|
||||
<span className="text-white font-bold">{totalSupply.toLocaleString()} {selectedToken}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,63 +111,63 @@ const TokenomicsSection: React.FC = () => {
|
||||
<div className="bg-gray-950/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
|
||||
<div className="flex items-center mb-6">
|
||||
<ArrowRightLeft className={`w-6 h-6 text-${tokenColor}-400 mr-3`} />
|
||||
<h3 className="text-xl font-semibold text-white">{selectedToken} Features</h3>
|
||||
<h3 className="text-xl font-semibold text-white">{t('tokenomics.hezFeatures')}</h3>
|
||||
</div>
|
||||
|
||||
{selectedToken === 'PEZ' ? (
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-purple-900/20 rounded-lg border border-purple-500/30">
|
||||
<h4 className="text-purple-400 font-semibold mb-2">Governance Token</h4>
|
||||
<p className="text-gray-300 text-sm">Vote on proposals and participate in DAO decisions</p>
|
||||
<h4 className="text-purple-400 font-semibold mb-2">{t('tokenomics.govToken')}</h4>
|
||||
<p className="text-gray-300 text-sm">{t('tokenomics.govTokenDesc')}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-purple-900/20 rounded-lg border border-purple-500/30">
|
||||
<h4 className="text-purple-400 font-semibold mb-2">Staking Rewards</h4>
|
||||
<p className="text-gray-300 text-sm">Earn HEZ tokens by staking PEZ</p>
|
||||
<h4 className="text-purple-400 font-semibold mb-2">{t('tokenomics.stakingRewardsTitle')}</h4>
|
||||
<p className="text-gray-300 text-sm">{t('tokenomics.stakingRewardsDesc')}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-purple-900/20 rounded-lg border border-purple-500/30">
|
||||
<h4 className="text-purple-400 font-semibold mb-2">Treasury Access</h4>
|
||||
<p className="text-gray-300 text-sm">Propose and vote on treasury fund allocation</p>
|
||||
<h4 className="text-purple-400 font-semibold mb-2">{t('tokenomics.treasuryAccess')}</h4>
|
||||
<p className="text-gray-300 text-sm">{t('tokenomics.treasuryAccessDesc')}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-purple-900/20 rounded-lg border border-purple-500/30">
|
||||
<h4 className="text-purple-400 font-semibold mb-2">Deflationary</h4>
|
||||
<p className="text-gray-300 text-sm">Synthetic halving every 48 months</p>
|
||||
<h4 className="text-purple-400 font-semibold mb-2">{t('tokenomics.deflationary')}</h4>
|
||||
<p className="text-gray-300 text-sm">{t('tokenomics.deflationaryDesc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-yellow-900/20 rounded-lg border border-yellow-500/30">
|
||||
<h4 className="text-yellow-400 font-semibold mb-2">Utility Token</h4>
|
||||
<p className="text-gray-300 text-sm">Used for platform transactions and services</p>
|
||||
<h4 className="text-yellow-400 font-semibold mb-2">{t('tokenomics.utilityToken')}</h4>
|
||||
<p className="text-gray-300 text-sm">{t('tokenomics.utilityTokenDesc')}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-yellow-900/20 rounded-lg border border-yellow-500/30">
|
||||
<h4 className="text-yellow-400 font-semibold mb-2">P2P Trading</h4>
|
||||
<p className="text-gray-300 text-sm">Primary currency for peer-to-peer marketplace</p>
|
||||
<h4 className="text-yellow-400 font-semibold mb-2">{t('tokenomics.p2pTrading')}</h4>
|
||||
<p className="text-gray-300 text-sm">{t('tokenomics.p2pTradingDesc')}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-yellow-900/20 rounded-lg border border-yellow-500/30">
|
||||
<h4 className="text-yellow-400 font-semibold mb-2">Fee Discounts</h4>
|
||||
<p className="text-gray-300 text-sm">Reduced platform fees when using HEZ</p>
|
||||
<h4 className="text-yellow-400 font-semibold mb-2">{t('tokenomics.feeDiscounts')}</h4>
|
||||
<p className="text-gray-300 text-sm">{t('tokenomics.feeDiscountsDesc')}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-yellow-900/20 rounded-lg border border-yellow-500/30">
|
||||
<h4 className="text-yellow-400 font-semibold mb-2">Reward Distribution</h4>
|
||||
<p className="text-gray-300 text-sm">Earned through staking and participation</p>
|
||||
<h4 className="text-yellow-400 font-semibold mb-2">{t('tokenomics.rewardDistribution')}</h4>
|
||||
<p className="text-gray-300 text-sm">{t('tokenomics.rewardDistributionDesc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 p-4 bg-gradient-to-r from-purple-900/20 to-yellow-900/20 rounded-lg border border-gray-700">
|
||||
<h4 className="text-white font-semibold mb-3">Token Synergy</h4>
|
||||
<h4 className="text-white font-semibold mb-3">{t('tokenomics.tokenSynergy')}</h4>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center text-gray-300">
|
||||
<span className="w-2 h-2 bg-purple-400 rounded-full mr-2"></span>
|
||||
Stake PEZ → Earn HEZ rewards
|
||||
{t('tokenomics.synergy1')}
|
||||
</div>
|
||||
<div className="flex items-center text-gray-300">
|
||||
<span className="w-2 h-2 bg-yellow-400 rounded-full mr-2"></span>
|
||||
Use HEZ → Boost governance power
|
||||
{t('tokenomics.synergy2')}
|
||||
</div>
|
||||
<div className="flex items-center text-gray-300">
|
||||
<span className="w-2 h-2 bg-green-400 rounded-full mr-2"></span>
|
||||
Hold both → Maximum platform benefits
|
||||
{t('tokenomics.synergy3')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -32,6 +33,7 @@ interface Transaction {
|
||||
export const TransactionHistory: React.FC<TransactionHistoryProps> = ({ isOpen, onClose }) => {
|
||||
const { api, isApiReady, selectedAccount } = usePezkuwi();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@@ -234,8 +236,8 @@ export const TransactionHistory: React.FC<TransactionHistoryProps> = ({ isOpen,
|
||||
} catch {
|
||||
if (import.meta.env.DEV) console.error('Failed to fetch transactions:', error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to fetch transaction history",
|
||||
title: t('transfer.error'),
|
||||
description: t('txHistory.fetchError'),
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
@@ -272,9 +274,9 @@ export const TransactionHistory: React.FC<TransactionHistoryProps> = ({ isOpen,
|
||||
<DialogHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<DialogTitle className="text-white">Transaction History</DialogTitle>
|
||||
<DialogTitle className="text-white">{t('txHistory.title')}</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Recent transactions involving your account
|
||||
{t('txHistory.description')}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<Button
|
||||
@@ -293,14 +295,14 @@ export const TransactionHistory: React.FC<TransactionHistoryProps> = ({ isOpen,
|
||||
{isLoading ? (
|
||||
<div className="text-center py-12">
|
||||
<RefreshCw className="w-12 h-12 text-gray-600 mx-auto mb-3 animate-spin" />
|
||||
<p className="text-gray-400">Loading transactions...</p>
|
||||
<p className="text-gray-400">{t('txHistory.loading')}</p>
|
||||
</div>
|
||||
) : transactions.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<History className="w-12 h-12 text-gray-600 mx-auto mb-3" />
|
||||
<p className="text-gray-500">No transactions found</p>
|
||||
<p className="text-gray-500">{t('txHistory.noTx')}</p>
|
||||
<p className="text-gray-600 text-sm mt-1">
|
||||
Your recent transactions will appear here
|
||||
{t('txHistory.noTxDesc')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -322,7 +324,7 @@ export const TransactionHistory: React.FC<TransactionHistoryProps> = ({ isOpen,
|
||||
)}
|
||||
<div>
|
||||
<div className="text-white font-semibold">
|
||||
{isIncoming(tx) ? 'Received' : 'Sent'}
|
||||
{isIncoming(tx) ? t('txHistory.received') : t('txHistory.sent')}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
{tx.section}.{tx.method}
|
||||
@@ -341,14 +343,14 @@ export const TransactionHistory: React.FC<TransactionHistoryProps> = ({ isOpen,
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div>
|
||||
<span className="text-gray-500">From:</span>
|
||||
<span className="text-gray-500">{t('txHistory.from')}</span>
|
||||
<div className="text-gray-300 font-mono">
|
||||
{tx.from.slice(0, 8)}...{tx.from.slice(-6)}
|
||||
</div>
|
||||
</div>
|
||||
{tx.to && (
|
||||
<div>
|
||||
<span className="text-gray-500">To:</span>
|
||||
<span className="text-gray-500">{t('txHistory.to')}</span>
|
||||
<div className="text-gray-300 font-mono">
|
||||
{tx.to.slice(0, 8)}...{tx.to.slice(-6)}
|
||||
</div>
|
||||
@@ -366,12 +368,12 @@ export const TransactionHistory: React.FC<TransactionHistoryProps> = ({ isOpen,
|
||||
className="text-xs text-blue-400 hover:text-blue-300"
|
||||
onClick={() => {
|
||||
toast({
|
||||
title: "Transaction Details",
|
||||
description: `Block #${tx.blockNumber}, Extrinsic #${tx.extrinsicIndex}`,
|
||||
title: t('txHistory.txDetails'),
|
||||
description: t('txHistory.blockExtrinsic', { block: tx.blockNumber, extrinsic: tx.extrinsicIndex }),
|
||||
});
|
||||
}}
|
||||
>
|
||||
View Details
|
||||
{t('txHistory.viewDetails')}
|
||||
<ExternalLink className="w-3 h-3 ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -68,6 +69,7 @@ const TOKENS: Token[] = [
|
||||
export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, selectedAsset }) => {
|
||||
const { api, assetHubApi, isApiReady, isAssetHubReady, selectedAccount } = usePezkuwi();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [selectedToken, setSelectedToken] = useState<TokenType>('HEZ');
|
||||
const [recipient, setRecipient] = useState('');
|
||||
@@ -90,8 +92,8 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, s
|
||||
const handleTransfer = async () => {
|
||||
if (!api || !isApiReady || !selectedAccount) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Wallet not connected",
|
||||
title: t('transfer.error'),
|
||||
description: t('transfer.walletNotConnected'),
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
@@ -107,8 +109,8 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, s
|
||||
currentToken.assetId === 1000; // wUSDT
|
||||
if (isAssetHubTransfer && (!assetHubApi || !isAssetHubReady)) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Asset Hub connection not ready. This token is on Asset Hub.",
|
||||
title: t('transfer.error'),
|
||||
description: t('transfer.assetHubNotReady'),
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
@@ -116,8 +118,8 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, s
|
||||
|
||||
if (!recipient || !amount) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Please fill in all fields",
|
||||
title: t('transfer.error'),
|
||||
description: t('transfer.fillAllFields'),
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
@@ -178,15 +180,15 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, s
|
||||
|
||||
setTxStatus('error');
|
||||
toast({
|
||||
title: "Transfer Failed",
|
||||
title: t('transfer.failed'),
|
||||
description: errorMessage,
|
||||
variant: "destructive",
|
||||
});
|
||||
} else {
|
||||
setTxStatus('success');
|
||||
toast({
|
||||
title: "Transfer Successful!",
|
||||
description: `Sent ${amount} ${currentToken.symbol} to ${recipient.slice(0, 8)}...${recipient.slice(-6)}`,
|
||||
title: t('transfer.success'),
|
||||
description: t('transfer.sentAmount', { amount, token: currentToken.symbol, recipient: `${recipient.slice(0, 8)}...${recipient.slice(-6)}` }),
|
||||
});
|
||||
|
||||
// Reset form after 2 seconds
|
||||
@@ -210,8 +212,8 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, s
|
||||
setIsTransferring(false);
|
||||
|
||||
toast({
|
||||
title: "Transfer Failed",
|
||||
description: error instanceof Error ? error.message : "An error occurred during transfer",
|
||||
title: t('transfer.failed'),
|
||||
description: error instanceof Error ? error.message : t('transfer.errorOccurred'),
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
@@ -233,23 +235,23 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, s
|
||||
<DialogContent className="bg-gray-900 border-gray-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">
|
||||
{selectedAsset ? `Send ${selectedAsset.symbol}` : 'Send Tokens'}
|
||||
{selectedAsset ? t('transfer.sendToken', { token: selectedAsset.symbol }) : t('transfer.sendTokens')}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
{selectedAsset
|
||||
? `Transfer ${selectedAsset.name} to another account`
|
||||
: 'Transfer tokens to another account'}
|
||||
? t('transfer.transferTo', { name: selectedAsset.name })
|
||||
: t('transfer.transferTokens')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{txStatus === 'success' ? (
|
||||
<div className="py-8 text-center">
|
||||
<CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold text-white mb-2">Transfer Successful!</h3>
|
||||
<p className="text-gray-400 mb-4">Your transaction has been finalized</p>
|
||||
<h3 className="text-xl font-semibold text-white mb-2">{t('transfer.success')}</h3>
|
||||
<p className="text-gray-400 mb-4">{t('transfer.finalized')}</p>
|
||||
{txHash && (
|
||||
<div className="bg-gray-800/50 rounded-lg p-3">
|
||||
<div className="text-xs text-gray-400 mb-1">Transaction Hash</div>
|
||||
<div className="text-xs text-gray-400 mb-1">{t('transfer.txHash')}</div>
|
||||
<div className="text-white font-mono text-xs break-all">
|
||||
{txHash}
|
||||
</div>
|
||||
@@ -259,13 +261,13 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, s
|
||||
) : txStatus === 'error' ? (
|
||||
<div className="py-8 text-center">
|
||||
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold text-white mb-2">Transfer Failed</h3>
|
||||
<p className="text-gray-400">Please try again</p>
|
||||
<h3 className="text-xl font-semibold text-white mb-2">{t('transfer.failed')}</h3>
|
||||
<p className="text-gray-400">{t('transfer.pleaseTryAgain')}</p>
|
||||
<Button
|
||||
onClick={() => setTxStatus('idle')}
|
||||
className="mt-4 bg-gray-800 hover:bg-gray-700"
|
||||
>
|
||||
Try Again
|
||||
{t('transfer.tryAgain')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -273,10 +275,10 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, s
|
||||
{/* Token Selection - Only show if no asset is pre-selected */}
|
||||
{!selectedAsset && (
|
||||
<div>
|
||||
<Label htmlFor="token" className="text-white">Select Token</Label>
|
||||
<Label htmlFor="token" className="text-white">{t('transfer.selectToken')}</Label>
|
||||
<Select value={selectedToken} onValueChange={(value) => setSelectedToken(value as TokenType)} disabled={isTransferring}>
|
||||
<SelectTrigger className="bg-gray-800 border-gray-700 text-white mt-2">
|
||||
<SelectValue placeholder="Select token" />
|
||||
<SelectValue placeholder={t('transfer.selectTokenPlaceholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-gray-800 border-gray-700">
|
||||
{TOKENS.map((token) => (
|
||||
@@ -302,38 +304,38 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, s
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label htmlFor="recipient" className="text-white">Recipient Address</Label>
|
||||
<Label htmlFor="recipient" className="text-white">{t('transfer.recipientAddress')}</Label>
|
||||
<Input
|
||||
id="recipient"
|
||||
value={recipient}
|
||||
onChange={(e) => setRecipient(e.target.value)}
|
||||
placeholder="Recipient address"
|
||||
placeholder={t('transfer.recipientPlaceholder')}
|
||||
className="bg-gray-800 border-gray-700 text-white mt-2 placeholder:text-gray-500 placeholder:opacity-50"
|
||||
disabled={isTransferring}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="amount" className="text-white">Amount ({selectedToken})</Label>
|
||||
<Label htmlFor="amount" className="text-white">{t('transfer.amountLabel', { token: selectedToken })}</Label>
|
||||
<Input
|
||||
id="amount"
|
||||
type="number"
|
||||
step={selectedToken === 'HEZ' || selectedToken === 'PEZ' ? '0.0001' : '0.000001'}
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
placeholder="Amount"
|
||||
placeholder={t('transfer.amountPlaceholder')}
|
||||
className="bg-gray-800 border-gray-700 text-white mt-2 placeholder:text-gray-500 placeholder:opacity-50"
|
||||
disabled={isTransferring}
|
||||
/>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
Decimals: {currentToken.decimals}
|
||||
{t('transfer.decimals', { decimals: currentToken.decimals })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{txStatus === 'signing' && (
|
||||
<div className="bg-yellow-500/10 border border-yellow-500/30 rounded-lg p-3">
|
||||
<p className="text-yellow-400 text-sm">
|
||||
Please sign the transaction in your Pezkuwi.js extension
|
||||
{t('transfer.signTransaction')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -342,7 +344,7 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, s
|
||||
<div className="bg-blue-500/10 border border-blue-500/30 rounded-lg p-3">
|
||||
<p className="text-blue-400 text-sm flex items-center gap-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Transaction pending... Waiting for finalization
|
||||
{t('transfer.txPending')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -355,11 +357,11 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, s
|
||||
{isTransferring ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
{txStatus === 'signing' ? 'Waiting for signature...' : 'Processing...'}
|
||||
{txStatus === 'signing' ? t('transfer.waitingSignature') : t('transfer.processing')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Send {selectedToken}
|
||||
{t('transfer.sendToken', { token: selectedToken })}
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Calculator, TrendingUp, Users, BookOpen, Award } from 'lucide-react';
|
||||
|
||||
const TrustScoreCalculator: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [stakedAmount, setStakedAmount] = useState(100);
|
||||
const [stakingMonths, setStakingMonths] = useState(6);
|
||||
const [referralCount, setReferralCount] = useState(5);
|
||||
@@ -56,10 +58,10 @@ const TrustScoreCalculator: React.FC = () => {
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
|
||||
Trust Score Calculator
|
||||
{t('trustCalc.title')}
|
||||
</h2>
|
||||
<p className="text-gray-400 text-lg max-w-2xl mx-auto">
|
||||
Simulate your trust score based on staking, referrals, education, and roles
|
||||
{t('trustCalc.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -70,12 +72,12 @@ const TrustScoreCalculator: React.FC = () => {
|
||||
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<TrendingUp className="w-5 h-5 text-purple-400 mr-3" />
|
||||
<h3 className="text-lg font-semibold text-white">Staking Amount</h3>
|
||||
<h3 className="text-lg font-semibold text-white">{t('trustCalc.stakingTitle')}</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-gray-400 text-sm">Staked Amount (HEZ)</label>
|
||||
<label className="text-gray-400 text-sm">{t('trustCalc.stakedAmount')}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
@@ -87,12 +89,12 @@ const TrustScoreCalculator: React.FC = () => {
|
||||
/>
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
<span className="text-cyan-400">{stakedAmount} HEZ</span>
|
||||
<span className="text-purple-400">Score: {getAmountScore(stakedAmount)}</span>
|
||||
<span className="text-purple-400">{t('trustCalc.score', { score: getAmountScore(stakedAmount) })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-gray-400 text-sm">Staking Duration (Months)</label>
|
||||
<label className="text-gray-400 text-sm">{t('trustCalc.stakingDuration')}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
@@ -102,8 +104,8 @@ const TrustScoreCalculator: React.FC = () => {
|
||||
className="w-full mt-2"
|
||||
/>
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
<span className="text-cyan-400">{stakingMonths} months</span>
|
||||
<span className="text-purple-400">×{getStakingMultiplier(stakingMonths).toFixed(1)} multiplier</span>
|
||||
<span className="text-cyan-400">{t('trustCalc.months', { count: stakingMonths })}</span>
|
||||
<span className="text-purple-400">{t('trustCalc.multiplier', { value: getStakingMultiplier(stakingMonths).toFixed(1) })}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,11 +115,11 @@ const TrustScoreCalculator: React.FC = () => {
|
||||
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<Users className="w-5 h-5 text-cyan-400 mr-3" />
|
||||
<h3 className="text-lg font-semibold text-white">Referral Score</h3>
|
||||
<h3 className="text-lg font-semibold text-white">{t('trustCalc.referralTitle')}</h3>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-gray-400 text-sm">Number of Referrals</label>
|
||||
<label className="text-gray-400 text-sm">{t('trustCalc.referralCount')}</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
@@ -127,7 +129,7 @@ const TrustScoreCalculator: React.FC = () => {
|
||||
className="w-full mt-2 px-4 py-2 bg-gray-800 text-white rounded-lg border border-gray-700 focus:border-cyan-500 focus:outline-none"
|
||||
/>
|
||||
<div className="mt-2 text-sm text-cyan-400">
|
||||
Score: {getReferralScore(referralCount)} points
|
||||
{t('trustCalc.scorePoints', { score: getReferralScore(referralCount) })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -137,7 +139,7 @@ const TrustScoreCalculator: React.FC = () => {
|
||||
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<BookOpen className="w-5 h-5 text-teal-400 mr-3" />
|
||||
<h3 className="text-sm font-semibold text-white">Perwerde Score</h3>
|
||||
<h3 className="text-sm font-semibold text-white">{t('trustCalc.perwerdeTitle')}</h3>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
@@ -153,7 +155,7 @@ const TrustScoreCalculator: React.FC = () => {
|
||||
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<Award className="w-5 h-5 text-purple-400 mr-3" />
|
||||
<h3 className="text-sm font-semibold text-white">Tiki Score</h3>
|
||||
<h3 className="text-sm font-semibold text-white">{t('trustCalc.tikiTitle')}</h3>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
@@ -173,18 +175,18 @@ const TrustScoreCalculator: React.FC = () => {
|
||||
{/* Final Score */}
|
||||
<div className="bg-gradient-to-br from-purple-900/30 to-cyan-900/30 backdrop-blur-sm rounded-xl border border-purple-500/50 p-8 text-center">
|
||||
<Calculator className="w-12 h-12 text-cyan-400 mx-auto mb-4" />
|
||||
<h3 className="text-2xl font-semibold text-white mb-2">Final Trust Score</h3>
|
||||
<h3 className="text-2xl font-semibold text-white mb-2">{t('trustCalc.resultTitle')}</h3>
|
||||
<div className="text-6xl font-bold bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
|
||||
{finalScore}
|
||||
</div>
|
||||
<div className="mt-4 text-gray-400">
|
||||
Out of theoretical maximum
|
||||
{t('trustCalc.resultHint')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Formula Breakdown */}
|
||||
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Formula Breakdown</h3>
|
||||
<h3 className="text-lg font-semibold text-white mb-4">{t('trustCalc.formulaTitle')}</h3>
|
||||
|
||||
<div className="bg-gray-950/50 rounded-lg p-4 font-mono text-sm">
|
||||
<div className="text-purple-400 mb-2">
|
||||
@@ -209,19 +211,19 @@ const TrustScoreCalculator: React.FC = () => {
|
||||
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Staking Component:</span>
|
||||
<span className="text-gray-400">{t('trustCalc.formulaStaking')}:</span>
|
||||
<span className="text-purple-400">{Math.min(Math.round(getAmountScore(stakedAmount) * getStakingMultiplier(stakingMonths)), 100)} × 100</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Referral Component:</span>
|
||||
<span className="text-gray-400">{t('trustCalc.formulaReferral')}:</span>
|
||||
<span className="text-cyan-400">{getReferralScore(referralCount)} × 300</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Perwerde Component:</span>
|
||||
<span className="text-gray-400">{t('trustCalc.formulaPerwerde')}:</span>
|
||||
<span className="text-teal-400">{perwerdeScore} × 300</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Tiki Component:</span>
|
||||
<span className="text-gray-400">{t('trustCalc.formulaTiki')}:</span>
|
||||
<span className="text-purple-400">{tikiScore} × 300</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -229,16 +231,16 @@ const TrustScoreCalculator: React.FC = () => {
|
||||
|
||||
{/* Score Impact */}
|
||||
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Score Impact</h3>
|
||||
<h3 className="text-lg font-semibold text-white mb-4">{t('trustCalc.impactTitle')}</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 bg-gray-800/50 rounded-lg">
|
||||
<span className="text-gray-400">Monthly Rewards Eligibility</span>
|
||||
<span className="text-gray-400">{t('trustCalc.rewards')}</span>
|
||||
<span className={`px-3 py-1 rounded-full text-sm ${finalScore > 100 ? 'bg-green-900/30 text-green-400' : 'bg-red-900/30 text-red-400'}`}>
|
||||
{finalScore > 100 ? 'Eligible' : 'Not Eligible'}
|
||||
{finalScore > 100 ? t('trustCalc.eligible') : t('trustCalc.notEligible')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 bg-gray-800/50 rounded-lg">
|
||||
<span className="text-gray-400">Governance Voting Weight</span>
|
||||
<span className="text-gray-400">{t('trustCalc.votingWeight')}</span>
|
||||
<span className="text-cyan-400 font-semibold">{Math.min(Math.floor(finalScore / 100), 10)}x</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { X, ArrowDown, ArrowUp, AlertCircle, Info, Clock, CheckCircle2 } from 'lucide-react';
|
||||
import { web3FromAddress } from '@pezkuwi/extension-dapp';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
@@ -28,6 +29,7 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
|
||||
onClose,
|
||||
specificAddresses = {},
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { api, selectedAccount, isApiReady } = usePezkuwi();
|
||||
const { refreshBalances } = useWallet();
|
||||
|
||||
@@ -59,7 +61,7 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
|
||||
// Handle deposit (user requests deposit)
|
||||
const handleDeposit = async () => {
|
||||
if (!depositAmount || parseFloat(depositAmount) <= 0) {
|
||||
setError('Please enter a valid amount');
|
||||
setError(t('bridge.invalidAmount'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,7 +77,7 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
|
||||
|
||||
// For now, just show instructions
|
||||
setSuccess(
|
||||
`Deposit request for ${depositAmount} USDT created. Please follow the instructions to complete the deposit.`
|
||||
t('bridge.depositSuccess', { amount: depositAmount })
|
||||
);
|
||||
setDepositAmount('');
|
||||
} catch (err) {
|
||||
@@ -93,17 +95,17 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
|
||||
const amount = parseFloat(withdrawAmount);
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
setError('Please enter a valid amount');
|
||||
setError(t('bridge.invalidAmount'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (amount > wusdtBalance) {
|
||||
setError('Insufficient wUSDT balance');
|
||||
setError(t('bridge.insufficientBalance'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!withdrawAddress) {
|
||||
setError('Please enter withdrawal address');
|
||||
setError(t('bridge.noAddress'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -122,7 +124,7 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
|
||||
if (status.isFinalized) {
|
||||
const delay = calculateWithdrawalDelay(amount);
|
||||
setSuccess(
|
||||
`Withdrawal request submitted! wUSDT burned. USDT will be sent to ${withdrawAddress} after ${formatDelay(delay)}.`
|
||||
t('bridge.withdrawSuccess', { address: withdrawAddress, delay: formatDelay(delay) })
|
||||
);
|
||||
setWithdrawAmount('');
|
||||
setWithdrawAddress('');
|
||||
@@ -148,8 +150,8 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">USDT Bridge</h2>
|
||||
<p className="text-sm text-gray-400 mt-1">Deposit or withdraw USDT</p>
|
||||
<h2 className="text-2xl font-bold text-white">{t('bridge.title')}</h2>
|
||||
<p className="text-sm text-gray-400 mt-1">{t('bridge.subtitle')}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -161,11 +163,11 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
|
||||
|
||||
{/* Balance Display */}
|
||||
<div className="mb-6 p-4 bg-gray-800/50 rounded-lg">
|
||||
<p className="text-sm text-gray-400 mb-1">Your wUSDT Balance</p>
|
||||
<p className="text-sm text-gray-400 mb-1">{t('bridge.balance')}</p>
|
||||
<p className="text-3xl font-bold text-white">{formatWUSDT(wusdtBalance)}</p>
|
||||
{isMultisigMemberState && (
|
||||
<Badge variant="outline" className="mt-2">
|
||||
Multisig Member
|
||||
{t('bridge.multisigMember')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -188,8 +190,8 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
|
||||
{/* Tabs */}
|
||||
<Tabs defaultValue="deposit" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2 bg-gray-800">
|
||||
<TabsTrigger value="deposit">Deposit</TabsTrigger>
|
||||
<TabsTrigger value="withdraw">Withdraw</TabsTrigger>
|
||||
<TabsTrigger value="deposit">{t('bridge.tabDeposit')}</TabsTrigger>
|
||||
<TabsTrigger value="withdraw">{t('bridge.tabWithdraw')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Deposit Tab */}
|
||||
@@ -197,25 +199,25 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
|
||||
<Alert className="bg-blue-900/20 border-blue-500">
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription className="text-sm">
|
||||
<p className="font-semibold mb-2">How to Deposit:</p>
|
||||
<p className="font-semibold mb-2">{t('bridge.depositHow')}</p>
|
||||
<ol className="list-decimal list-inside space-y-1">
|
||||
<li>Transfer USDT to the treasury account (off-chain)</li>
|
||||
<li>Notary verifies and records your transaction</li>
|
||||
<li>Multisig (3/5) approves and mints wUSDT to your account</li>
|
||||
<li>Receive wUSDT in 2-5 minutes</li>
|
||||
<li>{t('bridge.depositStep1')}</li>
|
||||
<li>{t('bridge.depositStep2')}</li>
|
||||
<li>{t('bridge.depositStep3')}</li>
|
||||
<li>{t('bridge.depositStep4')}</li>
|
||||
</ol>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
USDT Amount
|
||||
{t('bridge.usdtAmount')}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={depositAmount}
|
||||
onChange={(e) => setDepositAmount(e.target.value)}
|
||||
placeholder="Amount"
|
||||
placeholder={t('bridge.amountPlaceholder')}
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500 placeholder:text-gray-500 placeholder:opacity-50"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
@@ -223,18 +225,18 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
|
||||
|
||||
<div className="p-4 bg-gray-800 rounded-lg space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">You will receive:</span>
|
||||
<span className="text-gray-400">{t('bridge.willReceive')}</span>
|
||||
<span className="text-white font-semibold">
|
||||
{depositAmount || '0.00'} wUSDT
|
||||
{depositAmount || '0.00'} {t('bridge.wusdt')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Exchange rate:</span>
|
||||
<span className="text-white">1:1</span>
|
||||
<span className="text-gray-400">{t('bridge.exchangeRate')}</span>
|
||||
<span className="text-white">{t('bridge.rate')}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Estimated time:</span>
|
||||
<span className="text-white">2-5 minutes</span>
|
||||
<span className="text-gray-400">{t('bridge.estimatedTime')}</span>
|
||||
<span className="text-white">{t('bridge.time')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -246,12 +248,12 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
Processing...
|
||||
{t('bridge.processing')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowDown className="h-5 w-5" />
|
||||
Request Deposit
|
||||
{t('bridge.requestDeposit')}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
@@ -262,25 +264,25 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
|
||||
<Alert className="bg-orange-900/20 border-orange-500">
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription className="text-sm">
|
||||
<p className="font-semibold mb-2">How to Withdraw:</p>
|
||||
<p className="font-semibold mb-2">{t('bridge.withdrawHow')}</p>
|
||||
<ol className="list-decimal list-inside space-y-1">
|
||||
<li>Burn your wUSDT on-chain</li>
|
||||
<li>Wait for security delay ({withdrawalDelay > 0 && formatDelay(withdrawalDelay)})</li>
|
||||
<li>Multisig (3/5) approves and sends USDT</li>
|
||||
<li>Receive USDT to your specified address</li>
|
||||
<li>{t('bridge.withdrawStep1')}</li>
|
||||
<li>{t('bridge.withdrawStep2', { delay: withdrawalDelay > 0 ? formatDelay(withdrawalDelay) : '' })}</li>
|
||||
<li>{t('bridge.withdrawStep3')}</li>
|
||||
<li>{t('bridge.withdrawStep4')}</li>
|
||||
</ol>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
wUSDT Amount
|
||||
{t('bridge.wusdtAmount')}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={withdrawAmount}
|
||||
onChange={(e) => setWithdrawAmount(e.target.value)}
|
||||
placeholder="Amount"
|
||||
placeholder={t('bridge.amountPlaceholder')}
|
||||
max={wusdtBalance}
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500 placeholder:text-gray-500 placeholder:opacity-50"
|
||||
disabled={isLoading}
|
||||
@@ -289,19 +291,19 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
|
||||
onClick={() => setWithdrawAmount(wusdtBalance.toString())}
|
||||
className="text-xs text-blue-400 hover:text-blue-300 mt-1"
|
||||
>
|
||||
Max: {formatWUSDT(wusdtBalance)}
|
||||
{t('bridge.max')}: {formatWUSDT(wusdtBalance)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Withdrawal Address (Bank Account or Crypto Address)
|
||||
{t('bridge.withdrawAddress')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={withdrawAddress}
|
||||
onChange={(e) => setWithdrawAddress(e.target.value)}
|
||||
placeholder="Bank account or crypto address"
|
||||
placeholder={t('bridge.addressPlaceholder')}
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500 placeholder:text-gray-500 placeholder:opacity-50"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
@@ -310,17 +312,17 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
|
||||
{withdrawAmount && parseFloat(withdrawAmount) > 0 && (
|
||||
<div className="p-4 bg-gray-800 rounded-lg space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">You will receive:</span>
|
||||
<span className="text-gray-400">{t('bridge.willReceive')}</span>
|
||||
<span className="text-white font-semibold">{withdrawAmount} USDT</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Withdrawal tier:</span>
|
||||
<span className="text-gray-400">{t('bridge.withdrawTier')}</span>
|
||||
<Badge variant={withdrawalTier === 'Large' ? 'destructive' : 'outline'}>
|
||||
{withdrawalTier}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-400">Security delay:</span>
|
||||
<span className="text-gray-400">{t('bridge.securityDelay')}</span>
|
||||
<span className="text-white flex items-center gap-1">
|
||||
<Clock className="h-4 w-4" />
|
||||
{formatDelay(withdrawalDelay)}
|
||||
@@ -337,12 +339,12 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
Processing...
|
||||
{t('bridge.processing')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowUp className="h-5 w-5" />
|
||||
Withdraw USDT
|
||||
{t('bridge.withdrawBtn')}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -55,6 +56,7 @@ interface XCMTeleportModalProps {
|
||||
export const XCMTeleportModal: React.FC<XCMTeleportModalProps> = ({ isOpen, onClose }) => {
|
||||
const { api, assetHubApi, peopleApi, isApiReady, isAssetHubReady, isPeopleReady, selectedAccount } = usePezkuwi();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [targetChain, setTargetChain] = useState<TargetChain>('asset-hub');
|
||||
const [amount, setAmount] = useState('');
|
||||
@@ -66,6 +68,8 @@ export const XCMTeleportModal: React.FC<XCMTeleportModalProps> = ({ isOpen, onCl
|
||||
const [peopleBalance, setPeopleBalance] = useState<string>('0');
|
||||
|
||||
const selectedChain = TARGET_CHAINS.find(c => c.id === targetChain)!;
|
||||
const chainName = targetChain === 'asset-hub' ? t('xcm.assetHubName') : t('xcm.peopleName');
|
||||
const chainDesc = targetChain === 'asset-hub' ? t('xcm.assetHubDesc') : t('xcm.peopleDesc');
|
||||
|
||||
// Fetch balances
|
||||
useEffect(() => {
|
||||
@@ -121,8 +125,8 @@ export const XCMTeleportModal: React.FC<XCMTeleportModalProps> = ({ isOpen, onCl
|
||||
const handleTeleport = async () => {
|
||||
if (!api || !isApiReady || !selectedAccount) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Wallet not connected",
|
||||
title: t('common.error'),
|
||||
description: t('xcm.walletNotConnected'),
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
@@ -130,8 +134,8 @@ export const XCMTeleportModal: React.FC<XCMTeleportModalProps> = ({ isOpen, onCl
|
||||
|
||||
if (!amount || parseFloat(amount) <= 0) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Please enter a valid amount",
|
||||
title: t('common.error'),
|
||||
description: t('xcm.invalidAmount'),
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
@@ -142,8 +146,8 @@ export const XCMTeleportModal: React.FC<XCMTeleportModalProps> = ({ isOpen, onCl
|
||||
|
||||
if (sendAmount > currentBalance) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Insufficient balance on Relay Chain",
|
||||
title: t('common.error'),
|
||||
description: t('xcm.insufficientBalance'),
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
@@ -236,7 +240,7 @@ export const XCMTeleportModal: React.FC<XCMTeleportModalProps> = ({ isOpen, onCl
|
||||
|
||||
if (status.isFinalized) {
|
||||
if (dispatchError) {
|
||||
let errorMessage = 'Teleport failed';
|
||||
let errorMessage = t('xcm.failed');
|
||||
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
||||
@@ -245,15 +249,15 @@ export const XCMTeleportModal: React.FC<XCMTeleportModalProps> = ({ isOpen, onCl
|
||||
|
||||
setTxStatus('error');
|
||||
toast({
|
||||
title: "Teleport Failed",
|
||||
title: t('xcm.failed'),
|
||||
description: errorMessage,
|
||||
variant: "destructive",
|
||||
});
|
||||
} else {
|
||||
setTxStatus('success');
|
||||
toast({
|
||||
title: "Teleport Successful!",
|
||||
description: `${amount} HEZ teleported to ${selectedChain.name}!`,
|
||||
title: t('xcm.success'),
|
||||
description: t('xcm.sentTo', { amount, chain: chainName }),
|
||||
});
|
||||
|
||||
// Reset after success
|
||||
@@ -276,8 +280,8 @@ export const XCMTeleportModal: React.FC<XCMTeleportModalProps> = ({ isOpen, onCl
|
||||
setIsTransferring(false);
|
||||
|
||||
toast({
|
||||
title: "Teleport Failed",
|
||||
description: error instanceof Error ? error.message : "An error occurred",
|
||||
title: t('xcm.failed'),
|
||||
description: error instanceof Error ? error.message : t('xcm.errorOccurred'),
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
@@ -306,21 +310,21 @@ export const XCMTeleportModal: React.FC<XCMTeleportModalProps> = ({ isOpen, onCl
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white flex items-center gap-2">
|
||||
<img src="/tokens/HEZ.png" alt="HEZ" className="w-6 h-6 rounded-full" />
|
||||
Teleport HEZ to Teyrchain
|
||||
{t('xcm.title')}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Transfer HEZ from Pezkuwi (Relay Chain) to a teyrchain for transaction fees
|
||||
{t('xcm.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{txStatus === 'success' ? (
|
||||
<div className="py-8 text-center">
|
||||
<CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold text-white mb-2">Teleport Successful!</h3>
|
||||
<p className="text-gray-400 mb-4">{amount} HEZ sent to {selectedChain.name}</p>
|
||||
<h3 className="text-xl font-semibold text-white mb-2">{t('xcm.success')}</h3>
|
||||
<p className="text-gray-400 mb-4">{t('xcm.sentTo', { amount, chain: chainName })}</p>
|
||||
{txHash && (
|
||||
<div className="bg-gray-800/50 rounded-lg p-3">
|
||||
<div className="text-xs text-gray-400 mb-1">Transaction Hash</div>
|
||||
<div className="text-xs text-gray-400 mb-1">{t('xcm.txHash')}</div>
|
||||
<div className="text-white font-mono text-xs break-all">{txHash}</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -328,20 +332,20 @@ export const XCMTeleportModal: React.FC<XCMTeleportModalProps> = ({ isOpen, onCl
|
||||
) : txStatus === 'error' ? (
|
||||
<div className="py-8 text-center">
|
||||
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold text-white mb-2">Teleport Failed</h3>
|
||||
<p className="text-gray-400">Please try again</p>
|
||||
<h3 className="text-xl font-semibold text-white mb-2">{t('xcm.failed')}</h3>
|
||||
<p className="text-gray-400">{t('xcm.pleaseTryAgain')}</p>
|
||||
<Button
|
||||
onClick={() => setTxStatus('idle')}
|
||||
className="mt-4 bg-gray-800 hover:bg-gray-700"
|
||||
>
|
||||
Try Again
|
||||
{t('xcm.tryAgain')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{/* Target Chain Selection */}
|
||||
<div>
|
||||
<Label className="text-white">Target Teyrchain</Label>
|
||||
<Label className="text-white">{t('xcm.targetTeyrchain')}</Label>
|
||||
<Select value={targetChain} onValueChange={(v) => setTargetChain(v as TargetChain)}>
|
||||
<SelectTrigger className="bg-gray-800 border-gray-700 text-white mt-2">
|
||||
<SelectValue />
|
||||
@@ -355,8 +359,8 @@ export const XCMTeleportModal: React.FC<XCMTeleportModalProps> = ({ isOpen, onCl
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full bg-${chain.color}-500`}></div>
|
||||
<span>{chain.name}</span>
|
||||
<span className="text-gray-400 text-xs">- {chain.description}</span>
|
||||
<span>{chain.id === 'asset-hub' ? t('xcm.assetHubName') : t('xcm.peopleName')}</span>
|
||||
<span className="text-gray-400 text-xs">- {chain.id === 'asset-hub' ? t('xcm.assetHubDesc') : t('xcm.peopleDesc')}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
@@ -369,7 +373,7 @@ export const XCMTeleportModal: React.FC<XCMTeleportModalProps> = ({ isOpen, onCl
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500"></div>
|
||||
<span className="text-sm text-gray-400">Pezkuwi (Relay Chain)</span>
|
||||
<span className="text-sm text-gray-400">{t('xcm.relayChain')}</span>
|
||||
</div>
|
||||
<span className="text-white font-mono">{relayBalance} HEZ</span>
|
||||
</div>
|
||||
@@ -381,7 +385,7 @@ export const XCMTeleportModal: React.FC<XCMTeleportModalProps> = ({ isOpen, onCl
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full bg-${selectedChain.color}-500`}></div>
|
||||
<span className="text-sm text-gray-400">{selectedChain.name}</span>
|
||||
<span className="text-sm text-gray-400">{chainName}</span>
|
||||
</div>
|
||||
<span className="text-white font-mono">{getTargetBalance()} HEZ</span>
|
||||
</div>
|
||||
@@ -391,13 +395,13 @@ export const XCMTeleportModal: React.FC<XCMTeleportModalProps> = ({ isOpen, onCl
|
||||
<div className={`bg-${selectedChain.color}-500/10 border border-${selectedChain.color}-500/30 rounded-lg p-3 flex gap-2`}>
|
||||
<Info className={`w-5 h-5 text-${selectedChain.color}-400 flex-shrink-0 mt-0.5`} />
|
||||
<p className={`text-${selectedChain.color}-400 text-sm`}>
|
||||
{selectedChain.description}. Teleport at least 0.1 HEZ for fees.
|
||||
{chainDesc}. {t('xcm.teleportMinHez')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Amount Input */}
|
||||
<div>
|
||||
<Label htmlFor="amount" className="text-white">Amount (HEZ)</Label>
|
||||
<Label htmlFor="amount" className="text-white">{t('xcm.amountHez')}</Label>
|
||||
<Input
|
||||
id="amount"
|
||||
type="number"
|
||||
@@ -428,7 +432,7 @@ export const XCMTeleportModal: React.FC<XCMTeleportModalProps> = ({ isOpen, onCl
|
||||
{txStatus === 'signing' && (
|
||||
<div className="bg-yellow-500/10 border border-yellow-500/30 rounded-lg p-3">
|
||||
<p className="text-yellow-400 text-sm">
|
||||
Please sign the transaction in your wallet extension
|
||||
{t('xcm.signTransaction')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -437,7 +441,7 @@ export const XCMTeleportModal: React.FC<XCMTeleportModalProps> = ({ isOpen, onCl
|
||||
<div className="bg-blue-500/10 border border-blue-500/30 rounded-lg p-3">
|
||||
<p className="text-blue-400 text-sm flex items-center gap-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
XCM Teleport in progress... This may take a moment.
|
||||
{t('xcm.inProgress')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -451,11 +455,11 @@ export const XCMTeleportModal: React.FC<XCMTeleportModalProps> = ({ isOpen, onCl
|
||||
{isTransferring ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
{txStatus === 'signing' ? 'Waiting for signature...' : 'Processing XCM...'}
|
||||
{txStatus === 'signing' ? t('xcm.waitingSignature') : t('xcm.processingXcm')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Teleport HEZ to {selectedChain.name}
|
||||
{t('xcm.teleportTo', { chain: chainName })}
|
||||
<ArrowDown className="w-4 h-4 ml-2" />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -9,6 +10,7 @@ import { COMMISSIONS } from '@/config/commissions';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
|
||||
export function CommissionSetupTab() {
|
||||
const { t } = useTranslation();
|
||||
const { api, isApiReady, selectedAccount } = usePezkuwi();
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -50,8 +52,8 @@ export function CommissionSetupTab() {
|
||||
const handleAddMember = async () => {
|
||||
if (!api || !selectedAccount) {
|
||||
toast({
|
||||
title: 'Wallet Not Connected',
|
||||
description: 'Please connect your admin wallet',
|
||||
title: t('commission.setup.walletNotConnected'),
|
||||
description: t('commission.setup.connectWallet'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -59,8 +61,8 @@ export function CommissionSetupTab() {
|
||||
|
||||
if (!newMemberAddress) {
|
||||
toast({
|
||||
title: 'No Addresses',
|
||||
description: 'Please enter at least one address',
|
||||
title: t('commission.setup.noAddresses'),
|
||||
description: t('commission.setup.enterAtLeastOne'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -79,8 +81,8 @@ export function CommissionSetupTab() {
|
||||
|
||||
if (newAddresses.length === 0) {
|
||||
toast({
|
||||
title: 'No Valid Addresses',
|
||||
description: 'Please enter at least one valid address',
|
||||
title: t('commission.setup.noValidAddresses'),
|
||||
description: t('commission.setup.enterValidAddress'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
setProcessing(false);
|
||||
@@ -96,8 +98,8 @@ export function CommissionSetupTab() {
|
||||
|
||||
if (newMembers.length === 0) {
|
||||
toast({
|
||||
title: 'Already Members',
|
||||
description: 'All addresses are already commission members',
|
||||
title: t('commission.setup.alreadyInitialized'),
|
||||
description: t('commission.setup.alreadyInitialized'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
setProcessing(false);
|
||||
@@ -125,21 +127,20 @@ export function CommissionSetupTab() {
|
||||
({ status, dispatchError }) => {
|
||||
if (status.isInBlock || status.isFinalized) {
|
||||
if (dispatchError) {
|
||||
let errorMessage = 'Failed to add member';
|
||||
let errorMessage = t('commission.setup.addMemberFailed');
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
||||
errorMessage = `${decoded.section}.${decoded.name}`;
|
||||
}
|
||||
toast({
|
||||
title: 'Error',
|
||||
title: t('commission.setup.addMemberFailed'),
|
||||
description: errorMessage,
|
||||
variant: 'destructive',
|
||||
});
|
||||
reject(new Error(errorMessage));
|
||||
} else {
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: `${newMembers.length} member(s) added successfully!`,
|
||||
title: t('commission.setup.addMemberSuccess', { count: newMembers.length }),
|
||||
});
|
||||
setNewMemberAddress('');
|
||||
setTimeout(() => checkSetup(), 2000);
|
||||
@@ -152,8 +153,8 @@ export function CommissionSetupTab() {
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Error adding member:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error instanceof Error ? error.message : 'Failed to add member',
|
||||
title: t('commission.setup.addMemberFailed'),
|
||||
description: error instanceof Error ? error.message : t('commission.setup.addMemberFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
@@ -164,8 +165,8 @@ export function CommissionSetupTab() {
|
||||
const handleInitializeCommission = async () => {
|
||||
if (!api || !selectedAccount) {
|
||||
toast({
|
||||
title: 'Wallet Not Connected',
|
||||
description: 'Please connect your admin wallet',
|
||||
title: t('commission.setup.walletNotConnected'),
|
||||
description: t('commission.setup.connectWallet'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -209,7 +210,7 @@ export function CommissionSetupTab() {
|
||||
|
||||
if (import.meta.env.DEV) console.error('Setup error:', errorMessage);
|
||||
toast({
|
||||
title: 'Setup Failed',
|
||||
title: t('commission.setup.setupFailed'),
|
||||
description: errorMessage,
|
||||
variant: 'destructive',
|
||||
});
|
||||
@@ -225,8 +226,7 @@ export function CommissionSetupTab() {
|
||||
if (sudidEvent) {
|
||||
if (import.meta.env.DEV) console.log('✅ KYC Commission initialized');
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'KYC Commission initialized successfully!',
|
||||
title: t('commission.setup.kycInitialized'),
|
||||
});
|
||||
resolve();
|
||||
} else {
|
||||
@@ -238,8 +238,8 @@ export function CommissionSetupTab() {
|
||||
).catch((error) => {
|
||||
if (import.meta.env.DEV) console.error('Failed to sign and send:', error);
|
||||
toast({
|
||||
title: 'Transaction Error',
|
||||
description: error instanceof Error ? error.message : 'Failed to submit transaction',
|
||||
title: t('commission.setup.transactionError'),
|
||||
description: error instanceof Error ? error.message : t('commission.setup.failedToSubmit'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
reject(error);
|
||||
@@ -252,8 +252,8 @@ export function CommissionSetupTab() {
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Error initializing commission:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error instanceof Error ? error.message : 'Failed to initialize commission',
|
||||
title: t('commission.setup.setupFailed'),
|
||||
description: error instanceof Error ? error.message : t('commission.setup.failedToInitialize'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
@@ -267,7 +267,7 @@ export function CommissionSetupTab() {
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-cyan-500" />
|
||||
<span className="ml-3 text-gray-400">Connecting to blockchain...</span>
|
||||
<span className="ml-3 text-gray-400">{t('commission.setup.connecting')}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -281,7 +281,7 @@ export function CommissionSetupTab() {
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Please connect your admin wallet to manage commission setup.
|
||||
{t('commission.setup.connectWalletAlert')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
@@ -296,7 +296,7 @@ export function CommissionSetupTab() {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Shield className="w-5 h-5" />
|
||||
KYC Commission Setup
|
||||
{t('commission.setup.statusLabel')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
@@ -308,28 +308,28 @@ export function CommissionSetupTab() {
|
||||
<>
|
||||
<div className="flex items-center justify-between p-4 bg-gray-800/50 rounded-lg border border-gray-700">
|
||||
<div>
|
||||
<p className="font-medium">Commission Status</p>
|
||||
<p className="font-medium">{t('commission.setup.statusLabel')}</p>
|
||||
<p className="text-sm text-gray-400 mt-1">
|
||||
{setupComplete
|
||||
? 'Commission is initialized and ready'
|
||||
: 'Commission needs to be initialized'}
|
||||
? t('commission.setup.initialized')
|
||||
: t('commission.setup.notInitialized')}
|
||||
</p>
|
||||
</div>
|
||||
{setupComplete ? (
|
||||
<Badge className="bg-green-600">
|
||||
<CheckCircle className="w-3 h-3 mr-1" />
|
||||
Ready
|
||||
{t('commission.setup.ready')}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary">
|
||||
<AlertTriangle className="w-3 h-3 mr-1" />
|
||||
Not Initialized
|
||||
{t('commission.setup.notInitializedBadge')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-gray-400">Proxy Account</p>
|
||||
<p className="text-sm font-medium text-gray-400">{t('commission.setup.proxyAccount')}</p>
|
||||
<div className="p-3 bg-gray-800/50 rounded border border-gray-700">
|
||||
<p className="font-mono text-xs">{COMMISSIONS.KYC.proxyAccount}</p>
|
||||
</div>
|
||||
@@ -337,11 +337,11 @@ export function CommissionSetupTab() {
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-gray-400">
|
||||
Commission Members ({commissionMembers.length})
|
||||
{t('commission.setup.membersLabel')} ({commissionMembers.length})
|
||||
</p>
|
||||
{commissionMembers.length === 0 ? (
|
||||
<div className="p-4 bg-gray-800/50 rounded border border-gray-700 text-center text-gray-500">
|
||||
No members yet
|
||||
{t('commission.setup.noMembers')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
@@ -352,7 +352,7 @@ export function CommissionSetupTab() {
|
||||
>
|
||||
<p className="font-mono text-xs">{member}</p>
|
||||
{member === COMMISSIONS.KYC.proxyAccount && (
|
||||
<Badge className="mt-2 bg-cyan-600 text-xs">KYC Proxy</Badge>
|
||||
<Badge className="mt-2 bg-cyan-600 text-xs">{t('commission.setup.kycProxy')}</Badge>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
@@ -364,34 +364,33 @@ export function CommissionSetupTab() {
|
||||
<Alert className="bg-yellow-500/10 border-yellow-500/30">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<strong>Required:</strong> Initialize the commission before members can join.
|
||||
This requires sudo privileges.
|
||||
{t('commission.setup.initRequired')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{setupComplete && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm font-medium text-gray-400">Add Members</p>
|
||||
<p className="text-sm font-medium text-gray-400">{t('commission.setup.addMembersTitle')}</p>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
// Get wallet addresses from Pezkuwi.js extension
|
||||
// For now, show instruction
|
||||
toast({
|
||||
title: 'Get Addresses',
|
||||
description: 'Copy addresses from Pezkuwi.js and paste below',
|
||||
title: t('commission.setup.getAddresses'),
|
||||
description: t('commission.setup.getAddressesToast'),
|
||||
});
|
||||
}}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
How to get addresses
|
||||
{t('commission.setup.howToGetAddresses')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<textarea
|
||||
placeholder="Member addresses, one per line"
|
||||
placeholder={t('commission.setup.addressPlaceholder')}
|
||||
value={newMemberAddress}
|
||||
onChange={(e) => setNewMemberAddress(e.target.value)}
|
||||
className="flex-1 font-mono text-sm p-3 bg-gray-800 border border-gray-700 rounded min-h-[120px] placeholder:text-gray-500 placeholder:opacity-50"
|
||||
@@ -406,7 +405,7 @@ export function CommissionSetupTab() {
|
||||
) : (
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{processing ? 'Adding Members...' : 'Add Members'}
|
||||
{processing ? t('commission.setup.addingMembers') : t('commission.setup.addMembersBtn')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -421,17 +420,17 @@ export function CommissionSetupTab() {
|
||||
{processing ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Initializing...
|
||||
{t('commission.setup.initializing')}
|
||||
</>
|
||||
) : setupComplete ? (
|
||||
<>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
Already Initialized
|
||||
{t('commission.setup.alreadyInitialized')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Initialize Commission
|
||||
{t('commission.setup.initializeBtn')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -441,7 +440,7 @@ export function CommissionSetupTab() {
|
||||
variant="outline"
|
||||
disabled={loading}
|
||||
>
|
||||
Refresh
|
||||
{t('commission.setup.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
@@ -452,22 +451,13 @@ export function CommissionSetupTab() {
|
||||
{/* Instructions */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Setup Instructions</CardTitle>
|
||||
<CardTitle>{t('commission.setup.instructionsTitle')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ol className="list-decimal list-inside space-y-2 text-sm text-gray-400">
|
||||
<li>
|
||||
<strong className="text-white">Initialize Commission</strong> - Add proxy to
|
||||
DynamicCommissionCollective (requires sudo)
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-white">Join Commission</strong> - Members add proxy rights
|
||||
via Commission Voting tab
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-white">Start Voting</strong> - Create proposals and vote on
|
||||
KYC applications
|
||||
</li>
|
||||
<li>{t('commission.setup.step1')}</li>
|
||||
<li>{t('commission.setup.step2')}</li>
|
||||
<li>{t('commission.setup.step3')}</li>
|
||||
</ol>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -26,6 +27,7 @@ interface Proposal {
|
||||
}
|
||||
|
||||
export function CommissionVotingTab() {
|
||||
const { t } = useTranslation();
|
||||
const { api, isApiReady, selectedAccount } = usePezkuwi();
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -120,8 +122,7 @@ export function CommissionVotingTab() {
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Error loading proposals:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to load proposals',
|
||||
title: t('commission.voting.loadFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
@@ -132,8 +133,8 @@ export function CommissionVotingTab() {
|
||||
const handleVote = async (proposal: Proposal, approve: boolean) => {
|
||||
if (!api || !selectedAccount) {
|
||||
toast({
|
||||
title: 'Wallet Not Connected',
|
||||
description: 'Please connect your wallet first',
|
||||
title: t('commission.voting.walletNotConnected'),
|
||||
description: t('commission.voting.connectFirst'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -141,8 +142,7 @@ export function CommissionVotingTab() {
|
||||
|
||||
if (!isCommissionMember) {
|
||||
toast({
|
||||
title: 'Not a Commission Member',
|
||||
description: 'You are not a member of the KYC Approval Commission',
|
||||
title: t('commission.voting.notMemberTitle'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -182,7 +182,7 @@ export function CommissionVotingTab() {
|
||||
|
||||
if (import.meta.env.DEV) console.error('Vote error:', errorMessage);
|
||||
toast({
|
||||
title: 'Vote Failed',
|
||||
title: t('commission.voting.voteFailed'),
|
||||
description: errorMessage,
|
||||
variant: 'destructive',
|
||||
});
|
||||
@@ -203,14 +203,14 @@ export function CommissionVotingTab() {
|
||||
if (executedEvent) {
|
||||
if (import.meta.env.DEV) console.log('✅ Proposal executed (threshold reached)');
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Proposal passed and executed! KYC approved.',
|
||||
title: t('commission.voting.proposalPassed'),
|
||||
description: t('commission.voting.kycApproved'),
|
||||
});
|
||||
} else if (votedEvent) {
|
||||
if (import.meta.env.DEV) console.log('✅ Vote recorded');
|
||||
toast({
|
||||
title: 'Vote Recorded',
|
||||
description: `Your ${approve ? 'AYE' : 'NAY'} vote has been recorded`,
|
||||
title: t('commission.voting.voteRecorded'),
|
||||
description: approve ? t('commission.voting.ayeRecorded') : t('commission.voting.nayRecorded'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -220,8 +220,8 @@ export function CommissionVotingTab() {
|
||||
).catch((error) => {
|
||||
if (import.meta.env.DEV) console.error('Failed to sign and send:', error);
|
||||
toast({
|
||||
title: 'Transaction Error',
|
||||
description: error instanceof Error ? error.message : 'Failed to submit transaction',
|
||||
title: t('commission.voting.submitFailed'),
|
||||
description: error instanceof Error ? error.message : t('commission.voting.submitFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
reject(error);
|
||||
@@ -236,8 +236,8 @@ export function CommissionVotingTab() {
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Error voting:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error instanceof Error ? error.message : 'Failed to vote',
|
||||
title: t('commission.voting.voteFailed'),
|
||||
description: error instanceof Error ? error.message : t('commission.voting.voteFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
@@ -248,8 +248,8 @@ export function CommissionVotingTab() {
|
||||
const handleExecute = async (proposal: Proposal) => {
|
||||
if (!api || !selectedAccount) {
|
||||
toast({
|
||||
title: 'Wallet Not Connected',
|
||||
description: 'Please connect your wallet first',
|
||||
title: t('commission.voting.walletNotConnected'),
|
||||
description: t('commission.voting.connectFirst'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -297,7 +297,7 @@ export function CommissionVotingTab() {
|
||||
|
||||
if (import.meta.env.DEV) console.error('Execute error:', errorMessage);
|
||||
toast({
|
||||
title: 'Execute Failed',
|
||||
title: t('commission.voting.executeFailed'),
|
||||
description: errorMessage,
|
||||
variant: 'destructive',
|
||||
});
|
||||
@@ -324,21 +324,21 @@ export function CommissionVotingTab() {
|
||||
if (result && typeof result === 'object' && 'Err' in result) {
|
||||
if (import.meta.env.DEV) console.error('Execution failed:', result.Err);
|
||||
toast({
|
||||
title: 'Execution Failed',
|
||||
description: `Proposal closed but execution failed: ${JSON.stringify(result.Err)}`,
|
||||
title: t('commission.voting.executeFailed'),
|
||||
description: JSON.stringify(result.Err),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: 'Proposal Executed!',
|
||||
description: 'KYC approved and NFT minted successfully!',
|
||||
title: t('commission.voting.executeSuccess'),
|
||||
description: t('commission.voting.kycApproved'),
|
||||
});
|
||||
}
|
||||
} else if (closedEvent) {
|
||||
if (import.meta.env.DEV) console.log('Proposal closed');
|
||||
toast({
|
||||
title: 'Proposal Closed',
|
||||
description: 'Proposal has been closed',
|
||||
title: t('commission.voting.proposalClosed'),
|
||||
description: t('commission.voting.proposalClosedDesc'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -348,8 +348,8 @@ export function CommissionVotingTab() {
|
||||
).catch((error) => {
|
||||
if (import.meta.env.DEV) console.error('Failed to sign and send:', error);
|
||||
toast({
|
||||
title: 'Transaction Error',
|
||||
description: error instanceof Error ? error.message : 'Failed to submit transaction',
|
||||
title: t('commission.voting.submitFailed'),
|
||||
description: error instanceof Error ? error.message : t('commission.voting.submitFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
reject(error);
|
||||
@@ -363,8 +363,8 @@ export function CommissionVotingTab() {
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Error executing:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error instanceof Error ? error.message : 'Failed to execute proposal',
|
||||
title: t('commission.voting.executeFailed'),
|
||||
description: error instanceof Error ? error.message : t('commission.voting.executeFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
@@ -373,19 +373,19 @@ export function CommissionVotingTab() {
|
||||
};
|
||||
|
||||
const getProposalDescription = (call: Record<string, unknown>): string => {
|
||||
if (!call) return 'Unknown proposal';
|
||||
if (!call) return t('commission.voting.unknownProposal');
|
||||
|
||||
try {
|
||||
const callStr = JSON.stringify(call);
|
||||
if (callStr.includes('approveKyc')) {
|
||||
return 'KYC Approval';
|
||||
return t('commission.voting.kycApproval');
|
||||
}
|
||||
if (callStr.includes('rejectKyc')) {
|
||||
return 'KYC Rejection';
|
||||
return t('commission.voting.kycRejection');
|
||||
}
|
||||
return 'Commission Action';
|
||||
return t('commission.voting.commissionAction');
|
||||
} catch {
|
||||
return 'Unknown proposal';
|
||||
return t('commission.voting.unknownProposal');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -393,12 +393,12 @@ export function CommissionVotingTab() {
|
||||
const progress = (proposal.ayes.length / proposal.threshold) * 100;
|
||||
|
||||
if (proposal.ayes.length >= proposal.threshold) {
|
||||
return <Badge variant="default" className="bg-green-600">PASSED</Badge>;
|
||||
return <Badge variant="default" className="bg-green-600">{t('commission.voting.statusPassed')}</Badge>;
|
||||
}
|
||||
if (progress >= 50) {
|
||||
return <Badge variant="default" className="bg-yellow-600">VOTING ({progress.toFixed(0)}%)</Badge>;
|
||||
return <Badge variant="default" className="bg-yellow-600">{t('commission.voting.statusVoting', { progress: progress.toFixed(0) })}</Badge>;
|
||||
}
|
||||
return <Badge variant="secondary">VOTING ({progress.toFixed(0)}%)</Badge>;
|
||||
return <Badge variant="secondary">{t('commission.voting.statusVoting', { progress: progress.toFixed(0) })}</Badge>;
|
||||
};
|
||||
|
||||
if (!isApiReady) {
|
||||
@@ -407,7 +407,7 @@ export function CommissionVotingTab() {
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||||
<span>Connecting to blockchain...</span>
|
||||
<span>{t('commission.voting.connecting')}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -419,7 +419,7 @@ export function CommissionVotingTab() {
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p>Please connect your wallet to view commission proposals</p>
|
||||
<p>{t('commission.voting.noWallet')}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -461,20 +461,19 @@ export function CommissionVotingTab() {
|
||||
({ status, dispatchError }) => {
|
||||
if (status.isInBlock || status.isFinalized) {
|
||||
if (dispatchError) {
|
||||
let errorMessage = 'Failed to join commission';
|
||||
let errorMessage = t('commission.voting.joinFailed');
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
||||
errorMessage = `${decoded.section}.${decoded.name}`;
|
||||
}
|
||||
toast({
|
||||
title: 'Error',
|
||||
title: t('commission.voting.joinFailed'),
|
||||
description: errorMessage,
|
||||
variant: 'destructive',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'You have joined the KYC Commission!',
|
||||
title: t('commission.voting.joinSuccess'),
|
||||
});
|
||||
setTimeout(() => checkMembership(), 2000);
|
||||
}
|
||||
@@ -483,8 +482,8 @@ export function CommissionVotingTab() {
|
||||
);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error instanceof Error ? error.message : 'Failed to join commission',
|
||||
title: t('commission.voting.joinFailed'),
|
||||
description: error instanceof Error ? error.message : t('commission.voting.joinFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@@ -494,13 +493,13 @@ export function CommissionVotingTab() {
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<p className="text-muted-foreground mb-4">You are not a member of the KYC Approval Commission</p>
|
||||
<p className="text-sm text-muted-foreground mb-6">Only commission members can view and vote on proposals</p>
|
||||
<p className="text-muted-foreground mb-4">{t('commission.voting.notMember')}</p>
|
||||
<p className="text-sm text-muted-foreground mb-6">{t('commission.voting.onlyMembers')}</p>
|
||||
<Button
|
||||
onClick={handleJoinCommission}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
Join Commission
|
||||
{t('commission.voting.joinBtn')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -512,9 +511,9 @@ export function CommissionVotingTab() {
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold">Commission Proposals</h2>
|
||||
<h2 className="text-2xl font-bold">{t('commission.voting.title')}</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Active voting proposals for {COMMISSIONS.KYC.name}
|
||||
{t('commission.voting.subtitle', { name: COMMISSIONS.KYC.name })}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -523,7 +522,7 @@ export function CommissionVotingTab() {
|
||||
variant="outline"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
{t('commission.voting.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -532,7 +531,7 @@ export function CommissionVotingTab() {
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||||
<span>Loading proposals...</span>
|
||||
<span>{t('commission.voting.loading')}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -541,25 +540,25 @@ export function CommissionVotingTab() {
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<Clock className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||
<p>No active proposals</p>
|
||||
<p className="text-sm mt-2">Proposals will appear here when commission members create them</p>
|
||||
<p>{t('commission.voting.noProposals')}</p>
|
||||
<p className="text-sm mt-2">{t('commission.voting.noProposalsHelp')}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Active Proposals ({proposals.length})</CardTitle>
|
||||
<CardTitle>{t('commission.voting.activeProposals', { count: proposals.length })}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Proposal</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Votes</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
<TableHead>{t('commission.voting.tableProposal')}</TableHead>
|
||||
<TableHead>{t('commission.voting.tableType')}</TableHead>
|
||||
<TableHead>{t('commission.voting.tableStatus')}</TableHead>
|
||||
<TableHead>{t('commission.voting.tableVotes')}</TableHead>
|
||||
<TableHead className="text-right">{t('commission.voting.tableActions')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -602,7 +601,7 @@ export function CommissionVotingTab() {
|
||||
{voting === proposal.hash ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>Execute Proposal</>
|
||||
<>{t('commission.voting.execute')}</>
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
@@ -619,7 +618,7 @@ export function CommissionVotingTab() {
|
||||
) : (
|
||||
<>
|
||||
<ThumbsUp className="h-4 w-4 mr-1" />
|
||||
Aye
|
||||
{t('commission.voting.aye')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -634,7 +633,7 @@ export function CommissionVotingTab() {
|
||||
) : (
|
||||
<>
|
||||
<ThumbsDown className="h-4 w-4 mr-1" />
|
||||
Nay
|
||||
{t('commission.voting.nay')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -82,12 +83,12 @@ interface Evidence {
|
||||
review_notes?: string;
|
||||
}
|
||||
|
||||
// Decision options
|
||||
const DECISION_OPTIONS = [
|
||||
{ value: 'release_to_buyer', label: 'Release to Buyer', description: 'Release escrowed crypto to the buyer' },
|
||||
{ value: 'refund_to_seller', label: 'Refund to Seller', description: 'Return escrowed crypto to the seller' },
|
||||
{ value: 'split', label: 'Split 50/50', description: 'Split the escrowed amount between both parties' },
|
||||
{ value: 'escalate', label: 'Escalate', description: 'Escalate to higher authority for complex cases' }
|
||||
// Decision option values - labels are translated via t() in the component
|
||||
const DECISION_OPTION_KEYS = [
|
||||
{ value: 'release_to_buyer', labelKey: 'dispute.releaseToBuyer' },
|
||||
{ value: 'refund_to_seller', labelKey: 'dispute.refundToSeller' },
|
||||
{ value: 'split', labelKey: 'dispute.split' },
|
||||
{ value: 'escalate', labelKey: 'dispute.escalate' },
|
||||
];
|
||||
|
||||
// Status badge colors
|
||||
@@ -99,18 +100,19 @@ const STATUS_COLORS: Record<string, string> = {
|
||||
closed: 'bg-gray-500/20 text-gray-400 border-gray-500/30'
|
||||
};
|
||||
|
||||
// Category labels
|
||||
const CATEGORY_LABELS: Record<string, string> = {
|
||||
payment_not_received: 'Payment Not Received',
|
||||
wrong_amount: 'Wrong Amount',
|
||||
fake_payment_proof: 'Fake Payment Proof',
|
||||
seller_not_responding: 'Seller Not Responding',
|
||||
buyer_not_responding: 'Buyer Not Responding',
|
||||
fraudulent_behavior: 'Fraudulent Behavior',
|
||||
other: 'Other'
|
||||
// Category translation keys
|
||||
const CATEGORY_KEYS: Record<string, string> = {
|
||||
payment_not_received: 'dispute.categoryPaymentNotReceived',
|
||||
wrong_amount: 'dispute.categoryWrongAmount',
|
||||
fake_payment_proof: 'dispute.categoryFakePaymentProof',
|
||||
seller_not_responding: 'dispute.categorySellerNotResponding',
|
||||
buyer_not_responding: 'dispute.categoryBuyerNotResponding',
|
||||
fraudulent_behavior: 'dispute.categoryFraudulentBehavior',
|
||||
other: 'dispute.categoryOther'
|
||||
};
|
||||
|
||||
export function DisputeResolutionPanel() {
|
||||
const { t } = useTranslation();
|
||||
const [disputes, setDisputes] = useState<Dispute[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedDispute, setSelectedDispute] = useState<Dispute | null>(null);
|
||||
@@ -164,7 +166,7 @@ export function DisputeResolutionPanel() {
|
||||
setDisputes(disputesWithEvidence);
|
||||
} catch (error) {
|
||||
console.error('Error fetching disputes:', error);
|
||||
toast.error('Failed to load disputes');
|
||||
toast.error(t('dispute.loadFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -216,18 +218,18 @@ export function DisputeResolutionPanel() {
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
toast.success('Dispute claimed for review');
|
||||
toast.success(t('dispute.claimedToast'));
|
||||
fetchDisputes();
|
||||
} catch (error) {
|
||||
console.error('Error claiming dispute:', error);
|
||||
toast.error('Failed to claim dispute');
|
||||
toast.error(t('dispute.claimFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
// Resolve dispute
|
||||
const resolveDispute = async () => {
|
||||
if (!selectedDispute || !decision || !reasoning) {
|
||||
toast.error('Please select a decision and provide reasoning');
|
||||
toast.error(t('dispute.noDecision'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -265,7 +267,7 @@ export function DisputeResolutionPanel() {
|
||||
p_user_id: selectedDispute.trade.seller_id,
|
||||
p_type: 'dispute_resolved',
|
||||
p_title: 'Dispute Resolved',
|
||||
p_message: `The dispute has been resolved: ${DECISION_OPTIONS.find(o => o.value === decision)?.label}`,
|
||||
p_message: `The dispute has been resolved: ${t(DECISION_OPTION_KEYS.find(o => o.value === decision)?.labelKey || '')}`,
|
||||
p_reference_type: 'dispute',
|
||||
p_reference_id: selectedDispute.id
|
||||
}),
|
||||
@@ -273,7 +275,7 @@ export function DisputeResolutionPanel() {
|
||||
p_user_id: selectedDispute.trade.buyer_id,
|
||||
p_type: 'dispute_resolved',
|
||||
p_title: 'Dispute Resolved',
|
||||
p_message: `The dispute has been resolved: ${DECISION_OPTIONS.find(o => o.value === decision)?.label}`,
|
||||
p_message: `The dispute has been resolved: ${t(DECISION_OPTION_KEYS.find(o => o.value === decision)?.labelKey || '')}`,
|
||||
p_reference_type: 'dispute',
|
||||
p_reference_id: selectedDispute.id
|
||||
})
|
||||
@@ -281,7 +283,7 @@ export function DisputeResolutionPanel() {
|
||||
await Promise.all(notificationPromises);
|
||||
}
|
||||
|
||||
toast.success('Dispute resolved successfully');
|
||||
toast.success(t('dispute.resolvedToast'));
|
||||
setResolveOpen(false);
|
||||
setSelectedDispute(null);
|
||||
setDecision('');
|
||||
@@ -289,7 +291,7 @@ export function DisputeResolutionPanel() {
|
||||
fetchDisputes();
|
||||
} catch (error) {
|
||||
console.error('Error resolving dispute:', error);
|
||||
toast.error('Failed to resolve dispute');
|
||||
toast.error(t('dispute.resolveFailed'));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@@ -322,15 +324,15 @@ export function DisputeResolutionPanel() {
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold flex items-center gap-2">
|
||||
<Gavel className="h-6 w-6 text-kurdish-green" />
|
||||
Dispute Resolution
|
||||
{t('dispute.title')}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm mt-1">
|
||||
Review and resolve P2P trading disputes
|
||||
{t('dispute.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={fetchDisputes} disabled={loading}>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
{t('dispute.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -340,7 +342,7 @@ export function DisputeResolutionPanel() {
|
||||
<CardContent className="pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Open</p>
|
||||
<p className="text-sm text-muted-foreground">{t('dispute.statsOpen')}</p>
|
||||
<p className="text-2xl font-bold text-yellow-500">{stats.open}</p>
|
||||
</div>
|
||||
<AlertTriangle className="h-8 w-8 text-yellow-500/50" />
|
||||
@@ -352,7 +354,7 @@ export function DisputeResolutionPanel() {
|
||||
<CardContent className="pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Under Review</p>
|
||||
<p className="text-sm text-muted-foreground">{t('dispute.statsUnderReview')}</p>
|
||||
<p className="text-2xl font-bold text-blue-500">{stats.under_review}</p>
|
||||
</div>
|
||||
<Clock className="h-8 w-8 text-blue-500/50" />
|
||||
@@ -364,7 +366,7 @@ export function DisputeResolutionPanel() {
|
||||
<CardContent className="pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Resolved</p>
|
||||
<p className="text-sm text-muted-foreground">{t('dispute.statsResolved')}</p>
|
||||
<p className="text-2xl font-bold text-green-500">{stats.resolved}</p>
|
||||
</div>
|
||||
<CheckCircle2 className="h-8 w-8 text-green-500/50" />
|
||||
@@ -376,7 +378,7 @@ export function DisputeResolutionPanel() {
|
||||
<CardContent className="pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Escalated</p>
|
||||
<p className="text-sm text-muted-foreground">{t('dispute.statsEscalated')}</p>
|
||||
<p className="text-2xl font-bold text-purple-500">{stats.escalated}</p>
|
||||
</div>
|
||||
<Scale className="h-8 w-8 text-purple-500/50" />
|
||||
@@ -389,16 +391,16 @@ export function DisputeResolutionPanel() {
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid grid-cols-4 w-full max-w-md">
|
||||
<TabsTrigger value="open" className="gap-1">
|
||||
Open
|
||||
{t('dispute.statsOpen')}
|
||||
{stats.open > 0 && (
|
||||
<Badge variant="secondary" className="ml-1 h-5 px-1.5">
|
||||
{stats.open}
|
||||
</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="under_review">In Review</TabsTrigger>
|
||||
<TabsTrigger value="resolved">Resolved</TabsTrigger>
|
||||
<TabsTrigger value="escalated">Escalated</TabsTrigger>
|
||||
<TabsTrigger value="under_review">{t('dispute.tabInReview')}</TabsTrigger>
|
||||
<TabsTrigger value="resolved">{t('dispute.statsResolved')}</TabsTrigger>
|
||||
<TabsTrigger value="escalated">{t('dispute.statsEscalated')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value={activeTab} className="mt-4">
|
||||
@@ -412,7 +414,7 @@ export function DisputeResolutionPanel() {
|
||||
<Card>
|
||||
<CardContent className="py-12 text-center">
|
||||
<Shield className="h-12 w-12 mx-auto text-muted-foreground/50 mb-4" />
|
||||
<p className="text-muted-foreground">No disputes in this category</p>
|
||||
<p className="text-muted-foreground">{t('dispute.empty')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
@@ -427,12 +429,12 @@ export function DisputeResolutionPanel() {
|
||||
{dispute.status.replace('_', ' ').toUpperCase()}
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
{CATEGORY_LABELS[dispute.category] || dispute.category}
|
||||
{t(CATEGORY_KEYS[dispute.category] || dispute.category)}
|
||||
</Badge>
|
||||
{dispute.evidence && dispute.evidence.length > 0 && (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
<ImageIcon className="h-3 w-3" />
|
||||
{dispute.evidence.length} evidence
|
||||
{t('dispute.evidence', { count: dispute.evidence.length })}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -466,7 +468,7 @@ export function DisputeResolutionPanel() {
|
||||
onClick={() => openDetails(dispute)}
|
||||
>
|
||||
<Eye className="h-4 w-4 mr-1" />
|
||||
View
|
||||
{t('dispute.view')}
|
||||
</Button>
|
||||
|
||||
{dispute.status === 'open' && (
|
||||
@@ -474,7 +476,7 @@ export function DisputeResolutionPanel() {
|
||||
size="sm"
|
||||
onClick={() => claimDispute(dispute.id)}
|
||||
>
|
||||
Claim
|
||||
{t('dispute.claim')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -485,7 +487,7 @@ export function DisputeResolutionPanel() {
|
||||
onClick={() => openResolve(dispute)}
|
||||
>
|
||||
<Gavel className="h-4 w-4 mr-1" />
|
||||
Resolve
|
||||
{t('dispute.resolve')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -504,10 +506,10 @@ export function DisputeResolutionPanel() {
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Scale className="h-5 w-5" />
|
||||
Dispute Details
|
||||
{t('dispute.detailsTitle')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Review all information related to this dispute
|
||||
{t('dispute.detailsDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -520,13 +522,13 @@ export function DisputeResolutionPanel() {
|
||||
{selectedDispute.status.replace('_', ' ').toUpperCase()}
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
{CATEGORY_LABELS[selectedDispute.category] || selectedDispute.category}
|
||||
{t(CATEGORY_KEYS[selectedDispute.category] || selectedDispute.category)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Reason */}
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Reason</h4>
|
||||
<h4 className="font-medium mb-2">{t('dispute.reason')}</h4>
|
||||
<p className="text-sm text-muted-foreground bg-muted p-3 rounded-lg">
|
||||
{selectedDispute.reason}
|
||||
</p>
|
||||
@@ -535,22 +537,22 @@ export function DisputeResolutionPanel() {
|
||||
{/* Trade Info */}
|
||||
{selectedDispute.trade && (
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Trade Information</h4>
|
||||
<h4 className="font-medium mb-2">{t('dispute.tradeInfo')}</h4>
|
||||
<div className="bg-muted p-3 rounded-lg space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Trade ID:</span>
|
||||
<span className="text-muted-foreground">{t('dispute.tradeId')}:</span>
|
||||
<span className="font-mono">{formatAddress(selectedDispute.trade_id)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Amount:</span>
|
||||
<span className="text-muted-foreground">{t('dispute.amount')}:</span>
|
||||
<span>{selectedDispute.trade.crypto_amount} crypto</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Fiat:</span>
|
||||
<span className="text-muted-foreground">{t('dispute.fiat')}:</span>
|
||||
<span>{selectedDispute.trade.fiat_amount}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Trade Status:</span>
|
||||
<span className="text-muted-foreground">{t('dispute.tradeStatus')}:</span>
|
||||
<Badge variant="secondary">{selectedDispute.trade.status}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
@@ -560,12 +562,12 @@ export function DisputeResolutionPanel() {
|
||||
{/* Parties */}
|
||||
{selectedDispute.trade && (
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Parties</h4>
|
||||
<h4 className="font-medium mb-2">{t('dispute.parties')}</h4>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="bg-muted p-3 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<User className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Seller</span>
|
||||
<span className="text-sm font-medium">{t('dispute.seller')}</span>
|
||||
</div>
|
||||
<p className="text-xs font-mono text-muted-foreground">
|
||||
{formatAddress(selectedDispute.trade.seller_id)}
|
||||
@@ -574,7 +576,7 @@ export function DisputeResolutionPanel() {
|
||||
<div className="bg-muted p-3 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<User className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Buyer</span>
|
||||
<span className="text-sm font-medium">{t('dispute.buyer')}</span>
|
||||
</div>
|
||||
<p className="text-xs font-mono text-muted-foreground">
|
||||
{formatAddress(selectedDispute.trade.buyer_id)}
|
||||
@@ -587,7 +589,7 @@ export function DisputeResolutionPanel() {
|
||||
{/* Evidence */}
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">
|
||||
Evidence ({selectedDispute.evidence?.length || 0})
|
||||
{t('dispute.evidence', { count: selectedDispute.evidence?.length || 0 })}
|
||||
</h4>
|
||||
{selectedDispute.evidence && selectedDispute.evidence.length > 0 ? (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
@@ -624,30 +626,30 @@ export function DisputeResolutionPanel() {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">No evidence uploaded</p>
|
||||
<p className="text-sm text-muted-foreground">{t('dispute.noEvidence')}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Timeline */}
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Timeline</h4>
|
||||
<h4 className="font-medium mb-2">{t('dispute.timeline')}</h4>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-yellow-500" />
|
||||
<span className="text-muted-foreground">Opened:</span>
|
||||
<span className="text-muted-foreground">{t('dispute.opened')}:</span>
|
||||
<span>{formatDate(selectedDispute.created_at)}</span>
|
||||
</div>
|
||||
{selectedDispute.assigned_at && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-500" />
|
||||
<span className="text-muted-foreground">Claimed:</span>
|
||||
<span className="text-muted-foreground">{t('dispute.claimed')}:</span>
|
||||
<span>{formatDate(selectedDispute.assigned_at)}</span>
|
||||
</div>
|
||||
)}
|
||||
{selectedDispute.resolved_at && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500" />
|
||||
<span className="text-muted-foreground">Resolved:</span>
|
||||
<span className="text-muted-foreground">{t('dispute.resolved')}:</span>
|
||||
<span>{formatDate(selectedDispute.resolved_at)}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -657,10 +659,10 @@ export function DisputeResolutionPanel() {
|
||||
{/* Resolution (if resolved) */}
|
||||
{selectedDispute.decision && (
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Resolution</h4>
|
||||
<h4 className="font-medium mb-2">{t('dispute.resolution')}</h4>
|
||||
<div className="bg-green-500/10 border border-green-500/20 p-3 rounded-lg">
|
||||
<Badge className="bg-green-500/20 text-green-500 mb-2">
|
||||
{DECISION_OPTIONS.find(o => o.value === selectedDispute.decision)?.label}
|
||||
{t(DECISION_OPTION_KEYS.find(o => o.value === selectedDispute.decision)?.labelKey || '')}
|
||||
</Badge>
|
||||
{selectedDispute.decision_reasoning && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -676,7 +678,7 @@ export function DisputeResolutionPanel() {
|
||||
|
||||
<DialogFooter className="mt-4">
|
||||
<Button variant="outline" onClick={() => setDetailsOpen(false)}>
|
||||
Close
|
||||
{t('dispute.close')}
|
||||
</Button>
|
||||
{selectedDispute?.status === 'under_review' && (
|
||||
<Button
|
||||
@@ -687,7 +689,7 @@ export function DisputeResolutionPanel() {
|
||||
}}
|
||||
>
|
||||
<Gavel className="h-4 w-4 mr-2" />
|
||||
Resolve Dispute
|
||||
{t('dispute.resolve')}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
@@ -700,29 +702,26 @@ export function DisputeResolutionPanel() {
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Gavel className="h-5 w-5 text-kurdish-green" />
|
||||
Resolve Dispute
|
||||
{t('dispute.resolveTitle')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Make a final decision on this dispute. This action cannot be undone.
|
||||
{t('dispute.resolveDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Decision */}
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Decision</label>
|
||||
<label className="text-sm font-medium mb-2 block">{t('dispute.decision')}</label>
|
||||
<Select value={decision} onValueChange={setDecision}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a decision..." />
|
||||
<SelectValue placeholder={t('dispute.decisionPlaceholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DECISION_OPTIONS.map((option) => (
|
||||
{DECISION_OPTION_KEYS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<div className="flex flex-col">
|
||||
<span>{option.label}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{option.description}
|
||||
</span>
|
||||
<span>{t(option.labelKey)}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
@@ -733,16 +732,16 @@ export function DisputeResolutionPanel() {
|
||||
{/* Reasoning */}
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">
|
||||
Reasoning <span className="text-muted-foreground">(required)</span>
|
||||
{t('dispute.reasoning')} <span className="text-muted-foreground">({t('dispute.required')})</span>
|
||||
</label>
|
||||
<Textarea
|
||||
value={reasoning}
|
||||
onChange={(e) => setReasoning(e.target.value)}
|
||||
placeholder="Explain your decision based on the evidence..."
|
||||
placeholder={t('dispute.reasoningPlaceholder')}
|
||||
rows={4}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
This will be visible to both parties
|
||||
{t('dispute.reasoningHint')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -750,10 +749,9 @@ export function DisputeResolutionPanel() {
|
||||
<div className="flex items-start gap-2 p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
|
||||
<AlertTriangle className="h-5 w-5 text-yellow-500 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm">
|
||||
<p className="font-medium text-yellow-500">Important</p>
|
||||
<p className="font-medium text-yellow-500">{t('dispute.warningTitle')}</p>
|
||||
<p className="text-muted-foreground">
|
||||
Your decision will trigger automatic actions on the escrowed funds.
|
||||
Make sure you have reviewed all evidence carefully.
|
||||
{t('dispute.warningText')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -765,7 +763,7 @@ export function DisputeResolutionPanel() {
|
||||
onClick={() => setResolveOpen(false)}
|
||||
disabled={submitting}
|
||||
>
|
||||
Cancel
|
||||
{t('dispute.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-kurdish-green hover:bg-kurdish-green-dark"
|
||||
@@ -777,7 +775,7 @@ export function DisputeResolutionPanel() {
|
||||
) : (
|
||||
<CheckCircle2 className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Confirm Resolution
|
||||
{t('dispute.confirmResolution')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -18,6 +19,7 @@ import { approveReferral, getPendingApprovalsForReferrer } from '@pezkuwi/lib/ci
|
||||
import type { PendingApproval } from '@pezkuwi/lib/citizenship-workflow';
|
||||
|
||||
export function KycApprovalTab() {
|
||||
const { t } = useTranslation();
|
||||
// identityKyc pallet is on People Chain - use peopleApi
|
||||
const { peopleApi, isPeopleReady, selectedAccount, connectWallet } = usePezkuwi();
|
||||
const { toast } = useToast();
|
||||
@@ -52,8 +54,8 @@ export function KycApprovalTab() {
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Error loading pending applications:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to load pending applications',
|
||||
title: t('kyc.approval.failed'),
|
||||
description: t('kyc.approval.failedDesc'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
@@ -64,8 +66,8 @@ export function KycApprovalTab() {
|
||||
const handleApproveReferral = async (applicantAddress: string) => {
|
||||
if (!peopleApi || !selectedAccount) {
|
||||
toast({
|
||||
title: 'Wallet Not Connected',
|
||||
description: 'Please connect your wallet first',
|
||||
title: t('kyc.approval.walletNotConnected'),
|
||||
description: t('kyc.approval.connectFirst'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -77,16 +79,16 @@ export function KycApprovalTab() {
|
||||
|
||||
if (!result.success) {
|
||||
toast({
|
||||
title: 'Approval Failed',
|
||||
description: result.error || 'Failed to approve referral',
|
||||
title: t('kyc.approval.failed'),
|
||||
description: result.error || t('kyc.approval.failedDesc'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: 'Referral Approved',
|
||||
description: `Successfully vouched for ${applicantAddress.slice(0, 8)}...${applicantAddress.slice(-4)}`,
|
||||
title: t('kyc.approval.success'),
|
||||
description: t('kyc.approval.successDesc', { address: `${applicantAddress.slice(0, 8)}...${applicantAddress.slice(-4)}` }),
|
||||
});
|
||||
|
||||
// Reload after approval
|
||||
@@ -94,8 +96,8 @@ export function KycApprovalTab() {
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Error approving referral:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error instanceof Error ? error.message : 'Failed to approve referral',
|
||||
title: t('kyc.approval.failed'),
|
||||
description: error instanceof Error ? error.message : t('kyc.approval.failedDesc'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
@@ -109,7 +111,7 @@ export function KycApprovalTab() {
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-cyan-500" />
|
||||
<span className="ml-3 text-gray-400">Connecting to People Chain...</span>
|
||||
<span className="ml-3 text-gray-400">{t('kyc.approval.connecting')}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -123,9 +125,9 @@ export function KycApprovalTab() {
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Please connect your wallet to view referral approvals.
|
||||
{t('kyc.approval.noWallet')}
|
||||
<Button onClick={connectWallet} variant="outline" className="ml-4">
|
||||
Connect Wallet
|
||||
{t('kyc.approval.connectWallet')}
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
@@ -137,9 +139,9 @@ export function KycApprovalTab() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Pending Referral Approvals</CardTitle>
|
||||
<CardTitle>{t('kyc.approval.title')}</CardTitle>
|
||||
<Button onClick={loadPendingApplications} variant="outline" size="sm" disabled={loading}>
|
||||
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Refresh'}
|
||||
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : t('kyc.approval.refresh')}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -150,21 +152,21 @@ export function KycApprovalTab() {
|
||||
) : pendingApps.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<CheckCircle className="w-12 h-12 text-green-500 mx-auto mb-3" />
|
||||
<p className="text-gray-400">No pending approvals</p>
|
||||
<p className="text-sm text-gray-600 mt-2">No one is waiting for your referral approval</p>
|
||||
<p className="text-gray-400">{t('kyc.approval.noApprovals')}</p>
|
||||
<p className="text-sm text-gray-600 mt-2">{t('kyc.approval.noApprovalsHelp')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
These users listed you as their referrer. Approve to vouch for their identity.
|
||||
{t('kyc.approval.helpText')}
|
||||
</p>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Applicant</TableHead>
|
||||
<TableHead>Identity Hash</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
<TableHead>{t('kyc.approval.tableApplicant')}</TableHead>
|
||||
<TableHead>{t('kyc.approval.tableIdentityHash')}</TableHead>
|
||||
<TableHead>{t('kyc.approval.tableStatus')}</TableHead>
|
||||
<TableHead>{t('kyc.approval.tableActions')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -186,7 +188,7 @@ export function KycApprovalTab() {
|
||||
<TableCell>
|
||||
<Badge className="bg-yellow-500/20 text-yellow-400 border-yellow-500/30">
|
||||
<Clock className="w-3 h-3 mr-1" />
|
||||
Pending Referral
|
||||
{t('kyc.approval.statusPending')}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@@ -201,7 +203,7 @@ export function KycApprovalTab() {
|
||||
) : (
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
Approve
|
||||
{t('kyc.approval.approve')}
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import {
|
||||
@@ -65,6 +66,7 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
onClose,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
// Use Asset Hub API for asset registration (Step 5) and XCM testing (Step 6)
|
||||
// Steps 1-4 connect to relay chain directly via xcm-wizard functions
|
||||
const { assetHubApi, isAssetHubReady } = usePezkuwi();
|
||||
@@ -138,8 +140,8 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
const handleReserveParaId = async () => {
|
||||
if (!account || !signer) {
|
||||
toast({
|
||||
title: 'Wallet not connected',
|
||||
description: 'Please connect your wallet first',
|
||||
title: t('xcmWizard.walletNotConnected'),
|
||||
description: t('xcmWizard.connectFirst'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -163,8 +165,7 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
}));
|
||||
|
||||
toast({
|
||||
title: 'ParaId Reserved!',
|
||||
description: `Successfully reserved ParaId ${paraId} on ${relayChain}`,
|
||||
title: t('xcmWizard.reserveSuccess', { paraId, chain: relayChain }),
|
||||
});
|
||||
|
||||
// Auto-advance to next step
|
||||
@@ -178,8 +179,8 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
1: { completed: false, error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
}));
|
||||
toast({
|
||||
title: 'Reservation Failed',
|
||||
description: error instanceof Error ? error.message : 'Failed to reserve ParaId',
|
||||
title: t('xcmWizard.reserveFailed'),
|
||||
description: error instanceof Error ? error.message : t('xcmWizard.reserveFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
@@ -205,8 +206,7 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
}));
|
||||
|
||||
toast({
|
||||
title: 'Artifacts Generated!',
|
||||
description: 'Genesis state and runtime WASM are ready for download',
|
||||
title: t('xcmWizard.artifactsReady'),
|
||||
});
|
||||
|
||||
setCurrentStep(3);
|
||||
@@ -217,8 +217,7 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
2: { completed: false, error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
}));
|
||||
toast({
|
||||
title: 'Generation Failed',
|
||||
description: 'Failed to generate chain artifacts',
|
||||
title: t('xcmWizard.artifactsFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
@@ -232,8 +231,7 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
const handleRegisterParachain = async () => {
|
||||
if (!reservedParaId || !genesisFile || !wasmFile || !account || !signer) {
|
||||
toast({
|
||||
title: 'Missing Data',
|
||||
description: 'Please upload both genesis and WASM files',
|
||||
title: t('xcmWizard.missingFiles'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -254,8 +252,7 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
}));
|
||||
|
||||
toast({
|
||||
title: 'Parachain Registered!',
|
||||
description: `ParaId ${reservedParaId} registered on ${relayChain}`,
|
||||
title: t('xcmWizard.registerSuccess'),
|
||||
});
|
||||
|
||||
setCurrentStep(4);
|
||||
@@ -268,8 +265,8 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
3: { completed: false, error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
}));
|
||||
toast({
|
||||
title: 'Registration Failed',
|
||||
description: error instanceof Error ? error.message : 'Failed to register parachain',
|
||||
title: t('xcmWizard.registerFailed'),
|
||||
description: error instanceof Error ? error.message : t('xcmWizard.registerFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
@@ -302,8 +299,7 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
}));
|
||||
|
||||
toast({
|
||||
title: 'HRMP Channels Opened!',
|
||||
description: `Opened ${channels.length} channel(s) with Asset Hub`,
|
||||
title: t('xcmWizard.hrmpSuccess', { count: channels.length }),
|
||||
});
|
||||
|
||||
setCurrentStep(5);
|
||||
@@ -316,8 +312,8 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
4: { completed: false, error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
}));
|
||||
toast({
|
||||
title: 'Channel Opening Failed',
|
||||
description: error instanceof Error ? error.message : 'Failed to open HRMP channels',
|
||||
title: t('xcmWizard.hrmpFailed'),
|
||||
description: error instanceof Error ? error.message : t('xcmWizard.hrmpFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
@@ -331,8 +327,7 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
const handleRegisterAssets = async () => {
|
||||
if (!assetHubApi || !isAssetHubReady || !account || !signer) {
|
||||
toast({
|
||||
title: 'Not Ready',
|
||||
description: 'Please wait for Asset Hub connection',
|
||||
title: t('xcmWizard.notReady'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -381,8 +376,7 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
}));
|
||||
|
||||
toast({
|
||||
title: 'Assets Registered!',
|
||||
description: `Registered ${registered.length} foreign asset(s)`,
|
||||
title: t('xcmWizard.assetsSuccess', { count: registered.length }),
|
||||
});
|
||||
|
||||
setCurrentStep(6);
|
||||
@@ -393,8 +387,8 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
5: { completed: false, error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
}));
|
||||
toast({
|
||||
title: 'Asset Registration Failed',
|
||||
description: error instanceof Error ? error.message : 'Failed to register foreign assets',
|
||||
title: t('xcmWizard.assetsFailed'),
|
||||
description: error instanceof Error ? error.message : t('xcmWizard.assetsFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
@@ -408,8 +402,7 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
const handleTestXCMTransfer = async () => {
|
||||
if (!assetHubApi || !isAssetHubReady || !account || !signer) {
|
||||
toast({
|
||||
title: 'Not Ready',
|
||||
description: 'Please wait for Asset Hub connection',
|
||||
title: t('xcmWizard.notReady'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -428,13 +421,12 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
|
||||
if (result.success) {
|
||||
toast({
|
||||
title: 'XCM Test Successful!',
|
||||
description: `Received ${result.balance} wUSDT`,
|
||||
title: t('xcmWizard.testSuccess', { balance: result.balance }),
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: 'XCM Test Failed',
|
||||
description: result.error || 'Test transfer failed',
|
||||
title: t('xcmWizard.testFailed'),
|
||||
description: result.error || t('xcmWizard.testFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@@ -445,8 +437,8 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
6: { completed: false, error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
}));
|
||||
toast({
|
||||
title: 'Test Failed',
|
||||
description: error instanceof Error ? error.message : 'XCM test failed',
|
||||
title: t('xcmWizard.testFailed'),
|
||||
description: error instanceof Error ? error.message : t('xcmWizard.testFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
@@ -463,15 +455,15 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Select Relay Chain</Label>
|
||||
<Label>{t('xcmWizard.relayChainLabel')}</Label>
|
||||
<Select value={relayChain} onValueChange={(value: RelayChain) => setRelayChain(value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="westend">Westend (Testnet)</SelectItem>
|
||||
<SelectItem value="rococo">Rococo (Testnet)</SelectItem>
|
||||
<SelectItem value="polkadot">Polkadot (Mainnet)</SelectItem>
|
||||
<SelectItem value="westend">{t('xcmWizard.westend')}</SelectItem>
|
||||
<SelectItem value="rococo">{t('xcmWizard.rococo')}</SelectItem>
|
||||
<SelectItem value="polkadot">{t('xcmWizard.polkadot')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -496,15 +488,15 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
{reserving ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Reserving ParaId...
|
||||
{t('xcmWizard.reserving')}
|
||||
</>
|
||||
) : steps[1].completed ? (
|
||||
<>
|
||||
<CheckCircle className="mr-2 h-4 w-4" />
|
||||
ParaId Reserved
|
||||
{t('xcmWizard.reserved')}
|
||||
</>
|
||||
) : (
|
||||
'Reserve ParaId'
|
||||
t('xcmWizard.reserveBtn')
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -550,15 +542,15 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
{generating ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Generating Artifacts...
|
||||
{t('xcmWizard.generating')}
|
||||
</>
|
||||
) : steps[2].completed ? (
|
||||
<>
|
||||
<CheckCircle className="mr-2 h-4 w-4" />
|
||||
Artifacts Generated
|
||||
{t('xcmWizard.generated')}
|
||||
</>
|
||||
) : (
|
||||
'Generate Chain Artifacts'
|
||||
t('xcmWizard.generateBtn')
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -568,7 +560,7 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Upload Genesis State</Label>
|
||||
<Label>{t('xcmWizard.genesisLabel')}</Label>
|
||||
<Input
|
||||
type="file"
|
||||
accept=".hex,.txt"
|
||||
@@ -577,7 +569,7 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Upload Runtime WASM</Label>
|
||||
<Label>{t('xcmWizard.wasmLabel')}</Label>
|
||||
<Input
|
||||
type="file"
|
||||
accept=".wasm"
|
||||
@@ -609,15 +601,15 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
{registering ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Registering Parachain...
|
||||
{t('xcmWizard.registering')}
|
||||
</>
|
||||
) : steps[3].completed ? (
|
||||
<>
|
||||
<CheckCircle className="mr-2 h-4 w-4" />
|
||||
Parachain Registered
|
||||
{t('xcmWizard.registered')}
|
||||
</>
|
||||
) : (
|
||||
'Register Parachain'
|
||||
t('xcmWizard.registerBtn')
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -657,15 +649,15 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
{openingChannels ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Opening HRMP Channels...
|
||||
{t('xcmWizard.openingHrmp')}
|
||||
</>
|
||||
) : steps[4].completed ? (
|
||||
<>
|
||||
<CheckCircle className="mr-2 h-4 w-4" />
|
||||
Channels Opened
|
||||
{t('xcmWizard.channelsOpened')}
|
||||
</>
|
||||
) : (
|
||||
'Open HRMP Channels'
|
||||
t('xcmWizard.hrmpBtn')
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -675,7 +667,7 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Register foreign assets: USDT, DOT, and other cross-chain tokens
|
||||
{t('xcmWizard.assetsDesc')}
|
||||
</p>
|
||||
|
||||
{registeredAssets.length > 0 && (
|
||||
@@ -705,15 +697,15 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
{registeringAssets ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Registering Assets...
|
||||
{t('xcmWizard.registeringAssets')}
|
||||
</>
|
||||
) : steps[5].completed ? (
|
||||
<>
|
||||
<CheckCircle className="mr-2 h-4 w-4" />
|
||||
Assets Registered
|
||||
{t('xcmWizard.assetsRegistered')}
|
||||
</>
|
||||
) : (
|
||||
'Register Foreign Assets'
|
||||
t('xcmWizard.assetsBtn')
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -723,7 +715,7 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Test XCM transfer from Asset Hub to verify bridge functionality
|
||||
{t('xcmWizard.testDesc')}
|
||||
</p>
|
||||
|
||||
{testResult && (
|
||||
@@ -731,8 +723,8 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
{testResult.success ? <CheckCircle className="h-4 w-4" /> : <AlertCircle className="h-4 w-4" />}
|
||||
<AlertDescription>
|
||||
{testResult.success
|
||||
? `Test successful! Balance: ${testResult.balance} wUSDT`
|
||||
: `Test failed: ${testResult.error}`}
|
||||
? t('xcmWizard.testSuccess', { balance: testResult.balance })
|
||||
: testResult.error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -748,15 +740,15 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
{testing ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Testing XCM Transfer...
|
||||
{t('xcmWizard.testing')}
|
||||
</>
|
||||
) : steps[6].completed ? (
|
||||
<>
|
||||
<CheckCircle className="mr-2 h-4 w-4" />
|
||||
XCM Test Passed
|
||||
{t('xcmWizard.testPassed')}
|
||||
</>
|
||||
) : (
|
||||
'Test XCM Transfer'
|
||||
t('xcmWizard.testBtn')
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -773,8 +765,8 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
// Handle Finish Configuration
|
||||
const handleFinishConfiguration = () => {
|
||||
toast({
|
||||
title: 'XCM Configuration Complete!',
|
||||
description: 'Your parachain is fully configured and ready for cross-chain transfers',
|
||||
title: t('xcmWizard.complete'),
|
||||
description: t('xcmWizard.completeDesc'),
|
||||
});
|
||||
|
||||
if (onSuccess) {
|
||||
@@ -792,9 +784,9 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>XCM Configuration Wizard</CardTitle>
|
||||
<CardTitle>{t('xcmWizard.title')}</CardTitle>
|
||||
<CardDescription>
|
||||
Complete parachain setup and cross-chain integration
|
||||
{t('xcmWizard.subtitle')}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" onClick={onClose}>
|
||||
@@ -805,7 +797,7 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
<div className="mt-4">
|
||||
<Progress value={progress} className="h-2" />
|
||||
<p className="mt-2 text-xs text-muted-foreground text-center">
|
||||
{Object.values(steps).filter(s => s.completed).length} / {totalSteps} steps completed
|
||||
{t('xcmWizard.progress', { completed: Object.values(steps).filter(s => s.completed).length, total: totalSteps })}
|
||||
</p>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -842,12 +834,12 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline">Step {currentStep}</Badge>
|
||||
<h3 className="font-semibold">
|
||||
{currentStep === 1 && 'Reserve ParaId'}
|
||||
{currentStep === 2 && 'Generate Chain Artifacts'}
|
||||
{currentStep === 3 && 'Register Parachain'}
|
||||
{currentStep === 4 && 'Open HRMP Channels'}
|
||||
{currentStep === 5 && 'Register Foreign Assets'}
|
||||
{currentStep === 6 && 'Test XCM Transfer'}
|
||||
{currentStep === 1 && t('xcmWizard.stepReserve')}
|
||||
{currentStep === 2 && t('xcmWizard.stepArtifacts')}
|
||||
{currentStep === 3 && t('xcmWizard.stepParachain')}
|
||||
{currentStep === 4 && t('xcmWizard.stepHrmp')}
|
||||
{currentStep === 5 && t('xcmWizard.stepAssets')}
|
||||
{currentStep === 6 && t('xcmWizard.stepTest')}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -861,20 +853,20 @@ export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
|
||||
onClick={() => setCurrentStep(Math.max(1, currentStep - 1))}
|
||||
disabled={currentStep === 1}
|
||||
>
|
||||
Previous
|
||||
{t('xcmWizard.previous')}
|
||||
</Button>
|
||||
|
||||
{allStepsCompleted ? (
|
||||
<Button onClick={handleFinishConfiguration} className="bg-kurdish-green hover:bg-kurdish-green-dark">
|
||||
<CheckCircle className="mr-2 h-4 w-4" />
|
||||
Finish Configuration
|
||||
{t('xcmWizard.finish')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => setCurrentStep(Math.min(totalSteps, currentStep + 1))}
|
||||
disabled={currentStep === totalSteps || !steps[currentStep].completed}
|
||||
>
|
||||
Next
|
||||
{t('xcmWizard.next')}
|
||||
<ChevronRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -11,6 +12,7 @@ import { useToast } from '@/hooks/use-toast';
|
||||
export function TwoFactorSetup() {
|
||||
const { user } = useAuth();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const [isEnabled, setIsEnabled] = useState(false);
|
||||
const [secret, setSecret] = useState('');
|
||||
const [backupCodes, setBackupCodes] = useState<string[]>([]);
|
||||
@@ -33,12 +35,12 @@ export function TwoFactorSetup() {
|
||||
setShowSetup(true);
|
||||
|
||||
toast({
|
||||
title: '2FA Setup Started',
|
||||
description: 'Scan the QR code with your authenticator app',
|
||||
title: t('twoFactor.setupStarted'),
|
||||
description: t('twoFactor.scanQrDesc'),
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Setup Failed',
|
||||
title: t('twoFactor.setupFailed'),
|
||||
description: error.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
@@ -50,8 +52,8 @@ export function TwoFactorSetup() {
|
||||
const handleEnable = async () => {
|
||||
if (!verificationCode) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Please enter verification code',
|
||||
title: t('common.error'),
|
||||
description: t('twoFactor.enterVerification'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -73,12 +75,12 @@ export function TwoFactorSetup() {
|
||||
setShowSetup(false);
|
||||
|
||||
toast({
|
||||
title: '2FA Enabled',
|
||||
description: 'Your account is now protected with two-factor authentication',
|
||||
title: t('twoFactor.enabled'),
|
||||
description: t('twoFactor.enabledDesc'),
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Verification Failed',
|
||||
title: t('twoFactor.verificationFailed'),
|
||||
description: error.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
@@ -101,12 +103,12 @@ export function TwoFactorSetup() {
|
||||
setBackupCodes([]);
|
||||
|
||||
toast({
|
||||
title: '2FA Disabled',
|
||||
description: 'Two-factor authentication has been disabled',
|
||||
title: t('twoFactor.disabled'),
|
||||
description: t('twoFactor.disabledDesc'),
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
title: t('common.error'),
|
||||
description: error.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
@@ -126,10 +128,10 @@ export function TwoFactorSetup() {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Shield className="h-5 w-5" />
|
||||
Two-Factor Authentication
|
||||
{t('twoFactor.title')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Add an extra layer of security to your account
|
||||
{t('twoFactor.description')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
@@ -138,11 +140,11 @@ export function TwoFactorSetup() {
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Two-factor authentication adds an extra layer of security by requiring a code from your authenticator app
|
||||
{t('twoFactor.infoAlert')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Button onClick={handleSetup} disabled={isLoading}>
|
||||
Set Up Two-Factor Authentication
|
||||
{t('twoFactor.setupBtn')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -150,9 +152,9 @@ export function TwoFactorSetup() {
|
||||
{showSetup && (
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 border rounded-lg">
|
||||
<p className="text-sm font-medium mb-2">1. Scan QR Code</p>
|
||||
<p className="text-sm font-medium mb-2">{t('twoFactor.scanQrTitle')}</p>
|
||||
<p className="text-xs text-muted-foreground mb-4">
|
||||
Use your authenticator app to scan this QR code or enter the secret manually
|
||||
{t('twoFactor.scanQrHint')}
|
||||
</p>
|
||||
<div className="bg-muted p-2 rounded font-mono text-xs break-all">
|
||||
{secret}
|
||||
@@ -160,9 +162,9 @@ export function TwoFactorSetup() {
|
||||
</div>
|
||||
|
||||
<div className="p-4 border rounded-lg">
|
||||
<p className="text-sm font-medium mb-2">2. Save Backup Codes</p>
|
||||
<p className="text-sm font-medium mb-2">{t('twoFactor.saveBackup')}</p>
|
||||
<p className="text-xs text-muted-foreground mb-4">
|
||||
Store these codes in a safe place. You can use them to access your account if you lose your device.
|
||||
{t('twoFactor.saveBackupHint')}
|
||||
</p>
|
||||
<div className="bg-muted p-3 rounded space-y-1">
|
||||
{backupCodes.map((code, i) => (
|
||||
@@ -176,14 +178,14 @@ export function TwoFactorSetup() {
|
||||
onClick={copyBackupCodes}
|
||||
>
|
||||
{copiedCodes ? <Check className="h-4 w-4 mr-2" /> : <Copy className="h-4 w-4 mr-2" />}
|
||||
{copiedCodes ? 'Copied!' : 'Copy Codes'}
|
||||
{copiedCodes ? t('twoFactor.copied') : t('twoFactor.copyCodes')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border rounded-lg">
|
||||
<p className="text-sm font-medium mb-2">3. Verify Setup</p>
|
||||
<p className="text-sm font-medium mb-2">{t('twoFactor.verifySetup')}</p>
|
||||
<p className="text-xs text-muted-foreground mb-4">
|
||||
Enter the 6-digit code from your authenticator app
|
||||
{t('twoFactor.enterCode')}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
@@ -193,7 +195,7 @@ export function TwoFactorSetup() {
|
||||
maxLength={6}
|
||||
/>
|
||||
<Button onClick={handleEnable} disabled={isLoading}>
|
||||
Enable 2FA
|
||||
{t('twoFactor.enableBtn')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -205,11 +207,11 @@ export function TwoFactorSetup() {
|
||||
<Alert className="border-green-200 bg-green-50">
|
||||
<Check className="h-4 w-4 text-green-600" />
|
||||
<AlertDescription className="text-green-800">
|
||||
Two-factor authentication is enabled for your account
|
||||
{t('twoFactor.enabledAlert')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Button variant="destructive" onClick={handleDisable} disabled={isLoading}>
|
||||
Disable Two-Factor Authentication
|
||||
{t('twoFactor.disableBtn')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -16,6 +17,7 @@ interface TwoFactorVerifyProps {
|
||||
|
||||
export function TwoFactorVerify({ userId, onSuccess, onCancel }: TwoFactorVerifyProps) {
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const [verificationCode, setVerificationCode] = useState('');
|
||||
const [backupCode, setBackupCode] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -25,8 +27,8 @@ export function TwoFactorVerify({ userId, onSuccess, onCancel }: TwoFactorVerify
|
||||
|
||||
if (!code) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Please enter a code',
|
||||
title: t('common.error'),
|
||||
description: t('twoFactor.pleaseEnterCode'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -47,8 +49,8 @@ export function TwoFactorVerify({ userId, onSuccess, onCancel }: TwoFactorVerify
|
||||
|
||||
if (data.success) {
|
||||
toast({
|
||||
title: 'Verification Successful',
|
||||
description: 'You have been authenticated',
|
||||
title: t('twoFactor.verifySuccess'),
|
||||
description: t('twoFactor.authenticated'),
|
||||
});
|
||||
onSuccess();
|
||||
} else {
|
||||
@@ -56,7 +58,7 @@ export function TwoFactorVerify({ userId, onSuccess, onCancel }: TwoFactorVerify
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Verification Failed',
|
||||
title: t('twoFactor.verificationFailed'),
|
||||
description: error.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
@@ -70,23 +72,23 @@ export function TwoFactorVerify({ userId, onSuccess, onCancel }: TwoFactorVerify
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Shield className="h-5 w-5" />
|
||||
Two-Factor Authentication
|
||||
{t('twoFactor.title')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your authentication code to continue
|
||||
{t('twoFactor.enterAuthCode')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="authenticator" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="authenticator">Authenticator App</TabsTrigger>
|
||||
<TabsTrigger value="backup">Backup Code</TabsTrigger>
|
||||
<TabsTrigger value="authenticator">{t('twoFactor.authenticatorTab')}</TabsTrigger>
|
||||
<TabsTrigger value="backup">{t('twoFactor.backupTab')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="authenticator" className="space-y-4">
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
Enter the 6-digit code from your authenticator app
|
||||
{t('twoFactor.enterCode')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Input
|
||||
@@ -99,15 +101,15 @@ export function TwoFactorVerify({ userId, onSuccess, onCancel }: TwoFactorVerify
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
className="flex-1"
|
||||
onClick={() => handleVerify(false)}
|
||||
onClick={() => handleVerify(false)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Verify
|
||||
{t('twoFactor.verify')}
|
||||
</Button>
|
||||
{onCancel && (
|
||||
<Button variant="outline" onClick={onCancel} disabled={isLoading}>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -116,7 +118,7 @@ export function TwoFactorVerify({ userId, onSuccess, onCancel }: TwoFactorVerify
|
||||
<TabsContent value="backup" className="space-y-4">
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
Enter one of your backup codes
|
||||
{t('twoFactor.enterBackupCode')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Input
|
||||
@@ -128,15 +130,15 @@ export function TwoFactorVerify({ userId, onSuccess, onCancel }: TwoFactorVerify
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
className="flex-1"
|
||||
onClick={() => handleVerify(true)}
|
||||
onClick={() => handleVerify(true)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Verify
|
||||
{t('twoFactor.verify')}
|
||||
</Button>
|
||||
{onCancel && (
|
||||
<Button variant="outline" onClick={onCancel} disabled={isLoading}>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -17,6 +18,7 @@ interface CitizenshipModalProps {
|
||||
}
|
||||
|
||||
export const CitizenshipModal: React.FC<CitizenshipModalProps> = ({ isOpen, onClose, referrerAddress }) => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<'existing' | 'new'>(referrerAddress ? 'new' : 'existing');
|
||||
|
||||
return (
|
||||
@@ -24,19 +26,19 @@ export const CitizenshipModal: React.FC<CitizenshipModalProps> = ({ isOpen, onCl
|
||||
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-2xl">
|
||||
🏛️ Digital Kurdistan Citizenship
|
||||
🏛️ {t('citizenModal.title')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{referrerAddress
|
||||
? 'You have been invited to join Digital Kurdistan! Complete the application below.'
|
||||
: 'Join the Digital Kurdistan State as a citizen or authenticate your existing citizenship'}
|
||||
? t('citizenModal.invitedDesc')
|
||||
: t('citizenModal.defaultDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as 'existing' | 'new')} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="existing">I am Already a Citizen</TabsTrigger>
|
||||
<TabsTrigger value="new">I Want to Become a Citizen</TabsTrigger>
|
||||
<TabsTrigger value="existing">{t('citizenModal.existingTab')}</TabsTrigger>
|
||||
<TabsTrigger value="new">{t('citizenModal.newTab')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="existing" className="mt-6">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -15,6 +16,7 @@ interface ExistingCitizenAuthProps {
|
||||
}
|
||||
|
||||
export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClose }) => {
|
||||
const { t } = useTranslation();
|
||||
const { peopleApi, isPeopleReady, selectedAccount, connectWallet } = usePezkuwi();
|
||||
|
||||
const [citizenNumber, setCitizenNumber] = useState('');
|
||||
@@ -24,12 +26,12 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
|
||||
const handleVerifyNFT = async () => {
|
||||
if (!peopleApi || !isPeopleReady || !selectedAccount) {
|
||||
setError('Please connect your wallet first');
|
||||
setError(t('existingAuth.connectWallet'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!citizenNumber.trim()) {
|
||||
setError('Please enter your Citizen Number');
|
||||
setError(t('existingAuth.enterCitizenNumber'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -41,7 +43,7 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
const isValid = await verifyCitizenNumber(peopleApi, citizenNumber, selectedAccount.address);
|
||||
|
||||
if (!isValid) {
|
||||
setError(`Invalid Citizen Number or it doesn't match your wallet`);
|
||||
setError(t('existingAuth.invalidNumber'));
|
||||
setStep('error');
|
||||
return;
|
||||
}
|
||||
@@ -52,14 +54,14 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
setStep('signing');
|
||||
} catch {
|
||||
if (import.meta.env.DEV) console.error('Verification error:', err);
|
||||
setError('Failed to verify Citizen Number');
|
||||
setError(t('existingAuth.verificationFailed'));
|
||||
setStep('error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignChallenge = async () => {
|
||||
if (!selectedAccount || !challenge) {
|
||||
setError('Missing authentication data');
|
||||
setError(t('existingAuth.missingAuthData'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -73,7 +75,7 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
const isValid = await verifySignature(signature, challenge, selectedAccount.address);
|
||||
|
||||
if (!isValid) {
|
||||
setError('Signature verification failed');
|
||||
setError(t('existingAuth.signatureFailed'));
|
||||
setStep('error');
|
||||
return;
|
||||
}
|
||||
@@ -98,7 +100,7 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
}, 2000);
|
||||
} catch {
|
||||
if (import.meta.env.DEV) console.error('Signature error:', err);
|
||||
setError('Failed to sign authentication challenge');
|
||||
setError(t('existingAuth.signError'));
|
||||
setStep('error');
|
||||
}
|
||||
};
|
||||
@@ -107,7 +109,7 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
try {
|
||||
await connectWallet();
|
||||
} catch {
|
||||
setError('Failed to connect wallet');
|
||||
setError(t('existingAuth.walletConnectFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -117,10 +119,10 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Shield className="h-5 w-5 text-cyan-500" />
|
||||
Authenticate as Citizen
|
||||
{t('existingAuth.title')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your Citizen Number from your Dashboard to authenticate
|
||||
{t('existingAuth.description')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
@@ -128,7 +130,7 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
{step === 'input' && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="citizenNumber">Citizen Number</Label>
|
||||
<Label htmlFor="citizenNumber">{t('existingAuth.citizenNumber')}</Label>
|
||||
<Input
|
||||
id="citizenNumber"
|
||||
placeholder="#42-0-123456"
|
||||
@@ -138,17 +140,17 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
className="bg-gray-100 dark:bg-gray-800 placeholder:text-gray-400 dark:placeholder:text-gray-500 font-mono"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Enter your full Citizen Number from your Dashboard (format: #CollectionID-ItemID-6digits)
|
||||
{t('existingAuth.citizenNumberHint')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!selectedAccount ? (
|
||||
<Button onClick={handleConnectWallet} className="w-full">
|
||||
Connect Wallet First
|
||||
{t('existingAuth.connectWalletFirst')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleVerifyNFT} className="w-full">
|
||||
Verify Citizen Number
|
||||
{t('existingAuth.verifyCitizenNumber')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
@@ -158,7 +160,7 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
{step === 'verifying' && (
|
||||
<div className="flex flex-col items-center justify-center py-8 space-y-4">
|
||||
<Loader2 className="h-12 w-12 animate-spin text-cyan-500" />
|
||||
<p className="text-sm text-muted-foreground">Verifying Citizen Number on blockchain...</p>
|
||||
<p className="text-sm text-muted-foreground">{t('existingAuth.verifying')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -168,19 +170,19 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
<Alert>
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
<AlertDescription>
|
||||
NFT ownership verified! Now sign to prove you control this wallet.
|
||||
{t('existingAuth.nftVerified')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="bg-muted p-4 rounded-lg space-y-2">
|
||||
<p className="text-sm font-medium">Authentication Challenge:</p>
|
||||
<p className="text-sm font-medium">{t('existingAuth.authChallenge')}</p>
|
||||
<p className="text-xs text-muted-foreground font-mono break-all">
|
||||
{challenge?.nonce}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleSignChallenge} className="w-full">
|
||||
Sign Message to Authenticate
|
||||
{t('existingAuth.signMessage')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -189,12 +191,12 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
{step === 'success' && (
|
||||
<div className="flex flex-col items-center justify-center py-8 space-y-4">
|
||||
<CheckCircle className="h-16 w-16 text-green-500" />
|
||||
<h3 className="text-lg font-semibold">Authentication Successful!</h3>
|
||||
<h3 className="text-lg font-semibold">{t('existingAuth.authSuccess')}</h3>
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
Welcome back, Citizen #{citizenNumber}
|
||||
{t('existingAuth.welcomeBack', { number: citizenNumber })}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Redirecting to citizen dashboard...
|
||||
{t('existingAuth.redirecting')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -209,7 +211,7 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
|
||||
{step === 'error' && (
|
||||
<Button onClick={() => { setStep('input'); setError(null); }} variant="outline" className="w-full">
|
||||
Try Again
|
||||
{t('existingAuth.tryAgain')}
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
@@ -221,13 +223,13 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
<div className="space-y-2 text-sm">
|
||||
<h4 className="font-semibold flex items-center gap-2">
|
||||
<Shield className="h-4 w-4" />
|
||||
Security Information
|
||||
{t('existingAuth.securityInfo')}
|
||||
</h4>
|
||||
<ul className="space-y-1 text-xs text-muted-foreground">
|
||||
<li>• Your NFT number is cryptographically verified on-chain</li>
|
||||
<li>• Signature proves you control the wallet without revealing private keys</li>
|
||||
<li>• Session expires after 24 hours for your security</li>
|
||||
<li>• No personal data is transmitted or stored on-chain</li>
|
||||
<li>• {t('existingAuth.securityNft')}</li>
|
||||
<li>• {t('existingAuth.securitySig')}</li>
|
||||
<li>• {t('existingAuth.securityExpire')}</li>
|
||||
<li>• {t('existingAuth.securityNoData')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Loader2, AlertTriangle, CheckCircle, User, Users as UsersIcon, MapPin, Briefcase, Mail, Check, X, AlertCircle } from 'lucide-react';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { blake2AsHex } from '@pezkuwi/util-crypto';
|
||||
import type { CitizenshipData, Region, MaritalStatus, KycStatus } from '@pezkuwi/lib/citizenship-workflow';
|
||||
import { submitKycApplication, subscribeToKycApproval, getKycStatus, cancelApplication, confirmCitizenship } from '@pezkuwi/lib/citizenship-workflow';
|
||||
@@ -25,6 +26,7 @@ type FormData = Omit<CitizenshipData, 'walletAddress' | 'timestamp'>;
|
||||
export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ onClose, referrerAddress }) => {
|
||||
// identityKyc pallet is on People Chain
|
||||
const { peopleApi, isPeopleReady, selectedAccount, connectWallet } = usePezkuwi();
|
||||
const { t } = useTranslation();
|
||||
const { register, handleSubmit, watch, setValue, formState: { errors } } = useForm<FormData>();
|
||||
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
@@ -42,7 +44,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
|
||||
const handleConfirmCitizenship = async () => {
|
||||
if (!peopleApi || !isPeopleReady || !selectedAccount) {
|
||||
setError('Please connect your wallet and wait for People Chain connection');
|
||||
setError(t('newCitizen.connectWalletError'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -52,7 +54,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
const result = await confirmCitizenship(peopleApi, selectedAccount);
|
||||
|
||||
if (!result.success) {
|
||||
setError(result.error || 'Failed to confirm citizenship');
|
||||
setError(result.error || t('newCitizen.failedToConfirm'));
|
||||
setConfirming(false);
|
||||
return;
|
||||
}
|
||||
@@ -66,7 +68,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) console.error('Confirmation error:', err);
|
||||
setError((err as Error).message || 'Failed to confirm citizenship');
|
||||
setError((err as Error).message || t('newCitizen.failedToConfirm'));
|
||||
} finally {
|
||||
setConfirming(false);
|
||||
}
|
||||
@@ -74,7 +76,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
|
||||
const handleCancelApplication = async () => {
|
||||
if (!peopleApi || !isPeopleReady || !selectedAccount) {
|
||||
setError('Please connect your wallet and wait for People Chain connection');
|
||||
setError(t('newCitizen.connectWalletError'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -84,7 +86,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
const result = await cancelApplication(peopleApi, selectedAccount);
|
||||
|
||||
if (!result.success) {
|
||||
setError(result.error || 'Failed to cancel application');
|
||||
setError(result.error || t('newCitizen.failedToCancel'));
|
||||
setCanceling(false);
|
||||
return;
|
||||
}
|
||||
@@ -94,7 +96,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
window.location.href = '/';
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) console.error('Cancel error:', err);
|
||||
setError((err as Error).message || 'Failed to cancel application');
|
||||
setError((err as Error).message || t('newCitizen.failedToCancel'));
|
||||
} finally {
|
||||
setCanceling(false);
|
||||
}
|
||||
@@ -168,12 +170,12 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
const onSubmit = async (data: FormData) => {
|
||||
// identityKyc pallet is on People Chain
|
||||
if (!peopleApi || !isPeopleReady || !selectedAccount) {
|
||||
setError('Please connect your wallet and wait for People Chain connection');
|
||||
setError(t('newCitizen.connectWalletError'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!agreed) {
|
||||
setError('Please agree to the terms');
|
||||
setError(t('newCitizen.agreeToTerms'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -185,7 +187,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
const status = await getKycStatus(peopleApi, selectedAccount.address);
|
||||
|
||||
if (status === 'Approved') {
|
||||
setError('Your citizenship is already approved! Redirecting to dashboard...');
|
||||
setError(t('newCitizen.alreadyApproved'));
|
||||
setKycApproved(true);
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
@@ -195,7 +197,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
}
|
||||
|
||||
if (status === 'PendingReferral' || status === 'ReferrerApproved') {
|
||||
setError('You already have a pending citizenship application.');
|
||||
setError(t('newCitizen.alreadyPending'));
|
||||
setCurrentStatus(status);
|
||||
return;
|
||||
}
|
||||
@@ -245,7 +247,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
setError(result.error || 'Failed to submit citizenship application');
|
||||
setError(result.error || t('newCitizen.failedToSubmit'));
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
@@ -262,7 +264,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) console.error('Submission error:', err);
|
||||
setError('Failed to submit citizenship application');
|
||||
setError(t('newCitizen.failedToSubmit'));
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
@@ -271,14 +273,14 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Connect Wallet Required</CardTitle>
|
||||
<CardTitle>{t('newCitizen.connectRequired')}</CardTitle>
|
||||
<CardDescription>
|
||||
You need to connect your wallet to apply for citizenship
|
||||
{t('newCitizen.connectDesc')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button onClick={connectWallet} className="w-full">
|
||||
Connect Wallet
|
||||
{t('newCitizen.connectWallet')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -291,9 +293,9 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
<Card>
|
||||
<CardContent className="pt-6 flex flex-col items-center justify-center py-8 space-y-4">
|
||||
<CheckCircle className="h-16 w-16 text-green-500 animate-pulse" />
|
||||
<h3 className="text-lg font-semibold text-center text-green-500">KYC Approved!</h3>
|
||||
<h3 className="text-lg font-semibold text-center text-green-500">{t('newCitizen.kycApproved')}</h3>
|
||||
<p className="text-sm text-muted-foreground text-center max-w-md">
|
||||
Congratulations! Your citizenship application has been approved. Redirecting to citizen dashboard...
|
||||
{t('newCitizen.kycApprovedDesc')}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -312,9 +314,9 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
</div>
|
||||
|
||||
<div className="text-center space-y-2">
|
||||
<h3 className="text-lg font-semibold">Waiting for Referrer Approval</h3>
|
||||
<h3 className="text-lg font-semibold">{t('newCitizen.waitingReferrer')}</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-md">
|
||||
Your application has been submitted. Your referrer needs to vouch for your identity before you can proceed.
|
||||
{t('newCitizen.waitingReferrerDesc')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -324,8 +326,8 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
<Check className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">Application Submitted</p>
|
||||
<p className="text-xs text-muted-foreground">Transaction hash: {applicationHash || 'Confirmed'}</p>
|
||||
<p className="text-sm font-medium">{t('newCitizen.appSubmitted')}</p>
|
||||
<p className="text-xs text-muted-foreground">{t('newCitizen.txHash', { hash: applicationHash || 'Confirmed' })}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -334,8 +336,8 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
<Check className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">1 HEZ Deposit Reserved</p>
|
||||
<p className="text-xs text-muted-foreground">Deposit will be returned if you cancel</p>
|
||||
<p className="text-sm font-medium">{t('newCitizen.depositReserved')}</p>
|
||||
<p className="text-xs text-muted-foreground">{t('newCitizen.depositReturn')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -344,8 +346,8 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
<Loader2 className="h-5 w-5 text-yellow-600 dark:text-yellow-400 animate-spin" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">Waiting for Referrer</p>
|
||||
<p className="text-xs text-muted-foreground">Your referrer must approve your identity</p>
|
||||
<p className="text-sm font-medium">{t('newCitizen.waitingForReferrer')}</p>
|
||||
<p className="text-xs text-muted-foreground">{t('newCitizen.referrerMustApprove')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -360,12 +362,12 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
{canceling ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Canceling...
|
||||
{t('newCitizen.canceling')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
Cancel Application
|
||||
{t('newCitizen.cancelApplication')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -379,7 +381,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
)}
|
||||
|
||||
<Button variant="outline" onClick={onClose} className="mt-2">
|
||||
Close
|
||||
{t('newCitizen.close')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -398,9 +400,9 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
</div>
|
||||
|
||||
<div className="text-center space-y-2">
|
||||
<h3 className="text-lg font-semibold text-green-600">Referrer Approved!</h3>
|
||||
<h3 className="text-lg font-semibold text-green-600">{t('newCitizen.referrerApproved')}</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-md">
|
||||
Your referrer has vouched for you. Confirm your citizenship to mint your Welati Tiki NFT.
|
||||
{t('newCitizen.referrerApprovedDesc')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -410,7 +412,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
<Check className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">Application Submitted</p>
|
||||
<p className="text-sm font-medium">{t('newCitizen.appSubmitted')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -419,7 +421,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
<Check className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">Referrer Approved</p>
|
||||
<p className="text-sm font-medium">{t('newCitizen.referrerApproved')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -428,8 +430,8 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
<AlertCircle className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">Confirm Citizenship</p>
|
||||
<p className="text-xs text-muted-foreground">Click below to mint your Welati Tiki NFT</p>
|
||||
<p className="text-sm font-medium">{t('newCitizen.confirmCitizenship')}</p>
|
||||
<p className="text-xs text-muted-foreground">{t('newCitizen.confirmMintDesc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -443,12 +445,12 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
{confirming ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Confirming...
|
||||
{t('newCitizen.confirming')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
Confirm Citizenship
|
||||
{t('newCitizen.confirmCitizenship')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -462,7 +464,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
)}
|
||||
|
||||
<Button variant="outline" onClick={onClose} className="mt-2">
|
||||
Close
|
||||
{t('newCitizen.close')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -475,9 +477,9 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
<Card>
|
||||
<CardContent className="pt-6 flex flex-col items-center justify-center py-8 space-y-4">
|
||||
<Loader2 className="h-16 w-16 text-cyan-500 animate-spin" />
|
||||
<h3 className="text-lg font-semibold text-center">Processing Application...</h3>
|
||||
<h3 className="text-lg font-semibold text-center">{t('newCitizen.processing')}</h3>
|
||||
<p className="text-sm text-muted-foreground text-center max-w-md">
|
||||
Encrypting your data and submitting to the blockchain. Please wait...
|
||||
{t('newCitizen.processingDesc')}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -491,32 +493,32 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<User className="h-5 w-5" />
|
||||
Nasnameya Kesane (Personal Identity)
|
||||
{t('newCitizen.personalIdentity')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="fullName">Navê Te (Your Full Name) *</Label>
|
||||
<Label htmlFor="fullName">{t('newCitizen.fullName')} *</Label>
|
||||
<Input {...register('fullName', { required: true })} placeholder="e.g., Berzê Ronahî" />
|
||||
{errors.fullName && <p className="text-xs text-red-500">Required</p>}
|
||||
{errors.fullName && <p className="text-xs text-red-500">{t('newCitizen.required')}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="fatherName">Navê Bavê Te (Father's Name) *</Label>
|
||||
<Label htmlFor="fatherName">{t('newCitizen.fatherName')} *</Label>
|
||||
<Input {...register('fatherName', { required: true })} placeholder="e.g., Şêrko" />
|
||||
{errors.fatherName && <p className="text-xs text-red-500">Required</p>}
|
||||
{errors.fatherName && <p className="text-xs text-red-500">{t('newCitizen.required')}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="grandfatherName">Navê Bavkalê Te (Grandfather's Name) *</Label>
|
||||
<Label htmlFor="grandfatherName">{t('newCitizen.grandfatherName')} *</Label>
|
||||
<Input {...register('grandfatherName', { required: true })} placeholder="e.g., Welat" />
|
||||
{errors.grandfatherName && <p className="text-xs text-red-500">Required</p>}
|
||||
{errors.grandfatherName && <p className="text-xs text-red-500">{t('newCitizen.required')}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="motherName">Navê Dayika Te (Mother's Name) *</Label>
|
||||
<Label htmlFor="motherName">{t('newCitizen.motherName')} *</Label>
|
||||
<Input {...register('motherName', { required: true })} placeholder="e.g., Gula" />
|
||||
{errors.motherName && <p className="text-xs text-red-500">Required</p>}
|
||||
{errors.motherName && <p className="text-xs text-red-500">{t('newCitizen.required')}</p>}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -524,13 +526,13 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
{/* Tribal Affiliation */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Eşîra Te (Tribal Affiliation)</CardTitle>
|
||||
<CardTitle>{t('newCitizen.tribalAffiliation')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tribe">Eşîra Te (Your Tribe) *</Label>
|
||||
<Label htmlFor="tribe">{t('newCitizen.yourTribe')} *</Label>
|
||||
<Input {...register('tribe', { required: true })} placeholder="e.g., Barzanî, Soran, Hewramî..." />
|
||||
{errors.tribe && <p className="text-xs text-red-500">Required</p>}
|
||||
{errors.tribe && <p className="text-xs text-red-500">{t('newCitizen.required')}</p>}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -540,23 +542,23 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<UsersIcon className="h-5 w-5" />
|
||||
Rewşa Malbatê (Family Status)
|
||||
{t('newCitizen.familyStatus')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Zewicî / Nezewicî (Married / Unmarried) *</Label>
|
||||
<Label>{t('newCitizen.maritalStatus')} *</Label>
|
||||
<RadioGroup
|
||||
onValueChange={(value) => setValue('maritalStatus', value as MaritalStatus)}
|
||||
defaultValue="nezewici"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="zewici" id="married" />
|
||||
<Label htmlFor="married">Zewicî (Married)</Label>
|
||||
<Label htmlFor="married">{t('newCitizen.married')}</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="nezewici" id="unmarried" />
|
||||
<Label htmlFor="unmarried">Nezewicî (Unmarried)</Label>
|
||||
<Label htmlFor="unmarried">{t('newCitizen.unmarried')}</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
@@ -564,7 +566,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
{maritalStatus === 'zewici' && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="childrenCount">Hejmara Zarokan (Number of Children)</Label>
|
||||
<Label htmlFor="childrenCount">{t('newCitizen.childrenCount')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
{...register('childrenCount', { valueAsNumber: true })}
|
||||
@@ -575,17 +577,17 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
|
||||
{childrenCount && childrenCount > 0 && (
|
||||
<div className="space-y-3">
|
||||
<Label>Navên Zarokan (Children's Names)</Label>
|
||||
<Label>{t('newCitizen.childrenNames')}</Label>
|
||||
{Array.from({ length: childrenCount }).map((_, i) => (
|
||||
<div key={i} className="grid grid-cols-2 gap-2">
|
||||
<Input
|
||||
{...register(`children.${i}.name` as const)}
|
||||
placeholder={`Zaroka ${i + 1} - Nav`}
|
||||
placeholder={t('newCitizen.childName', { number: i + 1 })}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
{...register(`children.${i}.birthYear` as const, { valueAsNumber: true })}
|
||||
placeholder="Sala Dayikbûnê"
|
||||
placeholder={t('newCitizen.birthYear')}
|
||||
min="1900"
|
||||
max={new Date().getFullYear()}
|
||||
/>
|
||||
@@ -603,26 +605,26 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<MapPin className="h-5 w-5" />
|
||||
Herêma Te (Your Region)
|
||||
{t('newCitizen.yourRegion')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="region">Ji Kuderê yî? (Where are you from?) *</Label>
|
||||
<Label htmlFor="region">{t('newCitizen.whereAreYouFrom')} *</Label>
|
||||
<Select onValueChange={(value) => setValue('region', value as Region)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Herêmeke hilbijêre (Select a region)" />
|
||||
<SelectValue placeholder={t('newCitizen.selectRegion')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="bakur">Bakur (North - Turkey/Türkiye)</SelectItem>
|
||||
<SelectItem value="basur">Başûr (South - Iraq)</SelectItem>
|
||||
<SelectItem value="rojava">Rojava (West - Syria)</SelectItem>
|
||||
<SelectItem value="rojhelat">Rojhilat (East - Iran)</SelectItem>
|
||||
<SelectItem value="kurdistan_a_sor">Kurdistan a Sor (Red Kurdistan - Armenia/Azerbaijan)</SelectItem>
|
||||
<SelectItem value="diaspora">Diaspora (Living Abroad)</SelectItem>
|
||||
<SelectItem value="bakur">{t('newCitizen.bakur')}</SelectItem>
|
||||
<SelectItem value="basur">{t('newCitizen.basur')}</SelectItem>
|
||||
<SelectItem value="rojava">{t('newCitizen.rojava')}</SelectItem>
|
||||
<SelectItem value="rojhelat">{t('newCitizen.rojhelat')}</SelectItem>
|
||||
<SelectItem value="kurdistan_a_sor">{t('newCitizen.sorKurdistan')}</SelectItem>
|
||||
<SelectItem value="diaspora">{t('newCitizen.diaspora')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.region && <p className="text-xs text-red-500">Required</p>}
|
||||
{errors.region && <p className="text-xs text-red-500">{t('newCitizen.required')}</p>}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -632,7 +634,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Briefcase className="h-5 w-5" />
|
||||
Têkilî û Pîşe (Contact & Profession)
|
||||
{t('newCitizen.contactProfession')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
@@ -646,13 +648,13 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
{...register('email', { required: true, pattern: /^\S+@\S+$/i })}
|
||||
placeholder="example@email.com"
|
||||
/>
|
||||
{errors.email && <p className="text-xs text-red-500">Valid email required</p>}
|
||||
{errors.email && <p className="text-xs text-red-500">{t('newCitizen.validEmailRequired')}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="profession">Pîşeya Te (Your Profession) *</Label>
|
||||
<Label htmlFor="profession">{t('newCitizen.yourProfession')} *</Label>
|
||||
<Input {...register('profession', { required: true })} placeholder="e.g., Mamosta, Bijîşk, Xebatkar..." />
|
||||
{errors.profession && <p className="text-xs text-red-500">Required</p>}
|
||||
{errors.profession && <p className="text-xs text-red-500">{t('newCitizen.required')}</p>}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -660,15 +662,15 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
{/* Referral */}
|
||||
<Card className="bg-purple-500/10 border-purple-500/30">
|
||||
<CardHeader>
|
||||
<CardTitle>Koda Referral (Referral Code - Optional)</CardTitle>
|
||||
<CardTitle>{t('newCitizen.referralCode')}</CardTitle>
|
||||
<CardDescription>
|
||||
If you were invited by another citizen, enter their referral code
|
||||
{t('newCitizen.referralDesc')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Input {...register('referralCode')} placeholder="Referral code (optional)" className="placeholder:text-gray-500 placeholder:opacity-50" />
|
||||
<Input {...register('referralCode')} placeholder={t('newCitizen.referralPlaceholder')} className="placeholder:text-gray-500 placeholder:opacity-50" />
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
If empty, you will be automatically linked to the Founder (Satoshi Qazi Muhammed)
|
||||
{t('newCitizen.referralNote')}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -679,9 +681,9 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertTriangle className="h-5 w-5 text-yellow-500 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm">
|
||||
<p className="font-semibold text-yellow-600">1 HEZ Deposit Required</p>
|
||||
<p className="font-semibold text-yellow-600">{t('newCitizen.depositRequired')}</p>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
A deposit of 1 HEZ will be reserved when you submit your application. It will be returned if you cancel your application.
|
||||
{t('newCitizen.depositDesc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -694,11 +696,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
<div className="flex items-start space-x-2">
|
||||
<Checkbox id="terms" checked={agreed} onCheckedChange={(checked) => setAgreed(checked as boolean)} />
|
||||
<Label htmlFor="terms" className="text-sm leading-relaxed cursor-pointer">
|
||||
Ez pejirandim ku daneyên min bi awayekî ewle (ZK-proof) tên hilanîn û li ser blockchain-ê hash-a wan tê tomarkirin.
|
||||
<br />
|
||||
<span className="text-xs text-muted-foreground">
|
||||
(I agree that my data is securely stored with ZK-proof and only its hash is recorded on the blockchain)
|
||||
</span>
|
||||
{t('newCitizen.termsAgree')}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
@@ -713,10 +711,10 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
{submitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Şandina Daxwazê...
|
||||
{t('newCitizen.submitting')}
|
||||
</>
|
||||
) : (
|
||||
'Şandina Daxwazê (Submit Application)'
|
||||
t('newCitizen.submitApplication')
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -17,6 +18,7 @@ interface Proposal {
|
||||
}
|
||||
|
||||
export function CommissionProposalsCard() {
|
||||
const { t } = useTranslation();
|
||||
const { api, isApiReady, selectedAccount } = usePezkuwi();
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -98,8 +100,8 @@ export function CommissionProposalsCard() {
|
||||
const handleVote = async (proposal: Proposal, approve: boolean) => {
|
||||
if (!api || !selectedAccount) {
|
||||
toast({
|
||||
title: 'Wallet Not Connected',
|
||||
description: 'Please connect your wallet first',
|
||||
title: t('proposalCard.walletNotConnected'),
|
||||
description: t('proposalCard.connectFirst'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -132,7 +134,7 @@ export function CommissionProposalsCard() {
|
||||
}
|
||||
|
||||
toast({
|
||||
title: 'Vote Failed',
|
||||
title: t('proposalCard.voteFailed'),
|
||||
description: errorMessage,
|
||||
variant: 'destructive',
|
||||
});
|
||||
@@ -150,13 +152,13 @@ export function CommissionProposalsCard() {
|
||||
|
||||
if (executedEvent) {
|
||||
toast({
|
||||
title: 'Proposal Passed!',
|
||||
description: 'Threshold reached and executed. KYC approved!',
|
||||
title: t('proposalCard.proposalPassed'),
|
||||
description: t('proposalCard.thresholdReached'),
|
||||
});
|
||||
} else if (votedEvent) {
|
||||
toast({
|
||||
title: 'Vote Recorded',
|
||||
description: `Your ${approve ? 'AYE' : 'NAY'} vote has been recorded`,
|
||||
title: t('proposalCard.voteRecorded'),
|
||||
description: approve ? t('proposalCard.ayeRecorded') : t('proposalCard.nayRecorded'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -165,8 +167,8 @@ export function CommissionProposalsCard() {
|
||||
}
|
||||
).catch((error) => {
|
||||
toast({
|
||||
title: 'Transaction Error',
|
||||
description: error instanceof Error ? error.message : 'Failed to submit transaction',
|
||||
title: t('proposalCard.voteFailed'),
|
||||
description: error instanceof Error ? error.message : t('proposalCard.voteFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
reject(error);
|
||||
@@ -176,8 +178,8 @@ export function CommissionProposalsCard() {
|
||||
setTimeout(() => loadProposals(), 2000);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error instanceof Error ? error.message : 'Failed to vote',
|
||||
title: t('proposalCard.voteFailed'),
|
||||
description: error instanceof Error ? error.message : t('proposalCard.voteFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
@@ -188,8 +190,8 @@ export function CommissionProposalsCard() {
|
||||
const handleExecute = async (proposal: Proposal) => {
|
||||
if (!api || !selectedAccount) {
|
||||
toast({
|
||||
title: 'Wallet Not Connected',
|
||||
description: 'Please connect your wallet first',
|
||||
title: t('proposalCard.walletNotConnected'),
|
||||
description: t('proposalCard.connectFirst'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -231,7 +233,7 @@ export function CommissionProposalsCard() {
|
||||
}
|
||||
|
||||
toast({
|
||||
title: 'Execute Failed',
|
||||
title: t('proposalCard.executeFailed'),
|
||||
description: errorMessage,
|
||||
variant: 'destructive',
|
||||
});
|
||||
@@ -258,20 +260,20 @@ export function CommissionProposalsCard() {
|
||||
if (result && typeof result === 'object' && 'Err' in result) {
|
||||
if (import.meta.env.DEV) console.error('Execution failed:', result.Err);
|
||||
toast({
|
||||
title: 'Execution Failed',
|
||||
description: `Proposal closed but execution failed: ${JSON.stringify(result.Err)}`,
|
||||
title: t('proposalCard.executeFailed'),
|
||||
description: `${JSON.stringify(result.Err)}`,
|
||||
variant: 'destructive',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: 'Proposal Executed!',
|
||||
description: 'KYC approved and NFT minted successfully!',
|
||||
title: t('proposalCard.executeSuccess'),
|
||||
description: t('proposalCard.executeKycApproved'),
|
||||
});
|
||||
}
|
||||
} else if (closedEvent) {
|
||||
toast({
|
||||
title: 'Proposal Closed',
|
||||
description: 'Proposal has been closed',
|
||||
title: t('proposalCard.proposalClosed'),
|
||||
description: t('proposalCard.proposalClosedDesc'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -280,8 +282,8 @@ export function CommissionProposalsCard() {
|
||||
}
|
||||
).catch((error) => {
|
||||
toast({
|
||||
title: 'Transaction Error',
|
||||
description: error instanceof Error ? error.message : 'Failed to submit transaction',
|
||||
title: t('proposalCard.executeFailed'),
|
||||
description: error instanceof Error ? error.message : t('proposalCard.executeFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
reject(error);
|
||||
@@ -291,8 +293,8 @@ export function CommissionProposalsCard() {
|
||||
setTimeout(() => loadProposals(), 2000);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error instanceof Error ? error.message : 'Failed to execute proposal',
|
||||
title: t('proposalCard.executeFailed'),
|
||||
description: error instanceof Error ? error.message : t('proposalCard.executeFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
@@ -310,13 +312,13 @@ export function CommissionProposalsCard() {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Vote className="h-5 w-5" />
|
||||
Commission Proposals
|
||||
{t('proposalCard.title')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||||
<span>Loading proposals...</span>
|
||||
<span>{t('proposalCard.loading')}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -329,11 +331,11 @@ export function CommissionProposalsCard() {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Vote className="h-5 w-5" />
|
||||
Commission Proposals
|
||||
{t('proposalCard.title')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-center text-muted-foreground py-4">No active proposals</p>
|
||||
<p className="text-center text-muted-foreground py-4">{t('proposalCard.empty')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -344,7 +346,7 @@ export function CommissionProposalsCard() {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Vote className="h-5 w-5" />
|
||||
Commission Proposals ({proposals.length})
|
||||
{t('proposalCard.titleCount', { count: proposals.length })}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
@@ -357,11 +359,11 @@ export function CommissionProposalsCard() {
|
||||
<div key={proposal.hash} className="border rounded-lg p-4 space-y-3">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<p className="font-medium">Proposal #{proposal.proposalIndex}</p>
|
||||
<p className="text-sm text-muted-foreground">KYC Approval</p>
|
||||
<p className="font-medium">{t('proposalCard.proposal', { index: proposal.proposalIndex })}</p>
|
||||
<p className="text-sm text-muted-foreground">{t('proposalCard.kycApproval')}</p>
|
||||
</div>
|
||||
<Badge variant={progress >= 100 ? 'default' : 'secondary'} className={progress >= 100 ? 'bg-green-600' : ''}>
|
||||
{progress >= 100 ? 'PASSED' : `${progress.toFixed(0)}%`}
|
||||
{progress >= 100 ? t('proposalCard.passed') : `${progress.toFixed(0)}%`}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -387,11 +389,11 @@ export function CommissionProposalsCard() {
|
||||
{voting === proposal.hash ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>Execute Proposal</>
|
||||
<>{t('proposalCard.execute')}</>
|
||||
)}
|
||||
</Button>
|
||||
) : hasVoted ? (
|
||||
<p className="text-sm text-green-600">✓ You already voted</p>
|
||||
<p className="text-sm text-green-600">{t('proposalCard.voted')}</p>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
@@ -405,7 +407,7 @@ export function CommissionProposalsCard() {
|
||||
) : (
|
||||
<>
|
||||
<ThumbsUp className="h-4 w-4 mr-1" />
|
||||
Aye
|
||||
{t('proposalCard.aye')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -420,7 +422,7 @@ export function CommissionProposalsCard() {
|
||||
) : (
|
||||
<>
|
||||
<ThumbsDown className="h-4 w-4 mr-1" />
|
||||
Nay
|
||||
{t('proposalCard.nay')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { X, Plus, AlertCircle, Loader2, CheckCircle, Info } from 'lucide-react';
|
||||
@@ -34,6 +35,7 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
||||
// Use Asset Hub API for DEX operations
|
||||
const { assetHubApi, isAssetHubReady } = usePezkuwi();
|
||||
const { account, signer } = useWallet();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [amount1Input, setAmount1Input] = useState('');
|
||||
const [amount2Input, setAmount2Input] = useState('');
|
||||
@@ -125,22 +127,22 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
||||
};
|
||||
|
||||
const validateInputs = (): string | null => {
|
||||
if (!pool) return 'No pool selected';
|
||||
if (!amount1Input || !amount2Input) return 'Please enter amounts';
|
||||
if (!pool) return t('addLiquidity.noPoolSelected');
|
||||
if (!amount1Input || !amount2Input) return t('addLiquidity.enterAmounts');
|
||||
|
||||
const amount1Raw = parseTokenInput(amount1Input, pool.asset1Decimals);
|
||||
const amount2Raw = parseTokenInput(amount2Input, pool.asset2Decimals);
|
||||
|
||||
if (BigInt(amount1Raw) <= BigInt(0) || BigInt(amount2Raw) <= BigInt(0)) {
|
||||
return 'Amounts must be greater than zero';
|
||||
return t('common.amountGtZero');
|
||||
}
|
||||
|
||||
if (BigInt(amount1Raw) > BigInt(balance1)) {
|
||||
return `Insufficient ${pool.asset1Symbol} balance`;
|
||||
return t('common.insufficientBalance', { symbol: pool.asset1Symbol });
|
||||
}
|
||||
|
||||
if (BigInt(amount2Raw) > BigInt(balance2)) {
|
||||
return `Insufficient ${pool.asset2Symbol} balance`;
|
||||
return t('common.insufficientBalance', { symbol: pool.asset2Symbol });
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -148,7 +150,7 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
||||
|
||||
const handleAddLiquidity = async () => {
|
||||
if (!assetHubApi || !isAssetHubReady || !signer || !account || !pool) {
|
||||
setErrorMessage('Wallet not connected');
|
||||
setErrorMessage(t('common.walletNotConnected'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -213,7 +215,7 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
||||
);
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Add liquidity failed:', error);
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Transaction failed');
|
||||
setErrorMessage(error instanceof Error ? error.message : t('common.txFailed'));
|
||||
setTxStatus('error');
|
||||
}
|
||||
};
|
||||
@@ -248,7 +250,7 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
||||
<CardHeader className="border-b border-gray-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-xl font-bold text-white">
|
||||
Add Liquidity
|
||||
{t('addLiquidity.title')}
|
||||
</CardTitle>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -259,7 +261,7 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-sm text-gray-400 mt-2">
|
||||
{pool.asset1Symbol} / {pool.asset2Symbol} Pool
|
||||
{t('addLiquidity.pool', { asset1: pool.asset1Symbol, asset2: pool.asset2Symbol })}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -268,7 +270,7 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
||||
<div className="flex items-start gap-2 p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
|
||||
<Info className="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<span className="text-sm text-blue-400">
|
||||
Add liquidity in proportion to the pool's current ratio. You'll receive LP tokens representing your share.
|
||||
{t('addLiquidity.info')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -277,7 +279,7 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm text-gray-400">{pool.asset1Symbol}</label>
|
||||
<span className="text-xs text-gray-500">
|
||||
Balance: {formatTokenBalance(balance1, pool.asset1Decimals, 4)}
|
||||
{t('common.balance')}: {formatTokenBalance(balance1, pool.asset1Decimals, 4)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative">
|
||||
@@ -296,7 +298,7 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 px-3 py-1 bg-green-600/20 hover:bg-green-600/30 text-green-400 text-xs rounded border border-green-600/30 transition-colors"
|
||||
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||
>
|
||||
MAX
|
||||
{t('common.max')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -313,7 +315,7 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm text-gray-400">{pool.asset2Symbol}</label>
|
||||
<span className="text-xs text-gray-500">
|
||||
Balance: {formatTokenBalance(balance2, pool.asset2Decimals, 4)}
|
||||
{t('common.balance')}: {formatTokenBalance(balance2, pool.asset2Decimals, 4)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative">
|
||||
@@ -332,14 +334,14 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 px-3 py-1 bg-green-600/20 hover:bg-green-600/30 text-green-400 text-xs rounded border border-green-600/30 transition-colors"
|
||||
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||
>
|
||||
MAX
|
||||
{t('common.max')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Slippage Tolerance */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm text-gray-400">Slippage Tolerance</label>
|
||||
<label className="text-sm text-gray-400">{t('common.slippageTolerance')}</label>
|
||||
<div className="flex gap-2">
|
||||
{[0.5, 1, 2].map((value) => (
|
||||
<button
|
||||
@@ -362,11 +364,11 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
||||
{amount1Input && amount2Input && (
|
||||
<div className="p-4 bg-gray-800/50 rounded-lg border border-gray-700 space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-400">Share of Pool</span>
|
||||
<span className="text-gray-400">{t('addLiquidity.shareOfPool')}</span>
|
||||
<span className="text-white font-mono">{shareOfPool}%</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-400">Exchange Rate</span>
|
||||
<span className="text-gray-400">{t('common.exchangeRate')}</span>
|
||||
<span className="text-cyan-400 font-mono">
|
||||
1 {pool.asset1Symbol} ={' '}
|
||||
{(
|
||||
@@ -392,7 +394,7 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
||||
<div className="flex items-start gap-2 p-3 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||
<CheckCircle className="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5" />
|
||||
<span className="text-sm text-green-400">
|
||||
Liquidity added successfully!
|
||||
{t('addLiquidity.success')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -404,7 +406,7 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
||||
className="flex-1 px-6 py-3 bg-gray-800 hover:bg-gray-700 text-white rounded-lg transition-colors border border-gray-700"
|
||||
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||
>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAddLiquidity}
|
||||
@@ -418,21 +420,21 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
||||
{txStatus === 'signing' && (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Signing...
|
||||
{t('common.signing')}
|
||||
</>
|
||||
)}
|
||||
{txStatus === 'submitting' && (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Adding...
|
||||
{t('addLiquidity.adding')}
|
||||
</>
|
||||
)}
|
||||
{txStatus === 'idle' && 'Add Liquidity'}
|
||||
{txStatus === 'error' && 'Retry'}
|
||||
{txStatus === 'idle' && t('addLiquidity.title')}
|
||||
{txStatus === 'error' && t('common.retry')}
|
||||
{txStatus === 'success' && (
|
||||
<>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
Success
|
||||
{t('common.success')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { KNOWN_TOKENS, NATIVE_TOKEN_ID } from '@/types/dex';
|
||||
import { parseTokenInput, formatTokenBalance } from '@pezkuwi/utils/dex';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface CreatePoolModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -23,6 +24,7 @@ export const CreatePoolModal: React.FC<CreatePoolModalProps> = ({
|
||||
// Use Asset Hub API for DEX operations (assetConversion pallet is on Asset Hub)
|
||||
const { assetHubApi, isAssetHubReady } = usePezkuwi();
|
||||
const { account, signer } = useWallet();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [asset1Id, setAsset1Id] = useState<number | null>(null);
|
||||
const [asset2Id, setAsset2Id] = useState<number | null>(null);
|
||||
@@ -99,22 +101,22 @@ export const CreatePoolModal: React.FC<CreatePoolModalProps> = ({
|
||||
|
||||
const validateInputs = (): string | null => {
|
||||
if (asset1Id === null || asset2Id === null) {
|
||||
return 'Please select both tokens';
|
||||
return t('createPool.selectBothTokens');
|
||||
}
|
||||
|
||||
if (asset1Id === asset2Id) {
|
||||
return 'Cannot create pool with same token';
|
||||
return t('createPool.sameToken');
|
||||
}
|
||||
|
||||
if (!amount1Input || !amount2Input) {
|
||||
return 'Please enter amounts for both tokens';
|
||||
return t('createPool.enterBothAmounts');
|
||||
}
|
||||
|
||||
const token1 = KNOWN_TOKENS[asset1Id];
|
||||
const token2 = KNOWN_TOKENS[asset2Id];
|
||||
|
||||
if (!token1 || !token2) {
|
||||
return 'Invalid token selected';
|
||||
return t('createPool.invalidToken');
|
||||
}
|
||||
|
||||
const amount1Raw = parseTokenInput(amount1Input, token1.decimals);
|
||||
@@ -134,15 +136,15 @@ export const CreatePoolModal: React.FC<CreatePoolModalProps> = ({
|
||||
});
|
||||
|
||||
if (BigInt(amount1Raw) <= BigInt(0) || BigInt(amount2Raw) <= BigInt(0)) {
|
||||
return 'Amounts must be greater than zero';
|
||||
return t('common.amountGtZero');
|
||||
}
|
||||
|
||||
if (BigInt(amount1Raw) > BigInt(balance1)) {
|
||||
return `Insufficient ${token1.symbol} balance`;
|
||||
return t('common.insufficientBalance', { symbol: token1.symbol });
|
||||
}
|
||||
|
||||
if (BigInt(amount2Raw) > BigInt(balance2)) {
|
||||
return `Insufficient ${token2.symbol} balance`;
|
||||
return t('common.insufficientBalance', { symbol: token2.symbol });
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -150,13 +152,13 @@ export const CreatePoolModal: React.FC<CreatePoolModalProps> = ({
|
||||
|
||||
const handleCreatePool = async () => {
|
||||
if (!assetHubApi || !isAssetHubReady || !signer || !account) {
|
||||
setErrorMessage('Wallet not connected or Asset Hub not ready');
|
||||
setErrorMessage(t('createPool.walletNotReady'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if assetConversion pallet is available on Asset Hub
|
||||
if (!assetHubApi.tx.assetConversion || !assetHubApi.tx.assetConversion.createPool) {
|
||||
setErrorMessage('AssetConversion pallet is not available on Asset Hub. Pool creation requires this pallet.');
|
||||
setErrorMessage(t('createPool.palletNotAvailable'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -238,7 +240,7 @@ export const CreatePoolModal: React.FC<CreatePoolModalProps> = ({
|
||||
);
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Pool creation failed:', error);
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Transaction failed');
|
||||
setErrorMessage(error instanceof Error ? error.message : t('common.txFailed'));
|
||||
setTxStatus('error');
|
||||
}
|
||||
};
|
||||
@@ -259,7 +261,7 @@ export const CreatePoolModal: React.FC<CreatePoolModalProps> = ({
|
||||
<CardHeader className="border-b border-gray-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-xl font-bold text-white">
|
||||
Create New Pool
|
||||
{t('createPool.title')}
|
||||
</CardTitle>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -270,21 +272,21 @@ export const CreatePoolModal: React.FC<CreatePoolModalProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
<Badge className="bg-green-600/20 text-green-400 border-green-600/30 w-fit mt-2">
|
||||
Founder Only
|
||||
{t('createPool.founderOnly')}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6 pt-6">
|
||||
{/* Token 1 Selection */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm text-gray-400">Token 1</label>
|
||||
<label className="text-sm text-gray-400">{t('createPool.token1')}</label>
|
||||
<select
|
||||
value={asset1Id ?? ''}
|
||||
onChange={(e) => setAsset1Id(Number(e.target.value))}
|
||||
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||
>
|
||||
<option value="">Select token...</option>
|
||||
<option value="">{t('createPool.selectToken')}</option>
|
||||
{availableTokens.map((token) => (
|
||||
<option key={token.id} value={token.id}>
|
||||
{token.symbol} - {token.name}
|
||||
@@ -293,7 +295,7 @@ export const CreatePoolModal: React.FC<CreatePoolModalProps> = ({
|
||||
</select>
|
||||
{token1 && (
|
||||
<div className="text-xs text-gray-500">
|
||||
Balance: {formatTokenBalance(balance1, token1.decimals, 4)} {token1.symbol}
|
||||
{t('common.balance')}: {formatTokenBalance(balance1, token1.decimals, 4)} {token1.symbol}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -302,7 +304,7 @@ export const CreatePoolModal: React.FC<CreatePoolModalProps> = ({
|
||||
{token1 && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm text-gray-400">
|
||||
Amount of {token1.symbol}
|
||||
{t('createPool.amountOf', { symbol: token1.symbol })}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -324,14 +326,14 @@ export const CreatePoolModal: React.FC<CreatePoolModalProps> = ({
|
||||
|
||||
{/* Token 2 Selection */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm text-gray-400">Token 2</label>
|
||||
<label className="text-sm text-gray-400">{t('createPool.token2')}</label>
|
||||
<select
|
||||
value={asset2Id ?? ''}
|
||||
onChange={(e) => setAsset2Id(Number(e.target.value))}
|
||||
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||
>
|
||||
<option value="">Select token...</option>
|
||||
<option value="">{t('createPool.selectToken')}</option>
|
||||
{availableTokens.map((token) => (
|
||||
<option key={token.id} value={token.id} disabled={token.id === asset1Id}>
|
||||
{token.symbol} - {token.name}
|
||||
@@ -340,7 +342,7 @@ export const CreatePoolModal: React.FC<CreatePoolModalProps> = ({
|
||||
</select>
|
||||
{token2 && (
|
||||
<div className="text-xs text-gray-500">
|
||||
Balance: {formatTokenBalance(balance2, token2.decimals, 4)} {token2.symbol}
|
||||
{t('common.balance')}: {formatTokenBalance(balance2, token2.decimals, 4)} {token2.symbol}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -349,7 +351,7 @@ export const CreatePoolModal: React.FC<CreatePoolModalProps> = ({
|
||||
{token2 && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm text-gray-400">
|
||||
Amount of {token2.symbol}
|
||||
{t('createPool.amountOf', { symbol: token2.symbol })}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -365,7 +367,7 @@ export const CreatePoolModal: React.FC<CreatePoolModalProps> = ({
|
||||
{/* Exchange Rate Preview */}
|
||||
{token1 && token2 && amount1Input && amount2Input && (
|
||||
<div className="p-4 bg-gray-800/50 rounded-lg border border-gray-700">
|
||||
<div className="text-sm text-gray-400 mb-2">Initial Exchange Rate</div>
|
||||
<div className="text-sm text-gray-400 mb-2">{t('createPool.initialRate')}</div>
|
||||
<div className="text-white font-mono">
|
||||
1 {token1.symbol} = {exchangeRate} {token2.symbol}
|
||||
</div>
|
||||
@@ -385,7 +387,7 @@ export const CreatePoolModal: React.FC<CreatePoolModalProps> = ({
|
||||
<div className="flex items-start gap-2 p-3 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||
<CheckCircle className="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5" />
|
||||
<span className="text-sm text-green-400">
|
||||
Pool created successfully!
|
||||
{t('createPool.success')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -397,7 +399,7 @@ export const CreatePoolModal: React.FC<CreatePoolModalProps> = ({
|
||||
className="flex-1 px-6 py-3 bg-gray-800 hover:bg-gray-700 text-white rounded-lg transition-colors border border-gray-700"
|
||||
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||
>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCreatePool}
|
||||
@@ -411,21 +413,21 @@ export const CreatePoolModal: React.FC<CreatePoolModalProps> = ({
|
||||
{txStatus === 'signing' && (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Signing...
|
||||
{t('common.signing')}
|
||||
</>
|
||||
)}
|
||||
{txStatus === 'submitting' && (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Creating...
|
||||
{t('createPool.creating')}
|
||||
</>
|
||||
)}
|
||||
{txStatus === 'idle' && 'Create Pool'}
|
||||
{txStatus === 'error' && 'Retry'}
|
||||
{txStatus === 'idle' && t('createPool.createPool')}
|
||||
{txStatus === 'error' && t('common.retry')}
|
||||
{txStatus === 'success' && (
|
||||
<>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
Success
|
||||
{t('common.success')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
// import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
@@ -16,6 +17,7 @@ import { isFounderWallet } from '@pezkuwi/utils/auth';
|
||||
|
||||
// DEX Dashboard - Asset Hub API migration complete
|
||||
export const DEXDashboard: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { account } = useWallet();
|
||||
const { sudoKey } = usePezkuwi();
|
||||
const [activeTab, setActiveTab] = useState('swap');
|
||||
@@ -53,17 +55,17 @@ export const DEXDashboard: React.FC = () => {
|
||||
<div className="bg-gradient-to-r from-green-900/30 via-yellow-900/30 to-red-900/30 border-b border-gray-800 py-8">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<h1 className="text-4xl font-bold mb-2 bg-gradient-to-r from-green-400 via-yellow-400 to-red-400 bg-clip-text text-transparent">
|
||||
Pezkuwi DEX
|
||||
{t('dex.title')}
|
||||
</h1>
|
||||
<p className="text-gray-400 text-lg">
|
||||
Decentralized exchange for trading tokens on PezkuwiChain
|
||||
{t('dex.description')}
|
||||
</p>
|
||||
|
||||
{/* Wallet status */}
|
||||
{account && (
|
||||
<div className="mt-4 flex items-center gap-4">
|
||||
<div className="px-4 py-2 bg-gray-900/80 rounded-lg border border-gray-800">
|
||||
<span className="text-xs text-gray-400">Connected: </span>
|
||||
<span className="text-xs text-gray-400">{t('dex.connected')} </span>
|
||||
<span className="text-sm font-mono text-white">
|
||||
{account.slice(0, 6)}...{account.slice(-4)}
|
||||
</span>
|
||||
@@ -71,7 +73,7 @@ export const DEXDashboard: React.FC = () => {
|
||||
{isFounder && (
|
||||
<div className="px-4 py-2 bg-green-600/20 border border-green-600/30 rounded-lg">
|
||||
<span className="text-xs text-green-400 font-semibold">
|
||||
FOUNDER ACCESS
|
||||
{t('dex.founderAccess')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -85,7 +87,7 @@ export const DEXDashboard: React.FC = () => {
|
||||
{!account ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="mb-4 text-gray-400 text-lg">
|
||||
Please connect your Pezkuwi wallet to use the DEX
|
||||
{t('dex.connectWallet')}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -93,16 +95,16 @@ export const DEXDashboard: React.FC = () => {
|
||||
<TabsList className={`grid w-full ${isFounder ? 'grid-cols-3' : 'grid-cols-2'} gap-2 bg-gray-900/50 p-1 rounded-lg mb-8`}>
|
||||
<TabsTrigger value="swap" className="flex items-center gap-2">
|
||||
<ArrowRightLeft className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Swap</span>
|
||||
<span className="hidden sm:inline">{t('dex.tabs.swap')}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="pools" className="flex items-center gap-2">
|
||||
<Droplet className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Pools</span>
|
||||
<span className="hidden sm:inline">{t('dex.tabs.pools')}</span>
|
||||
</TabsTrigger>
|
||||
{isFounder && (
|
||||
<TabsTrigger value="admin" className="flex items-center gap-2">
|
||||
<Settings className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Admin</span>
|
||||
<span className="hidden sm:inline">{t('dex.tabs.admin')}</span>
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
@@ -119,23 +121,23 @@ export const DEXDashboard: React.FC = () => {
|
||||
<TabsContent value="admin" className="mt-6">
|
||||
<div className="max-w-2xl mx-auto space-y-6">
|
||||
<div className="p-6 bg-gray-900 border border-blue-900/30 rounded-lg">
|
||||
<h3 className="text-xl font-bold text-white mb-2">Token Wrapping</h3>
|
||||
<h3 className="text-xl font-bold text-white mb-2">{t('dex.admin.tokenWrapping')}</h3>
|
||||
<p className="text-gray-400 mb-6">
|
||||
Convert native HEZ to wrapped wHEZ for use in DEX pools
|
||||
{t('dex.admin.tokenWrappingDesc')}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setShowInitializeHezPoolModal(true)}
|
||||
className="w-full px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors font-medium"
|
||||
>
|
||||
Wrap HEZ to wHEZ
|
||||
{t('dex.admin.wrapHez')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Token Minting Section */}
|
||||
<div className="p-6 bg-gray-900 border border-gray-800 rounded-lg">
|
||||
<h3 className="text-xl font-bold text-white mb-2">Token Minting</h3>
|
||||
<h3 className="text-xl font-bold text-white mb-2">{t('dex.admin.tokenMinting')}</h3>
|
||||
<p className="text-gray-400 mb-6">
|
||||
Mint wrapped tokens for testing and liquidity provision
|
||||
{t('dex.admin.tokenMintingDesc')}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
<button
|
||||
@@ -143,62 +145,62 @@ export const DEXDashboard: React.FC = () => {
|
||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors font-medium"
|
||||
>
|
||||
<img src="/tokens/USDT.png" alt="USDT" className="w-5 h-5 rounded-full" />
|
||||
Mint wUSDT
|
||||
{t('dex.admin.mintToken', { symbol: 'wUSDT' })}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMintModalAsset(MINTABLE_ASSETS.wDOT)}
|
||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-pink-600 hover:bg-pink-700 text-white rounded-lg transition-colors font-medium"
|
||||
>
|
||||
<img src="/tokens/DOT.png" alt="DOT" className="w-5 h-5 rounded-full" />
|
||||
Mint wDOT
|
||||
{t('dex.admin.mintToken', { symbol: 'wDOT' })}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMintModalAsset(MINTABLE_ASSETS.wETH)}
|
||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors font-medium"
|
||||
>
|
||||
<img src="/tokens/ETH.png" alt="ETH" className="w-5 h-5 rounded-full" />
|
||||
Mint wETH
|
||||
{t('dex.admin.mintToken', { symbol: 'wETH' })}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMintModalAsset(MINTABLE_ASSETS.wBTC)}
|
||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-orange-600 hover:bg-orange-700 text-white rounded-lg transition-colors font-medium"
|
||||
>
|
||||
<img src="/tokens/BTC.png" alt="BTC" className="w-5 h-5 rounded-full" />
|
||||
Mint wBTC
|
||||
{t('dex.admin.mintToken', { symbol: 'wBTC' })}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-gray-900 border border-purple-900/30 rounded-lg">
|
||||
<h3 className="text-xl font-bold text-white mb-2">XCM Configuration Wizard</h3>
|
||||
<h3 className="text-xl font-bold text-white mb-2">{t('dex.admin.xcmWizard')}</h3>
|
||||
<p className="text-gray-400 mb-6">
|
||||
Complete 6-step parachain setup: Reserve ParaId, generate artifacts, register parachain, open HRMP channels, register foreign assets, and test XCM transfers.
|
||||
{t('dex.admin.xcmWizardDesc')}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setShowXcmBridgeModal(true)}
|
||||
className="w-full px-6 py-3 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors font-medium"
|
||||
>
|
||||
Open XCM Configuration Wizard
|
||||
{t('dex.admin.openXcmWizard')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-gray-900 border border-gray-800 rounded-lg">
|
||||
<h3 className="text-xl font-bold text-white mb-2">Pool Management</h3>
|
||||
<h3 className="text-xl font-bold text-white mb-2">{t('dex.admin.poolManagement')}</h3>
|
||||
<p className="text-gray-400 mb-6">
|
||||
Create new liquidity pools for token pairs on PezkuwiChain
|
||||
{t('dex.admin.poolManagementDesc')}
|
||||
</p>
|
||||
<button
|
||||
onClick={handleCreatePool}
|
||||
className="w-full px-6 py-3 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors font-medium"
|
||||
>
|
||||
Create New Pool
|
||||
{t('dex.admin.createNewPool')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-gray-900 border border-gray-800 rounded-lg">
|
||||
<h3 className="text-xl font-bold text-white mb-2">Pool Statistics</h3>
|
||||
<h3 className="text-xl font-bold text-white mb-2">{t('dex.admin.poolStatistics')}</h3>
|
||||
<p className="text-gray-400 text-sm">
|
||||
View detailed pool statistics in the Pools tab
|
||||
{t('dex.admin.poolStatsDesc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface InitializeHezPoolModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -28,6 +29,7 @@ export const InitializeHezPoolModal: React.FC<InitializeHezPoolModalProps> = ({
|
||||
const { assetHubApi, isAssetHubReady } = usePezkuwi();
|
||||
const { account, signer } = useWallet();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [mode, setMode] = useState<WrapMode>('wrap');
|
||||
const [amount, setAmount] = useState('10000');
|
||||
@@ -112,18 +114,18 @@ export const InitializeHezPoolModal: React.FC<InitializeHezPoolModalProps> = ({
|
||||
const handleTransaction = async () => {
|
||||
if (!assetHubApi || !isAssetHubReady || !signer || !account) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Please connect your wallet and wait for Asset Hub connection',
|
||||
title: t('common.error'),
|
||||
description: t('mint.connectWallet'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!palletAvailable) {
|
||||
setErrorMessage('TokenWrapper pallet is not available on Asset Hub.');
|
||||
setErrorMessage(t('tokenWrapping.palletNotAvailable'));
|
||||
toast({
|
||||
title: 'Pallet Not Available',
|
||||
description: 'The TokenWrapper pallet is not deployed on Asset Hub.',
|
||||
title: t('mint.palletToast'),
|
||||
description: t('tokenWrapping.palletNotAvailable'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -132,13 +134,13 @@ export const InitializeHezPoolModal: React.FC<InitializeHezPoolModalProps> = ({
|
||||
const amountRaw = BigInt(parseFloat(amount) * 10 ** 12);
|
||||
|
||||
if (amountRaw <= BigInt(0)) {
|
||||
setErrorMessage('Amount must be greater than zero');
|
||||
setErrorMessage(t('common.amountGtZero'));
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceBalance = mode === 'wrap' ? hezBalance : whezBalance;
|
||||
if (amountRaw > BigInt(sourceBalance)) {
|
||||
setErrorMessage(`Insufficient ${mode === 'wrap' ? 'HEZ' : 'wHEZ'} balance`);
|
||||
setErrorMessage(t('common.insufficientBalance', { symbol: mode === 'wrap' ? 'HEZ' : 'wHEZ' }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -184,7 +186,7 @@ export const InitializeHezPoolModal: React.FC<InitializeHezPoolModalProps> = ({
|
||||
setErrorMessage(errorMsg);
|
||||
setTxStatus('error');
|
||||
toast({
|
||||
title: 'Transaction Failed',
|
||||
title: t('common.txFailed'),
|
||||
description: errorMsg,
|
||||
variant: 'destructive',
|
||||
});
|
||||
@@ -193,10 +195,10 @@ export const InitializeHezPoolModal: React.FC<InitializeHezPoolModalProps> = ({
|
||||
if (import.meta.env.DEV) console.log('📋 Events:', events.map(e => e.event.method).join(', '));
|
||||
setTxStatus('success');
|
||||
toast({
|
||||
title: 'Success!',
|
||||
title: t('common.success'),
|
||||
description: isWrap
|
||||
? `Successfully wrapped ${amount} HEZ to wHEZ`
|
||||
: `Successfully unwrapped ${amount} wHEZ to HEZ`,
|
||||
? t('tokenWrapping.wrapSuccess', { amount })
|
||||
: t('tokenWrapping.unwrapSuccess', { amount }),
|
||||
});
|
||||
onSuccess?.();
|
||||
}
|
||||
@@ -205,11 +207,11 @@ export const InitializeHezPoolModal: React.FC<InitializeHezPoolModalProps> = ({
|
||||
);
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Transaction failed:', error);
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Transaction failed');
|
||||
setErrorMessage(error instanceof Error ? error.message : t('common.txFailed'));
|
||||
setTxStatus('error');
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error instanceof Error ? error.message : 'Transaction failed',
|
||||
title: t('common.error'),
|
||||
description: error instanceof Error ? error.message : t('common.txFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@@ -232,7 +234,7 @@ export const InitializeHezPoolModal: React.FC<InitializeHezPoolModalProps> = ({
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<ArrowDownUp className="w-5 h-5" />
|
||||
Token Wrapping
|
||||
{t('tokenWrapping.title')}
|
||||
</CardTitle>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -243,7 +245,7 @@ export const InitializeHezPoolModal: React.FC<InitializeHezPoolModalProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
<Badge className="bg-blue-600/20 text-blue-400 border-blue-600/30 w-fit mt-2">
|
||||
Admin Only - Token Wrapping
|
||||
{t('tokenWrapping.adminOnly')}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
|
||||
@@ -253,7 +255,7 @@ export const InitializeHezPoolModal: React.FC<InitializeHezPoolModalProps> = ({
|
||||
<Alert className="bg-yellow-500/10 border-yellow-500/30">
|
||||
<AlertCircle className="h-4 w-4 text-yellow-400" />
|
||||
<AlertDescription className="text-yellow-300 text-sm">
|
||||
<strong>TokenWrapper pallet not deployed.</strong> Please contact the development team.
|
||||
<strong>{t('tokenWrapping.palletNotDeployed')}</strong> {t('tokenWrapping.contactDev')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -273,7 +275,7 @@ export const InitializeHezPoolModal: React.FC<InitializeHezPoolModalProps> = ({
|
||||
<Alert className="bg-blue-500/10 border-blue-500/30">
|
||||
<Info className="h-4 w-4 text-blue-400" />
|
||||
<AlertDescription className="text-blue-300 text-sm">
|
||||
Convert native HEZ to wHEZ (wrapped). Ratio: 1:1
|
||||
{t('tokenWrapping.wrapInfo')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</TabsContent>
|
||||
@@ -282,7 +284,7 @@ export const InitializeHezPoolModal: React.FC<InitializeHezPoolModalProps> = ({
|
||||
<Alert className="bg-green-500/10 border-green-500/30">
|
||||
<Info className="h-4 w-4 text-green-400" />
|
||||
<AlertDescription className="text-green-300 text-sm">
|
||||
Convert wHEZ back to native HEZ. Ratio: 1:1
|
||||
{t('tokenWrapping.unwrapInfo')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</TabsContent>
|
||||
@@ -291,13 +293,13 @@ export const InitializeHezPoolModal: React.FC<InitializeHezPoolModalProps> = ({
|
||||
{/* Balances Display */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className={`p-4 rounded-lg border ${mode === 'wrap' ? 'bg-blue-500/10 border-blue-500/30' : 'bg-gray-800/50 border-gray-700'}`}>
|
||||
<div className="text-sm text-gray-400 mb-1">Native HEZ</div>
|
||||
<div className="text-sm text-gray-400 mb-1">{t('tokenWrapping.nativeHez')}</div>
|
||||
<div className="text-xl font-bold text-blue-400 font-mono">
|
||||
{hezBalanceDisplay}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`p-4 rounded-lg border ${mode === 'unwrap' ? 'bg-green-500/10 border-green-500/30' : 'bg-gray-800/50 border-gray-700'}`}>
|
||||
<div className="text-sm text-gray-400 mb-1">Wrapped wHEZ</div>
|
||||
<div className="text-sm text-gray-400 mb-1">{t('tokenWrapping.wrappedWhez')}</div>
|
||||
<div className="text-xl font-bold text-cyan-400 font-mono">
|
||||
{whezBalanceDisplay}
|
||||
</div>
|
||||
@@ -307,9 +309,9 @@ export const InitializeHezPoolModal: React.FC<InitializeHezPoolModalProps> = ({
|
||||
{/* Amount Input */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm text-gray-400">{sourceToken} Amount</label>
|
||||
<label className="text-sm text-gray-400">{t('tokenWrapping.amount', { token: sourceToken })}</label>
|
||||
<span className="text-xs text-gray-500">
|
||||
Available: {sourceBalance} {sourceToken}
|
||||
{t('tokenWrapping.available')} {sourceBalance} {sourceToken}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative">
|
||||
@@ -326,11 +328,11 @@ export const InitializeHezPoolModal: React.FC<InitializeHezPoolModalProps> = ({
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 px-3 py-1 bg-green-600/20 hover:bg-green-600/30 text-green-400 text-xs rounded border border-green-600/30 transition-colors"
|
||||
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||
>
|
||||
MAX
|
||||
{t('common.max')}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
💡 You will receive {amount} {targetToken} (1:1 ratio)
|
||||
{t('tokenWrapping.receiveInfo', { amount, token: targetToken })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -349,7 +351,7 @@ export const InitializeHezPoolModal: React.FC<InitializeHezPoolModalProps> = ({
|
||||
<Alert className="bg-green-500/10 border-green-500/30">
|
||||
<CheckCircle className="h-4 w-4 text-green-400" />
|
||||
<AlertDescription className="text-green-300 text-sm">
|
||||
Successfully {mode === 'wrap' ? 'wrapped' : 'unwrapped'} {amount} {sourceToken} to {targetToken}!
|
||||
{mode === 'wrap' ? t('tokenWrapping.wrapSuccess', { amount }) : t('tokenWrapping.unwrapSuccess', { amount })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -362,7 +364,7 @@ export const InitializeHezPoolModal: React.FC<InitializeHezPoolModalProps> = ({
|
||||
className="flex-1 border-gray-700 hover:bg-gray-800"
|
||||
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||
>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleTransaction}
|
||||
@@ -376,21 +378,21 @@ export const InitializeHezPoolModal: React.FC<InitializeHezPoolModalProps> = ({
|
||||
{txStatus === 'signing' && (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
||||
Signing...
|
||||
{t('common.signing')}
|
||||
</>
|
||||
)}
|
||||
{txStatus === 'submitting' && (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
||||
{mode === 'wrap' ? 'Wrapping...' : 'Unwrapping...'}
|
||||
{mode === 'wrap' ? t('tokenWrapping.wrapping') : t('tokenWrapping.unwrapping')}
|
||||
</>
|
||||
)}
|
||||
{txStatus === 'idle' && (mode === 'wrap' ? 'Wrap HEZ → wHEZ' : 'Unwrap wHEZ → HEZ')}
|
||||
{txStatus === 'error' && 'Retry'}
|
||||
{txStatus === 'idle' && (mode === 'wrap' ? t('tokenWrapping.wrapBtn') : t('tokenWrapping.unwrapBtn'))}
|
||||
{txStatus === 'error' && t('common.retry')}
|
||||
{txStatus === 'success' && (
|
||||
<>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
Success
|
||||
{t('common.success')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface InitializeUsdtModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -29,6 +30,7 @@ export const InitializeUsdtModal: React.FC<InitializeUsdtModalProps> = ({
|
||||
const { assetHubApi, isAssetHubReady } = usePezkuwi();
|
||||
const { account, signer } = useWallet();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [usdtAmount, setUsdtAmount] = useState('10000');
|
||||
|
||||
@@ -66,8 +68,8 @@ export const InitializeUsdtModal: React.FC<InitializeUsdtModalProps> = ({
|
||||
const handleMint = async () => {
|
||||
if (!assetHubApi || !isAssetHubReady || !signer || !account) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Please connect your wallet and wait for Asset Hub connection',
|
||||
title: t('common.error'),
|
||||
description: t('mint.connectWallet'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -75,10 +77,10 @@ export const InitializeUsdtModal: React.FC<InitializeUsdtModalProps> = ({
|
||||
|
||||
// Check if assets pallet is available on Asset Hub
|
||||
if (!assetHubApi.tx.assets || !assetHubApi.tx.assets.mint) {
|
||||
setErrorMessage('Assets pallet is not available on Asset Hub.');
|
||||
setErrorMessage(t('mint.palletNotAvailable'));
|
||||
toast({
|
||||
title: 'Pallet Not Available',
|
||||
description: 'The Assets pallet is not deployed on Asset Hub.',
|
||||
title: t('mint.palletToast'),
|
||||
description: t('mint.palletToastDesc'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -87,7 +89,7 @@ export const InitializeUsdtModal: React.FC<InitializeUsdtModalProps> = ({
|
||||
const usdtAmountRaw = BigInt(parseFloat(usdtAmount) * 10 ** USDT_DECIMALS);
|
||||
|
||||
if (usdtAmountRaw <= BigInt(0)) {
|
||||
setErrorMessage('Amount must be greater than zero');
|
||||
setErrorMessage(t('common.amountGtZero'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -129,7 +131,7 @@ export const InitializeUsdtModal: React.FC<InitializeUsdtModalProps> = ({
|
||||
setErrorMessage(errorMsg);
|
||||
setTxStatus('error');
|
||||
toast({
|
||||
title: 'Transaction Failed',
|
||||
title: t('common.txFailed'),
|
||||
description: errorMsg,
|
||||
variant: 'destructive',
|
||||
});
|
||||
@@ -138,8 +140,8 @@ export const InitializeUsdtModal: React.FC<InitializeUsdtModalProps> = ({
|
||||
if (import.meta.env.DEV) console.log('📋 Events:', events.map(e => e.event.method).join(', '));
|
||||
setTxStatus('success');
|
||||
toast({
|
||||
title: 'Success!',
|
||||
description: `Successfully minted ${usdtAmount} wUSDT`,
|
||||
title: t('common.success'),
|
||||
description: t('mintUsdt.minted', { amount: usdtAmount }),
|
||||
});
|
||||
setTimeout(() => {
|
||||
onSuccess?.();
|
||||
@@ -151,11 +153,11 @@ export const InitializeUsdtModal: React.FC<InitializeUsdtModalProps> = ({
|
||||
);
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Mint failed:', error);
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Transaction failed');
|
||||
setErrorMessage(error instanceof Error ? error.message : t('common.txFailed'));
|
||||
setTxStatus('error');
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error instanceof Error ? error.message : 'Mint failed',
|
||||
title: t('common.error'),
|
||||
description: error instanceof Error ? error.message : t('common.txFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@@ -171,7 +173,7 @@ export const InitializeUsdtModal: React.FC<InitializeUsdtModalProps> = ({
|
||||
<CardHeader className="border-b border-gray-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-xl font-bold text-white">
|
||||
Mint wUSDT Tokens
|
||||
{t('mintUsdt.title')}
|
||||
</CardTitle>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -182,7 +184,7 @@ export const InitializeUsdtModal: React.FC<InitializeUsdtModalProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
<Badge className="bg-green-600/20 text-green-400 border-green-600/30 w-fit mt-2">
|
||||
Admin Only - Token Minting
|
||||
{t('mintUsdt.adminOnly')}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
|
||||
@@ -191,17 +193,16 @@ export const InitializeUsdtModal: React.FC<InitializeUsdtModalProps> = ({
|
||||
<Alert className="bg-green-500/10 border-green-500/30">
|
||||
<Info className="h-4 w-4 text-green-400" />
|
||||
<AlertDescription className="text-green-300 text-sm">
|
||||
Mint wUSDT (Wrapped USDT) tokens for testing and liquidity pool creation.
|
||||
Use responsibly!
|
||||
{t('mintUsdt.info')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{/* USDT Amount */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm text-gray-400">wUSDT Amount</label>
|
||||
<label className="text-sm text-gray-400">{t('mintUsdt.amount')}</label>
|
||||
<span className="text-xs text-gray-500">
|
||||
Current: {wusdtBalanceDisplay} wUSDT
|
||||
{t('mint.current')} {wusdtBalanceDisplay} wUSDT
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative">
|
||||
@@ -231,13 +232,13 @@ export const InitializeUsdtModal: React.FC<InitializeUsdtModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
💡 wUSDT has 6 decimals (same as real USDT)
|
||||
{t('mintUsdt.decimalsInfo')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Current wUSDT Balance */}
|
||||
<div className="p-4 bg-gray-800/50 rounded-lg border border-gray-700">
|
||||
<div className="text-sm text-gray-400 mb-1">Current wUSDT Balance</div>
|
||||
<div className="text-sm text-gray-400 mb-1">{t('mintUsdt.currentBalance')}</div>
|
||||
<div className="text-2xl font-bold text-green-400 font-mono">
|
||||
{wusdtBalanceDisplay} wUSDT
|
||||
</div>
|
||||
@@ -258,7 +259,7 @@ export const InitializeUsdtModal: React.FC<InitializeUsdtModalProps> = ({
|
||||
<Alert className="bg-green-500/10 border-green-500/30">
|
||||
<CheckCircle className="h-4 w-4 text-green-400" />
|
||||
<AlertDescription className="text-green-300 text-sm">
|
||||
Successfully minted {usdtAmount} wUSDT!
|
||||
{t('mintUsdt.success', { amount: usdtAmount })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -271,7 +272,7 @@ export const InitializeUsdtModal: React.FC<InitializeUsdtModalProps> = ({
|
||||
className="flex-1 border-gray-700 hover:bg-gray-800"
|
||||
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||
>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleMint}
|
||||
@@ -285,21 +286,21 @@ export const InitializeUsdtModal: React.FC<InitializeUsdtModalProps> = ({
|
||||
{txStatus === 'signing' && (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
||||
Signing...
|
||||
{t('common.signing')}
|
||||
</>
|
||||
)}
|
||||
{txStatus === 'submitting' && (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
||||
Minting...
|
||||
{t('mintUsdt.minting')}
|
||||
</>
|
||||
)}
|
||||
{txStatus === 'idle' && 'Mint wUSDT'}
|
||||
{txStatus === 'error' && 'Retry'}
|
||||
{txStatus === 'idle' && t('mintUsdt.mintBtn')}
|
||||
{txStatus === 'error' && t('common.retry')}
|
||||
{txStatus === 'success' && (
|
||||
<>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
Success
|
||||
{t('common.success')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import type { AssetConfig } from './mintableAssets';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface MintAssetModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -28,6 +29,7 @@ export const MintAssetModal: React.FC<MintAssetModalProps> = ({
|
||||
const { assetHubApi, isAssetHubReady } = usePezkuwi();
|
||||
const { account, signer } = useWallet();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [amount, setAmount] = useState(asset.defaultAmount || '1000');
|
||||
const [balance, setBalance] = useState<string>('0');
|
||||
@@ -73,18 +75,18 @@ export const MintAssetModal: React.FC<MintAssetModalProps> = ({
|
||||
const handleMint = async () => {
|
||||
if (!assetHubApi || !isAssetHubReady || !signer || !account) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Please connect your wallet and wait for Asset Hub connection',
|
||||
title: t('common.error'),
|
||||
description: t('mint.connectWallet'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!assetHubApi.tx.assets || !assetHubApi.tx.assets.mint) {
|
||||
setErrorMessage('Assets pallet is not available on Asset Hub.');
|
||||
setErrorMessage(t('mint.palletNotAvailable'));
|
||||
toast({
|
||||
title: 'Pallet Not Available',
|
||||
description: 'The Assets pallet is not deployed on Asset Hub.',
|
||||
title: t('mint.palletToast'),
|
||||
description: t('mint.palletToastDesc'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -93,7 +95,7 @@ export const MintAssetModal: React.FC<MintAssetModalProps> = ({
|
||||
const amountRaw = BigInt(Math.floor(parseFloat(amount) * 10 ** asset.decimals));
|
||||
|
||||
if (amountRaw <= BigInt(0)) {
|
||||
setErrorMessage('Amount must be greater than zero');
|
||||
setErrorMessage(t('common.amountGtZero'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -135,7 +137,7 @@ export const MintAssetModal: React.FC<MintAssetModalProps> = ({
|
||||
setErrorMessage(errorMsg);
|
||||
setTxStatus('error');
|
||||
toast({
|
||||
title: 'Transaction Failed',
|
||||
title: t('common.txFailed'),
|
||||
description: errorMsg,
|
||||
variant: 'destructive',
|
||||
});
|
||||
@@ -143,8 +145,8 @@ export const MintAssetModal: React.FC<MintAssetModalProps> = ({
|
||||
if (import.meta.env.DEV) console.log('Mint successful!');
|
||||
setTxStatus('success');
|
||||
toast({
|
||||
title: 'Success!',
|
||||
description: `Successfully minted ${amount} ${asset.symbol}`,
|
||||
title: t('common.success'),
|
||||
description: t('mint.minted', { amount, symbol: asset.symbol }),
|
||||
});
|
||||
setTimeout(() => {
|
||||
onSuccess?.();
|
||||
@@ -156,11 +158,11 @@ export const MintAssetModal: React.FC<MintAssetModalProps> = ({
|
||||
);
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Mint failed:', error);
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Transaction failed');
|
||||
setErrorMessage(error instanceof Error ? error.message : t('common.txFailed'));
|
||||
setTxStatus('error');
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error instanceof Error ? error.message : 'Mint failed',
|
||||
title: t('common.error'),
|
||||
description: error instanceof Error ? error.message : t('common.txFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@@ -180,7 +182,7 @@ export const MintAssetModal: React.FC<MintAssetModalProps> = ({
|
||||
<img src={asset.logo} alt={asset.symbol} className="w-8 h-8 rounded-full" />
|
||||
)}
|
||||
<CardTitle className="text-xl font-bold text-white">
|
||||
Mint {asset.symbol}
|
||||
{t('mint.title', { symbol: asset.symbol })}
|
||||
</CardTitle>
|
||||
</div>
|
||||
<button
|
||||
@@ -192,7 +194,7 @@ export const MintAssetModal: React.FC<MintAssetModalProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
<Badge className={`${colors.bg} ${colors.text} ${colors.border} w-fit mt-2`}>
|
||||
Admin Only - Token Minting
|
||||
{t('mint.adminOnly')}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
|
||||
@@ -201,16 +203,16 @@ export const MintAssetModal: React.FC<MintAssetModalProps> = ({
|
||||
<Alert className={`${colors.bg} ${colors.border}`}>
|
||||
<Info className={`h-4 w-4 ${colors.text}`} />
|
||||
<AlertDescription className={`${colors.text.replace('400', '300')} text-sm`}>
|
||||
Mint {asset.name} tokens for testing and liquidity pool creation.
|
||||
{t('mint.info', { name: asset.name })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{/* Amount Input */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm text-gray-400">{asset.symbol} Amount</label>
|
||||
<label className="text-sm text-gray-400">{t('mint.amount', { symbol: asset.symbol })}</label>
|
||||
<span className="text-xs text-gray-500">
|
||||
Current: {balanceDisplay} {asset.symbol}
|
||||
{t('mint.current')}: {balanceDisplay} {asset.symbol}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative">
|
||||
@@ -240,13 +242,13 @@ export const MintAssetModal: React.FC<MintAssetModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
{asset.symbol} has {asset.decimals} decimals
|
||||
{t('mint.decimalsInfo', { symbol: asset.symbol, decimals: asset.decimals })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Current Balance */}
|
||||
<div className="p-4 bg-gray-800/50 rounded-lg border border-gray-700">
|
||||
<div className="text-sm text-gray-400 mb-1">Current {asset.symbol} Balance</div>
|
||||
<div className="text-sm text-gray-400 mb-1">{t('mint.currentBalance', { symbol: asset.symbol })}</div>
|
||||
<div className={`text-2xl font-bold ${colors.text} font-mono`}>
|
||||
{balanceDisplay} {asset.symbol}
|
||||
</div>
|
||||
@@ -267,7 +269,7 @@ export const MintAssetModal: React.FC<MintAssetModalProps> = ({
|
||||
<Alert className={`${colors.bg} ${colors.border}`}>
|
||||
<CheckCircle className={`h-4 w-4 ${colors.text}`} />
|
||||
<AlertDescription className={`${colors.text.replace('400', '300')} text-sm`}>
|
||||
Successfully minted {amount} {asset.symbol}!
|
||||
{t('mint.success', { amount, symbol: asset.symbol })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -280,7 +282,7 @@ export const MintAssetModal: React.FC<MintAssetModalProps> = ({
|
||||
className="flex-1 border-gray-700 hover:bg-gray-800"
|
||||
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||
>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleMint}
|
||||
@@ -294,21 +296,21 @@ export const MintAssetModal: React.FC<MintAssetModalProps> = ({
|
||||
{txStatus === 'signing' && (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
||||
Signing...
|
||||
{t('common.signing')}
|
||||
</>
|
||||
)}
|
||||
{txStatus === 'submitting' && (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
||||
Minting...
|
||||
{t('mint.minting')}
|
||||
</>
|
||||
)}
|
||||
{txStatus === 'idle' && `Mint ${asset.symbol}`}
|
||||
{txStatus === 'error' && 'Retry'}
|
||||
{txStatus === 'idle' && t('mint.mintBtn', { symbol: asset.symbol })}
|
||||
{txStatus === 'error' && t('common.retry')}
|
||||
{txStatus === 'success' && (
|
||||
<>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
Success
|
||||
{t('common.success')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
@@ -22,6 +23,7 @@ export const PoolBrowser: React.FC<PoolBrowserProps> = ({
|
||||
onSwap,
|
||||
onCreatePool,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
// Use Asset Hub API for DEX operations
|
||||
const { assetHubApi, isAssetHubReady, sudoKey } = usePezkuwi();
|
||||
const { account } = useWallet();
|
||||
@@ -64,7 +66,7 @@ export const PoolBrowser: React.FC<PoolBrowserProps> = ({
|
||||
});
|
||||
|
||||
if (loading && pools.length === 0) {
|
||||
return <LoadingState message="Loading liquidity pools..." />;
|
||||
return <LoadingState message={t('pools.loading')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -75,7 +77,7 @@ export const PoolBrowser: React.FC<PoolBrowserProps> = ({
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search pools by token..."
|
||||
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"
|
||||
@@ -88,7 +90,7 @@ export const PoolBrowser: React.FC<PoolBrowserProps> = ({
|
||||
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" />
|
||||
Create Pool
|
||||
{t('pools.createPool')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -99,8 +101,8 @@ export const PoolBrowser: React.FC<PoolBrowserProps> = ({
|
||||
<CardContent className="py-12">
|
||||
<div className="text-center text-gray-400">
|
||||
{searchTerm
|
||||
? 'No pools found matching your search'
|
||||
: 'No liquidity pools available yet'}
|
||||
? t('pools.noSearchResults')
|
||||
: t('pools.noPools')}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -134,6 +136,7 @@ const PoolCard: React.FC<PoolCardProps> = ({
|
||||
onRemoveLiquidity,
|
||||
onSwap,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const reserve1Display = formatTokenBalance(
|
||||
pool.reserve1,
|
||||
pool.asset1Decimals,
|
||||
@@ -161,7 +164,7 @@ const PoolCard: React.FC<PoolCardProps> = ({
|
||||
<span className="text-yellow-400">{pool.asset2Symbol}</span>
|
||||
</CardTitle>
|
||||
<Badge className="bg-green-500/10 text-green-400 border-green-500/20">
|
||||
Active
|
||||
{t('pools.active')}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -170,13 +173,13 @@ const PoolCard: React.FC<PoolCardProps> = ({
|
||||
{/* Reserves */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-400">Reserve {pool.asset1Symbol}</span>
|
||||
<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">Reserve {pool.asset2Symbol}</span>
|
||||
<span className="text-gray-400">{t('pools.reserve', { symbol: pool.asset2Symbol })}</span>
|
||||
<span className="text-white font-mono">
|
||||
{reserve2Display} {pool.asset2Symbol}
|
||||
</span>
|
||||
@@ -186,7 +189,7 @@ const PoolCard: React.FC<PoolCardProps> = ({
|
||||
{/* 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">Exchange Rate</span>
|
||||
<span className="text-gray-400">{t('common.exchangeRate')}</span>
|
||||
<span className="text-cyan-400 font-mono">
|
||||
1 {pool.asset1Symbol} = {rate} {pool.asset2Symbol}
|
||||
</span>
|
||||
@@ -196,21 +199,21 @@ const PoolCard: React.FC<PoolCardProps> = ({
|
||||
{/* 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">Fee</div>
|
||||
<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">Volume 24h</div>
|
||||
<div className="text-xs text-gray-500">{t('pools.volume24h')}</div>
|
||||
<div className="text-sm font-semibold text-white">
|
||||
{pool.volume24h || 'N/A'}
|
||||
{pool.volume24h || t('pools.na')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-xs text-gray-500">APR</div>
|
||||
<div className="text-xs text-gray-500">{t('pools.apr')}</div>
|
||||
<div className="text-sm font-semibold text-green-400">
|
||||
{pool.apr7d || 'N/A'}
|
||||
{pool.apr7d || t('pools.na')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -223,7 +226,7 @@ const PoolCard: React.FC<PoolCardProps> = ({
|
||||
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" />
|
||||
Add
|
||||
{t('pools.add')}
|
||||
</button>
|
||||
)}
|
||||
{onRemoveLiquidity && (
|
||||
@@ -231,7 +234,7 @@ const PoolCard: React.FC<PoolCardProps> = ({
|
||||
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"
|
||||
>
|
||||
Remove
|
||||
{t('pools.remove')}
|
||||
</button>
|
||||
)}
|
||||
{onSwap && (
|
||||
@@ -240,7 +243,7 @@ const PoolCard: React.FC<PoolCardProps> = ({
|
||||
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" />
|
||||
Swap
|
||||
{t('pools.swap')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { X, Minus, AlertCircle, Loader2, CheckCircle, Info } from 'lucide-react';
|
||||
@@ -34,6 +35,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
||||
// Use Asset Hub API for DEX operations (assetConversion pallet is on Asset Hub)
|
||||
const { assetHubApi, isAssetHubReady } = usePezkuwi();
|
||||
const { account, signer } = useWallet();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [lpTokenBalance, setLpTokenBalance] = useState<string>('0');
|
||||
const [removePercentage, setRemovePercentage] = useState(25);
|
||||
@@ -112,12 +114,12 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
||||
|
||||
const handleRemoveLiquidity = async () => {
|
||||
if (!assetHubApi || !isAssetHubReady || !signer || !account || !pool) {
|
||||
setErrorMessage('Wallet not connected');
|
||||
setErrorMessage(t('common.walletNotConnected'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (BigInt(lpTokenBalance) === BigInt(0)) {
|
||||
setErrorMessage('No liquidity to remove');
|
||||
setErrorMessage(t('removeLiquidity.noLiquidity'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -175,7 +177,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
||||
);
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Remove liquidity failed:', error);
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Transaction failed');
|
||||
setErrorMessage(error instanceof Error ? error.message : t('common.txFailed'));
|
||||
setTxStatus('error');
|
||||
}
|
||||
};
|
||||
@@ -190,7 +192,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
||||
<CardHeader className="border-b border-gray-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-xl font-bold text-white">
|
||||
Remove Liquidity
|
||||
{t('removeLiquidity.title')}
|
||||
</CardTitle>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -201,7 +203,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-sm text-gray-400 mt-2">
|
||||
{pool.asset1Symbol} / {pool.asset2Symbol} Pool
|
||||
{t('removeLiquidity.pool', { asset1: pool.asset1Symbol, asset2: pool.asset2Symbol })}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -210,13 +212,13 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
||||
<div className="flex items-start gap-2 p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
|
||||
<Info className="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<span className="text-sm text-blue-400">
|
||||
Remove liquidity to receive your tokens back. You'll burn LP tokens in proportion to your withdrawal.
|
||||
{t('removeLiquidity.info')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* LP Token Balance */}
|
||||
<div className="p-4 bg-gray-800/50 rounded-lg border border-gray-700">
|
||||
<div className="text-sm text-gray-400 mb-1">Your LP Tokens</div>
|
||||
<div className="text-sm text-gray-400 mb-1">{t('removeLiquidity.yourLpTokens')}</div>
|
||||
<div className="text-2xl font-bold text-white font-mono">
|
||||
{formatTokenBalance(lpTokenBalance, 12, 6)}
|
||||
</div>
|
||||
@@ -225,7 +227,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
||||
{/* Percentage Selector */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm text-gray-400">Remove Amount</label>
|
||||
<label className="text-sm text-gray-400">{t('removeLiquidity.removeAmount')}</label>
|
||||
<span className="text-lg font-bold text-white">{removePercentage}%</span>
|
||||
</div>
|
||||
|
||||
@@ -266,7 +268,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
||||
|
||||
{/* Output Preview */}
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm text-gray-400 mb-2">You will receive</div>
|
||||
<div className="text-sm text-gray-400 mb-2">{t('removeLiquidity.youWillReceive')}</div>
|
||||
|
||||
<div className="p-4 bg-gray-800 border border-gray-700 rounded-lg space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -286,7 +288,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
||||
|
||||
{/* Slippage Tolerance */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm text-gray-400">Slippage Tolerance</label>
|
||||
<label className="text-sm text-gray-400">{t('common.slippageTolerance')}</label>
|
||||
<div className="flex gap-2">
|
||||
{[0.5, 1, 2].map((value) => (
|
||||
<button
|
||||
@@ -318,7 +320,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
||||
<div className="flex items-start gap-2 p-3 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||
<CheckCircle className="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5" />
|
||||
<span className="text-sm text-green-400">
|
||||
Liquidity removed successfully!
|
||||
{t('removeLiquidity.success')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -330,7 +332,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
||||
className="flex-1 px-6 py-3 bg-gray-800 hover:bg-gray-700 text-white rounded-lg transition-colors border border-gray-700"
|
||||
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||
>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRemoveLiquidity}
|
||||
@@ -345,21 +347,21 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
||||
{txStatus === 'signing' && (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Signing...
|
||||
{t('common.signing')}
|
||||
</>
|
||||
)}
|
||||
{txStatus === 'submitting' && (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Removing...
|
||||
{t('removeLiquidity.removing')}
|
||||
</>
|
||||
)}
|
||||
{txStatus === 'idle' && 'Remove Liquidity'}
|
||||
{txStatus === 'error' && 'Retry'}
|
||||
{txStatus === 'idle' && t('removeLiquidity.removeLiquidity')}
|
||||
{txStatus === 'error' && t('common.retry')}
|
||||
{txStatus === 'success' && (
|
||||
<>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
Success
|
||||
{t('common.success')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { ArrowDownUp, AlertCircle, Loader2, Info, Settings, AlertTriangle, RefreshCw } from 'lucide-react';
|
||||
@@ -50,6 +51,7 @@ const getAvailableTokens = (pools: PoolInfo[]) => {
|
||||
};
|
||||
|
||||
export const SwapInterface: React.FC<SwapInterfaceProps> = ({ pools }) => {
|
||||
const { t } = useTranslation();
|
||||
// Use Asset Hub API for DEX operations
|
||||
const { assetHubApi, isAssetHubReady } = usePezkuwi();
|
||||
const { account, signer } = useWallet();
|
||||
@@ -247,8 +249,8 @@ export const SwapInterface: React.FC<SwapInterfaceProps> = ({ pools }) => {
|
||||
const handleConfirmSwap = async () => {
|
||||
if (!assetHubApi || !signer || !account || !fromTokenInfo || !toTokenInfo) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Please connect your wallet',
|
||||
title: t('common.error'),
|
||||
description: t('common.connectWalletAlert'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -256,8 +258,8 @@ export const SwapInterface: React.FC<SwapInterfaceProps> = ({ pools }) => {
|
||||
|
||||
if (!activePool) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'No liquidity pool available for this pair',
|
||||
title: t('common.error'),
|
||||
description: t('swap.noPool'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -340,15 +342,15 @@ export const SwapInterface: React.FC<SwapInterfaceProps> = ({ pools }) => {
|
||||
}
|
||||
setTxStatus('error');
|
||||
toast({
|
||||
title: 'Transaction Failed',
|
||||
title: t('swap.swapFailed'),
|
||||
description: errorMessage,
|
||||
variant: 'destructive',
|
||||
});
|
||||
} else {
|
||||
setTxStatus('success');
|
||||
toast({
|
||||
title: 'Success!',
|
||||
description: `Swapped ${fromAmount} ${fromToken} for ~${toAmount} ${toToken}`,
|
||||
title: t('common.success'),
|
||||
description: t('swap.swapped', { fromAmount, fromToken, toAmount, toToken }),
|
||||
});
|
||||
setTimeout(() => {
|
||||
setFromAmount('');
|
||||
@@ -361,11 +363,11 @@ export const SwapInterface: React.FC<SwapInterfaceProps> = ({ pools }) => {
|
||||
);
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Swap failed:', error);
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Transaction failed');
|
||||
setErrorMessage(error instanceof Error ? error.message : t('common.txFailed'));
|
||||
setTxStatus('error');
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error instanceof Error ? error.message : 'Swap transaction failed',
|
||||
title: t('common.error'),
|
||||
description: error instanceof Error ? error.message : t('swap.swapFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@@ -382,7 +384,7 @@ export const SwapInterface: React.FC<SwapInterfaceProps> = ({ pools }) => {
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Loader2 className="w-16 h-16 animate-spin text-green-400" />
|
||||
<p className="text-white text-xl font-semibold">
|
||||
{txStatus === 'signing' ? 'Waiting for signature...' : 'Processing swap...'}
|
||||
{txStatus === 'signing' ? t('swap.waitingSignature') : t('swap.processingSwap')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -391,7 +393,7 @@ export const SwapInterface: React.FC<SwapInterfaceProps> = ({ pools }) => {
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader className="border-b border-gray-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-xl font-bold text-white">Swap Tokens</CardTitle>
|
||||
<CardTitle className="text-xl font-bold text-white">{t('swap.title')}</CardTitle>
|
||||
<Button variant="ghost" size="icon" onClick={() => setShowSettings(true)}>
|
||||
<Settings className="h-5 w-5 text-gray-400" />
|
||||
</Button>
|
||||
@@ -403,7 +405,7 @@ export const SwapInterface: React.FC<SwapInterfaceProps> = ({ pools }) => {
|
||||
<Alert className="bg-yellow-500/10 border-yellow-500/30">
|
||||
<AlertCircle className="h-4 w-4 text-yellow-500" />
|
||||
<AlertDescription className="text-yellow-300">
|
||||
Please connect your wallet to swap tokens
|
||||
{t('swap.connectWalletAlert')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -411,9 +413,9 @@ export const SwapInterface: React.FC<SwapInterfaceProps> = ({ pools }) => {
|
||||
{/* From Token */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">From</span>
|
||||
<span className="text-gray-400">{t('swap.from')}</span>
|
||||
<span className="text-gray-400">
|
||||
Balance: {formatTokenBalance(fromBalance, fromTokenInfo?.decimals ?? 12, 4)} {fromToken}
|
||||
{t('common.balance')} {formatTokenBalance(fromBalance, fromTokenInfo?.decimals ?? 12, 4)} {fromToken}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -460,7 +462,7 @@ export const SwapInterface: React.FC<SwapInterfaceProps> = ({ pools }) => {
|
||||
className="px-3 py-1 bg-green-600/20 hover:bg-green-600/30 text-green-400 text-xs rounded border border-green-600/30 transition-colors"
|
||||
disabled={!account}
|
||||
>
|
||||
MAX
|
||||
{t('common.max')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -481,9 +483,9 @@ export const SwapInterface: React.FC<SwapInterfaceProps> = ({ pools }) => {
|
||||
{/* To Token */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">To</span>
|
||||
<span className="text-gray-400">{t('swap.to')}</span>
|
||||
<span className="text-gray-400">
|
||||
Balance: {formatTokenBalance(toBalance, toTokenInfo?.decimals ?? 12, 4)} {toToken}
|
||||
{t('common.balance')} {formatTokenBalance(toBalance, toTokenInfo?.decimals ?? 12, 4)} {toToken}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -532,12 +534,12 @@ export const SwapInterface: React.FC<SwapInterfaceProps> = ({ pools }) => {
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-400 flex items-center gap-1">
|
||||
<Info className="w-3 h-3" />
|
||||
Exchange Rate
|
||||
<span className="text-xs text-green-500">(CoinGecko)</span>
|
||||
{t('common.exchangeRate')}
|
||||
<span className="text-xs text-green-500">{t('swap.coinGecko')}</span>
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-white">
|
||||
{oracleRate ? `1 ${fromToken} = ${exchangeRate} ${toToken}` : 'Loading...'}
|
||||
{oracleRate ? `1 ${fromToken} = ${exchangeRate} ${toToken}` : t('common.loading')}
|
||||
</span>
|
||||
<button onClick={fetchPrices} className="text-gray-400 hover:text-white">
|
||||
<RefreshCw className={`w-3 h-3 ${pricesLoading ? 'animate-spin' : ''}`} />
|
||||
@@ -558,7 +560,7 @@ export const SwapInterface: React.FC<SwapInterfaceProps> = ({ pools }) => {
|
||||
{/* Route */}
|
||||
{swapRoute.length > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Route</span>
|
||||
<span className="text-gray-400">{t('swap.route')}</span>
|
||||
<span className="text-purple-400 text-xs">
|
||||
{swapRoute.join(' → ')}
|
||||
</span>
|
||||
@@ -567,15 +569,15 @@ export const SwapInterface: React.FC<SwapInterfaceProps> = ({ pools }) => {
|
||||
|
||||
{/* Fees */}
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Swap Fee</span>
|
||||
<span className="text-gray-400">{t('swap.swapFee')}</span>
|
||||
<span className="text-yellow-400">
|
||||
{swapRoute.length > 2 ? '0.6%' : '0.3%'}
|
||||
{swapRoute.length > 2 && <span className="text-xs text-gray-500 ml-1">(2 hops)</span>}
|
||||
{swapRoute.length > 2 && <span className="text-xs text-gray-500 ml-1">{t('swap.twoHops')}</span>}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between pt-2 border-t border-gray-700">
|
||||
<span className="text-gray-400">Slippage Tolerance</span>
|
||||
<span className="text-gray-400">{t('common.slippageTolerance')}</span>
|
||||
<span className="text-blue-400">{slippage}%</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -585,7 +587,7 @@ export const SwapInterface: React.FC<SwapInterfaceProps> = ({ pools }) => {
|
||||
<Alert className="bg-red-900/20 border-red-500/30">
|
||||
<AlertTriangle className="h-4 w-4 text-red-500" />
|
||||
<AlertDescription className="text-red-300 text-sm">
|
||||
Insufficient {fromToken} balance
|
||||
{t('swap.insufficientBalance', { token: fromToken })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -594,7 +596,7 @@ export const SwapInterface: React.FC<SwapInterfaceProps> = ({ pools }) => {
|
||||
<Alert className="bg-yellow-900/20 border-yellow-500/30">
|
||||
<Info className="h-4 w-4 text-yellow-500" />
|
||||
<AlertDescription className="text-yellow-300 text-sm">
|
||||
This swap uses multi-hop routing ({swapRoute.join(' → ')}). Double fee applies.
|
||||
{t('swap.multiHopWarning', { route: swapRoute.join(' → ') })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -615,14 +617,14 @@ export const SwapInterface: React.FC<SwapInterfaceProps> = ({ pools }) => {
|
||||
}
|
||||
>
|
||||
{!account
|
||||
? 'Connect Wallet'
|
||||
? t('swap.connectWallet')
|
||||
: hasInsufficientBalance
|
||||
? `Insufficient ${fromToken} Balance`
|
||||
? t('swap.insufficientBalanceBtn', { token: fromToken })
|
||||
: !oracleRate
|
||||
? 'Price Not Available'
|
||||
? t('swap.priceNotAvailable')
|
||||
: pricesLoading
|
||||
? 'Loading Prices...'
|
||||
: 'Swap Tokens'}
|
||||
? t('swap.loadingPrices')
|
||||
: t('swap.swapTokens')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -631,11 +633,11 @@ export const SwapInterface: React.FC<SwapInterfaceProps> = ({ pools }) => {
|
||||
<Dialog open={showSettings} onOpenChange={setShowSettings}>
|
||||
<DialogContent className="bg-gray-900 border-gray-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Swap Settings</DialogTitle>
|
||||
<DialogTitle className="text-white">{t('swap.settings')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-300">Slippage Tolerance</label>
|
||||
<label className="text-sm font-medium text-gray-300">{t('common.slippageTolerance')}</label>
|
||||
<div className="flex gap-2 mt-2">
|
||||
{[0.1, 0.5, 1.0, 2.0].map(val => (
|
||||
<Button
|
||||
@@ -657,24 +659,24 @@ export const SwapInterface: React.FC<SwapInterfaceProps> = ({ pools }) => {
|
||||
<Dialog open={showConfirm} onOpenChange={setShowConfirm}>
|
||||
<DialogContent className="bg-gray-900 border-gray-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Confirm Swap</DialogTitle>
|
||||
<DialogTitle className="text-white">{t('swap.confirmSwap')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-gray-800 border border-gray-700 rounded-lg space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-300">You Pay</span>
|
||||
<span className="text-gray-300">{t('swap.youPay')}</span>
|
||||
<span className="font-bold text-white">{fromAmount} {fromToken}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-300">You Receive</span>
|
||||
<span className="text-gray-300">{t('swap.youReceive')}</span>
|
||||
<span className="font-bold text-white">{toAmount} {toToken}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm pt-2 border-t border-gray-700">
|
||||
<span className="text-gray-400">Exchange Rate</span>
|
||||
<span className="text-gray-400">{t('common.exchangeRate')}</span>
|
||||
<span className="text-gray-400">1 {fromToken} = {exchangeRate} {toToken}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Slippage</span>
|
||||
<span className="text-gray-400">{t('swap.slippage')}</span>
|
||||
<span className="text-gray-400">{slippage}%</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -683,7 +685,7 @@ export const SwapInterface: React.FC<SwapInterfaceProps> = ({ pools }) => {
|
||||
onClick={handleConfirmSwap}
|
||||
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||
>
|
||||
{txStatus === 'signing' ? 'Signing...' : txStatus === 'submitting' ? 'Swapping...' : 'Confirm Swap'}
|
||||
{txStatus === 'signing' ? t('common.signing') : txStatus === 'submitting' ? t('swap.swapping') : t('swap.confirmSwap')}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
checkBridgeStatus,
|
||||
fetchAssetHubUsdtInfo,
|
||||
@@ -38,6 +39,7 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
const { assetHubApi, isAssetHubReady } = usePezkuwi();
|
||||
const { account, signer } = useWallet();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// State
|
||||
const [step, setStep] = useState<SetupStep>('idle');
|
||||
@@ -56,7 +58,7 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
if (!assetHubApi || !isAssetHubReady) return;
|
||||
|
||||
setStep('checking');
|
||||
setStatusMessage('Checking bridge status...');
|
||||
setStatusMessage(t('xcmBridge.checkingStatus'));
|
||||
setErrorMessage('');
|
||||
|
||||
try {
|
||||
@@ -65,11 +67,11 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
setBridgeStatus(status);
|
||||
|
||||
// Fetch Asset Hub USDT info
|
||||
setStatusMessage('Fetching Asset Hub USDT info...');
|
||||
setStatusMessage(t('xcmBridge.fetchingInfo'));
|
||||
const info = await fetchAssetHubUsdtInfo();
|
||||
setAssetHubInfo(info);
|
||||
|
||||
setStatusMessage('Status check complete');
|
||||
setStatusMessage(t('xcmBridge.statusComplete'));
|
||||
setStep('idle');
|
||||
} catch (error) {
|
||||
console.error('Initial check failed:', error);
|
||||
@@ -99,8 +101,8 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
const handleConfigureBridge = async () => {
|
||||
if (!assetHubApi || !isAssetHubReady || !signer || !account) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Please connect your wallet',
|
||||
title: t('common.error'),
|
||||
description: t('common.connectWalletAlert'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -118,22 +120,22 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
);
|
||||
|
||||
toast({
|
||||
title: 'Success!',
|
||||
description: 'XCM bridge configured successfully',
|
||||
title: t('common.success'),
|
||||
description: t('xcmBridge.bridgeConfigured'),
|
||||
});
|
||||
|
||||
// Refresh status
|
||||
await performInitialCheck();
|
||||
|
||||
setStep('success');
|
||||
setStatusMessage('Bridge configuration complete!');
|
||||
setStatusMessage(t('xcmBridge.configComplete'));
|
||||
} catch (error) {
|
||||
console.error('Bridge configuration failed:', error);
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Configuration failed');
|
||||
setStep('error');
|
||||
toast({
|
||||
title: 'Configuration Failed',
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
title: t('xcmBridge.configFailed'),
|
||||
description: error instanceof Error ? error.message : t('common.error'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@@ -145,8 +147,8 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
const handleCreatePool = async () => {
|
||||
if (!assetHubApi || !isAssetHubReady || !signer || !account) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Please connect your wallet',
|
||||
title: t('common.error'),
|
||||
description: t('common.connectWalletAlert'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -170,12 +172,12 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
);
|
||||
|
||||
toast({
|
||||
title: 'Success!',
|
||||
description: 'wUSDT/HEZ pool created successfully',
|
||||
title: t('common.success'),
|
||||
description: t('xcmBridge.poolCreated'),
|
||||
});
|
||||
|
||||
setStep('success');
|
||||
setStatusMessage('Pool creation complete!');
|
||||
setStatusMessage(t('xcmBridge.poolComplete'));
|
||||
|
||||
setTimeout(() => {
|
||||
onSuccess?.();
|
||||
@@ -186,8 +188,8 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Pool creation failed');
|
||||
setStep('error');
|
||||
toast({
|
||||
title: 'Pool Creation Failed',
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
title: t('xcmBridge.poolFailed'),
|
||||
description: error instanceof Error ? error.message : t('common.error'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@@ -203,7 +205,7 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
<CardHeader className="border-b border-gray-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-xl font-bold text-white">
|
||||
XCM Bridge Setup
|
||||
{t('xcmBridge.title')}
|
||||
</CardTitle>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -214,7 +216,7 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
<Badge className="bg-purple-600/20 text-purple-400 border-purple-600/30 w-fit mt-2">
|
||||
Admin Only - XCM Configuration
|
||||
{t('xcmBridge.adminOnly')}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
|
||||
@@ -223,18 +225,17 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
<Alert className="bg-purple-500/10 border-purple-500/30">
|
||||
<Zap className="h-4 w-4 text-purple-400" />
|
||||
<AlertDescription className="text-purple-300 text-sm">
|
||||
Configure Asset Hub USDT → wUSDT bridge with one click. This enables
|
||||
cross-chain transfers from Westend Asset Hub to PezkuwiChain.
|
||||
{t('xcmBridge.info')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{/* Current Status */}
|
||||
{bridgeStatus && (
|
||||
<div className="p-4 bg-gray-800/50 rounded-lg border border-gray-700 space-y-3">
|
||||
<div className="text-sm font-semibold text-gray-300 mb-2">Current Status</div>
|
||||
<div className="text-sm font-semibold text-gray-300 mb-2">{t('xcmBridge.currentStatus')}</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-400">Asset Hub Connection:</span>
|
||||
<span className="text-sm text-gray-400">{t('xcmBridge.assetHubConnection')}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{bridgeStatus.assetHubConnected ? (
|
||||
<CheckCircle className="w-4 h-4 text-green-400" />
|
||||
@@ -242,13 +243,13 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
<AlertCircle className="w-4 h-4 text-yellow-400" />
|
||||
)}
|
||||
<span className={bridgeStatus.assetHubConnected ? 'text-green-400' : 'text-yellow-400'}>
|
||||
{bridgeStatus.assetHubConnected ? 'Connected' : 'Checking...'}
|
||||
{bridgeStatus.assetHubConnected ? t('xcmBridge.connected') : t('xcmBridge.checking')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-400">wUSDT Asset Exists:</span>
|
||||
<span className="text-sm text-gray-400">{t('xcmBridge.wusdtAssetExists')}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{bridgeStatus.wusdtExists ? (
|
||||
<CheckCircle className="w-4 h-4 text-green-400" />
|
||||
@@ -256,13 +257,13 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
<AlertCircle className="w-4 h-4 text-red-400" />
|
||||
)}
|
||||
<span className={bridgeStatus.wusdtExists ? 'text-green-400' : 'text-red-400'}>
|
||||
{bridgeStatus.wusdtExists ? 'Yes (ID: 1000)' : 'Not Found'}
|
||||
{bridgeStatus.wusdtExists ? t('xcmBridge.yesId') : t('xcmBridge.notFound')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-400">XCM Bridge Configured:</span>
|
||||
<span className="text-sm text-gray-400">{t('xcmBridge.xcmConfigured')}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{bridgeStatus.isConfigured ? (
|
||||
<CheckCircle className="w-4 h-4 text-green-400" />
|
||||
@@ -270,7 +271,7 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
<AlertCircle className="w-4 h-4 text-yellow-400" />
|
||||
)}
|
||||
<span className={bridgeStatus.isConfigured ? 'text-green-400' : 'text-yellow-400'}>
|
||||
{bridgeStatus.isConfigured ? 'Configured' : 'Not Configured'}
|
||||
{bridgeStatus.isConfigured ? t('xcmBridge.configured') : t('xcmBridge.notConfigured')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -280,7 +281,7 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
{/* Asset Hub USDT Info */}
|
||||
{assetHubInfo && (
|
||||
<div className="p-4 bg-gray-800/50 rounded-lg border border-gray-700 space-y-2">
|
||||
<div className="text-sm font-semibold text-gray-300 mb-2">Asset Hub USDT Info</div>
|
||||
<div className="text-sm font-semibold text-gray-300 mb-2">{t('xcmBridge.assetHubInfo')}</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<span className="text-gray-400">Asset ID:</span>
|
||||
<span className="text-white font-mono">{assetHubInfo.id}</span>
|
||||
@@ -307,7 +308,7 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
|
||||
{/* Configuration Details */}
|
||||
<div className="p-4 bg-gray-800/50 rounded-lg border border-gray-700 space-y-2">
|
||||
<div className="text-sm font-semibold text-gray-300 mb-2">Configuration Details</div>
|
||||
<div className="text-sm font-semibold text-gray-300 mb-2">{t('xcmBridge.configDetails')}</div>
|
||||
<div className="text-xs space-y-1 text-gray-400 font-mono">
|
||||
<div>Asset Hub Endpoint: {ASSET_HUB_ENDPOINT}</div>
|
||||
<div>Asset Hub USDT ID: {ASSET_HUB_USDT_ID}</div>
|
||||
@@ -349,10 +350,10 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
{/* Pool Creation Section (Optional) */}
|
||||
{showPoolCreation && (
|
||||
<div className="p-4 bg-gray-800/50 rounded-lg border border-gray-700 space-y-4">
|
||||
<div className="text-sm font-semibold text-gray-300">Create wUSDT/HEZ Pool (Optional)</div>
|
||||
<div className="text-sm font-semibold text-gray-300">{t('xcmBridge.createPoolSection')}</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-xs text-gray-400">wUSDT Amount</label>
|
||||
<label className="text-xs text-gray-400">{t('xcmBridge.wusdtAmount')}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={wusdtAmount}
|
||||
@@ -362,7 +363,7 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-400">HEZ Amount</label>
|
||||
<label className="text-xs text-gray-400">{t('xcmBridge.hezAmount')}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={hezAmount}
|
||||
@@ -383,7 +384,7 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
className="flex-1 border-gray-700 hover:bg-gray-800"
|
||||
disabled={isLoading}
|
||||
>
|
||||
Close
|
||||
{t('xcmBridge.close')}
|
||||
</Button>
|
||||
|
||||
{!bridgeStatus?.isConfigured && (
|
||||
@@ -395,10 +396,10 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
{step === 'configuring' ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
||||
Configuring...
|
||||
{t('xcmBridge.configuring')}
|
||||
</>
|
||||
) : (
|
||||
'Configure Bridge'
|
||||
t('xcmBridge.configureBridge')
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
@@ -408,7 +409,7 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
onClick={() => setShowPoolCreation(true)}
|
||||
className="flex-1 bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
Create Pool (Optional)
|
||||
{t('xcmBridge.createPoolOptional')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -421,10 +422,10 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
{step === 'pool-creation' ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
||||
Creating...
|
||||
{t('xcmBridge.creatingPool')}
|
||||
</>
|
||||
) : (
|
||||
'Create Pool'
|
||||
t('xcmBridge.createPool')
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
@@ -432,7 +433,7 @@ export const XCMBridgeSetupModal: React.FC<XCMBridgeSetupModalProps> = ({
|
||||
|
||||
{/* Note */}
|
||||
<div className="text-xs text-gray-500 text-center">
|
||||
⚠️ XCM bridge configuration requires sudo access
|
||||
{t('xcmBridge.sudoWarning')}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Textarea } from '@/components/ui/textarea';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { ThumbsUp, ThumbsDown, MessageSquare, Shield, MoreVertical, Flag, Edit, Trash2 } from 'lucide-react';
|
||||
// import { useTranslation } from 'react-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||
import { useWebSocket } from '@/contexts/WebSocketContext';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
@@ -27,6 +27,7 @@ interface Comment {
|
||||
}
|
||||
|
||||
export function DiscussionThread({ proposalId }: { proposalId: string }) {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const { subscribe, unsubscribe, sendMessage, isConnected } = useWebSocket();
|
||||
const [comments, setComments] = useState<Comment[]>([
|
||||
@@ -91,8 +92,8 @@ export function DiscussionThread({ proposalId }: { proposalId: string }) {
|
||||
// Show notification for mentions
|
||||
if (data.content.includes('@currentUser')) {
|
||||
toast({
|
||||
title: "You were mentioned",
|
||||
description: `${data.author} mentioned you in a comment`,
|
||||
title: t('discussion.youWereMentioned'),
|
||||
description: t('discussion.mentionedYou', { author: data.author as string }),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -226,7 +227,7 @@ export function DiscussionThread({ proposalId }: { proposalId: string }) {
|
||||
</Badge>
|
||||
))}
|
||||
<span className="text-sm text-gray-500">
|
||||
{comment.isLive ? 'Just now' : comment.timestamp}
|
||||
{comment.isLive ? t('discussion.justNow') : comment.timestamp}
|
||||
</span>
|
||||
{isConnected && (
|
||||
<div className="h-2 w-2 bg-green-500 rounded-full" title="Real-time updates active" />
|
||||
@@ -259,7 +260,7 @@ export function DiscussionThread({ proposalId }: { proposalId: string }) {
|
||||
onClick={() => setReplyTo(comment.id)}
|
||||
>
|
||||
<MessageSquare className="h-4 w-4 mr-1" />
|
||||
Reply
|
||||
{t('discussion.reply')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -273,15 +274,15 @@ export function DiscussionThread({ proposalId }: { proposalId: string }) {
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<Flag className="h-4 w-4 mr-2" />
|
||||
Report
|
||||
{t('discussion.report')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
{t('discussion.edit')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-red-600">
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
{t('discussion.delete')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -289,14 +290,14 @@ export function DiscussionThread({ proposalId }: { proposalId: string }) {
|
||||
{replyTo === comment.id && (
|
||||
<div className="mt-4">
|
||||
<Textarea
|
||||
placeholder="Write your reply... @mention users to notify them"
|
||||
placeholder={t('discussion.replyPlaceholder')}
|
||||
value={newComment}
|
||||
onChange={(e) => setNewComment(e.target.value)}
|
||||
className="min-h-[80px]"
|
||||
/>
|
||||
<div className="flex justify-end space-x-2 mt-2">
|
||||
<Button variant="outline" onClick={() => setReplyTo(null)}>
|
||||
Cancel
|
||||
{t('discussion.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
@@ -317,7 +318,7 @@ export function DiscussionThread({ proposalId }: { proposalId: string }) {
|
||||
}}
|
||||
disabled={!newComment.trim()}
|
||||
>
|
||||
Post Reply
|
||||
{t('discussion.postReply')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -344,12 +345,12 @@ export function DiscussionThread({ proposalId }: { proposalId: string }) {
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h3 className="text-xl font-semibold">Discussion Forum</h3>
|
||||
<p className="text-sm text-gray-600">Share your thoughts and feedback on this proposal</p>
|
||||
<h3 className="text-xl font-semibold">{t('discussion.title')}</h3>
|
||||
<p className="text-sm text-gray-600">{t('discussion.subtitle')}</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Textarea
|
||||
placeholder="Write your comment... (Markdown supported)"
|
||||
placeholder={t('discussion.commentPlaceholder')}
|
||||
value={newComment}
|
||||
onChange={(e) => setNewComment(e.target.value)}
|
||||
className="min-h-[120px]"
|
||||
@@ -360,13 +361,13 @@ export function DiscussionThread({ proposalId }: { proposalId: string }) {
|
||||
size="sm"
|
||||
onClick={() => setShowMarkdownHelp(!showMarkdownHelp)}
|
||||
>
|
||||
Markdown Help
|
||||
{t('discussion.markdownHelp')}
|
||||
</Button>
|
||||
<Button>Post Comment</Button>
|
||||
<Button>{t('discussion.postComment')}</Button>
|
||||
</div>
|
||||
{showMarkdownHelp && (
|
||||
<Card className="mt-4 p-4 bg-gray-50 text-gray-900">
|
||||
<p className="text-sm font-semibold mb-2 text-gray-900">Markdown Formatting:</p>
|
||||
<p className="text-sm font-semibold mb-2 text-gray-900">{t('discussion.markdownFormatting')}</p>
|
||||
<ul className="text-sm space-y-1 text-gray-900">
|
||||
<li>**bold** → <strong>bold</strong></li>
|
||||
<li>*italic* → <em>italic</em></li>
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
CheckCircle,
|
||||
Eye
|
||||
} from 'lucide-react';
|
||||
// import { useTranslation } from 'react-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useForum } from '@/hooks/useForum';
|
||||
import { DiscussionThread } from './DiscussionThread';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
@@ -34,6 +34,7 @@ import { formatDistanceToNow } from 'date-fns';
|
||||
|
||||
export function ForumOverview() {
|
||||
const { user } = useAuth();
|
||||
const { t } = useTranslation();
|
||||
const { announcements, categories, discussions, loading, reactToDiscussion } = useForum();
|
||||
const [selectedDiscussion, setSelectedDiscussion] = useState<string | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -96,7 +97,7 @@ export function ForumOverview() {
|
||||
variant="outline"
|
||||
onClick={() => setSelectedDiscussion(null)}
|
||||
>
|
||||
← Back to Forum
|
||||
{t('forum.backToForum')}
|
||||
</Button>
|
||||
<DiscussionThread proposalId={selectedDiscussion} />
|
||||
</div>
|
||||
@@ -104,7 +105,7 @@ export function ForumOverview() {
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <LoadingState message="Loading forum..." />;
|
||||
return <LoadingState message={t('forum.loading')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -145,7 +146,7 @@ export function ForumOverview() {
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Total Discussions</p>
|
||||
<p className="text-sm text-muted-foreground">{t('forum.totalDiscussions')}</p>
|
||||
<p className="text-2xl font-bold">{discussions.length}</p>
|
||||
</div>
|
||||
<MessageSquare className="h-8 w-8 text-blue-500" />
|
||||
@@ -157,7 +158,7 @@ export function ForumOverview() {
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Categories</p>
|
||||
<p className="text-sm text-muted-foreground">{t('forum.categories')}</p>
|
||||
<p className="text-2xl font-bold">{categories.length}</p>
|
||||
</div>
|
||||
<Filter className="h-8 w-8 text-purple-500" />
|
||||
@@ -169,7 +170,7 @@ export function ForumOverview() {
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Active Users</p>
|
||||
<p className="text-sm text-muted-foreground">{t('forum.activeUsers')}</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{new Set(discussions.map(d => d.author_id)).size}
|
||||
</p>
|
||||
@@ -183,7 +184,7 @@ export function ForumOverview() {
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Total Replies</p>
|
||||
<p className="text-sm text-muted-foreground">{t('forum.totalReplies')}</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{discussions.reduce((sum, d) => sum + d.replies_count, 0)}
|
||||
</p>
|
||||
@@ -201,7 +202,7 @@ export function ForumOverview() {
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="Search discussions..."
|
||||
placeholder={t('forum.searchPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
@@ -209,10 +210,10 @@ export function ForumOverview() {
|
||||
</div>
|
||||
<Select value={filterCategory} onValueChange={setFilterCategory}>
|
||||
<SelectTrigger className="w-full md:w-[180px]">
|
||||
<SelectValue placeholder="All Categories" />
|
||||
<SelectValue placeholder={t('forum.allCategories')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Categories</SelectItem>
|
||||
<SelectItem value="all">{t('forum.allCategories')}</SelectItem>
|
||||
{categories.map((cat) => (
|
||||
<SelectItem key={cat.id} value={cat.name.toLowerCase()}>
|
||||
{cat.icon} {cat.name}
|
||||
@@ -222,19 +223,19 @@ export function ForumOverview() {
|
||||
</Select>
|
||||
<Select value={sortBy} onValueChange={setSortBy}>
|
||||
<SelectTrigger className="w-full md:w-[180px]">
|
||||
<SelectValue placeholder="Sort by" />
|
||||
<SelectValue placeholder={t('forum.sortBy')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="recent">Most Recent</SelectItem>
|
||||
<SelectItem value="popular">Most Popular</SelectItem>
|
||||
<SelectItem value="replies">Most Replies</SelectItem>
|
||||
<SelectItem value="views">Most Viewed</SelectItem>
|
||||
<SelectItem value="recent">{t('forum.sortRecent')}</SelectItem>
|
||||
<SelectItem value="popular">{t('forum.sortPopular')}</SelectItem>
|
||||
<SelectItem value="replies">{t('forum.sortReplies')}</SelectItem>
|
||||
<SelectItem value="views">{t('forum.sortViewed')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{user && (
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
New Discussion
|
||||
{t('forum.newDiscussion')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -253,7 +254,7 @@ export function ForumOverview() {
|
||||
<div className="text-4xl mb-2">{category.icon}</div>
|
||||
<h3 className="font-semibold">{category.name}</h3>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{discussions.filter(d => d.category?.id === category.id).length} discussions
|
||||
{t('forum.discussionCount', { count: discussions.filter(d => d.category?.id === category.id).length })}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -266,9 +267,9 @@ export function ForumOverview() {
|
||||
<Card>
|
||||
<CardContent className="py-12 text-center">
|
||||
<MessageSquare className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
|
||||
<p className="text-muted-foreground">No discussions found</p>
|
||||
<p className="text-muted-foreground">{t('forum.noDiscussions')}</p>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
Try adjusting your search or filters
|
||||
{t('forum.adjustFilters')}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -287,13 +288,13 @@ export function ForumOverview() {
|
||||
{discussion.is_pinned && (
|
||||
<Badge variant="secondary" className="bg-yellow-500/10 text-yellow-700 border-yellow-500/20">
|
||||
<Pin className="h-3 w-3 mr-1" />
|
||||
Pinned
|
||||
{t('forum.pinned')}
|
||||
</Badge>
|
||||
)}
|
||||
{discussion.is_locked && (
|
||||
<Badge variant="secondary" className="bg-red-500/10 text-red-700 border-red-500/20">
|
||||
<Lock className="h-3 w-3 mr-1" />
|
||||
Locked
|
||||
{t('forum.locked')}
|
||||
</Badge>
|
||||
)}
|
||||
{discussion.category && (
|
||||
@@ -304,7 +305,7 @@ export function ForumOverview() {
|
||||
{(discussion.upvotes || 0) > 10 && (
|
||||
<Badge variant="destructive">
|
||||
<Flame className="h-3 w-3 mr-1" />
|
||||
Trending
|
||||
{t('forum.trending')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -316,14 +317,14 @@ export function ForumOverview() {
|
||||
|
||||
{/* Meta Info */}
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground flex-wrap">
|
||||
<span>by {discussion.author_name}</span>
|
||||
<span>{t('forum.by')} {discussion.author_name}</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
{discussion.replies_count} replies
|
||||
{discussion.replies_count} {t('forum.replies')}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Eye className="h-4 w-4" />
|
||||
{discussion.views_count} views
|
||||
{discussion.views_count} {t('forum.views')}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="h-4 w-4" />
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { AlertTriangle, Shield, Ban, CheckCircle, Clock, Flag, User } from 'lucide-react';
|
||||
// import { useTranslation } from 'react-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Report {
|
||||
id: string;
|
||||
@@ -21,6 +21,7 @@ interface Report {
|
||||
}
|
||||
|
||||
export function ModerationPanel() {
|
||||
const { t } = useTranslation();
|
||||
const [autoModeration, setAutoModeration] = useState(true);
|
||||
const [sentimentThreshold, setSentimentThreshold] = useState(30);
|
||||
|
||||
@@ -81,7 +82,7 @@ export function ModerationPanel() {
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Total Reports</p>
|
||||
<p className="text-sm text-gray-600">{t('moderation.totalReports')}</p>
|
||||
<p className="text-2xl font-bold">{moderationStats.totalReports}</p>
|
||||
</div>
|
||||
<Flag className="h-8 w-8 text-gray-400" />
|
||||
@@ -92,7 +93,7 @@ export function ModerationPanel() {
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Resolved</p>
|
||||
<p className="text-sm text-gray-600">{t('moderation.resolved')}</p>
|
||||
<p className="text-2xl font-bold text-green-600">{moderationStats.resolved}</p>
|
||||
</div>
|
||||
<CheckCircle className="h-8 w-8 text-green-400" />
|
||||
@@ -103,7 +104,7 @@ export function ModerationPanel() {
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Pending</p>
|
||||
<p className="text-sm text-gray-600">{t('moderation.pending')}</p>
|
||||
<p className="text-2xl font-bold text-yellow-600">{moderationStats.pending}</p>
|
||||
</div>
|
||||
<Clock className="h-8 w-8 text-yellow-400" />
|
||||
@@ -114,7 +115,7 @@ export function ModerationPanel() {
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Banned Users</p>
|
||||
<p className="text-sm text-gray-600">{t('moderation.bannedUsers')}</p>
|
||||
<p className="text-2xl font-bold text-red-600">{moderationStats.bannedUsers}</p>
|
||||
</div>
|
||||
<Ban className="h-8 w-8 text-red-400" />
|
||||
@@ -125,7 +126,7 @@ export function ModerationPanel() {
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Flagged Content</p>
|
||||
<p className="text-sm text-gray-600">{t('moderation.flaggedContent')}</p>
|
||||
<p className="text-2xl font-bold">{moderationStats.flaggedContent}</p>
|
||||
</div>
|
||||
<AlertTriangle className="h-8 w-8 text-orange-400" />
|
||||
@@ -136,9 +137,9 @@ export function ModerationPanel() {
|
||||
|
||||
<Tabs defaultValue="reports" className="space-y-4">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="reports">Reports Queue</TabsTrigger>
|
||||
<TabsTrigger value="settings">Auto-Moderation</TabsTrigger>
|
||||
<TabsTrigger value="users">User Management</TabsTrigger>
|
||||
<TabsTrigger value="reports">{t('moderation.reportsQueue')}</TabsTrigger>
|
||||
<TabsTrigger value="settings">{t('moderation.autoModeration')}</TabsTrigger>
|
||||
<TabsTrigger value="users">{t('moderation.userManagement')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="reports" className="space-y-4">
|
||||
@@ -155,16 +156,16 @@ export function ModerationPanel() {
|
||||
<Badge variant="outline">{report.type}</Badge>
|
||||
<span className="text-sm text-gray-500">{report.timestamp}</span>
|
||||
</div>
|
||||
<p className="font-medium mb-2">Reported User: {report.reportedUser}</p>
|
||||
<p className="font-medium mb-2">{t('moderation.reportedUser', { user: report.reportedUser })}</p>
|
||||
<p className="text-gray-600 mb-3">{report.reportedContent}</p>
|
||||
<p className="text-sm text-gray-500">Reported by: {report.reportedBy}</p>
|
||||
<p className="text-sm text-gray-500">{t('moderation.reportedBy', { user: report.reportedBy })}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
Review
|
||||
{t('moderation.review')}
|
||||
</Button>
|
||||
<Button variant="destructive" size="sm">
|
||||
Take Action
|
||||
{t('moderation.takeAction')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -176,13 +177,13 @@ export function ModerationPanel() {
|
||||
<TabsContent value="settings" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Auto-Moderation Settings</CardTitle>
|
||||
<CardTitle>{t('moderation.autoModSettings')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label htmlFor="auto-mod">Enable Auto-Moderation</Label>
|
||||
<p className="text-sm text-gray-600">Automatically flag suspicious content</p>
|
||||
<Label htmlFor="auto-mod">{t('moderation.enableAutoMod')}</Label>
|
||||
<p className="text-sm text-gray-600">{t('moderation.autoModDesc')}</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="auto-mod"
|
||||
@@ -191,9 +192,9 @@ export function ModerationPanel() {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Sentiment Threshold</Label>
|
||||
<Label>{t('moderation.sentimentThreshold')}</Label>
|
||||
<p className="text-sm text-gray-600">
|
||||
Flag comments with sentiment below {sentimentThreshold}%
|
||||
{t('moderation.sentimentThresholdDesc', { threshold: sentimentThreshold })}
|
||||
</p>
|
||||
<input
|
||||
type="range"
|
||||
@@ -207,7 +208,7 @@ export function ModerationPanel() {
|
||||
<Alert>
|
||||
<Shield className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Auto-moderation uses AI to detect potentially harmful content and automatically flags it for review.
|
||||
{t('moderation.autoModInfo')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
@@ -217,7 +218,7 @@ export function ModerationPanel() {
|
||||
<TabsContent value="users" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>User Moderation Actions</CardTitle>
|
||||
<CardTitle>{t('moderation.userModActions')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
@@ -226,13 +227,13 @@ export function ModerationPanel() {
|
||||
<User className="h-8 w-8 text-gray-400" />
|
||||
<div>
|
||||
<p className="font-medium">BadActor456</p>
|
||||
<p className="text-sm text-gray-600">3 reports, 2 warnings</p>
|
||||
<p className="text-sm text-gray-600">{t('moderation.reportsWarnings', { reports: 3, warnings: 2 })}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm">Warn</Button>
|
||||
<Button variant="outline" size="sm">Suspend</Button>
|
||||
<Button variant="destructive" size="sm">Ban</Button>
|
||||
<Button variant="outline" size="sm">{t('moderation.warn')}</Button>
|
||||
<Button variant="outline" size="sm">{t('moderation.suspend')}</Button>
|
||||
<Button variant="destructive" size="sm">{t('moderation.ban')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -28,6 +29,7 @@ interface ElectionWithCandidates extends ElectionInfo {
|
||||
}
|
||||
|
||||
const ElectionsInterface: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { api, isApiReady } = usePezkuwi();
|
||||
const { account, signer } = useWallet();
|
||||
const [elections, setElections] = useState<ElectionWithCandidates[]>([]);
|
||||
@@ -101,7 +103,7 @@ const ElectionsInterface: React.FC = () => {
|
||||
|
||||
const handleVote = async (electionId: number, candidateAccount: string, electionType: string) => {
|
||||
if (!api || !account || !signer) {
|
||||
toast.error('Please connect your wallet first');
|
||||
toast.error(t('elections.connectWallet'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -124,35 +126,35 @@ const ElectionsInterface: React.FC = () => {
|
||||
|
||||
await tx.signAndSend(account, { signer }, ({ status, dispatchError }) => {
|
||||
if (status.isInBlock) {
|
||||
toast.success('Vote submitted successfully!');
|
||||
toast.success(t('elections.voteSuccess'));
|
||||
setVotingElectionId(null);
|
||||
}
|
||||
if (dispatchError) {
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
||||
toast.error(`Vote failed: ${decoded.name}`);
|
||||
toast.error(t('elections.voteFailed', { error: decoded.name }));
|
||||
} else {
|
||||
toast.error(`Vote failed: ${dispatchError.toString()}`);
|
||||
toast.error(t('elections.voteFailed', { error: dispatchError.toString() }));
|
||||
}
|
||||
setVotingElectionId(null);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error voting:', err);
|
||||
toast.error('Failed to submit vote');
|
||||
toast.error(t('elections.voteError'));
|
||||
setVotingElectionId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const submitParliamentaryVotes = async (electionId: number) => {
|
||||
if (!api || !account || !signer) {
|
||||
toast.error('Please connect your wallet first');
|
||||
toast.error(t('elections.connectWallet'));
|
||||
return;
|
||||
}
|
||||
|
||||
const candidates = selectedCandidates.get(electionId) || [];
|
||||
if (candidates.length === 0) {
|
||||
toast.error('Please select at least one candidate');
|
||||
toast.error(t('elections.selectCandidate'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -163,34 +165,34 @@ const ElectionsInterface: React.FC = () => {
|
||||
|
||||
await tx.signAndSend(account, { signer }, ({ status, dispatchError }) => {
|
||||
if (status.isInBlock) {
|
||||
toast.success('Votes submitted successfully!');
|
||||
toast.success(t('elections.votesSuccess'));
|
||||
setSelectedCandidates(new Map(selectedCandidates.set(electionId, [])));
|
||||
setVotingElectionId(null);
|
||||
}
|
||||
if (dispatchError) {
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
||||
toast.error(`Vote failed: ${decoded.name}`);
|
||||
toast.error(t('elections.voteFailed', { error: decoded.name }));
|
||||
} else {
|
||||
toast.error(`Vote failed: ${dispatchError.toString()}`);
|
||||
toast.error(t('elections.voteFailed', { error: dispatchError.toString() }));
|
||||
}
|
||||
setVotingElectionId(null);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error voting:', err);
|
||||
toast.error('Failed to submit votes');
|
||||
toast.error(t('elections.votesError'));
|
||||
setVotingElectionId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const formatRemainingTime = (endBlock: number) => {
|
||||
const remaining = endBlock - currentBlock;
|
||||
if (remaining <= 0) return 'Ended';
|
||||
if (remaining <= 0) return t('elections.ended');
|
||||
const time = blocksToTime(remaining);
|
||||
if (time.days > 0) return `${time.days}d ${time.hours}h remaining`;
|
||||
if (time.hours > 0) return `${time.hours}h ${time.minutes}m remaining`;
|
||||
return `${time.minutes}m remaining`;
|
||||
if (time.days > 0) return t('elections.daysRemaining', { days: time.days, hours: time.hours });
|
||||
if (time.hours > 0) return t('elections.hoursRemaining', { hours: time.hours, minutes: time.minutes });
|
||||
return t('elections.minutesRemaining', { minutes: time.minutes });
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
@@ -206,7 +208,7 @@ const ElectionsInterface: React.FC = () => {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-green-500" />
|
||||
<span className="ml-3 text-gray-400">Loading elections from blockchain...</span>
|
||||
<span className="ml-3 text-gray-400">{t('elections.loading')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -217,7 +219,7 @@ const ElectionsInterface: React.FC = () => {
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center text-red-400">
|
||||
<AlertCircle className="w-5 h-5 mr-2" />
|
||||
Error loading elections: {error}
|
||||
{t('elections.loadError', { error })}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -231,17 +233,17 @@ const ElectionsInterface: React.FC = () => {
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="bg-green-500/10 border-green-500 text-green-400">
|
||||
<Activity className="h-3 w-3 mr-1" />
|
||||
Live Blockchain Data
|
||||
{t('elections.liveData')}
|
||||
</Badge>
|
||||
<span className="text-sm text-gray-500">Block #{currentBlock.toLocaleString()}</span>
|
||||
<span className="text-sm text-gray-500">{t('elections.block', { number: currentBlock.toLocaleString() })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="active" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3 bg-gray-800/50">
|
||||
<TabsTrigger value="active">Active Elections</TabsTrigger>
|
||||
<TabsTrigger value="register">Register</TabsTrigger>
|
||||
<TabsTrigger value="results">Results</TabsTrigger>
|
||||
<TabsTrigger value="active">{t('elections.activeElections')}</TabsTrigger>
|
||||
<TabsTrigger value="register">{t('elections.register')}</TabsTrigger>
|
||||
<TabsTrigger value="results">{t('elections.results')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="active" className="space-y-4">
|
||||
@@ -249,8 +251,8 @@ const ElectionsInterface: React.FC = () => {
|
||||
<Card className="bg-gray-900/50 border-gray-800">
|
||||
<CardContent className="pt-6 text-center text-gray-400">
|
||||
<Users className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||
<p>No active elections at this time</p>
|
||||
<p className="text-sm mt-2">Check back later for upcoming elections</p>
|
||||
<p>{t('elections.noActive')}</p>
|
||||
<p className="text-sm mt-2">{t('elections.checkBack')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
@@ -276,7 +278,7 @@ const ElectionsInterface: React.FC = () => {
|
||||
{election.userHasVoted && (
|
||||
<Badge variant="outline" className="border-green-500 text-green-400">
|
||||
<CheckCircle className="w-3 h-3 mr-1" />
|
||||
You Voted
|
||||
{t('elections.youVoted')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -286,7 +288,7 @@ const ElectionsInterface: React.FC = () => {
|
||||
{election.status === 'VotingPeriod' && (
|
||||
<div className="space-y-4">
|
||||
{election.candidates.length === 0 ? (
|
||||
<p className="text-gray-400 text-center py-4">No candidates registered</p>
|
||||
<p className="text-gray-400 text-center py-4">{t('elections.noCandidates')}</p>
|
||||
) : (
|
||||
<>
|
||||
{election.candidates.map(candidate => {
|
||||
@@ -304,13 +306,13 @@ const ElectionsInterface: React.FC = () => {
|
||||
{candidate.account.substring(0, 8)}...{candidate.account.slice(-6)}
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
{candidate.endorsersCount} endorsements
|
||||
{t('elections.endorsements', { count: candidate.endorsersCount })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-bold text-white">{percentage.toFixed(1)}%</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
{candidate.voteCount.toLocaleString()} votes
|
||||
{t('elections.votesCount', { count: candidate.voteCount.toLocaleString() })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -326,17 +328,17 @@ const ElectionsInterface: React.FC = () => {
|
||||
{votingElectionId === election.electionId ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Submitting...
|
||||
{t('elections.submitting')}
|
||||
</>
|
||||
) : isSelected ? (
|
||||
<>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
Selected
|
||||
{t('elections.selected')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Vote className="w-4 h-4 mr-2" />
|
||||
{election.electionType === 'Parliamentary' ? 'Select' : 'Vote'}
|
||||
{election.electionType === 'Parliamentary' ? t('elections.select') : t('elections.vote')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -348,7 +350,7 @@ const ElectionsInterface: React.FC = () => {
|
||||
{election.electionType === 'Parliamentary' && !election.userHasVoted && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-700">
|
||||
<p className="text-sm text-gray-400 text-center mb-3">
|
||||
Select multiple candidates for parliamentary election
|
||||
{t('elections.selectMultiple')}
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => submitParliamentaryVotes(election.electionId)}
|
||||
@@ -359,12 +361,12 @@ const ElectionsInterface: React.FC = () => {
|
||||
{votingElectionId === election.electionId ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Submitting Votes...
|
||||
{t('elections.submittingVotes')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Vote className="w-4 h-4 mr-2" />
|
||||
Submit {(selectedCandidates.get(election.electionId) || []).length} Vote(s)
|
||||
{t('elections.submitVotes', { count: (selectedCandidates.get(election.electionId) || []).length })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -378,17 +380,17 @@ const ElectionsInterface: React.FC = () => {
|
||||
{election.status === 'CandidacyPeriod' && (
|
||||
<div className="text-center py-4">
|
||||
<p className="text-gray-400 mb-4">
|
||||
{election.totalCandidates} candidates registered so far
|
||||
{t('elections.candidatesRegistered', { count: election.totalCandidates })}
|
||||
</p>
|
||||
<Button variant="outline">
|
||||
Register as Candidate
|
||||
{t('elections.registerAsCandidate')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{election.status === 'CampaignPeriod' && (
|
||||
<div className="text-center py-4 text-gray-400">
|
||||
<p>{election.totalCandidates} candidates competing</p>
|
||||
<p>{t('elections.candidatesCompeting', { count: election.totalCandidates })}</p>
|
||||
<p className="text-sm mt-2">Voting begins {formatRemainingTime(election.campaignEndBlock)}</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -401,9 +403,9 @@ const ElectionsInterface: React.FC = () => {
|
||||
<TabsContent value="register">
|
||||
<Card className="bg-gray-900/50 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Candidate Registration</CardTitle>
|
||||
<CardTitle className="text-white">{t('elections.candidateRegistration')}</CardTitle>
|
||||
<CardDescription>
|
||||
Register as a candidate for upcoming elections
|
||||
{t('elections.registerDescription')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
@@ -412,7 +414,7 @@ const ElectionsInterface: React.FC = () => {
|
||||
<AlertCircle className="w-5 h-5 text-amber-500 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium text-amber-400">
|
||||
Requirements
|
||||
{t('elections.requirements')}
|
||||
</p>
|
||||
<ul className="text-sm text-amber-300/80 mt-2 space-y-1">
|
||||
<li>• Minimum Trust Score: 300 (Parliamentary) / 600 (Presidential)</li>
|
||||
@@ -424,7 +426,7 @@ const ElectionsInterface: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<Button className="w-full bg-green-600 hover:bg-green-700" size="lg">
|
||||
Register as Candidate
|
||||
{t('elections.registerAsCandidate')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -433,14 +435,14 @@ const ElectionsInterface: React.FC = () => {
|
||||
<TabsContent value="results">
|
||||
<Card className="bg-gray-900/50 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Election Results</CardTitle>
|
||||
<CardDescription>Historical election outcomes</CardDescription>
|
||||
<CardTitle className="text-white">{t('elections.electionResults')}</CardTitle>
|
||||
<CardDescription>{t('elections.resultsDescription')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{completedResults.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
<Trophy className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||
<p>No completed elections yet</p>
|
||||
<p>{t('elections.noCompletedElections')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
@@ -448,36 +450,36 @@ const ElectionsInterface: React.FC = () => {
|
||||
<div key={result.electionId} className="p-4 border border-gray-700 rounded-lg">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<p className="font-medium text-white">Election #{result.electionId}</p>
|
||||
<p className="font-medium text-white">{t('governance.historyTab.election', { id: result.electionId })}</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
Finalized at block #{result.finalizedAt.toLocaleString()}
|
||||
{t('elections.finalizedAtBlock', { block: result.finalizedAt.toLocaleString() })}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="outline" className="border-green-500 text-green-400">
|
||||
<Trophy className="w-3 h-3 mr-1" />
|
||||
Completed
|
||||
{t('elections.completed')}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Winner(s)</span>
|
||||
<span className="text-gray-400">{t('elections.winners')}</span>
|
||||
<span className="font-medium text-white">
|
||||
{result.winners.length > 0
|
||||
? result.winners.map(w => `${w.substring(0, 8)}...`).join(', ')
|
||||
: 'N/A'}
|
||||
: t('elections.na')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Total Votes</span>
|
||||
<span className="text-gray-400">{t('elections.totalVotes')}</span>
|
||||
<span className="text-white">{result.totalVotes.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Turnout</span>
|
||||
<span className="text-gray-400">{t('elections.turnout')}</span>
|
||||
<span className="text-white">{result.turnoutPercentage}%</span>
|
||||
</div>
|
||||
{result.runoffRequired && (
|
||||
<Badge className="bg-yellow-500/20 text-yellow-400">
|
||||
Runoff Required
|
||||
{t('elections.runoffRequired')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
@@ -25,6 +26,7 @@ interface CompletedProposal {
|
||||
}
|
||||
|
||||
const GovernanceHistory: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { api, isApiReady } = usePezkuwi();
|
||||
const [completedElections, setCompletedElections] = useState<ElectionResult[]>([]);
|
||||
const [completedProposals, setCompletedProposals] = useState<CompletedProposal[]>([]);
|
||||
@@ -163,7 +165,7 @@ const GovernanceHistory: React.FC = () => {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-green-500" />
|
||||
<span className="ml-3 text-gray-400">Loading governance history...</span>
|
||||
<span className="ml-3 text-gray-400">{t('governance.historyTab.loading')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -174,7 +176,7 @@ const GovernanceHistory: React.FC = () => {
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center text-red-400">
|
||||
<XCircle className="w-5 h-5 mr-2" />
|
||||
Error: {error}
|
||||
{t('myVotes.error', { error })}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -192,7 +194,7 @@ const GovernanceHistory: React.FC = () => {
|
||||
<Trophy className="w-8 h-8 text-yellow-500" />
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-white">{stats.totalElectionsHeld}</div>
|
||||
<div className="text-sm text-gray-400">Elections Held</div>
|
||||
<div className="text-sm text-gray-400">{t('governance.historyTab.electionsHeld')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -203,7 +205,7 @@ const GovernanceHistory: React.FC = () => {
|
||||
<FileText className="w-8 h-8 text-purple-500" />
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-white">{stats.totalProposalsSubmitted}</div>
|
||||
<div className="text-sm text-gray-400">Total Proposals</div>
|
||||
<div className="text-sm text-gray-400">{t('governance.historyTab.totalProposals')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -214,7 +216,7 @@ const GovernanceHistory: React.FC = () => {
|
||||
<Users className="w-8 h-8 text-cyan-500" />
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-white">{stats.parliamentSize}</div>
|
||||
<div className="text-sm text-gray-400">Parliament Size</div>
|
||||
<div className="text-sm text-gray-400">{t('governance.historyTab.parliamentSize')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -225,7 +227,7 @@ const GovernanceHistory: React.FC = () => {
|
||||
<TrendingUp className="w-8 h-8 text-green-500" />
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-white">{stats.averageTurnout}%</div>
|
||||
<div className="text-sm text-gray-400">Avg Turnout</div>
|
||||
<div className="text-sm text-gray-400">{t('governance.historyTab.avgTurnout')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -237,15 +239,15 @@ const GovernanceHistory: React.FC = () => {
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="bg-green-500/10 border-green-500 text-green-400">
|
||||
<Activity className="h-3 w-3 mr-1" />
|
||||
Live Blockchain Data
|
||||
{t('governance.historyTab.liveData')}
|
||||
</Badge>
|
||||
<span className="text-sm text-gray-500">Block #{currentBlock.toLocaleString()}</span>
|
||||
<span className="text-sm text-gray-500">{t('governance.historyTab.block', { number: currentBlock.toLocaleString() })}</span>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="elections" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2 bg-gray-800/50">
|
||||
<TabsTrigger value="elections">Election History</TabsTrigger>
|
||||
<TabsTrigger value="proposals">Proposal History</TabsTrigger>
|
||||
<TabsTrigger value="elections">{t('governance.historyTab.electionHistory')}</TabsTrigger>
|
||||
<TabsTrigger value="proposals">{t('governance.historyTab.proposalHistory')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="elections" className="space-y-4">
|
||||
@@ -253,8 +255,8 @@ const GovernanceHistory: React.FC = () => {
|
||||
<Card className="bg-gray-900/50 border-gray-800">
|
||||
<CardContent className="pt-6 text-center text-gray-400">
|
||||
<Trophy className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||
<p>No completed elections in history</p>
|
||||
<p className="text-sm mt-2">Election results will appear here once voting concludes</p>
|
||||
<p>{t('governance.historyTab.noElections')}</p>
|
||||
<p className="text-sm mt-2">{t('governance.historyTab.electionsWillAppear')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
@@ -265,21 +267,21 @@ const GovernanceHistory: React.FC = () => {
|
||||
<div>
|
||||
<h4 className="font-medium text-white flex items-center gap-2">
|
||||
<Trophy className="w-4 h-4 text-yellow-500" />
|
||||
Election #{election.electionId}
|
||||
{t('governance.historyTab.election', { id: election.electionId })}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-400 mt-1 flex items-center gap-1">
|
||||
<Calendar className="w-3 h-3" />
|
||||
Finalized {formatBlockTime(election.finalizedAt)}
|
||||
{t('governance.historyTab.finalized', { time: formatBlockTime(election.finalizedAt) })}
|
||||
</p>
|
||||
</div>
|
||||
<Badge className="bg-green-500/20 text-green-400">
|
||||
Completed
|
||||
{t('governance.historyTab.completed')}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-400">Winner(s):</span>
|
||||
<span className="text-gray-400">{t('governance.historyTab.winners')}</span>
|
||||
<div className="mt-1 space-y-1">
|
||||
{election.winners.length > 0 ? (
|
||||
election.winners.map((winner, idx) => (
|
||||
@@ -288,24 +290,24 @@ const GovernanceHistory: React.FC = () => {
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-gray-500">No winners</span>
|
||||
<span className="text-gray-500">{t('governance.historyTab.noWinners')}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-gray-400">Total Votes</div>
|
||||
<div className="text-gray-400">{t('governance.historyTab.totalVotes')}</div>
|
||||
<div className="text-white font-medium">{election.totalVotes.toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-gray-700 flex justify-between text-sm">
|
||||
<div>
|
||||
<span className="text-gray-400">Turnout: </span>
|
||||
<span className="text-gray-400">{t('governance.historyTab.turnout')} </span>
|
||||
<span className="text-white">{election.turnoutPercentage}%</span>
|
||||
</div>
|
||||
{election.runoffRequired && (
|
||||
<Badge className="bg-yellow-500/20 text-yellow-400">
|
||||
Runoff Required
|
||||
{t('governance.historyTab.runoffRequired')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -320,8 +322,8 @@ const GovernanceHistory: React.FC = () => {
|
||||
<Card className="bg-gray-900/50 border-gray-800">
|
||||
<CardContent className="pt-6 text-center text-gray-400">
|
||||
<FileText className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||
<p>No completed proposals in history</p>
|
||||
<p className="text-sm mt-2">Proposal outcomes will appear here once voting concludes</p>
|
||||
<p>{t('governance.historyTab.noProposals')}</p>
|
||||
<p className="text-sm mt-2">{t('governance.historyTab.proposalsWillAppear')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
@@ -334,16 +336,16 @@ const GovernanceHistory: React.FC = () => {
|
||||
<div>
|
||||
<h4 className="font-medium text-white">{proposal.title}</h4>
|
||||
<p className="text-sm text-gray-400 mt-1">
|
||||
Proposed by {proposal.proposer.substring(0, 8)}...
|
||||
{t('governance.historyTab.proposedBy', { address: `${proposal.proposer.substring(0, 8)}...` })}
|
||||
</p>
|
||||
<div className="flex items-center gap-4 mt-2 text-sm">
|
||||
<span className="text-green-400">
|
||||
<CheckCircle className="w-3 h-3 inline mr-1" />
|
||||
{proposal.ayeVotes} Aye
|
||||
{proposal.ayeVotes} {t('governance.historyTab.aye')}
|
||||
</span>
|
||||
<span className="text-red-400">
|
||||
<XCircle className="w-3 h-3 inline mr-1" />
|
||||
{proposal.nayVotes} Nay
|
||||
{proposal.nayVotes} {t('governance.historyTab.nay')}
|
||||
</span>
|
||||
<span className="text-gray-500">
|
||||
{formatBlockTime(proposal.finalizedAt)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Users, Gavel, FileText, TrendingUpIcon,
|
||||
Shield
|
||||
@@ -30,6 +31,7 @@ const RELAY_TREASURY = '5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z'; // py
|
||||
const PEZ_TREASURY = '5EYCAe5iipewaoUvoNr8ttcKqj5czZPBvVAex6uWbT6HxQNU'; // pez/trea (Asset Hub)
|
||||
|
||||
const GovernanceOverview: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { api, isApiReady, assetHubApi, isAssetHubReady } = usePezkuwi();
|
||||
const [stats, setStats] = useState<GovernanceStats>({
|
||||
activeProposals: 0,
|
||||
@@ -165,7 +167,7 @@ const GovernanceOverview: React.FC = () => {
|
||||
}, [api, isApiReady, assetHubApi, isAssetHubReady]);
|
||||
|
||||
if (loading) {
|
||||
return <LoadingState message="Loading governance data..." />;
|
||||
return <LoadingState message={t('governance.overview.loading')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -176,7 +178,7 @@ const GovernanceOverview: React.FC = () => {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-gray-400 text-sm">Active Proposals</p>
|
||||
<p className="text-gray-400 text-sm">{t('governance.overview.activeProposals')}</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">{stats.activeProposals}</p>
|
||||
</div>
|
||||
<div className="p-3 bg-blue-500/10 rounded-lg">
|
||||
@@ -190,7 +192,7 @@ const GovernanceOverview: React.FC = () => {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-gray-400 text-sm">Active Elections</p>
|
||||
<p className="text-gray-400 text-sm">{t('governance.overview.activeElections')}</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">{stats.activeElections}</p>
|
||||
</div>
|
||||
<div className="p-3 bg-cyan-500/10 rounded-lg">
|
||||
@@ -204,7 +206,7 @@ const GovernanceOverview: React.FC = () => {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-gray-400 text-sm">Total Voters</p>
|
||||
<p className="text-gray-400 text-sm">{t('governance.overview.totalVoters')}</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">{stats.totalVoters}</p>
|
||||
</div>
|
||||
<div className="p-3 bg-kurdish-green/10 rounded-lg">
|
||||
@@ -218,7 +220,7 @@ const GovernanceOverview: React.FC = () => {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-gray-400 text-sm">Treasury</p>
|
||||
<p className="text-gray-400 text-sm">{t('governance.overview.treasury')}</p>
|
||||
<p className="text-lg font-bold text-white mt-1">{stats.pezTreasuryBalance}</p>
|
||||
<p className="text-sm text-gray-400 mt-1">{stats.relayTreasuryBalance}</p>
|
||||
</div>
|
||||
@@ -236,23 +238,23 @@ const GovernanceOverview: React.FC = () => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center">
|
||||
<Gavel className="w-5 h-5 mr-2 text-purple-400" />
|
||||
Parliament Status
|
||||
{t('governance.overview.parliamentStatus')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-400">Active Members</span>
|
||||
<span className="text-gray-400">{t('governance.overview.activeMembers')}</span>
|
||||
<span className="text-white font-semibold">{stats.parliamentMembers}/{stats.parliamentMax}</span>
|
||||
</div>
|
||||
{stats.activeElections > 0 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-400">Elections</span>
|
||||
<Badge className="bg-green-500/10 text-green-400 border-green-500/20">Active</Badge>
|
||||
<span className="text-gray-400">{t('governance.overview.elections')}</span>
|
||||
<Badge className="bg-green-500/10 text-green-400 border-green-500/20">{t('governance.status.active')}</Badge>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-400">Pending Votes</span>
|
||||
<span className="text-gray-400">{t('governance.overview.pendingVotes')}</span>
|
||||
<span className="text-white font-semibold">{stats.pendingVotes}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -263,17 +265,17 @@ const GovernanceOverview: React.FC = () => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center">
|
||||
<Shield className="w-5 h-5 mr-2 text-cyan-400" />
|
||||
Diwan (Constitutional Court)
|
||||
{t('governance.overview.diwan')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-400">Active Judges</span>
|
||||
<span className="text-gray-400">{t('governance.overview.activeJudges')}</span>
|
||||
<span className="text-white font-semibold">{stats.diwanMembers}/{stats.diwanMax}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-400">Pending Reviews</span>
|
||||
<span className="text-gray-400">{t('governance.overview.pendingReviews')}</span>
|
||||
<span className="text-white font-semibold">{stats.diwanPendingReviews}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -35,6 +36,7 @@ interface DelegationInfo {
|
||||
}
|
||||
|
||||
const MyVotes: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { api, isApiReady } = usePezkuwi();
|
||||
const { account, isConnected } = useWallet();
|
||||
const [proposalVotes, setProposalVotes] = useState<ProposalVote[]>([]);
|
||||
@@ -195,12 +197,12 @@ const MyVotes: React.FC = () => {
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center py-12">
|
||||
<Wallet className="w-16 h-16 mx-auto mb-4 text-gray-600" />
|
||||
<h3 className="text-xl font-semibold text-white mb-2">Connect Your Wallet</h3>
|
||||
<h3 className="text-xl font-semibold text-white mb-2">{t('myVotes.connectTitle')}</h3>
|
||||
<p className="text-gray-400 mb-6">
|
||||
Connect your wallet to view your voting history and delegations
|
||||
{t('myVotes.connectDescription')}
|
||||
</p>
|
||||
<Button className="bg-green-600 hover:bg-green-700">
|
||||
Connect Wallet
|
||||
{t('myVotes.connectButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -212,7 +214,7 @@ const MyVotes: React.FC = () => {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-green-500" />
|
||||
<span className="ml-3 text-gray-400">Loading your voting history...</span>
|
||||
<span className="ml-3 text-gray-400">{t('myVotes.loading')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -223,7 +225,7 @@ const MyVotes: React.FC = () => {
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center text-red-400">
|
||||
<XCircle className="w-5 h-5 mr-2" />
|
||||
Error: {error}
|
||||
{t('myVotes.error', { error })}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -244,7 +246,7 @@ const MyVotes: React.FC = () => {
|
||||
<Vote className="w-8 h-8 text-green-500" />
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-white">{totalVotes}</div>
|
||||
<div className="text-sm text-gray-400">Total Votes</div>
|
||||
<div className="text-sm text-gray-400">{t('myVotes.totalVotes')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -255,7 +257,7 @@ const MyVotes: React.FC = () => {
|
||||
<Activity className="w-8 h-8 text-blue-500" />
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-white">{activeVotes}</div>
|
||||
<div className="text-sm text-gray-400">Active Votes</div>
|
||||
<div className="text-sm text-gray-400">{t('myVotes.activeVotes')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -266,7 +268,7 @@ const MyVotes: React.FC = () => {
|
||||
<FileText className="w-8 h-8 text-purple-500" />
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-white">{proposalVotes.length}</div>
|
||||
<div className="text-sm text-gray-400">Proposal Votes</div>
|
||||
<div className="text-sm text-gray-400">{t('myVotes.proposalVotes')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -277,7 +279,7 @@ const MyVotes: React.FC = () => {
|
||||
<Users className="w-8 h-8 text-cyan-500" />
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-white">{electionVotes.length}</div>
|
||||
<div className="text-sm text-gray-400">Election Votes</div>
|
||||
<div className="text-sm text-gray-400">{t('myVotes.electionVotes')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -288,18 +290,18 @@ const MyVotes: React.FC = () => {
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="bg-green-500/10 border-green-500 text-green-400">
|
||||
<Activity className="h-3 w-3 mr-1" />
|
||||
Live Blockchain Data
|
||||
{t('myVotes.liveData')}
|
||||
</Badge>
|
||||
<span className="text-sm text-gray-500">
|
||||
Connected as {account?.substring(0, 8)}...{account?.slice(-6)}
|
||||
{t('myVotes.connectedAs', { address: `${account?.substring(0, 8)}...${account?.slice(-6)}` })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="proposals" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3 bg-gray-800/50">
|
||||
<TabsTrigger value="proposals">Proposal Votes</TabsTrigger>
|
||||
<TabsTrigger value="elections">Election Votes</TabsTrigger>
|
||||
<TabsTrigger value="delegations">My Delegations</TabsTrigger>
|
||||
<TabsTrigger value="proposals">{t('myVotes.proposalVotesTab')}</TabsTrigger>
|
||||
<TabsTrigger value="elections">{t('myVotes.electionVotesTab')}</TabsTrigger>
|
||||
<TabsTrigger value="delegations">{t('myVotes.delegationsTab')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="proposals" className="space-y-4">
|
||||
@@ -307,8 +309,8 @@ const MyVotes: React.FC = () => {
|
||||
<Card className="bg-gray-900/50 border-gray-800">
|
||||
<CardContent className="pt-6 text-center text-gray-400">
|
||||
<FileText className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||
<p>You have not voted on any proposals yet</p>
|
||||
<p className="text-sm mt-2">Check the Proposals tab to participate in governance</p>
|
||||
<p>{t('myVotes.noProposalVotes')}</p>
|
||||
<p className="text-sm mt-2">{t('myVotes.checkProposals')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
@@ -321,8 +323,7 @@ const MyVotes: React.FC = () => {
|
||||
<div>
|
||||
<h4 className="font-medium text-white">{vote.proposalTitle}</h4>
|
||||
<p className="text-sm text-gray-400 mt-1">
|
||||
Conviction: {vote.conviction}x •
|
||||
Amount: {formatTokenAmount(vote.amount)} HEZ
|
||||
{t('myVotes.conviction', { conviction: vote.conviction, amount: formatTokenAmount(vote.amount) })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -346,8 +347,8 @@ const MyVotes: React.FC = () => {
|
||||
<Card className="bg-gray-900/50 border-gray-800">
|
||||
<CardContent className="pt-6 text-center text-gray-400">
|
||||
<Users className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||
<p>You have not voted in any elections yet</p>
|
||||
<p className="text-sm mt-2">Check the Elections tab to participate</p>
|
||||
<p>{t('myVotes.noElectionVotes')}</p>
|
||||
<p className="text-sm mt-2">{t('myVotes.checkElections')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
@@ -356,10 +357,9 @@ const MyVotes: React.FC = () => {
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h4 className="font-medium text-white">{vote.electionType} Election</h4>
|
||||
<h4 className="font-medium text-white">{t('myVotes.electionName', { type: vote.electionType })}</h4>
|
||||
<p className="text-sm text-gray-400 mt-1">
|
||||
Election #{vote.electionId} •
|
||||
{vote.candidates.length} candidate(s) selected
|
||||
{t('myVotes.electionDetails', { id: vote.electionId, count: vote.candidates.length })}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{vote.candidates.map((candidate, idx) => (
|
||||
@@ -384,8 +384,8 @@ const MyVotes: React.FC = () => {
|
||||
<Card className="bg-gray-900/50 border-gray-800">
|
||||
<CardContent className="pt-6 text-center text-gray-400">
|
||||
<Users className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||
<p>You have not delegated your voting power</p>
|
||||
<p className="text-sm mt-2">Check the Delegation tab to delegate your votes</p>
|
||||
<p>{t('myVotes.noDelegations')}</p>
|
||||
<p className="text-sm mt-2">{t('myVotes.checkDelegation')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
@@ -395,11 +395,10 @@ const MyVotes: React.FC = () => {
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h4 className="font-medium text-white">
|
||||
Delegated to {delegation.delegateAddress.substring(0, 8)}...{delegation.delegateAddress.slice(-6)}
|
||||
{t('myVotes.delegatedTo', { address: `${delegation.delegateAddress.substring(0, 8)}...${delegation.delegateAddress.slice(-6)}` })}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-400 mt-1">
|
||||
Amount: {formatTokenAmount(delegation.amount)} HEZ •
|
||||
Conviction: {delegation.conviction}x
|
||||
{t('myVotes.delegationDetails', { amount: formatTokenAmount(delegation.amount), conviction: delegation.conviction })}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{delegation.tracks.map((track, tidx) => (
|
||||
@@ -416,7 +415,7 @@ const MyVotes: React.FC = () => {
|
||||
{delegation.status}
|
||||
</Badge>
|
||||
<Button size="sm" variant="outline" className="text-xs">
|
||||
Revoke
|
||||
{t('myVotes.revoke')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Clock, Users, AlertCircle, Activity } from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
||||
import { Badge } from '../ui/badge';
|
||||
@@ -24,6 +25,7 @@ interface Proposal {
|
||||
}
|
||||
|
||||
const ProposalsList: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { proposals: treasuryProposals, referenda, loading, error } = useGovernance();
|
||||
|
||||
// Format token amounts from blockchain units (12 decimals for HEZ)
|
||||
@@ -37,8 +39,8 @@ const ProposalsList: React.FC = () => {
|
||||
// Treasury proposals
|
||||
...treasuryProposals.map(p => ({
|
||||
id: p.proposalIndex,
|
||||
title: `Treasury Proposal #${p.proposalIndex}`,
|
||||
description: `Requesting ${formatTokenAmount(p.value)} HEZ for ${p.beneficiary.substring(0, 10)}...`,
|
||||
title: t('proposals.treasuryProposal', { id: p.proposalIndex }),
|
||||
description: t('proposals.treasuryDescription', { amount: formatTokenAmount(p.value), beneficiary: `${p.beneficiary.substring(0, 10)}...` }),
|
||||
proposer: p.proposer,
|
||||
type: 'treasury' as const,
|
||||
status: p.status as 'active' | 'passed' | 'rejected' | 'pending',
|
||||
@@ -46,14 +48,14 @@ const ProposalsList: React.FC = () => {
|
||||
nayVotes: 0,
|
||||
totalVotes: 0,
|
||||
quorum: 0,
|
||||
deadline: 'Pending referendum',
|
||||
deadline: t('proposals.pendingReferendum'),
|
||||
requestedAmount: `${formatTokenAmount(p.value)} HEZ`
|
||||
})),
|
||||
// Democracy referenda
|
||||
...referenda.map(r => ({
|
||||
id: r.index,
|
||||
title: `Referendum #${r.index}`,
|
||||
description: `Voting on proposal with ${r.threshold} threshold`,
|
||||
title: t('proposals.referendum', { id: r.index }),
|
||||
description: t('proposals.referendumDescription', { threshold: r.threshold }),
|
||||
proposer: 'Democracy',
|
||||
type: 'executive' as const,
|
||||
status: r.status as 'active' | 'passed' | 'rejected' | 'pending',
|
||||
@@ -67,24 +69,24 @@ const ProposalsList: React.FC = () => {
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch(status) {
|
||||
case 'active': return <Badge className="bg-blue-500/10 text-blue-400">Active</Badge>;
|
||||
case 'passed': return <Badge className="bg-green-500/10 text-green-400">Passed</Badge>;
|
||||
case 'rejected': return <Badge className="bg-red-500/10 text-red-400">Rejected</Badge>;
|
||||
default: return <Badge className="bg-gray-500/10 text-gray-400">Pending</Badge>;
|
||||
case 'active': return <Badge className="bg-blue-500/10 text-blue-400">{t('governance.status.active')}</Badge>;
|
||||
case 'passed': return <Badge className="bg-green-500/10 text-green-400">{t('governance.status.passed')}</Badge>;
|
||||
case 'rejected': return <Badge className="bg-red-500/10 text-red-400">{t('governance.status.rejected')}</Badge>;
|
||||
default: return <Badge className="bg-gray-500/10 text-gray-400">{t('governance.status.pending')}</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeBadge = (type: string) => {
|
||||
switch(type) {
|
||||
case 'treasury': return <Badge className="bg-yellow-500/10 text-yellow-400">Treasury</Badge>;
|
||||
case 'executive': return <Badge className="bg-kurdish-red/10 text-kurdish-red">Executive</Badge>;
|
||||
case 'constitutional': return <Badge className="bg-cyan-500/10 text-cyan-400">Constitutional</Badge>;
|
||||
default: return <Badge className="bg-gray-500/10 text-gray-400">Simple</Badge>;
|
||||
case 'treasury': return <Badge className="bg-yellow-500/10 text-yellow-400">{t('proposals.type.treasury')}</Badge>;
|
||||
case 'executive': return <Badge className="bg-kurdish-red/10 text-kurdish-red">{t('proposals.type.executive')}</Badge>;
|
||||
case 'constitutional': return <Badge className="bg-cyan-500/10 text-cyan-400">{t('proposals.type.constitutional')}</Badge>;
|
||||
default: return <Badge className="bg-gray-500/10 text-gray-400">{t('proposals.type.simple')}</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <LoadingState message="Loading proposals from blockchain..." />;
|
||||
return <LoadingState message={t('proposals.loading')} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
@@ -92,7 +94,7 @@ const ProposalsList: React.FC = () => {
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Failed to load proposals: {error}
|
||||
{t('proposals.loadError', { error })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
@@ -104,17 +106,17 @@ const ProposalsList: React.FC = () => {
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||
<Activity className="h-3 w-3 mr-1" />
|
||||
Live Blockchain Data
|
||||
{t('proposals.liveData')}
|
||||
</Badge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{proposals.length} active proposals & referenda
|
||||
{t('proposals.count', { count: proposals.length })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{proposals.length === 0 ? (
|
||||
<Card className="bg-gray-900/50 border-gray-800">
|
||||
<CardContent className="pt-6 text-center text-gray-500">
|
||||
No active proposals or referenda found on the blockchain.
|
||||
{t('proposals.noProposals')}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
@@ -152,17 +154,17 @@ const ProposalsList: React.FC = () => {
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-400">Voting Progress</span>
|
||||
<span className="text-white">{proposal.totalVotes} votes</span>
|
||||
<span className="text-gray-400">{t('proposals.votingProgress')}</span>
|
||||
<span className="text-white">{t('proposals.votes', { count: proposal.totalVotes })}</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-green-400 text-xs w-12">Aye</span>
|
||||
<span className="text-green-400 text-xs w-12">{t('proposals.aye')}</span>
|
||||
<Progress value={ayePercentage} className="flex-1 h-2" />
|
||||
<span className="text-white text-sm w-12 text-right">{ayePercentage.toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-red-400 text-xs w-12">Nay</span>
|
||||
<span className="text-red-400 text-xs w-12">{t('proposals.nay')}</span>
|
||||
<Progress value={nayPercentage} className="flex-1 h-2" />
|
||||
<span className="text-white text-sm w-12 text-right">{nayPercentage.toFixed(0)}%</span>
|
||||
</div>
|
||||
@@ -173,22 +175,22 @@ const ProposalsList: React.FC = () => {
|
||||
<div className="flex items-center space-x-4 text-sm">
|
||||
<div className="flex items-center">
|
||||
<Users className="w-4 h-4 mr-1 text-gray-400" />
|
||||
<span className="text-gray-400">Proposer: {proposal.proposer}</span>
|
||||
<span className="text-gray-400">{t('proposals.proposer', { address: proposal.proposer })}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
{quorumReached ? (
|
||||
<span className="text-green-400">✓ Quorum reached</span>
|
||||
<span className="text-green-400">✓ {t('proposals.quorumReached')}</span>
|
||||
) : (
|
||||
<span className="text-yellow-400">⚠ Quorum: {proposal.quorum}%</span>
|
||||
<span className="text-yellow-400">⚠ {t('proposals.quorum', { percent: proposal.quorum })}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button size="sm" variant="outline" className="border-gray-700">
|
||||
View Details
|
||||
{t('proposals.viewDetails')}
|
||||
</Button>
|
||||
<Button size="sm" className="bg-kurdish-green hover:bg-kurdish-green/80">
|
||||
Cast Vote
|
||||
{t('proposals.castVote')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Bell, MessageCircle, AtSign, Heart, Award, TrendingUp, X, Check } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -25,6 +26,7 @@ interface Notification {
|
||||
}
|
||||
|
||||
export const NotificationCenter: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { subscribe, unsubscribe } = useWebSocket();
|
||||
const { toast } = useToast();
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
@@ -50,8 +52,8 @@ export const NotificationCenter: React.FC = () => {
|
||||
const notification: Notification = {
|
||||
id: Date.now().toString(),
|
||||
type: 'mention',
|
||||
title: 'You were mentioned',
|
||||
message: `${data.sender} mentioned you in a discussion`,
|
||||
title: t('notifCenter.youWereMentioned'),
|
||||
message: t('notifCenter.mentionedYou', { sender: data.sender as string }),
|
||||
timestamp: new Date(),
|
||||
read: false,
|
||||
actionUrl: data.url,
|
||||
@@ -64,8 +66,8 @@ export const NotificationCenter: React.FC = () => {
|
||||
const notification: Notification = {
|
||||
id: Date.now().toString(),
|
||||
type: 'reply',
|
||||
title: 'New reply',
|
||||
message: `${data.sender} replied to your comment`,
|
||||
title: t('notifCenter.newReply'),
|
||||
message: t('notifCenter.repliedToComment', { sender: data.sender as string }),
|
||||
timestamp: new Date(),
|
||||
read: false,
|
||||
actionUrl: data.url,
|
||||
@@ -147,9 +149,9 @@ export const NotificationCenter: React.FC = () => {
|
||||
<Tabs defaultValue="all" className="w-full">
|
||||
<div className="flex items-center justify-between p-4 border-b">
|
||||
<TabsList>
|
||||
<TabsTrigger value="all">All</TabsTrigger>
|
||||
<TabsTrigger value="unread">Unread</TabsTrigger>
|
||||
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||
<TabsTrigger value="all">{t('notifCenter.all')}</TabsTrigger>
|
||||
<TabsTrigger value="unread">{t('notifCenter.unread')}</TabsTrigger>
|
||||
<TabsTrigger value="settings">{t('notifCenter.settings')}</TabsTrigger>
|
||||
</TabsList>
|
||||
<Button variant="ghost" size="icon" onClick={() => setIsOpen(false)}>
|
||||
<X className="h-4 w-4" />
|
||||
@@ -159,11 +161,11 @@ export const NotificationCenter: React.FC = () => {
|
||||
<TabsContent value="all" className="p-0">
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{notifications.length} notifications
|
||||
{t('notifCenter.notifications', { count: notifications.length })}
|
||||
</span>
|
||||
<Button variant="ghost" size="sm" onClick={markAllAsRead}>
|
||||
<Check className="h-3 w-3 mr-1" />
|
||||
Mark all read
|
||||
{t('notifCenter.markAllRead')}
|
||||
</Button>
|
||||
</div>
|
||||
<ScrollArea className="h-96">
|
||||
@@ -221,7 +223,7 @@ export const NotificationCenter: React.FC = () => {
|
||||
<TabsContent value="settings" className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="push">Push Notifications</Label>
|
||||
<Label htmlFor="push">{t('notifCenter.pushNotifications')}</Label>
|
||||
<Switch
|
||||
id="push"
|
||||
checked={settings.pushEnabled}
|
||||
@@ -237,7 +239,7 @@ export const NotificationCenter: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="mentions">Mentions</Label>
|
||||
<Label htmlFor="mentions">{t('notifCenter.mentions')}</Label>
|
||||
<Switch
|
||||
id="mentions"
|
||||
checked={settings.mentions}
|
||||
@@ -246,7 +248,7 @@ export const NotificationCenter: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="replies">Replies</Label>
|
||||
<Label htmlFor="replies">{t('notifCenter.replies')}</Label>
|
||||
<Switch
|
||||
id="replies"
|
||||
checked={settings.replies}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -23,6 +24,7 @@ interface OfferWithReputation extends P2PFiatOffer {
|
||||
}
|
||||
|
||||
export function AdList({ type, filters }: AdListProps) {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const [offers, setOffers] = useState<OfferWithReputation[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -184,7 +186,7 @@ export function AdList({ type, filters }: AdListProps) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-400">
|
||||
{type === 'my-ads' ? 'You have no active offers' : 'No offers available'}
|
||||
{type === 'my-ads' ? t('p2pAd.noActiveOffers') : t('p2pAd.noOffers')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -212,16 +214,16 @@ export function AdList({ type, filters }: AdListProps) {
|
||||
<MerchantTierBadge tier={offer.merchant_tier} size="sm" />
|
||||
)}
|
||||
{offer.seller_reputation?.verified_merchant && (
|
||||
<Shield className="w-4 h-4 text-blue-400" title="Verified Merchant" />
|
||||
<Shield className="w-4 h-4 text-blue-400" title={t('p2p.verifiedMerchant')} />
|
||||
)}
|
||||
{offer.seller_reputation?.fast_trader && (
|
||||
<Zap className="w-4 h-4 text-yellow-400" title="Fast Trader" />
|
||||
<Zap className="w-4 h-4 text-yellow-400" title={t('p2p.fastTrader')} />
|
||||
)}
|
||||
</div>
|
||||
{offer.seller_reputation && (
|
||||
<p className="text-sm text-gray-400">
|
||||
{offer.seller_reputation.completed_trades} trades • {' '}
|
||||
{((offer.seller_reputation.completed_trades / (offer.seller_reputation.total_trades || 1)) * 100).toFixed(0)}% completion
|
||||
{t('p2p.trades', { count: offer.seller_reputation.completed_trades })} • {' '}
|
||||
{t('p2p.completion', { percent: ((offer.seller_reputation.completed_trades / (offer.seller_reputation.total_trades || 1)) * 100).toFixed(0) })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -229,7 +231,7 @@ export function AdList({ type, filters }: AdListProps) {
|
||||
|
||||
{/* Price */}
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Price</p>
|
||||
<p className="text-sm text-gray-400">{t('p2p.price')}</p>
|
||||
<p className="text-xl font-bold text-green-400">
|
||||
{offer.price_per_unit.toFixed(2)} {offer.fiat_currency}
|
||||
</p>
|
||||
@@ -237,25 +239,25 @@ export function AdList({ type, filters }: AdListProps) {
|
||||
|
||||
{/* Available */}
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Available</p>
|
||||
<p className="text-sm text-gray-400">{t('p2p.available')}</p>
|
||||
<p className="text-lg font-semibold text-white">
|
||||
{offer.remaining_amount} {offer.token}
|
||||
</p>
|
||||
{offer.min_order_amount && (
|
||||
<p className="text-xs text-gray-500">
|
||||
Min: {offer.min_order_amount} {offer.token}
|
||||
{t('p2p.minLimit', { amount: offer.min_order_amount, token: offer.token })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Payment Method */}
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Payment</p>
|
||||
<p className="text-sm text-gray-400">{t('p2p.payment')}</p>
|
||||
<Badge variant="outline" className="mt-1">
|
||||
{offer.payment_method_name || 'N/A'}
|
||||
{offer.payment_method_name || t('p2p.na')}
|
||||
</Badge>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{offer.time_limit_minutes} min limit
|
||||
{t('p2p.timeLimit', { minutes: offer.time_limit_minutes })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -263,16 +265,16 @@ export function AdList({ type, filters }: AdListProps) {
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
{offer.seller_id === user?.id && type !== 'my-ads' && (
|
||||
<Badge variant="outline" className="text-xs bg-blue-500/10 text-blue-400 border-blue-500/30">
|
||||
Your Ad
|
||||
{t('p2pAd.yourAd')}
|
||||
</Badge>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => setSelectedOffer(offer)}
|
||||
disabled={type === 'my-ads' || offer.seller_id === user?.id}
|
||||
className="w-full md:w-auto"
|
||||
title={offer.seller_id === user?.id ? "You can't trade with your own ad" : ''}
|
||||
title={offer.seller_id === user?.id ? t('p2pAd.cantTradeOwnAd') : ''}
|
||||
>
|
||||
{type === 'buy' ? 'Buy' : 'Sell'} {offer.token}
|
||||
{type === 'buy' ? t('p2pAd.buyToken', { token: offer.token }) : t('p2pAd.sellToken', { token: offer.token })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -287,7 +289,7 @@ export function AdList({ type, filters }: AdListProps) {
|
||||
{offer.status.toUpperCase()}
|
||||
</Badge>
|
||||
<p className="text-sm text-gray-400">
|
||||
Created: {new Date(offer.created_at).toLocaleDateString()}
|
||||
{t('p2pAd.created', { date: new Date(offer.created_at).toLocaleDateString() })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { CryptoToken, FiatCurrency } from '@pezkuwi/lib/p2p-fiat';
|
||||
|
||||
interface BlockTradeRequest {
|
||||
@@ -78,6 +79,7 @@ export function BlockTrade() {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [requests, setRequests] = useState<BlockTradeRequest[]>([]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const fiatSymbol = SUPPORTED_FIATS.find(f => f.code === fiat)?.symbol || '';
|
||||
const minAmount = MINIMUM_BLOCK_AMOUNTS[token];
|
||||
@@ -103,13 +105,13 @@ export function BlockTrade() {
|
||||
|
||||
const handleSubmitRequest = async () => {
|
||||
if (!user) {
|
||||
toast.error('Please login to submit a block trade request');
|
||||
toast.error(t('p2pBlock.loginRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
const amountNum = parseFloat(amount);
|
||||
if (isNaN(amountNum) || amountNum < minAmount) {
|
||||
toast.error(`Minimum amount for ${token} block trade is ${minAmount.toLocaleString()} ${token}`);
|
||||
toast.error(t('p2pBlock.minimumError', { token, amount: minAmount.toLocaleString() }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -132,7 +134,7 @@ export function BlockTrade() {
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
toast.success('Block trade request submitted! Our OTC desk will contact you within 24 hours.');
|
||||
toast.success(t('p2pBlock.requestSubmitted'));
|
||||
setShowRequestModal(false);
|
||||
setAmount('');
|
||||
setTargetPrice('');
|
||||
@@ -142,7 +144,7 @@ export function BlockTrade() {
|
||||
setRequests(prev => [data, ...prev]);
|
||||
} catch (err) {
|
||||
console.error('Block trade request error:', err);
|
||||
toast.error('Failed to submit request');
|
||||
toast.error(t('p2pBlock.failedToSubmit'));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
@@ -170,14 +172,14 @@ export function BlockTrade() {
|
||||
<Blocks className="w-5 h-5 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg text-white">Block Trade (OTC)</CardTitle>
|
||||
<CardTitle className="text-lg text-white">{t('p2pBlock.title')}</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
Large volume trades with custom pricing
|
||||
{t('p2pBlock.description')}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Badge className="bg-purple-500/20 text-purple-400 border-purple-500/30">
|
||||
VIP
|
||||
{t('p2pBlock.vip')}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -186,25 +188,25 @@ export function BlockTrade() {
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<Lock className="w-4 h-4 text-purple-400" />
|
||||
<span>Private Negotiation</span>
|
||||
<span>{t('p2pBlock.privateNegotiation')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<Shield className="w-4 h-4 text-green-400" />
|
||||
<span>Escrow Protected</span>
|
||||
<span>{t('p2pBlock.escrowProtected')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<Building2 className="w-4 h-4 text-blue-400" />
|
||||
<span>Dedicated Support</span>
|
||||
<span>{t('p2pBlock.dedicatedSupport')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<Clock className="w-4 h-4 text-yellow-400" />
|
||||
<span>Flexible Settlement</span>
|
||||
<span>{t('p2pBlock.flexibleSettlement')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Minimum Amounts Info */}
|
||||
<div className="p-3 bg-gray-800/50 rounded-lg">
|
||||
<p className="text-xs text-gray-500 mb-2">Minimum Block Trade Amounts:</p>
|
||||
<p className="text-xs text-gray-500 mb-2">{t('p2pBlock.minimumAmounts')}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.entries(MINIMUM_BLOCK_AMOUNTS).map(([t, min]) => (
|
||||
<Badge key={t} variant="outline" className="border-gray-700 text-gray-300">
|
||||
@@ -220,14 +222,14 @@ export function BlockTrade() {
|
||||
onClick={() => setShowRequestModal(true)}
|
||||
>
|
||||
<MessageSquare className="w-4 h-4 mr-2" />
|
||||
Request Block Trade
|
||||
{t('p2pBlock.requestBlockTrade')}
|
||||
<ChevronRight className="w-4 h-4 ml-auto" />
|
||||
</Button>
|
||||
|
||||
{/* Active Requests */}
|
||||
{requests.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-gray-500">Your Requests:</p>
|
||||
<p className="text-xs text-gray-500">{t('p2pBlock.yourRequests')}</p>
|
||||
{requests.slice(0, 3).map(req => (
|
||||
<div
|
||||
key={req.id}
|
||||
@@ -257,10 +259,10 @@ export function BlockTrade() {
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white flex items-center gap-2">
|
||||
<Blocks className="w-5 h-5 text-purple-400" />
|
||||
Block Trade Request
|
||||
{t('p2pBlock.requestTitle')}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Submit a request for our OTC desk to handle your large volume trade.
|
||||
{t('p2pBlock.requestDescription')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -269,10 +271,10 @@ export function BlockTrade() {
|
||||
<Tabs value={type} onValueChange={(v) => setType(v as 'buy' | 'sell')}>
|
||||
<TabsList className="grid w-full grid-cols-2 bg-gray-800">
|
||||
<TabsTrigger value="buy" className="data-[state=active]:bg-green-600">
|
||||
Buy
|
||||
{t('p2p.buy')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="sell" className="data-[state=active]:bg-red-600">
|
||||
Sell
|
||||
{t('p2p.sell')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
@@ -280,7 +282,7 @@ export function BlockTrade() {
|
||||
{/* Token & Fiat */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-gray-400 text-xs">Token</Label>
|
||||
<Label className="text-gray-400 text-xs">{t('p2p.token')}</Label>
|
||||
<Select value={token} onValueChange={(v) => setToken(v as CryptoToken)}>
|
||||
<SelectTrigger className="bg-gray-800 border-gray-700">
|
||||
<SelectValue />
|
||||
@@ -293,7 +295,7 @@ export function BlockTrade() {
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-gray-400 text-xs">Currency</Label>
|
||||
<Label className="text-gray-400 text-xs">{t('p2p.currency')}</Label>
|
||||
<Select value={fiat} onValueChange={(v) => setFiat(v as FiatCurrency)}>
|
||||
<SelectTrigger className="bg-gray-800 border-gray-700">
|
||||
<SelectValue />
|
||||
@@ -311,7 +313,7 @@ export function BlockTrade() {
|
||||
|
||||
{/* Amount */}
|
||||
<div>
|
||||
<Label className="text-gray-400 text-xs">Amount ({token})</Label>
|
||||
<Label className="text-gray-400 text-xs">{t('p2pBlock.amountLabel', { token })}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={`Min: ${minAmount.toLocaleString()}`}
|
||||
@@ -320,17 +322,17 @@ export function BlockTrade() {
|
||||
className="bg-gray-800 border-gray-700"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Minimum: {minAmount.toLocaleString()} {token}
|
||||
{t('p2pBlock.minimumLabel', { amount: minAmount.toLocaleString(), token })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Target Price (Optional) */}
|
||||
<div>
|
||||
<Label className="text-gray-400 text-xs">Target Price (Optional)</Label>
|
||||
<Label className="text-gray-400 text-xs">{t('p2pBlock.targetPrice')}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Your desired price per unit"
|
||||
placeholder={t('p2pBlock.targetPricePlaceholder')}
|
||||
value={targetPrice}
|
||||
onChange={(e) => setTargetPrice(e.target.value)}
|
||||
className="bg-gray-800 border-gray-700 pr-16"
|
||||
@@ -343,9 +345,9 @@ export function BlockTrade() {
|
||||
|
||||
{/* Message */}
|
||||
<div>
|
||||
<Label className="text-gray-400 text-xs">Additional Details (Optional)</Label>
|
||||
<Label className="text-gray-400 text-xs">{t('p2pBlock.additionalDetails')}</Label>
|
||||
<Textarea
|
||||
placeholder="Settlement preferences, timeline, payment methods..."
|
||||
placeholder={t('p2pBlock.detailsPlaceholder')}
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
className="bg-gray-800 border-gray-700 min-h-[80px]"
|
||||
@@ -356,8 +358,7 @@ export function BlockTrade() {
|
||||
<div className="p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg flex items-start gap-2">
|
||||
<AlertTriangle className="w-4 h-4 text-yellow-400 mt-0.5" />
|
||||
<p className="text-xs text-yellow-400">
|
||||
Block trades require KYC verification and may take 24-48 hours to process.
|
||||
Our OTC desk will contact you via email.
|
||||
{t('p2pBlock.kycWarning')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -368,14 +369,14 @@ export function BlockTrade() {
|
||||
onClick={() => setShowRequestModal(false)}
|
||||
className="border-gray-700"
|
||||
>
|
||||
Cancel
|
||||
{t('p2p.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-purple-600 hover:bg-purple-700"
|
||||
onClick={handleSubmitRequest}
|
||||
disabled={isSubmitting || !amount || parseFloat(amount) < minAmount}
|
||||
>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit Request'}
|
||||
{isSubmitting ? t('p2p.submitting') : t('p2pBlock.submitRequest')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -22,6 +23,7 @@ interface CreateAdProps {
|
||||
}
|
||||
|
||||
export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const { account } = useWallet();
|
||||
|
||||
@@ -78,13 +80,13 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
console.log('🔥 handleCreateAd called', { account, user: user?.id });
|
||||
|
||||
if (!account || !user) {
|
||||
toast.error('Please connect your wallet and log in');
|
||||
toast.error(t('p2p.connectWalletAndLogin'));
|
||||
console.log('❌ No account or user', { account, user });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedPaymentMethod) {
|
||||
toast.error('Please select a payment method');
|
||||
toast.error(t('p2pCreate.selectPaymentMethodError'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -105,22 +107,22 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
const fiatAmt = parseFloat(fiatAmount);
|
||||
|
||||
if (!cryptoAmt || cryptoAmt <= 0) {
|
||||
toast.error('Invalid crypto amount');
|
||||
toast.error(t('p2pCreate.invalidCryptoAmount'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fiatAmt || fiatAmt <= 0) {
|
||||
toast.error('Invalid fiat amount');
|
||||
toast.error(t('p2pCreate.invalidFiatAmount'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedPaymentMethod.min_trade_amount && fiatAmt < selectedPaymentMethod.min_trade_amount) {
|
||||
toast.error(`Minimum trade amount: ${selectedPaymentMethod.min_trade_amount} ${fiatCurrency}`);
|
||||
toast.error(t('p2pCreate.minTradeAmount', { amount: selectedPaymentMethod.min_trade_amount, currency: fiatCurrency }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedPaymentMethod.max_trade_amount && fiatAmt > selectedPaymentMethod.max_trade_amount) {
|
||||
toast.error(`Maximum trade amount: ${selectedPaymentMethod.max_trade_amount} ${fiatCurrency}`);
|
||||
toast.error(t('p2pCreate.maxTradeAmount', { amount: selectedPaymentMethod.max_trade_amount, currency: fiatCurrency }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -152,16 +154,16 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
|
||||
if (error) {
|
||||
console.error('❌ Supabase error:', error);
|
||||
toast.error(error.message || 'Failed to create offer');
|
||||
toast.error(error.message || t('p2pCreate.failedToCreate'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ Offer created successfully:', data);
|
||||
toast.success('Ad created successfully!');
|
||||
toast.success(t('p2pCreate.adCreated'));
|
||||
onAdCreated();
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Create ad error:', error);
|
||||
toast.error('Failed to create offer');
|
||||
toast.error(t('p2pCreate.failedToCreate'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -170,15 +172,15 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
return (
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Create P2P Offer</CardTitle>
|
||||
<CardTitle className="text-white">{t('p2pCreate.title')}</CardTitle>
|
||||
<CardDescription>
|
||||
Lock your crypto in escrow and set your price
|
||||
{t('p2pCreate.description')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Ad Type Selection */}
|
||||
<div>
|
||||
<Label>I want to</Label>
|
||||
<Label>{t('p2pCreate.iWantTo')}</Label>
|
||||
<div className="grid grid-cols-2 gap-2 mt-2">
|
||||
<Button
|
||||
type="button"
|
||||
@@ -186,7 +188,7 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
className={adType === 'sell' ? 'bg-red-600 hover:bg-red-700' : ''}
|
||||
onClick={() => setAdType('sell')}
|
||||
>
|
||||
Sell {token}
|
||||
{t('p2pCreate.sellToken', { token })}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -194,20 +196,20 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
className={adType === 'buy' ? 'bg-green-600 hover:bg-green-700' : ''}
|
||||
onClick={() => setAdType('buy')}
|
||||
>
|
||||
Buy {token}
|
||||
{t('p2pCreate.buyToken', { token })}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
{adType === 'sell'
|
||||
? 'You will receive fiat payment and send crypto to buyer'
|
||||
: 'You will send fiat payment and receive crypto from seller'}
|
||||
? t('p2pCreate.sellDescription')
|
||||
: t('p2pCreate.buyDescription')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Crypto Details */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="token">Token</Label>
|
||||
<Label htmlFor="token">{t('p2p.token')}</Label>
|
||||
<Select value={token} onValueChange={(v) => setToken(v as CryptoToken)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
@@ -219,14 +221,14 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="amountCrypto">Amount ({token})</Label>
|
||||
<Label htmlFor="amountCrypto">{t('p2pCreate.amountLabel', { token })}</Label>
|
||||
<Input
|
||||
id="amountCrypto"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={amountCrypto}
|
||||
onChange={e => setAmountCrypto(e.target.value)}
|
||||
placeholder="Amount"
|
||||
placeholder={t('p2pCreate.amountPlaceholder')}
|
||||
className="placeholder:text-gray-500 placeholder:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
@@ -235,7 +237,7 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
{/* Fiat Details */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="fiatCurrency">Fiat Currency</Label>
|
||||
<Label htmlFor="fiatCurrency">{t('p2pCreate.fiatCurrency')}</Label>
|
||||
<Select value={fiatCurrency} onValueChange={(v) => setFiatCurrency(v as FiatCurrency)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
@@ -260,14 +262,14 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="fiatAmount">Total Amount ({fiatCurrency})</Label>
|
||||
<Label htmlFor="fiatAmount">{t('p2pCreate.totalFiatAmount', { currency: fiatCurrency })}</Label>
|
||||
<Input
|
||||
id="fiatAmount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={fiatAmount}
|
||||
onChange={e => setFiatAmount(e.target.value)}
|
||||
placeholder="Amount"
|
||||
placeholder={t('p2pCreate.amountPlaceholder')}
|
||||
className="placeholder:text-gray-500 placeholder:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
@@ -276,7 +278,7 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
{/* Price Display */}
|
||||
{amountCrypto && fiatAmount && (
|
||||
<div className="p-4 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||
<p className="text-sm text-gray-400">Price per {token}</p>
|
||||
<p className="text-sm text-gray-400">{t('p2pCreate.pricePerToken', { token })}</p>
|
||||
<p className="text-2xl font-bold text-green-400">
|
||||
{pricePerUnit} {fiatCurrency}
|
||||
</p>
|
||||
@@ -285,10 +287,10 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
|
||||
{/* Payment Method */}
|
||||
<div>
|
||||
<Label htmlFor="paymentMethod">Payment Method</Label>
|
||||
<Label htmlFor="paymentMethod">{t('p2pCreate.paymentMethod')}</Label>
|
||||
<Select onValueChange={handlePaymentMethodChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select payment method..." />
|
||||
<SelectValue placeholder={t('p2pCreate.selectPaymentMethod')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{paymentMethods.map(method => (
|
||||
@@ -303,7 +305,7 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
{/* Dynamic Payment Details Fields */}
|
||||
{selectedPaymentMethod && Object.keys(selectedPaymentMethod.fields).length > 0 && (
|
||||
<div className="space-y-4 p-4 border border-gray-700 rounded-lg">
|
||||
<h3 className="font-semibold text-white">Payment Details</h3>
|
||||
<h3 className="font-semibold text-white">{t('p2pCreate.paymentDetails')}</h3>
|
||||
{Object.entries(selectedPaymentMethod.fields).map(([field, placeholder]) => (
|
||||
<div key={field}>
|
||||
<Label htmlFor={field}>
|
||||
@@ -324,26 +326,26 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
{/* Order Limits */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="minOrder">Min Order (optional)</Label>
|
||||
<Label htmlFor="minOrder">{t('p2pCreate.minOrder')}</Label>
|
||||
<Input
|
||||
id="minOrder"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={minOrderAmount}
|
||||
onChange={e => setMinOrderAmount(e.target.value)}
|
||||
placeholder="Minimum amount (optional)"
|
||||
placeholder={t('p2pCreate.minOrderPlaceholder')}
|
||||
className="placeholder:text-gray-500 placeholder:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="maxOrder">Max Order (optional)</Label>
|
||||
<Label htmlFor="maxOrder">{t('p2pCreate.maxOrder')}</Label>
|
||||
<Input
|
||||
id="maxOrder"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={maxOrderAmount}
|
||||
onChange={e => setMaxOrderAmount(e.target.value)}
|
||||
placeholder="Maximum amount (optional)"
|
||||
placeholder={t('p2pCreate.maxOrderPlaceholder')}
|
||||
className="placeholder:text-gray-500 placeholder:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
@@ -351,16 +353,16 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
|
||||
{/* Time Limit */}
|
||||
<div>
|
||||
<Label htmlFor="timeLimit">Payment Time Limit (minutes)</Label>
|
||||
<Label htmlFor="timeLimit">{t('p2pCreate.paymentTimeLimit')}</Label>
|
||||
<Select value={timeLimit.toString()} onValueChange={(v) => setTimeLimit(parseInt(v))}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="15">15 minutes</SelectItem>
|
||||
<SelectItem value="30">30 minutes</SelectItem>
|
||||
<SelectItem value="60">1 hour</SelectItem>
|
||||
<SelectItem value="120">2 hours</SelectItem>
|
||||
<SelectItem value="15">{t('p2pCreate.15min')}</SelectItem>
|
||||
<SelectItem value="30">{t('p2pCreate.30min')}</SelectItem>
|
||||
<SelectItem value="60">{t('p2pCreate.1hour')}</SelectItem>
|
||||
<SelectItem value="120">{t('p2pCreate.2hours')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -373,10 +375,10 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Creating offer & locking escrow...
|
||||
{t('p2pCreate.creatingOffer')}
|
||||
</>
|
||||
) : (
|
||||
'Create Offer'
|
||||
t('p2pCreate.createOffer')
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -46,6 +47,7 @@ interface DepositModalProps {
|
||||
type DepositStep = 'select' | 'send' | 'verify' | 'success';
|
||||
|
||||
export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const { api, selectedAccount } = usePezkuwi();
|
||||
const { balances, signTransaction } = useWallet();
|
||||
|
||||
@@ -89,10 +91,10 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
try {
|
||||
await navigator.clipboard.writeText(platformWallet);
|
||||
setCopied(true);
|
||||
toast.success('Address copied to clipboard');
|
||||
toast.success(t('p2pDeposit.addressCopied'));
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
toast.error('Failed to copy address');
|
||||
toast.error(t('p2pDeposit.failedToCopy'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -104,13 +106,13 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
|
||||
const handleSendDeposit = async () => {
|
||||
if (!api || !selectedAccount) {
|
||||
toast.error('Please connect your wallet');
|
||||
toast.error(t('p2pDeposit.connectWallet'));
|
||||
return;
|
||||
}
|
||||
|
||||
const depositAmount = parseFloat(amount);
|
||||
if (isNaN(depositAmount) || depositAmount <= 0) {
|
||||
toast.error('Please enter a valid amount');
|
||||
toast.error(t('p2pDeposit.enterValidAmount'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -131,7 +133,7 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
tx = api.tx.assets.transfer(assetId, platformWallet, amountBN);
|
||||
}
|
||||
|
||||
toast.info('Please sign the transaction in your wallet...');
|
||||
toast.info(t('p2pDeposit.signTransaction'));
|
||||
|
||||
// Sign and send
|
||||
const hash = await signTransaction(tx);
|
||||
@@ -139,11 +141,11 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
if (hash) {
|
||||
setTxHash(hash);
|
||||
setStep('verify');
|
||||
toast.success('Transaction sent! Please verify your deposit.');
|
||||
toast.success(t('p2pDeposit.txSent'));
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error('Deposit transaction error:', error);
|
||||
const message = error instanceof Error ? error.message : 'Transaction failed';
|
||||
const message = error instanceof Error ? error.message : t('p2pDeposit.txFailed');
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -152,13 +154,13 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
|
||||
const handleVerifyDeposit = async () => {
|
||||
if (!txHash) {
|
||||
toast.error('Please enter the transaction hash');
|
||||
toast.error(t('p2pDeposit.enterTxHash'));
|
||||
return;
|
||||
}
|
||||
|
||||
const depositAmount = parseFloat(amount);
|
||||
if (isNaN(depositAmount) || depositAmount <= 0) {
|
||||
toast.error('Invalid amount');
|
||||
toast.error(t('p2pDeposit.invalidAmount'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -176,19 +178,19 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message || 'Verification failed');
|
||||
throw new Error(error.message || t('p2pDeposit.verificationFailed'));
|
||||
}
|
||||
|
||||
if (data?.success) {
|
||||
toast.success(`Deposit verified! ${data.amount} ${token} added to your balance.`);
|
||||
toast.success(t('p2pDeposit.depositVerified', { amount: data.amount, token }));
|
||||
setStep('success');
|
||||
onSuccess?.();
|
||||
} else {
|
||||
throw new Error(data?.error || 'Verification failed');
|
||||
throw new Error(data?.error || t('p2pDeposit.verificationFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Verify deposit error:', error);
|
||||
const message = error instanceof Error ? error.message : 'Verification failed';
|
||||
const message = error instanceof Error ? error.message : t('p2pDeposit.verificationFailed');
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setVerifying(false);
|
||||
@@ -201,20 +203,20 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label>Select Token</Label>
|
||||
<Label>{t('p2pDeposit.selectToken')}</Label>
|
||||
<Select value={token} onValueChange={(v) => setToken(v as CryptoToken)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="HEZ">HEZ (Native)</SelectItem>
|
||||
<SelectItem value="PEZ">PEZ</SelectItem>
|
||||
<SelectItem value="HEZ">{t('p2pDeposit.hezNative')}</SelectItem>
|
||||
<SelectItem value="PEZ">{t('p2pDeposit.pez')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Amount to Deposit</Label>
|
||||
<Label>{t('p2pDeposit.amountToDeposit')}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="number"
|
||||
@@ -229,27 +231,26 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Wallet Balance: {parseFloat(getAvailableBalance()).toFixed(4)} {token}
|
||||
{t('p2pDeposit.walletBalance', { amount: parseFloat(getAvailableBalance()).toFixed(4), token })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Alert>
|
||||
<Wallet className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You will send {token} from your connected wallet to the P2P platform escrow.
|
||||
After confirmation, the amount will be credited to your P2P internal balance.
|
||||
{t('p2pDeposit.depositInfo', { token })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setStep('send')}
|
||||
disabled={!amount || parseFloat(amount) <= 0}
|
||||
>
|
||||
Continue
|
||||
{t('continue')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
@@ -263,7 +264,7 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
<div className="w-16 h-16 mx-auto mb-3 rounded-xl bg-primary/10 flex items-center justify-center">
|
||||
<QrCode className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<p className="text-sm font-medium">Send {amount} {token} to:</p>
|
||||
<p className="text-sm font-medium">{t('p2pDeposit.sendAmountTo', { amount, token })}</p>
|
||||
</div>
|
||||
|
||||
{platformWallet ? (
|
||||
@@ -280,12 +281,12 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
{copied ? (
|
||||
<>
|
||||
<CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
|
||||
Copied!
|
||||
{t('p2pDeposit.copied')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
Copy Address
|
||||
{t('p2pDeposit.copyAddress')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -298,8 +299,7 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Only send {token} on the PezkuwiChain network. Sending other tokens or using
|
||||
other networks will result in permanent loss of funds.
|
||||
{t('p2pDeposit.networkWarning', { token })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -309,7 +309,7 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
className="flex-1"
|
||||
onClick={() => setStep('select')}
|
||||
>
|
||||
Back
|
||||
{t('back')}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
@@ -319,11 +319,11 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Sending...
|
||||
{t('p2pDeposit.sending')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Send {amount} {token}
|
||||
{t('p2pDeposit.sendToken', { amount, token })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -337,12 +337,12 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
<Alert>
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<AlertDescription>
|
||||
Transaction sent! Please verify your deposit to credit your P2P balance.
|
||||
{t('p2pDeposit.txSentVerify')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Transaction Hash</Label>
|
||||
<Label>{t('p2pDeposit.txHash')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={txHash}
|
||||
@@ -364,11 +364,11 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
<div className="p-4 rounded-lg bg-muted/50 border">
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-muted-foreground">Token</p>
|
||||
<p className="text-muted-foreground">{t('p2pDeposit.tokenLabel')}</p>
|
||||
<p className="font-semibold">{token}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Amount</p>
|
||||
<p className="text-muted-foreground">{t('p2pDeposit.amountLabel')}</p>
|
||||
<p className="font-semibold">{amount}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -376,7 +376,7 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleVerifyDeposit}
|
||||
@@ -385,10 +385,10 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
{verifying ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Verifying...
|
||||
{t('p2pDeposit.verifying')}
|
||||
</>
|
||||
) : (
|
||||
'Verify Deposit'
|
||||
t('p2pDeposit.verifyDeposit')
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -404,22 +404,21 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-green-500">
|
||||
Deposit Successful!
|
||||
{t('p2pDeposit.depositSuccess')}
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
{amount} {token} has been added to your P2P internal balance.
|
||||
{t('p2pDeposit.addedToBalance', { amount, token })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg bg-muted/50 border">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
You can now create sell offers or trade P2P using your internal balance.
|
||||
No blockchain fees during P2P trades!
|
||||
{t('p2pDeposit.successInfo')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleClose} className="w-full">
|
||||
Done
|
||||
{t('p2pDeposit.done')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -432,13 +431,13 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Wallet className="h-5 w-5" />
|
||||
Deposit to P2P Balance
|
||||
{t('p2pDeposit.title')}
|
||||
</DialogTitle>
|
||||
{step !== 'success' && (
|
||||
<DialogDescription>
|
||||
{step === 'select' && 'Deposit crypto from your wallet to P2P internal balance'}
|
||||
{step === 'send' && 'Send tokens to the platform escrow wallet'}
|
||||
{step === 'verify' && 'Verify your transaction to credit your balance'}
|
||||
{step === 'select' && t('p2pDeposit.selectStep')}
|
||||
{step === 'send' && t('p2pDeposit.sendStep')}
|
||||
{step === 'verify' && t('p2pDeposit.verifyStep')}
|
||||
</DialogDescription>
|
||||
)}
|
||||
</DialogHeader>
|
||||
|
||||
@@ -39,16 +39,18 @@ interface EvidenceFile {
|
||||
type: 'image' | 'document';
|
||||
}
|
||||
|
||||
const DISPUTE_REASONS = [
|
||||
{ value: 'payment_not_received', label: 'Payment not received' },
|
||||
{ value: 'wrong_amount', label: 'Wrong amount received' },
|
||||
{ value: 'seller_not_responding', label: 'Seller not responding' },
|
||||
{ value: 'buyer_not_responding', label: 'Buyer not responding' },
|
||||
{ value: 'fraudulent_behavior', label: 'Fraudulent behavior' },
|
||||
{ value: 'fake_payment_proof', label: 'Fake payment proof' },
|
||||
{ value: 'account_mismatch', label: 'Payment account name mismatch' },
|
||||
{ value: 'other', label: 'Other' },
|
||||
];
|
||||
const DISPUTE_REASON_KEYS: Record<string, string> = {
|
||||
payment_not_received: 'p2pDispute.paymentNotReceived',
|
||||
wrong_amount: 'p2pDispute.wrongAmount',
|
||||
seller_not_responding: 'p2pDispute.sellerNotResponding',
|
||||
buyer_not_responding: 'p2pDispute.buyerNotResponding',
|
||||
fraudulent_behavior: 'p2pDispute.fraudulentBehavior',
|
||||
fake_payment_proof: 'p2pDispute.fakePaymentProof',
|
||||
account_mismatch: 'p2pDispute.accountMismatch',
|
||||
other: 'p2pDispute.other',
|
||||
};
|
||||
|
||||
const DISPUTE_REASONS = Object.keys(DISPUTE_REASON_KEYS);
|
||||
|
||||
export function DisputeModal({
|
||||
isOpen,
|
||||
@@ -59,7 +61,7 @@ export function DisputeModal({
|
||||
counterpartyWallet,
|
||||
isBuyer,
|
||||
}: DisputeModalProps) {
|
||||
useTranslation();
|
||||
const { t } = useTranslation();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [reason, setReason] = useState('');
|
||||
@@ -69,11 +71,11 @@ export function DisputeModal({
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// Filter reasons based on role
|
||||
const availableReasons = DISPUTE_REASONS.filter((r) => {
|
||||
const availableReasons = DISPUTE_REASONS.filter((value) => {
|
||||
if (isBuyer) {
|
||||
return r.value !== 'buyer_not_responding' && r.value !== 'payment_not_received';
|
||||
return value !== 'buyer_not_responding' && value !== 'payment_not_received';
|
||||
} else {
|
||||
return r.value !== 'seller_not_responding' && r.value !== 'fake_payment_proof';
|
||||
return value !== 'seller_not_responding' && value !== 'fake_payment_proof';
|
||||
}
|
||||
});
|
||||
|
||||
@@ -85,12 +87,12 @@ export function DisputeModal({
|
||||
|
||||
Array.from(files).forEach((file) => {
|
||||
if (evidenceFiles.length + newFiles.length >= 5) {
|
||||
toast.error('Maximum 5 evidence files allowed');
|
||||
toast.error(t('p2pDispute.maxFilesError'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
toast.error(`File ${file.name} is too large (max 10MB)`);
|
||||
toast.error(t('p2pDispute.fileTooLarge', { name: file.name }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -153,17 +155,17 @@ export function DisputeModal({
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!reason) {
|
||||
toast.error('Please select a reason');
|
||||
toast.error(t('p2pDispute.selectReasonError'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!description || description.length < 20) {
|
||||
toast.error('Please provide a detailed description (at least 20 characters)');
|
||||
toast.error(t('p2pDispute.descriptionError'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!termsAccepted) {
|
||||
toast.error('Please accept the terms and conditions');
|
||||
toast.error(t('p2pDispute.acceptTermsError'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -237,11 +239,11 @@ export function DisputeModal({
|
||||
await supabase.from('p2p_notifications').insert(adminNotifications);
|
||||
}
|
||||
|
||||
toast.success('Dispute opened successfully');
|
||||
toast.success(t('p2pDispute.disputeOpened'));
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Failed to open dispute:', error);
|
||||
toast.error('Failed to open dispute. Please try again.');
|
||||
toast.error(t('p2pDispute.failedToOpen'));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
@@ -265,26 +267,25 @@ export function DisputeModal({
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-red-500">
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
Open Dispute
|
||||
{t('p2pDispute.title')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Please provide details about the issue. Our support team will review your case
|
||||
and contact both parties for resolution.
|
||||
{t('p2pDispute.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
{/* Reason Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="reason">Reason for Dispute *</Label>
|
||||
<Label htmlFor="reason">{t('p2pDispute.reasonLabel')}</Label>
|
||||
<Select value={reason} onValueChange={setReason}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a reason..." />
|
||||
<SelectValue placeholder={t('p2pDispute.selectReason')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableReasons.map((r) => (
|
||||
<SelectItem key={r.value} value={r.value}>
|
||||
{r.label}
|
||||
{availableReasons.map((value) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{t(DISPUTE_REASON_KEYS[value])}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -294,13 +295,13 @@ export function DisputeModal({
|
||||
{/* Description */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">
|
||||
Detailed Description * <span className="text-muted-foreground text-xs">(min 20 chars)</span>
|
||||
{t('p2pDispute.detailedDescription')} <span className="text-muted-foreground text-xs">{t('p2pDispute.minChars')}</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Please describe the issue in detail. Include relevant transaction IDs, timestamps, and any communication with the counterparty..."
|
||||
placeholder={t('p2pDispute.descriptionPlaceholder')}
|
||||
rows={4}
|
||||
maxLength={2000}
|
||||
/>
|
||||
@@ -311,7 +312,7 @@ export function DisputeModal({
|
||||
|
||||
{/* Evidence Upload */}
|
||||
<div className="space-y-2">
|
||||
<Label>Evidence (Optional - max 5 files, 10MB each)</Label>
|
||||
<Label>{t('p2pDispute.evidenceLabel')}</Label>
|
||||
<div className="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-4">
|
||||
<input
|
||||
type="file"
|
||||
@@ -330,10 +331,10 @@ export function DisputeModal({
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={evidenceFiles.length >= 5}
|
||||
>
|
||||
Upload Evidence
|
||||
{t('p2pDispute.uploadEvidence')}
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
Screenshots, bank statements, chat logs, receipts
|
||||
{t('p2pDispute.evidenceTypes')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -349,7 +350,7 @@ export function DisputeModal({
|
||||
{evidence.type === 'image' ? (
|
||||
<img
|
||||
src={evidence.preview}
|
||||
alt="Evidence"
|
||||
alt={t('p2pDispute.evidenceAlt')}
|
||||
className="w-10 h-10 object-cover rounded"
|
||||
/>
|
||||
) : (
|
||||
@@ -376,13 +377,13 @@ export function DisputeModal({
|
||||
<AlertTriangle className="h-5 w-5 text-amber-500 shrink-0 mt-0.5" />
|
||||
<div className="text-sm">
|
||||
<p className="font-medium text-amber-800 dark:text-amber-200">
|
||||
Important Notice
|
||||
{t('p2pDispute.importantNotice')}
|
||||
</p>
|
||||
<ul className="text-amber-700 dark:text-amber-300 text-xs mt-1 space-y-1">
|
||||
<li>• False disputes may result in account restrictions</li>
|
||||
<li>• Resolution typically takes 1-3 business days</li>
|
||||
<li>• Both parties can submit evidence</li>
|
||||
<li>• Admin decision is final</li>
|
||||
<li>• {t('p2pDispute.falseDisputes')}</li>
|
||||
<li>• {t('p2pDispute.resolutionTime')}</li>
|
||||
<li>• {t('p2pDispute.bothParties')}</li>
|
||||
<li>• {t('p2pDispute.adminFinal')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -396,22 +397,21 @@ export function DisputeModal({
|
||||
onCheckedChange={(checked) => setTermsAccepted(checked === true)}
|
||||
/>
|
||||
<Label htmlFor="terms" className="text-sm leading-tight cursor-pointer">
|
||||
I confirm that the information provided is accurate and understand that
|
||||
false claims may result in penalties.
|
||||
{t('p2pDispute.termsCheckbox')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<Button variant="outline" onClick={handleClose} disabled={isSubmitting}>
|
||||
Cancel
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting || !reason || !description || !termsAccepted}
|
||||
>
|
||||
{isSubmitting ? 'Submitting...' : 'Open Dispute'}
|
||||
{isSubmitting ? t('p2pDispute.submitting') : t('p2pDispute.openDispute')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { supabase } from '@/lib/supabase';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { CryptoToken, FiatCurrency } from '@pezkuwi/lib/p2p-fiat';
|
||||
|
||||
interface BestOffer {
|
||||
@@ -67,6 +68,7 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -139,17 +141,17 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
// Handle express trade
|
||||
const handleExpressTrade = async () => {
|
||||
if (!user) {
|
||||
toast.error('Please login to trade');
|
||||
toast.error(t('p2pExpress.loginRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!bestOffer) {
|
||||
toast.error('No offers available');
|
||||
toast.error(t('p2pExpress.noOffersAvailable'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cryptoAmount > bestOffer.remaining_amount) {
|
||||
toast.error(`Maximum available: ${bestOffer.remaining_amount} ${token}`);
|
||||
toast.error(t('p2pExpress.maxAvailable', { amount: bestOffer.remaining_amount, token }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -171,7 +173,7 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
throw new Error(response.error || 'Failed to start trade');
|
||||
}
|
||||
|
||||
toast.success('Express trade started!');
|
||||
toast.success(t('p2pExpress.tradeStarted'));
|
||||
|
||||
if (onTradeStarted) {
|
||||
onTradeStarted(response.trade_id);
|
||||
@@ -195,12 +197,12 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
<Zap className="w-5 h-5 text-yellow-400" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg text-white">Express Mode</CardTitle>
|
||||
<p className="text-xs text-gray-400">Instant best-rate matching</p>
|
||||
<CardTitle className="text-lg text-white">{t('p2pExpress.title')}</CardTitle>
|
||||
<p className="text-xs text-gray-400">{t('p2pExpress.subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge className="bg-yellow-500/20 text-yellow-400 border-yellow-500/30">
|
||||
Fast
|
||||
{t('p2pExpress.fast')}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -209,10 +211,10 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
<Tabs value={mode} onValueChange={(v) => setMode(v as 'buy' | 'sell')}>
|
||||
<TabsList className="grid w-full grid-cols-2 bg-gray-800">
|
||||
<TabsTrigger value="buy" className="data-[state=active]:bg-green-600">
|
||||
Buy {token}
|
||||
{t('p2pExpress.buyToken', { token })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="sell" className="data-[state=active]:bg-red-600">
|
||||
Sell {token}
|
||||
{t('p2pExpress.sellToken', { token })}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
@@ -220,7 +222,7 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
{/* Token & Fiat Selection */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-gray-400 text-xs">Crypto</Label>
|
||||
<Label className="text-gray-400 text-xs">{t('p2p.crypto')}</Label>
|
||||
<Select value={token} onValueChange={(v) => setToken(v as CryptoToken)}>
|
||||
<SelectTrigger className="bg-gray-800 border-gray-700">
|
||||
<SelectValue />
|
||||
@@ -233,7 +235,7 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-gray-400 text-xs">Currency</Label>
|
||||
<Label className="text-gray-400 text-xs">{t('p2p.currency')}</Label>
|
||||
<Select value={fiat} onValueChange={(v) => setFiat(v as FiatCurrency)}>
|
||||
<SelectTrigger className="bg-gray-800 border-gray-700">
|
||||
<SelectValue />
|
||||
@@ -253,7 +255,7 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<Label className="text-gray-400 text-xs">
|
||||
{inputType === 'fiat' ? `Amount (${fiat})` : `Amount (${token})`}
|
||||
{t('p2pExpress.amountLabel', { unit: inputType === 'fiat' ? fiat : token })}
|
||||
</Label>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -261,7 +263,7 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
className="text-xs text-yellow-400 h-auto p-0"
|
||||
onClick={() => setInputType(inputType === 'fiat' ? 'crypto' : 'fiat')}
|
||||
>
|
||||
Switch to {inputType === 'fiat' ? token : fiat}
|
||||
{t('p2pExpress.switchTo', { unit: inputType === 'fiat' ? token : fiat })}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative">
|
||||
@@ -282,7 +284,7 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
{bestOffer && parseFloat(amount) > 0 && (
|
||||
<div className="p-3 bg-gray-800/50 rounded-lg space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-400">You {mode === 'buy' ? 'pay' : 'receive'}</span>
|
||||
<span className="text-gray-400">{mode === 'buy' ? t('p2pExpress.youPay') : t('p2pExpress.youReceive')}</span>
|
||||
<span className="text-white font-medium">
|
||||
{fiatSymbol}{fiatAmount.toLocaleString(undefined, { maximumFractionDigits: 2 })} {fiat}
|
||||
</span>
|
||||
@@ -291,31 +293,31 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
<ArrowRight className="w-4 h-4 text-gray-500" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-400">You {mode === 'buy' ? 'receive' : 'send'}</span>
|
||||
<span className="text-gray-400">{mode === 'buy' ? t('p2pExpress.youReceive') : t('p2pExpress.youSend')}</span>
|
||||
<span className="text-white font-medium">
|
||||
{cryptoAmount.toLocaleString(undefined, { maximumFractionDigits: 6 })} {token}
|
||||
</span>
|
||||
</div>
|
||||
<div className="pt-2 border-t border-gray-700 space-y-1">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500">Rate</span>
|
||||
<span className="text-gray-500">{t('p2pExpress.rate')}</span>
|
||||
<span className="text-gray-300">
|
||||
1 {token} = {fiatSymbol}{bestOffer.price_per_unit.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500">Merchant Rating</span>
|
||||
<span className="text-gray-500">{t('p2pExpress.merchantRating')}</span>
|
||||
<span className="text-yellow-400 flex items-center gap-1">
|
||||
<Star className="w-3 h-3" />
|
||||
{bestOffer.seller_reputation}% ({bestOffer.seller_completed_trades} trades)
|
||||
{bestOffer.seller_reputation}% ({t('p2p.trades', { count: bestOffer.seller_completed_trades })})
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500">Payment</span>
|
||||
<span className="text-gray-500">{t('p2pExpress.payment')}</span>
|
||||
<span className="text-gray-300">{bestOffer.payment_method_name}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500">Time Limit</span>
|
||||
<span className="text-gray-500">{t('p2pExpress.timeLimit')}</span>
|
||||
<span className="text-gray-300 flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{bestOffer.time_limit_minutes} min
|
||||
@@ -330,7 +332,7 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
<div className="p-3 bg-red-500/10 border border-red-500/30 rounded-lg flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-red-400" />
|
||||
<span className="text-sm text-red-400">
|
||||
No offers available for this pair
|
||||
{t('p2pExpress.noOffers')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -343,11 +345,11 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
disabled={!bestOffer || isLoading || isProcessing || !user}
|
||||
>
|
||||
{isProcessing ? (
|
||||
<>Processing...</>
|
||||
<>{t('p2pExpress.processing')}</>
|
||||
) : (
|
||||
<>
|
||||
<Zap className="w-4 h-4 mr-2" />
|
||||
{mode === 'buy' ? 'Buy' : 'Sell'} {token} Instantly
|
||||
{mode === 'buy' ? t('p2pExpress.buyInstantly', { token }) : t('p2pExpress.sellInstantly', { token })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -356,11 +358,11 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
<div className="flex items-center justify-center gap-4 text-xs text-gray-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<Shield className="w-3 h-3 text-green-400" />
|
||||
Escrow Protected
|
||||
{t('p2p.escrowProtected')}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<CheckCircle2 className="w-3 h-3 text-blue-400" />
|
||||
Verified Merchants
|
||||
{t('p2p.verifiedMerchants')}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
@@ -19,6 +20,7 @@ interface InternalBalanceCardProps {
|
||||
}
|
||||
|
||||
export function InternalBalanceCard({ onDeposit, onWithdraw }: InternalBalanceCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const [balances, setBalances] = useState<InternalBalance[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
@@ -73,7 +75,7 @@ export function InternalBalanceCard({ onDeposit, onWithdraw }: InternalBalanceCa
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<Wallet className="h-5 w-5" />
|
||||
P2P Internal Balance
|
||||
{t('p2pBalance.title')}
|
||||
</CardTitle>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -85,15 +87,15 @@ export function InternalBalanceCard({ onDeposit, onWithdraw }: InternalBalanceCa
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Internal balance for P2P trading. Deposit to start selling.
|
||||
{t('p2pBalance.subtitle')}
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{balances.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<Wallet className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
||||
<p className="text-sm">No balance yet</p>
|
||||
<p className="text-xs">Deposit crypto to start P2P trading</p>
|
||||
<p className="text-sm">{t('p2pBalance.noBalance')}</p>
|
||||
<p className="text-xs">{t('p2pBalance.depositToStart')}</p>
|
||||
</div>
|
||||
) : (
|
||||
balances.map((balance) => (
|
||||
@@ -111,7 +113,7 @@ export function InternalBalanceCard({ onDeposit, onWithdraw }: InternalBalanceCa
|
||||
<span className="font-semibold">{balance.token}</span>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Total: {formatBalance(balance.total_balance)}
|
||||
{t('p2pBalance.total', { amount: formatBalance(balance.total_balance) })}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -119,7 +121,7 @@ export function InternalBalanceCard({ onDeposit, onWithdraw }: InternalBalanceCa
|
||||
<div className="flex items-center gap-2">
|
||||
<Unlock className="h-4 w-4 text-green-500" />
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs">Available</p>
|
||||
<p className="text-muted-foreground text-xs">{t('p2pBalance.available')}</p>
|
||||
<p className="font-medium text-green-600">
|
||||
{formatBalance(balance.available_balance)}
|
||||
</p>
|
||||
@@ -128,7 +130,7 @@ export function InternalBalanceCard({ onDeposit, onWithdraw }: InternalBalanceCa
|
||||
<div className="flex items-center gap-2">
|
||||
<Lock className="h-4 w-4 text-yellow-500" />
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs">Locked (Escrow)</p>
|
||||
<p className="text-muted-foreground text-xs">{t('p2pBalance.lockedEscrow')}</p>
|
||||
<p className="font-medium text-yellow-600">
|
||||
{formatBalance(balance.locked_balance)}
|
||||
</p>
|
||||
@@ -138,11 +140,11 @@ export function InternalBalanceCard({ onDeposit, onWithdraw }: InternalBalanceCa
|
||||
|
||||
<div className="mt-3 pt-3 border-t grid grid-cols-2 gap-2 text-xs text-muted-foreground">
|
||||
<div>
|
||||
<span>Total Deposited: </span>
|
||||
<span>{t('p2pBalance.totalDeposited')}</span>
|
||||
<span className="text-foreground">{formatBalance(balance.total_deposited, 2)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>Total Withdrawn: </span>
|
||||
<span>{t('p2pBalance.totalWithdrawn')}</span>
|
||||
<span className="text-foreground">{formatBalance(balance.total_withdrawn, 2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -158,7 +160,7 @@ export function InternalBalanceCard({ onDeposit, onWithdraw }: InternalBalanceCa
|
||||
onClick={onDeposit}
|
||||
>
|
||||
<ArrowDownToLine className="h-4 w-4 mr-2" />
|
||||
Deposit
|
||||
{t('p2pBalance.deposit')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -167,7 +169,7 @@ export function InternalBalanceCard({ onDeposit, onWithdraw }: InternalBalanceCa
|
||||
disabled={balances.every(b => b.available_balance <= 0)}
|
||||
>
|
||||
<ArrowUpFromLine className="h-4 w-4 mr-2" />
|
||||
Withdraw
|
||||
{t('p2pBalance.withdraw')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -105,6 +106,7 @@ const TIER_COLORS = {
|
||||
};
|
||||
|
||||
export function MerchantApplication() {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [requirements, setRequirements] = useState<TierRequirements[]>(DEFAULT_REQUIREMENTS);
|
||||
const [userStats, setUserStats] = useState<UserStats>({ completed_trades: 0, completion_rate: 0, volume_30d: 0 });
|
||||
@@ -217,7 +219,7 @@ export function MerchantApplication() {
|
||||
|
||||
if (data && data[0]) {
|
||||
if (data[0].success) {
|
||||
toast.success('Application submitted successfully!');
|
||||
toast.success(t('p2pMerchant.applicationSubmitted'));
|
||||
setApplyModalOpen(false);
|
||||
fetchData();
|
||||
} else {
|
||||
@@ -226,7 +228,7 @@ export function MerchantApplication() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error applying for tier:', error);
|
||||
toast.error('Failed to submit application');
|
||||
toast.error(t('p2pMerchant.applicationFailed'));
|
||||
} finally {
|
||||
setApplying(false);
|
||||
}
|
||||
@@ -257,9 +259,9 @@ export function MerchantApplication() {
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Crown className="h-5 w-5 text-kurdish-green" />
|
||||
Your Merchant Status
|
||||
{t('p2pMerchant.yourStatus')}
|
||||
</CardTitle>
|
||||
<CardDescription>Current tier and application status</CardDescription>
|
||||
<CardDescription>{t('p2pMerchant.currentTierStatus')}</CardDescription>
|
||||
</div>
|
||||
<MerchantTierBadge tier={currentTier.tier} size="lg" />
|
||||
</div>
|
||||
@@ -268,15 +270,15 @@ export function MerchantApplication() {
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="text-center p-4 bg-background/50 rounded-lg">
|
||||
<p className="text-2xl font-bold">{userStats.completed_trades}</p>
|
||||
<p className="text-sm text-muted-foreground">Completed Trades</p>
|
||||
<p className="text-sm text-muted-foreground">{t('p2pMerchant.completedTrades')}</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-background/50 rounded-lg">
|
||||
<p className="text-2xl font-bold">{userStats.completion_rate.toFixed(1)}%</p>
|
||||
<p className="text-sm text-muted-foreground">Completion Rate</p>
|
||||
<p className="text-sm text-muted-foreground">{t('p2pMerchant.completionRate')}</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-background/50 rounded-lg">
|
||||
<p className="text-2xl font-bold">${userStats.volume_30d.toLocaleString()}</p>
|
||||
<p className="text-sm text-muted-foreground">30-Day Volume</p>
|
||||
<p className="text-sm text-muted-foreground">{t('p2pMerchant.thirtyDayVolume')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -284,7 +286,7 @@ export function MerchantApplication() {
|
||||
<Alert className="mt-4 bg-yellow-500/10 border-yellow-500/30">
|
||||
<Clock className="h-4 w-4 text-yellow-500" />
|
||||
<AlertDescription className="text-yellow-500">
|
||||
Your application for {currentTier.applied_for_tier?.toUpperCase()} tier is pending review.
|
||||
{t('p2pMerchant.pendingReview', { tier: currentTier.applied_for_tier?.toUpperCase() })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -316,7 +318,7 @@ export function MerchantApplication() {
|
||||
{/* Current tier indicator */}
|
||||
{isCurrentTier && (
|
||||
<div className="absolute top-0 right-0 bg-kurdish-green text-white text-xs px-2 py-0.5 rounded-bl">
|
||||
Current
|
||||
{t('p2pMerchant.current')}
|
||||
</div>
|
||||
)}
|
||||
{isPastTier && (
|
||||
@@ -343,7 +345,7 @@ export function MerchantApplication() {
|
||||
{/* Trades */}
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>Completed Trades</span>
|
||||
<span>{t('p2pMerchant.completedTradesReq')}</span>
|
||||
<span>{userStats.completed_trades} / {tier.min_trades}</span>
|
||||
</div>
|
||||
<Progress
|
||||
@@ -356,7 +358,7 @@ export function MerchantApplication() {
|
||||
{tier.min_completion_rate > 0 && (
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>Completion Rate</span>
|
||||
<span>{t('p2pMerchant.completionRateReq')}</span>
|
||||
<span>{userStats.completion_rate.toFixed(1)}% / {tier.min_completion_rate}%</span>
|
||||
</div>
|
||||
<Progress
|
||||
@@ -370,7 +372,7 @@ export function MerchantApplication() {
|
||||
{tier.min_volume_30d > 0 && (
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>30-Day Volume</span>
|
||||
<span>{t('p2pMerchant.volumeReq')}</span>
|
||||
<span>${userStats.volume_30d.toLocaleString()} / ${tier.min_volume_30d.toLocaleString()}</span>
|
||||
</div>
|
||||
<Progress
|
||||
@@ -383,20 +385,20 @@ export function MerchantApplication() {
|
||||
|
||||
{/* Benefits */}
|
||||
<div className="pt-2 border-t border-border/50">
|
||||
<p className="text-xs text-muted-foreground mb-2">Benefits:</p>
|
||||
<p className="text-xs text-muted-foreground mb-2">{t('p2pMerchant.benefits')}</p>
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<CheckCircle2 className="h-3 w-3 text-kurdish-green" />
|
||||
<span>Up to {tier.max_pending_orders} pending orders</span>
|
||||
<span>{t('p2pMerchant.pendingOrders', { count: tier.max_pending_orders })}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<CheckCircle2 className="h-3 w-3 text-kurdish-green" />
|
||||
<span>Max ${tier.max_order_amount.toLocaleString()} per trade</span>
|
||||
<span>{t('p2pMerchant.maxPerTrade', { amount: tier.max_order_amount.toLocaleString() })}</span>
|
||||
</div>
|
||||
{tier.featured_ads_allowed > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<CheckCircle2 className="h-3 w-3 text-kurdish-green" />
|
||||
<span>{tier.featured_ads_allowed} featured ads</span>
|
||||
<span>{t('p2pMerchant.featuredAds', { count: tier.featured_ads_allowed })}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -406,7 +408,7 @@ export function MerchantApplication() {
|
||||
{tier.deposit_required > 0 && (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Lock className="h-3 w-3" />
|
||||
<span>Requires {tier.deposit_required.toLocaleString()} {tier.deposit_token} deposit</span>
|
||||
<span>{t('p2pMerchant.depositRequired', { amount: tier.deposit_required.toLocaleString(), token: tier.deposit_token })}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -417,7 +419,7 @@ export function MerchantApplication() {
|
||||
size="sm"
|
||||
onClick={() => openApplyModal(tier.tier)}
|
||||
>
|
||||
Apply for Upgrade
|
||||
{t('p2pMerchant.applyForUpgrade')}
|
||||
<ArrowRight className="h-4 w-4 ml-1" />
|
||||
</Button>
|
||||
)}
|
||||
@@ -433,10 +435,10 @@ export function MerchantApplication() {
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<TrendingUp className="h-5 w-5 text-kurdish-green" />
|
||||
Apply for {selectedTier?.toUpperCase()} Tier
|
||||
{t('p2pMerchant.applyForTier', { tier: selectedTier?.toUpperCase() })}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Submit your application for tier upgrade. Our team will review it shortly.
|
||||
{t('p2pMerchant.applyDescription')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -444,20 +446,20 @@ export function MerchantApplication() {
|
||||
<div className="space-y-4">
|
||||
{/* Requirements check */}
|
||||
<div className="bg-muted p-4 rounded-lg space-y-2">
|
||||
<p className="font-medium text-sm">Requirements Met:</p>
|
||||
<p className="font-medium text-sm">{t('p2pMerchant.requirementsMet')}</p>
|
||||
{requirements.find(r => r.tier === selectedTier) && (
|
||||
<>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<span>Completed trades requirement</span>
|
||||
<span>{t('p2pMerchant.completedTradesRequirement')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<span>Completion rate requirement</span>
|
||||
<span>{t('p2pMerchant.completionRateRequirement')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<span>30-day volume requirement</span>
|
||||
<span>{t('p2pMerchant.volumeRequirement')}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -468,12 +470,10 @@ export function MerchantApplication() {
|
||||
<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
This tier requires a deposit of{' '}
|
||||
<strong>
|
||||
{requirements.find(r => r.tier === selectedTier)?.deposit_required.toLocaleString()}{' '}
|
||||
{requirements.find(r => r.tier === selectedTier)?.deposit_token}
|
||||
</strong>
|
||||
. You will be prompted to complete the deposit after approval.
|
||||
{t('p2pMerchant.depositInfo', {
|
||||
amount: requirements.find(r => r.tier === selectedTier)?.deposit_required.toLocaleString(),
|
||||
token: requirements.find(r => r.tier === selectedTier)?.deposit_token
|
||||
})}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -482,7 +482,7 @@ export function MerchantApplication() {
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setApplyModalOpen(false)}>
|
||||
Cancel
|
||||
{t('p2p.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-kurdish-green hover:bg-kurdish-green-dark"
|
||||
@@ -494,7 +494,7 @@ export function MerchantApplication() {
|
||||
) : (
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Submit Application
|
||||
{t('p2pMerchant.submitApplication')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { Diamond, Star, Shield } from 'lucide-react';
|
||||
@@ -12,25 +13,28 @@ interface MerchantTierBadgeProps {
|
||||
|
||||
const TIER_CONFIG = {
|
||||
lite: {
|
||||
label: 'Lite',
|
||||
labelKey: 'p2pTier.lite',
|
||||
icon: Shield,
|
||||
className: 'bg-gray-500/20 text-gray-400 border-gray-500/30',
|
||||
iconClassName: 'text-gray-400',
|
||||
description: 'Basic verified trader'
|
||||
descKey: 'p2pTier.liteDesc',
|
||||
merchantKey: 'p2pTier.liteMerchant'
|
||||
},
|
||||
super: {
|
||||
label: 'Super',
|
||||
labelKey: 'p2pTier.super',
|
||||
icon: Star,
|
||||
className: 'bg-yellow-500/20 text-yellow-500 border-yellow-500/30',
|
||||
iconClassName: 'text-yellow-500',
|
||||
description: 'Professional trader with 20+ trades and 90%+ completion rate'
|
||||
descKey: 'p2pTier.superDesc',
|
||||
merchantKey: 'p2pTier.superMerchant'
|
||||
},
|
||||
diamond: {
|
||||
label: 'Diamond',
|
||||
labelKey: 'p2pTier.diamond',
|
||||
icon: Diamond,
|
||||
className: 'bg-purple-500/20 text-purple-500 border-purple-500/30',
|
||||
iconClassName: 'text-purple-500',
|
||||
description: 'Elite merchant with 100+ trades and 95%+ completion rate'
|
||||
descKey: 'p2pTier.diamondDesc',
|
||||
merchantKey: 'p2pTier.diamondMerchant'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -54,6 +58,7 @@ export function MerchantTierBadge({
|
||||
size = 'md',
|
||||
showLabel = true
|
||||
}: MerchantTierBadgeProps) {
|
||||
const { t } = useTranslation();
|
||||
const config = TIER_CONFIG[tier];
|
||||
const sizeConfig = SIZE_CONFIG[size];
|
||||
const Icon = config.icon;
|
||||
@@ -67,12 +72,12 @@ export function MerchantTierBadge({
|
||||
className={`${config.className} ${sizeConfig.badge} gap-1 cursor-help`}
|
||||
>
|
||||
<Icon className={`${sizeConfig.icon} ${config.iconClassName}`} />
|
||||
{showLabel && <span>{config.label}</span>}
|
||||
{showLabel && <span>{t(config.labelKey)}</span>}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="font-medium">{config.label} Merchant</p>
|
||||
<p className="text-xs text-muted-foreground">{config.description}</p>
|
||||
<p className="font-medium">{t(config.merchantKey)}</p>
|
||||
<p className="text-xs text-muted-foreground">{t(config.descKey)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -87,6 +92,7 @@ export function MerchantTierIcon({
|
||||
tier: MerchantTier;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const config = TIER_CONFIG[tier];
|
||||
const sizeConfig = SIZE_CONFIG[size];
|
||||
const Icon = config.icon;
|
||||
@@ -100,7 +106,7 @@ export function MerchantTierIcon({
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="font-medium">{config.label} Merchant</p>
|
||||
<p className="font-medium">{t(config.merchantKey)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -37,6 +38,7 @@ interface Notification {
|
||||
}
|
||||
|
||||
export function NotificationBell() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
@@ -173,13 +175,13 @@ export function NotificationBell() {
|
||||
// Format time ago
|
||||
const formatTimeAgo = (dateString: string) => {
|
||||
const seconds = Math.floor((Date.now() - new Date(dateString).getTime()) / 1000);
|
||||
if (seconds < 60) return 'Just now';
|
||||
if (seconds < 60) return t('p2p.justNow');
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
if (minutes < 60) return t('p2p.minutesAgo', { count: minutes });
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
if (hours < 24) return t('p2p.hoursAgo', { count: hours });
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days}d ago`;
|
||||
return t('p2p.daysAgo', { count: days });
|
||||
};
|
||||
|
||||
if (!user) return null;
|
||||
@@ -206,7 +208,7 @@ export function NotificationBell() {
|
||||
className="w-80 bg-gray-900 border-gray-800"
|
||||
>
|
||||
<DropdownMenuLabel className="flex items-center justify-between">
|
||||
<span className="text-white">Notifications</span>
|
||||
<span className="text-white">{t('p2pNotif.title')}</span>
|
||||
{unreadCount > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -215,7 +217,7 @@ export function NotificationBell() {
|
||||
className="text-xs text-gray-400 hover:text-white h-auto py-1"
|
||||
>
|
||||
<CheckCheck className="w-3 h-3 mr-1" />
|
||||
Mark all read
|
||||
{t('p2pNotif.markAllRead')}
|
||||
</Button>
|
||||
)}
|
||||
</DropdownMenuLabel>
|
||||
@@ -229,7 +231,7 @@ export function NotificationBell() {
|
||||
) : notifications.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-gray-500">
|
||||
<Bell className="w-8 h-8 mb-2" />
|
||||
<p className="text-sm">No notifications</p>
|
||||
<p className="text-sm">{t('p2pNotif.noNotifications')}</p>
|
||||
</div>
|
||||
) : (
|
||||
notifications.map((notification) => (
|
||||
@@ -274,7 +276,7 @@ export function NotificationBell() {
|
||||
}}
|
||||
className="justify-center text-gray-400 hover:text-white cursor-pointer"
|
||||
>
|
||||
View all trades
|
||||
{t('p2pNotif.viewAllTrades')}
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -50,6 +51,7 @@ export function OrderFilters({
|
||||
onFiltersChange,
|
||||
variant = 'inline'
|
||||
}: OrderFiltersProps) {
|
||||
const { t } = useTranslation();
|
||||
const [localFilters, setLocalFilters] = useState<P2PFilters>(filters);
|
||||
const [paymentMethods, setPaymentMethods] = useState<{ id: string; method_name: string }[]>([]);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -117,7 +119,7 @@ export function OrderFilters({
|
||||
<div className="space-y-4">
|
||||
{/* Token Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label>Cryptocurrency</Label>
|
||||
<Label>{t('p2pFilters.cryptocurrency')}</Label>
|
||||
<div className="flex gap-2">
|
||||
{['all', 'HEZ', 'PEZ'].map((token) => (
|
||||
<Button
|
||||
@@ -127,7 +129,7 @@ export function OrderFilters({
|
||||
onClick={() => updateFilter('token', token as P2PFilters['token'])}
|
||||
className={localFilters.token === token ? 'bg-kurdish-green hover:bg-kurdish-green-dark' : ''}
|
||||
>
|
||||
{token === 'all' ? 'All' : token}
|
||||
{token === 'all' ? t('p2pFilters.all') : token}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
@@ -136,7 +138,7 @@ export function OrderFilters({
|
||||
{/* Fiat Currency */}
|
||||
<Collapsible open={expandedSections.currency} onOpenChange={() => toggleSection('currency')}>
|
||||
<CollapsibleTrigger className="flex items-center justify-between w-full py-2">
|
||||
<Label className="cursor-pointer">Fiat Currency</Label>
|
||||
<Label className="cursor-pointer">{t('p2pFilters.fiatCurrency')}</Label>
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${expandedSections.currency ? 'rotate-180' : ''}`} />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pt-2">
|
||||
@@ -145,10 +147,10 @@ export function OrderFilters({
|
||||
onValueChange={(value) => updateFilter('fiatCurrency', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select currency" />
|
||||
<SelectValue placeholder={t('p2pFilters.selectCurrency')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Currencies</SelectItem>
|
||||
<SelectItem value="all">{t('p2pFilters.allCurrencies')}</SelectItem>
|
||||
{FIAT_CURRENCIES.map((currency) => (
|
||||
<SelectItem key={currency.value} value={currency.value}>
|
||||
{currency.label}
|
||||
@@ -162,7 +164,7 @@ export function OrderFilters({
|
||||
{/* Payment Methods */}
|
||||
<Collapsible open={expandedSections.payment} onOpenChange={() => toggleSection('payment')}>
|
||||
<CollapsibleTrigger className="flex items-center justify-between w-full py-2">
|
||||
<Label className="cursor-pointer">Payment Methods</Label>
|
||||
<Label className="cursor-pointer">{t('p2pFilters.paymentMethods')}</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
{localFilters.paymentMethods.length > 0 && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
@@ -197,13 +199,13 @@ export function OrderFilters({
|
||||
{/* Amount Range */}
|
||||
<Collapsible open={expandedSections.amount} onOpenChange={() => toggleSection('amount')}>
|
||||
<CollapsibleTrigger className="flex items-center justify-between w-full py-2">
|
||||
<Label className="cursor-pointer">Amount Range</Label>
|
||||
<Label className="cursor-pointer">{t('p2pFilters.amountRange')}</Label>
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${expandedSections.amount ? 'rotate-180' : ''}`} />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pt-2">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label className="text-xs">Min Amount</Label>
|
||||
<Label className="text-xs">{t('p2pFilters.minAmount')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="0"
|
||||
@@ -212,10 +214,10 @@ export function OrderFilters({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">Max Amount</Label>
|
||||
<Label className="text-xs">{t('p2pFilters.maxAmount')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="No limit"
|
||||
placeholder={t('p2pFilters.noLimit')}
|
||||
value={localFilters.maxAmount || ''}
|
||||
onChange={(e) => updateFilter('maxAmount', e.target.value ? Number(e.target.value) : null)}
|
||||
/>
|
||||
@@ -227,7 +229,7 @@ export function OrderFilters({
|
||||
{/* Merchant Tier */}
|
||||
<Collapsible open={expandedSections.merchant} onOpenChange={() => toggleSection('merchant')}>
|
||||
<CollapsibleTrigger className="flex items-center justify-between w-full py-2">
|
||||
<Label className="cursor-pointer">Merchant Tier</Label>
|
||||
<Label className="cursor-pointer">{t('p2pFilters.merchantTier')}</Label>
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${expandedSections.merchant ? 'rotate-180' : ''}`} />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pt-2 space-y-2">
|
||||
@@ -248,7 +250,7 @@ export function OrderFilters({
|
||||
/>
|
||||
<label htmlFor={`tier-${tier.value}`} className="flex items-center gap-1 text-sm cursor-pointer">
|
||||
<Icon className={`h-4 w-4 ${tier.color}`} />
|
||||
{tier.label}+ only
|
||||
{t(`p2pFilters.${tier.value}Plus`)}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
@@ -258,7 +260,7 @@ export function OrderFilters({
|
||||
|
||||
{/* Completion Rate */}
|
||||
<div className="space-y-2">
|
||||
<Label>Min Completion Rate: {localFilters.minCompletionRate}%</Label>
|
||||
<Label>{t('p2pFilters.minCompletionRate', { percent: localFilters.minCompletionRate })}</Label>
|
||||
<Slider
|
||||
value={[localFilters.minCompletionRate]}
|
||||
onValueChange={([value]) => updateFilter('minCompletionRate', value)}
|
||||
@@ -277,7 +279,7 @@ export function OrderFilters({
|
||||
onCheckedChange={(checked) => updateFilter('onlineOnly', !!checked)}
|
||||
/>
|
||||
<label htmlFor="online-only" className="text-sm cursor-pointer">
|
||||
Online traders only
|
||||
{t('p2pFilters.onlineOnly')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -288,14 +290,14 @@ export function OrderFilters({
|
||||
onCheckedChange={(checked) => updateFilter('verifiedOnly', !!checked)}
|
||||
/>
|
||||
<label htmlFor="verified-only" className="text-sm cursor-pointer">
|
||||
Verified merchants only
|
||||
{t('p2pFilters.verifiedOnly')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sort */}
|
||||
<div className="space-y-2">
|
||||
<Label>Sort By</Label>
|
||||
<Label>{t('p2pFilters.sortBy')}</Label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Select
|
||||
value={localFilters.sortBy}
|
||||
@@ -305,10 +307,10 @@ export function OrderFilters({
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="price">Price</SelectItem>
|
||||
<SelectItem value="completion_rate">Completion Rate</SelectItem>
|
||||
<SelectItem value="trades">Trade Count</SelectItem>
|
||||
<SelectItem value="newest">Newest</SelectItem>
|
||||
<SelectItem value="price">{t('p2pFilters.sortPrice')}</SelectItem>
|
||||
<SelectItem value="completion_rate">{t('p2pFilters.sortCompletionRate')}</SelectItem>
|
||||
<SelectItem value="trades">{t('p2pFilters.sortTradeCount')}</SelectItem>
|
||||
<SelectItem value="newest">{t('p2pFilters.sortNewest')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
@@ -319,8 +321,8 @@ export function OrderFilters({
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="asc">Low to High</SelectItem>
|
||||
<SelectItem value="desc">High to Low</SelectItem>
|
||||
<SelectItem value="asc">{t('p2pFilters.lowToHigh')}</SelectItem>
|
||||
<SelectItem value="desc">{t('p2pFilters.highToLow')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -335,7 +337,7 @@ export function OrderFilters({
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<SlidersHorizontal className="h-4 w-4" />
|
||||
Filters
|
||||
{t('p2pFilters.filters')}
|
||||
{activeFilterCount() > 0 && (
|
||||
<Badge variant="secondary" className="ml-1">
|
||||
{activeFilterCount()}
|
||||
@@ -348,11 +350,11 @@ export function OrderFilters({
|
||||
<SheetTitle className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<Filter className="h-5 w-5" />
|
||||
Filter Orders
|
||||
{t('p2pFilters.filterOrders')}
|
||||
</span>
|
||||
<Button variant="ghost" size="sm" onClick={resetFilters}>
|
||||
<RefreshCw className="h-4 w-4 mr-1" />
|
||||
Reset
|
||||
{t('p2pFilters.reset')}
|
||||
</Button>
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
@@ -361,11 +363,11 @@ export function OrderFilters({
|
||||
</div>
|
||||
<SheetFooter className="absolute bottom-0 left-0 right-0 p-4 bg-background border-t">
|
||||
<Button variant="outline" onClick={() => setIsOpen(false)} className="flex-1">
|
||||
Cancel
|
||||
{t('p2p.cancel')}
|
||||
</Button>
|
||||
<Button onClick={applyFilters} className="flex-1 bg-kurdish-green hover:bg-kurdish-green-dark">
|
||||
<Check className="h-4 w-4 mr-1" />
|
||||
Apply Filters
|
||||
{t('p2pFilters.applyFilters')}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
@@ -380,17 +382,17 @@ export function OrderFilters({
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-medium flex items-center gap-2">
|
||||
<Filter className="h-4 w-4" />
|
||||
Filters
|
||||
{t('p2pFilters.filters')}
|
||||
</h3>
|
||||
<Button variant="ghost" size="sm" onClick={resetFilters}>
|
||||
<RefreshCw className="h-4 w-4 mr-1" />
|
||||
Reset
|
||||
{t('p2pFilters.reset')}
|
||||
</Button>
|
||||
</div>
|
||||
<FilterContent />
|
||||
<Button onClick={applyFilters} className="w-full mt-4 bg-kurdish-green hover:bg-kurdish-green-dark">
|
||||
<Check className="h-4 w-4 mr-1" />
|
||||
Apply Filters
|
||||
{t('p2pFilters.applyFilters')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -405,6 +407,7 @@ export function QuickFilterBar({
|
||||
filters: P2PFilters;
|
||||
onFiltersChange: (filters: P2PFilters) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{/* Token quick select */}
|
||||
@@ -417,7 +420,7 @@ export function QuickFilterBar({
|
||||
onClick={() => onFiltersChange({ ...filters, token: token as P2PFilters['token'] })}
|
||||
className={filters.token === token ? 'bg-kurdish-green hover:bg-kurdish-green-dark' : ''}
|
||||
>
|
||||
{token === 'all' ? 'All' : token}
|
||||
{token === 'all' ? t('p2pFilters.all') : token}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
@@ -428,10 +431,10 @@ export function QuickFilterBar({
|
||||
onValueChange={(value) => onFiltersChange({ ...filters, fiatCurrency: value })}
|
||||
>
|
||||
<SelectTrigger className="w-[120px] h-9">
|
||||
<SelectValue placeholder="Currency" />
|
||||
<SelectValue placeholder={t('p2p.currency')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All</SelectItem>
|
||||
<SelectItem value="all">{t('p2pFilters.all')}</SelectItem>
|
||||
{FIAT_CURRENCIES.map((currency) => (
|
||||
<SelectItem key={currency.value} value={currency.value}>
|
||||
{currency.value}
|
||||
@@ -443,7 +446,7 @@ export function QuickFilterBar({
|
||||
{/* Amount input */}
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="I want to trade..."
|
||||
placeholder={t('p2pFilters.iWantToTrade')}
|
||||
className="w-[150px] h-9"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value ? Number(e.target.value) : null;
|
||||
@@ -474,7 +477,7 @@ export function QuickFilterBar({
|
||||
|
||||
{filters.minCompletionRate > 0 && (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
{filters.minCompletionRate}%+ rate
|
||||
{t('p2pFilters.rate', { percent: filters.minCompletionRate })}
|
||||
<button
|
||||
onClick={() => onFiltersChange({ ...filters, minCompletionRate: 0 })}
|
||||
className="ml-1 hover:text-destructive"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -25,6 +26,7 @@ interface UserStats {
|
||||
}
|
||||
|
||||
export function P2PDashboard() {
|
||||
const { t } = useTranslation();
|
||||
const [showCreateAd, setShowCreateAd] = useState(false);
|
||||
const [userStats, setUserStats] = useState<UserStats>({ activeTrades: 0, completedTrades: 0, totalVolume: 0 });
|
||||
const [filters, setFilters] = useState<P2PFilters>(DEFAULT_FILTERS);
|
||||
@@ -89,7 +91,7 @@ export function P2PDashboard() {
|
||||
className="text-gray-400 hover:text-white"
|
||||
>
|
||||
<Home className="w-4 h-4 mr-2" />
|
||||
Back to Home
|
||||
{t('p2p.backToHome')}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<NotificationBell />
|
||||
@@ -99,7 +101,7 @@ export function P2PDashboard() {
|
||||
className="border-gray-700 hover:bg-gray-800"
|
||||
>
|
||||
<Store className="w-4 h-4 mr-2" />
|
||||
Merchant
|
||||
{t('p2p.merchant')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -107,7 +109,7 @@ export function P2PDashboard() {
|
||||
className="border-gray-700 hover:bg-gray-800"
|
||||
>
|
||||
<ClipboardList className="w-4 h-4 mr-2" />
|
||||
My Trades
|
||||
{t('p2p.myTrades')}
|
||||
{userStats.activeTrades > 0 && (
|
||||
<Badge className="ml-2 bg-yellow-500 text-black">
|
||||
{userStats.activeTrades}
|
||||
@@ -138,7 +140,7 @@ export function P2PDashboard() {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-white">{userStats.activeTrades}</p>
|
||||
<p className="text-sm text-gray-400">Active Trades</p>
|
||||
<p className="text-sm text-gray-400">{t('p2p.activeTrades')}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -149,7 +151,7 @@ export function P2PDashboard() {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-white">{userStats.completedTrades}</p>
|
||||
<p className="text-sm text-gray-400">Completed</p>
|
||||
<p className="text-sm text-gray-400">{t('p2p.completed')}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -160,7 +162,7 @@ export function P2PDashboard() {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-white">${userStats.totalVolume.toLocaleString()}</p>
|
||||
<p className="text-sm text-gray-400">Volume</p>
|
||||
<p className="text-sm text-gray-400">{t('p2p.volume')}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -170,12 +172,12 @@ export function P2PDashboard() {
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl sm:text-4xl font-bold text-white">P2P Trading</h1>
|
||||
<p className="text-gray-400 text-sm sm:text-base">Buy and sell crypto with your local currency.</p>
|
||||
<h1 className="text-3xl sm:text-4xl font-bold text-white">{t('p2p.title')}</h1>
|
||||
<p className="text-gray-400 text-sm sm:text-base">{t('p2p.subtitle')}</p>
|
||||
</div>
|
||||
<Button onClick={() => setShowCreateAd(true)} className="w-full sm:w-auto">
|
||||
<PlusCircle className="w-4 h-4 mr-2" />
|
||||
Post a New Ad
|
||||
{t('p2p.postNewAd')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -190,14 +192,14 @@ export function P2PDashboard() {
|
||||
<TabsList className="grid w-full grid-cols-5 overflow-x-auto scrollbar-hide">
|
||||
<TabsTrigger value="express" className="flex items-center gap-1 text-xs sm:text-sm px-1 sm:px-3">
|
||||
<Zap className="w-3 h-3" />
|
||||
<span className="hidden xs:inline">Express</span>
|
||||
<span className="hidden xs:inline">{t('p2p.tabExpress')}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="buy" className="text-xs sm:text-sm px-1 sm:px-3">Buy</TabsTrigger>
|
||||
<TabsTrigger value="sell" className="text-xs sm:text-sm px-1 sm:px-3">Sell</TabsTrigger>
|
||||
<TabsTrigger value="my-ads" className="text-xs sm:text-sm px-1 sm:px-3">My Ads</TabsTrigger>
|
||||
<TabsTrigger value="buy" className="text-xs sm:text-sm px-1 sm:px-3">{t('p2p.tabBuy')}</TabsTrigger>
|
||||
<TabsTrigger value="sell" className="text-xs sm:text-sm px-1 sm:px-3">{t('p2p.tabSell')}</TabsTrigger>
|
||||
<TabsTrigger value="my-ads" className="text-xs sm:text-sm px-1 sm:px-3">{t('p2p.tabMyAds')}</TabsTrigger>
|
||||
<TabsTrigger value="otc" className="flex items-center gap-1 text-xs sm:text-sm px-1 sm:px-3">
|
||||
<Blocks className="w-3 h-3" />
|
||||
<span className="hidden xs:inline">OTC</span>
|
||||
<span className="hidden xs:inline">{t('p2p.tabOtc')}</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="express">
|
||||
@@ -206,23 +208,23 @@ export function P2PDashboard() {
|
||||
<div className="space-y-4">
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="pt-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-2">Why Express Mode?</h3>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">{t('p2p.whyExpress')}</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-400">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-green-400" />
|
||||
Instant best-rate matching
|
||||
{t('p2p.instantMatching')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-green-400" />
|
||||
Verified merchants only
|
||||
{t('p2p.verifiedMerchantsOnly')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-green-400" />
|
||||
Escrow protection
|
||||
{t('p2p.escrowProtection')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-green-400" />
|
||||
No manual offer selection
|
||||
{t('p2p.noManualSelection')}
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
@@ -245,27 +247,27 @@ export function P2PDashboard() {
|
||||
<div className="space-y-4">
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="pt-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-2">Block Trade Benefits</h3>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">{t('p2p.blockTradeBenefits')}</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-400">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-purple-400" />
|
||||
Custom pricing negotiation
|
||||
{t('p2p.customPricing')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-purple-400" />
|
||||
Dedicated OTC desk support
|
||||
{t('p2p.dedicatedSupport')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-purple-400" />
|
||||
Multi-tranche settlements
|
||||
{t('p2p.multiTranche')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-purple-400" />
|
||||
Enhanced privacy
|
||||
{t('p2p.enhancedPrivacy')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-purple-400" />
|
||||
Flexible payment terms
|
||||
{t('p2p.flexiblePayment')}
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Label } from '@/components/ui/label';
|
||||
import { Star, Loader2, ThumbsUp, ThumbsDown } from 'lucide-react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
|
||||
interface RatingModalProps {
|
||||
@@ -32,6 +33,7 @@ export function RatingModal({
|
||||
counterpartyWallet,
|
||||
isBuyer,
|
||||
}: RatingModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const [rating, setRating] = useState(0);
|
||||
const [hoveredRating, setHoveredRating] = useState(0);
|
||||
@@ -40,7 +42,7 @@ export function RatingModal({
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!user || rating === 0) {
|
||||
toast.error('Please select a rating');
|
||||
toast.error(t('p2pRating.selectRatingError'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -56,7 +58,7 @@ export function RatingModal({
|
||||
.single();
|
||||
|
||||
if (existingRating) {
|
||||
toast.error('You have already rated this trade');
|
||||
toast.error(t('p2pRating.alreadyRated'));
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
@@ -111,11 +113,11 @@ export function RatingModal({
|
||||
is_read: false,
|
||||
});
|
||||
|
||||
toast.success('Rating submitted successfully');
|
||||
toast.success(t('p2pRating.submitted'));
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Submit rating error:', error);
|
||||
toast.error('Failed to submit rating');
|
||||
toast.error(t('p2pRating.failedToSubmit'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -148,30 +150,30 @@ export function RatingModal({
|
||||
|
||||
const getRatingLabel = (r: number): string => {
|
||||
switch (r) {
|
||||
case 1: return 'Poor';
|
||||
case 2: return 'Fair';
|
||||
case 3: return 'Good';
|
||||
case 4: return 'Very Good';
|
||||
case 5: return 'Excellent';
|
||||
default: return 'Select a rating';
|
||||
case 1: return t('p2pRating.poor');
|
||||
case 2: return t('p2pRating.fair');
|
||||
case 3: return t('p2pRating.good');
|
||||
case 4: return t('p2pRating.veryGood');
|
||||
case 5: return t('p2pRating.excellent');
|
||||
default: return t('p2pRating.selectRating');
|
||||
}
|
||||
};
|
||||
|
||||
const quickReviews = [
|
||||
{ icon: ThumbsUp, text: 'Fast payment', positive: true },
|
||||
{ icon: ThumbsUp, text: 'Good communication', positive: true },
|
||||
{ icon: ThumbsUp, text: 'Smooth transaction', positive: true },
|
||||
{ icon: ThumbsDown, text: 'Slow response', positive: false },
|
||||
{ icon: ThumbsDown, text: 'Delayed payment', positive: false },
|
||||
{ icon: ThumbsUp, text: t('p2pRating.fastPayment'), positive: true },
|
||||
{ icon: ThumbsUp, text: t('p2pRating.goodCommunication'), positive: true },
|
||||
{ icon: ThumbsUp, text: t('p2pRating.smoothTransaction'), positive: true },
|
||||
{ icon: ThumbsDown, text: t('p2pRating.slowResponse'), positive: false },
|
||||
{ icon: ThumbsDown, text: t('p2pRating.delayedPayment'), positive: false },
|
||||
];
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="bg-gray-900 border-gray-800 text-white max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rate Your Experience</DialogTitle>
|
||||
<DialogTitle>{t('p2pRating.title')}</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
How was your trade with {counterpartyWallet.slice(0, 6)}...{counterpartyWallet.slice(-4)}?
|
||||
{t('p2pRating.description', { address: `${counterpartyWallet.slice(0, 6)}...${counterpartyWallet.slice(-4)}` })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -190,7 +192,7 @@ export function RatingModal({
|
||||
|
||||
{/* Quick Review Buttons */}
|
||||
<div>
|
||||
<Label className="text-gray-400 text-sm">Quick feedback (optional)</Label>
|
||||
<Label className="text-gray-400 text-sm">{t('p2pRating.quickFeedback')}</Label>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{quickReviews.map((qr, i) => (
|
||||
<button
|
||||
@@ -218,13 +220,13 @@ export function RatingModal({
|
||||
{/* Review Text */}
|
||||
<div>
|
||||
<Label htmlFor="review" className="text-gray-400 text-sm">
|
||||
Additional comments (optional)
|
||||
{t('p2pRating.additionalComments')}
|
||||
</Label>
|
||||
<Textarea
|
||||
id="review"
|
||||
value={review}
|
||||
onChange={(e) => setReview(e.target.value)}
|
||||
placeholder="Share your experience..."
|
||||
placeholder={t('p2pRating.sharePlaceholder')}
|
||||
className="mt-2 bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 resize-none"
|
||||
rows={3}
|
||||
maxLength={500}
|
||||
@@ -243,7 +245,7 @@ export function RatingModal({
|
||||
: 'bg-blue-500/20 text-blue-400'
|
||||
}
|
||||
`}>
|
||||
Rating as {isBuyer ? 'Buyer' : 'Seller'}
|
||||
{isBuyer ? t('p2pRating.ratingAsBuyer') : t('p2pRating.ratingAsSeller')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -255,7 +257,7 @@ export function RatingModal({
|
||||
disabled={loading}
|
||||
className="border-gray-700"
|
||||
>
|
||||
Skip
|
||||
{t('p2pRating.skip')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
@@ -265,10 +267,10 @@ export function RatingModal({
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Submitting...
|
||||
{t('p2pRating.submitting')}
|
||||
</>
|
||||
) : (
|
||||
'Submit Rating'
|
||||
t('p2pRating.submitRating')
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
|
||||
interface Message {
|
||||
@@ -41,6 +42,7 @@ export function TradeChat({
|
||||
counterpartyWallet,
|
||||
isTradeActive,
|
||||
}: TradeChatProps) {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [newMessage, setNewMessage] = useState('');
|
||||
@@ -158,7 +160,7 @@ export function TradeChat({
|
||||
await supabase.from('p2p_notifications').insert({
|
||||
user_id: counterpartyId,
|
||||
type: 'new_message',
|
||||
title: 'New Message',
|
||||
title: t('p2pChat.newMessage'),
|
||||
message: messageText.slice(0, 100),
|
||||
reference_type: 'trade',
|
||||
reference_id: tradeId,
|
||||
@@ -166,7 +168,7 @@ export function TradeChat({
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Send message error:', error);
|
||||
toast.error('Failed to send message');
|
||||
toast.error(t('p2pChat.failedToSend'));
|
||||
setNewMessage(messageText); // Restore message
|
||||
} finally {
|
||||
setSending(false);
|
||||
@@ -188,12 +190,12 @@ export function TradeChat({
|
||||
|
||||
// Validate file
|
||||
if (!file.type.startsWith('image/')) {
|
||||
toast.error('Please select an image file');
|
||||
toast.error(t('p2pChat.selectImage'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
toast.error('Image must be less than 5MB');
|
||||
toast.error(t('p2pChat.imageTooLarge'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -217,7 +219,7 @@ export function TradeChat({
|
||||
const { error: msgError } = await supabase.from('p2p_messages').insert({
|
||||
trade_id: tradeId,
|
||||
sender_id: user.id,
|
||||
message: 'Sent an image',
|
||||
message: t('p2pChat.sentImage'),
|
||||
message_type: 'image',
|
||||
attachment_url: urlData.publicUrl,
|
||||
is_read: false,
|
||||
@@ -229,17 +231,17 @@ export function TradeChat({
|
||||
await supabase.from('p2p_notifications').insert({
|
||||
user_id: counterpartyId,
|
||||
type: 'new_message',
|
||||
title: 'New Image',
|
||||
message: 'Sent an image',
|
||||
title: t('p2pChat.newImage'),
|
||||
message: t('p2pChat.sentImage'),
|
||||
reference_type: 'trade',
|
||||
reference_id: tradeId,
|
||||
is_read: false,
|
||||
});
|
||||
|
||||
toast.success('Image sent');
|
||||
toast.success(t('p2pChat.imageSent'));
|
||||
} catch (error) {
|
||||
console.error('Upload image error:', error);
|
||||
toast.error('Failed to upload image');
|
||||
toast.error(t('p2pChat.failedToUpload'));
|
||||
} finally {
|
||||
setUploading(false);
|
||||
if (fileInputRef.current) {
|
||||
@@ -301,7 +303,7 @@ export function TradeChat({
|
||||
>
|
||||
<img
|
||||
src={message.attachment_url}
|
||||
alt="Shared image"
|
||||
alt={t('p2pChat.sharedImage')}
|
||||
className="max-w-[200px] max-h-[200px] rounded-lg"
|
||||
/>
|
||||
</a>
|
||||
@@ -329,7 +331,7 @@ export function TradeChat({
|
||||
<Card className="bg-gray-900 border-gray-800 h-[400px] flex flex-col">
|
||||
<CardHeader className="py-3 px-4 border-b border-gray-800">
|
||||
<CardTitle className="text-white text-base flex items-center gap-2">
|
||||
<span>Chat</span>
|
||||
<span>{t('p2pChat.title')}</span>
|
||||
{messages.filter(m => m.sender_id !== user?.id && !m.is_read).length > 0 && (
|
||||
<span className="px-1.5 py-0.5 text-xs bg-green-500 text-white rounded-full">
|
||||
{messages.filter(m => m.sender_id !== user?.id && !m.is_read).length}
|
||||
@@ -348,8 +350,8 @@ export function TradeChat({
|
||||
) : messages.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-500">
|
||||
<AlertCircle className="w-8 h-8 mb-2" />
|
||||
<p className="text-sm">No messages yet</p>
|
||||
<p className="text-xs">Start the conversation</p>
|
||||
<p className="text-sm">{t('p2pChat.noMessages')}</p>
|
||||
<p className="text-xs">{t('p2pChat.startConversation')}</p>
|
||||
</div>
|
||||
) : (
|
||||
messages.map(renderMessage)
|
||||
@@ -384,7 +386,7 @@ export function TradeChat({
|
||||
value={newMessage}
|
||||
onChange={(e) => setNewMessage(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Type a message..."
|
||||
placeholder={t('p2pChat.placeholder')}
|
||||
disabled={sending}
|
||||
className="flex-1 bg-gray-800 border-gray-700 text-white placeholder:text-gray-500"
|
||||
/>
|
||||
@@ -405,7 +407,7 @@ export function TradeChat({
|
||||
) : (
|
||||
<div className="p-3 border-t border-gray-800 text-center">
|
||||
<p className="text-sm text-gray-500">
|
||||
Chat is disabled for completed/cancelled trades
|
||||
{t('p2pChat.chatDisabled')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -24,6 +25,7 @@ interface TradeModalProps {
|
||||
}
|
||||
|
||||
export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const { api, selectedAccount } = usePezkuwi();
|
||||
@@ -40,28 +42,28 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
|
||||
const handleInitiateTrade = async () => {
|
||||
if (!api || !selectedAccount || !user) {
|
||||
toast.error('Please connect your wallet and log in');
|
||||
toast.error(t('p2p.connectWalletAndLogin'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent self-trading
|
||||
if (offer.seller_id === user.id) {
|
||||
toast.error('You cannot trade with your own offer');
|
||||
toast.error(t('p2pTrade.cannotTradeOwn'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidAmount) {
|
||||
toast.error('Invalid amount');
|
||||
toast.error(t('p2pTrade.invalidAmount'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!meetsMinOrder) {
|
||||
toast.error(`Minimum order: ${offer.min_order_amount} ${offer.token}`);
|
||||
toast.error(t('p2p.minOrder', { amount: offer.min_order_amount, token: offer.token }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!meetsMaxOrder) {
|
||||
toast.error(`Maximum order: ${offer.max_order_amount} ${offer.token}`);
|
||||
toast.error(t('p2p.maxOrder', { amount: offer.max_order_amount, token: offer.token }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,7 +77,7 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
amount: cryptoAmount
|
||||
});
|
||||
|
||||
toast.success('Trade initiated! Proceed to payment.');
|
||||
toast.success(t('p2pTrade.tradeInitiated'));
|
||||
onClose();
|
||||
|
||||
// Navigate to trade page
|
||||
@@ -92,9 +94,9 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
<Dialog open={true} onOpenChange={onClose}>
|
||||
<DialogContent className="bg-gray-900 border-gray-800 text-white max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Buy {offer.token}</DialogTitle>
|
||||
<DialogTitle>{t('p2pTrade.buyToken', { token: offer.token })}</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Trading with {offer.seller_wallet.slice(0, 6)}...{offer.seller_wallet.slice(-4)}
|
||||
{t('p2pTrade.tradingWith', { address: `${offer.seller_wallet.slice(0, 6)}...${offer.seller_wallet.slice(-4)}` })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -102,37 +104,37 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
{/* Price Info */}
|
||||
<div className="p-4 bg-gray-800 rounded-lg">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-gray-400">Price</span>
|
||||
<span className="text-gray-400">{t('p2p.price')}</span>
|
||||
<span className="text-xl font-bold text-green-400">
|
||||
{offer.price_per_unit.toFixed(2)} {offer.fiat_currency}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-gray-400">Available</span>
|
||||
<span className="text-gray-400">{t('p2p.available')}</span>
|
||||
<span className="text-white">{offer.remaining_amount} {offer.token}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Amount Input */}
|
||||
<div>
|
||||
<Label htmlFor="buyAmount">Amount to Buy ({offer.token})</Label>
|
||||
<Label htmlFor="buyAmount">{t('p2pTrade.amountToBuy', { token: offer.token })}</Label>
|
||||
<Input
|
||||
id="buyAmount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
placeholder="Amount"
|
||||
placeholder={t('p2p.amount')}
|
||||
className="bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 placeholder:opacity-50"
|
||||
/>
|
||||
{offer.min_order_amount && (
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Min: {offer.min_order_amount} {offer.token}
|
||||
{t('p2p.minLimit', { amount: offer.min_order_amount, token: offer.token })}
|
||||
</p>
|
||||
)}
|
||||
{offer.max_order_amount && (
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Max: {offer.max_order_amount} {offer.token}
|
||||
{t('p2p.maxLimit', { amount: offer.max_order_amount, token: offer.token })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -140,7 +142,7 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
{/* Calculation */}
|
||||
{cryptoAmount > 0 && (
|
||||
<div className="p-4 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||
<p className="text-sm text-gray-400 mb-1">You will pay</p>
|
||||
<p className="text-sm text-gray-400 mb-1">{t('p2pTrade.youWillPay')}</p>
|
||||
<p className="text-2xl font-bold text-green-400">
|
||||
{fiatAmount.toFixed(2)} {offer.fiat_currency}
|
||||
</p>
|
||||
@@ -152,7 +154,7 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Minimum order: {offer.min_order_amount} {offer.token}
|
||||
{t('p2p.minOrder', { amount: offer.min_order_amount, token: offer.token })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -161,7 +163,7 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Maximum order: {offer.max_order_amount} {offer.token}
|
||||
{t('p2p.maxOrder', { amount: offer.max_order_amount, token: offer.token })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -170,7 +172,7 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
<Alert>
|
||||
<Clock className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Payment deadline: {offer.time_limit_minutes} minutes after accepting
|
||||
{t('p2pTrade.paymentDeadline', { minutes: offer.time_limit_minutes })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
@@ -182,7 +184,7 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
disabled={loading}
|
||||
className="bg-gray-800 border-gray-700 hover:bg-gray-700"
|
||||
>
|
||||
Cancel
|
||||
{t('p2p.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleInitiateTrade}
|
||||
@@ -191,10 +193,10 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Initiating...
|
||||
{t('p2pTrade.initiating')}
|
||||
</>
|
||||
) : (
|
||||
'Accept & Continue'
|
||||
t('p2pTrade.acceptAndContinue')
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -48,6 +49,7 @@ interface WithdrawModalProps {
|
||||
type WithdrawStep = 'form' | 'confirm' | 'success';
|
||||
|
||||
export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const { selectedAccount } = usePezkuwi();
|
||||
|
||||
const [step, setStep] = useState<WithdrawStep>('form');
|
||||
@@ -134,25 +136,25 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
const withdrawAmount = parseFloat(amount);
|
||||
|
||||
if (isNaN(withdrawAmount) || withdrawAmount <= 0) {
|
||||
return 'Please enter a valid amount';
|
||||
return t('p2pWithdraw.enterValidAmount');
|
||||
}
|
||||
|
||||
if (withdrawAmount < MIN_WITHDRAWAL) {
|
||||
return `Minimum withdrawal is ${MIN_WITHDRAWAL} ${token}`;
|
||||
return t('p2pWithdraw.minimumWithdrawal', { amount: MIN_WITHDRAWAL, token });
|
||||
}
|
||||
|
||||
if (withdrawAmount > getMaxWithdrawable()) {
|
||||
return 'Insufficient available balance';
|
||||
return t('p2pWithdraw.insufficientBalance');
|
||||
}
|
||||
|
||||
if (!walletAddress || walletAddress.length < 40) {
|
||||
return 'Please enter a valid wallet address';
|
||||
return t('p2pWithdraw.invalidAddress');
|
||||
}
|
||||
|
||||
// Check for pending requests
|
||||
const hasPendingForToken = pendingRequests.some(r => r.token === token);
|
||||
if (hasPendingForToken) {
|
||||
return `You already have a pending ${token} withdrawal request`;
|
||||
return t('p2pWithdraw.pendingForToken', { token });
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -201,14 +203,14 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
<>
|
||||
{/* Token Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label>Select Token</Label>
|
||||
<Label>{t('p2pWithdraw.selectToken')}</Label>
|
||||
<Select value={token} onValueChange={(v) => setToken(v as CryptoToken)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="HEZ">HEZ (Native)</SelectItem>
|
||||
<SelectItem value="PEZ">PEZ</SelectItem>
|
||||
<SelectItem value="HEZ">{t('p2pWithdraw.hezNative')}</SelectItem>
|
||||
<SelectItem value="PEZ">{t('p2pWithdraw.pez')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -217,13 +219,13 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
<div className="p-4 rounded-lg bg-muted/50 border">
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-muted-foreground">Available</p>
|
||||
<p className="text-muted-foreground">{t('p2pWithdraw.available')}</p>
|
||||
<p className="font-semibold text-green-500">
|
||||
{getAvailableBalance().toFixed(4)} {token}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Locked (Escrow)</p>
|
||||
<p className="text-muted-foreground">{t('p2pWithdraw.lockedEscrow')}</p>
|
||||
<p className="font-semibold text-yellow-500">
|
||||
{getLockedBalance().toFixed(4)} {token}
|
||||
</p>
|
||||
@@ -233,7 +235,7 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
|
||||
{/* Amount Input */}
|
||||
<div className="space-y-2">
|
||||
<Label>Withdrawal Amount</Label>
|
||||
<Label>{t('p2pWithdraw.withdrawalAmount')}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="number"
|
||||
@@ -253,17 +255,17 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 h-7 text-xs"
|
||||
onClick={handleSetMax}
|
||||
>
|
||||
MAX
|
||||
{t('p2pWithdraw.max')}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Min: {MIN_WITHDRAWAL} {token} | Max: {getMaxWithdrawable().toFixed(4)} {token}
|
||||
{t('p2pWithdraw.minMax', { min: MIN_WITHDRAWAL, max: getMaxWithdrawable().toFixed(4), token })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Wallet Address */}
|
||||
<div className="space-y-2">
|
||||
<Label>Destination Wallet Address</Label>
|
||||
<Label>{t('p2pWithdraw.destinationAddress')}</Label>
|
||||
<Input
|
||||
value={walletAddress}
|
||||
onChange={(e) => setWalletAddress(e.target.value)}
|
||||
@@ -271,7 +273,7 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Only PezkuwiChain addresses are supported
|
||||
{t('p2pWithdraw.onlyPezkuwiAddresses')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -280,7 +282,7 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Network fee: ~{NETWORK_FEE} HEZ (deducted from withdrawal amount)
|
||||
{t('p2pWithdraw.networkFee', { fee: NETWORK_FEE })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -290,21 +292,20 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You have {pendingRequests.length} pending withdrawal request(s).
|
||||
Please wait for them to complete.
|
||||
{t('p2pWithdraw.pendingWarning', { count: pendingRequests.length })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleContinue}
|
||||
disabled={!amount || parseFloat(amount) <= 0}
|
||||
>
|
||||
Continue
|
||||
{t('continue')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
@@ -321,28 +322,27 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Please review your withdrawal details carefully.
|
||||
This action cannot be undone.
|
||||
{t('p2pWithdraw.reviewWarning')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="p-4 rounded-lg bg-muted/50 border space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">Token</span>
|
||||
<span className="text-muted-foreground">{t('p2pWithdraw.tokenLabel')}</span>
|
||||
<span className="font-semibold">{token}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">Withdrawal Amount</span>
|
||||
<span className="text-muted-foreground">{t('p2pWithdraw.withdrawalAmountLabel')}</span>
|
||||
<span className="font-semibold">{withdrawAmount.toFixed(4)} {token}</span>
|
||||
</div>
|
||||
{token === 'HEZ' && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">Network Fee</span>
|
||||
<span className="text-muted-foreground">{t('p2pWithdraw.networkFeeLabel')}</span>
|
||||
<span className="text-yellow-500">-{NETWORK_FEE} HEZ</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t pt-4 flex justify-between items-center">
|
||||
<span className="text-muted-foreground">You Will Receive</span>
|
||||
<span className="text-muted-foreground">{t('p2pWithdraw.youWillReceive')}</span>
|
||||
<span className="font-bold text-lg text-green-500">
|
||||
{receiveAmount.toFixed(4)} {token}
|
||||
</span>
|
||||
@@ -350,18 +350,18 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg bg-muted/30 border">
|
||||
<p className="text-xs text-muted-foreground mb-1">Destination Address</p>
|
||||
<p className="text-xs text-muted-foreground mb-1">{t('p2pWithdraw.destinationAddressLabel')}</p>
|
||||
<p className="font-mono text-xs break-all">{walletAddress}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span>Processing time: Usually within 5-30 minutes</span>
|
||||
<span>{t('p2pWithdraw.processingTime')}</span>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setStep('form')}>
|
||||
Back
|
||||
{t('back')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmitWithdrawal}
|
||||
@@ -370,12 +370,12 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
{submitting ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Processing...
|
||||
{t('p2pWithdraw.processing')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ArrowUpFromLine className="h-4 w-4 mr-2" />
|
||||
Confirm Withdrawal
|
||||
{t('p2pWithdraw.confirmWithdrawal')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -392,29 +392,29 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-green-500">
|
||||
Withdrawal Request Submitted!
|
||||
{t('p2pWithdraw.requestSubmitted')}
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Your withdrawal request has been submitted for processing.
|
||||
{t('p2pWithdraw.requestSubmittedDesc')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg bg-muted/50 border space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Request ID</span>
|
||||
<span className="text-muted-foreground">{t('p2pWithdraw.requestId')}</span>
|
||||
<Badge variant="outline" className="font-mono text-xs">
|
||||
{requestId.slice(0, 8)}...
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Status</span>
|
||||
<span className="text-muted-foreground">{t('p2pWithdraw.statusLabel')}</span>
|
||||
<Badge className="bg-yellow-500/20 text-yellow-500 border-yellow-500/30">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
Processing
|
||||
{t('p2pWithdraw.statusProcessing')}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Amount</span>
|
||||
<span className="text-muted-foreground">{t('p2pWithdraw.amountLabel')}</span>
|
||||
<span className="font-semibold">{amount} {token}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -422,13 +422,12 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You can track your withdrawal status in the transaction history.
|
||||
Funds will arrive in your wallet within 5-30 minutes.
|
||||
{t('p2pWithdraw.trackInfo')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Button onClick={handleClose} className="w-full">
|
||||
Done
|
||||
{t('p2pWithdraw.done')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -439,12 +438,12 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<ArrowUpFromLine className="h-5 w-5" />
|
||||
Withdraw from P2P Balance
|
||||
{t('p2pWithdraw.title')}
|
||||
</DialogTitle>
|
||||
{step !== 'success' && (
|
||||
<DialogDescription>
|
||||
{step === 'form' && 'Withdraw crypto from your P2P balance to external wallet'}
|
||||
{step === 'confirm' && 'Review and confirm your withdrawal'}
|
||||
{step === 'form' && t('p2pWithdraw.formStep')}
|
||||
{step === 'confirm' && t('p2pWithdraw.confirmStep')}
|
||||
</DialogDescription>
|
||||
)}
|
||||
</DialogHeader>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -15,6 +16,7 @@ interface CourseCreatorProps {
|
||||
}
|
||||
|
||||
export function CourseCreator({ onCourseCreated }: CourseCreatorProps) {
|
||||
const { t } = useTranslation();
|
||||
const { api, selectedAccount } = usePezkuwi();
|
||||
const [name, setName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
@@ -23,12 +25,12 @@ export function CourseCreator({ onCourseCreated }: CourseCreatorProps) {
|
||||
|
||||
const handleCreateCourse = async () => {
|
||||
if (!api || !selectedAccount) {
|
||||
toast.error('Please connect your wallet first');
|
||||
toast.error(t('courseCreator.connectWallet'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!name || !description || !content) {
|
||||
toast.error('Please fill in all fields');
|
||||
toast.error(t('courseCreator.fillAllFields'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -66,37 +68,37 @@ export function CourseCreator({ onCourseCreated }: CourseCreatorProps) {
|
||||
return (
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Create New Course</CardTitle>
|
||||
<CardDescription>Fill in the details to create a new course on the Perwerde platform.</CardDescription>
|
||||
<CardTitle className="text-white">{t('courseCreator.title')}</CardTitle>
|
||||
<CardDescription>{t('courseCreator.subtitle')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name" className="text-white">Course Name</Label>
|
||||
<Label htmlFor="name" className="text-white">{t('courseCreator.courseName')}</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="e.g., Introduction to Blockchain"
|
||||
placeholder={t('courseCreator.courseNamePlaceholder')}
|
||||
className="bg-gray-800 border-gray-700 text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description" className="text-white">Course Description</Label>
|
||||
<Label htmlFor="description" className="text-white">{t('courseCreator.courseDesc')}</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="A brief summary of the course content and objectives."
|
||||
placeholder={t('courseCreator.courseDescPlaceholder')}
|
||||
className="bg-gray-800 border-gray-700 text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="content" className="text-white">Course Content (Markdown)</Label>
|
||||
<Label htmlFor="content" className="text-white">{t('courseCreator.courseContent')}</Label>
|
||||
<Textarea
|
||||
id="content"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder="Write your course material here using Markdown..."
|
||||
placeholder={t('courseCreator.courseContentPlaceholder')}
|
||||
className="bg-gray-800 border-gray-700 text-white h-48"
|
||||
/>
|
||||
</div>
|
||||
@@ -108,10 +110,10 @@ export function CourseCreator({ onCourseCreated }: CourseCreatorProps) {
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Creating...
|
||||
{t('courseCreator.creating')}
|
||||
</>
|
||||
) : (
|
||||
'Create Course'
|
||||
t('courseCreator.createBtn')
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -15,6 +16,7 @@ interface CourseListProps {
|
||||
}
|
||||
|
||||
export function CourseList({ enrolledCourseIds, onEnroll }: CourseListProps) {
|
||||
const { t } = useTranslation();
|
||||
const { api, selectedAccount } = usePezkuwi();
|
||||
const [courses, setCourses] = useState<Course[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -29,7 +31,7 @@ export function CourseList({ enrolledCourseIds, onEnroll }: CourseListProps) {
|
||||
if (import.meta.env.DEV) console.error('Failed to fetch courses:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to fetch courses',
|
||||
description: t('courseList.fetchFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
@@ -44,7 +46,7 @@ export function CourseList({ enrolledCourseIds, onEnroll }: CourseListProps) {
|
||||
if (!api || !selectedAccount) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Please connect your wallet first',
|
||||
description: t('courseList.connectWallet'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
@@ -65,16 +67,16 @@ export function CourseList({ enrolledCourseIds, onEnroll }: CourseListProps) {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white mb-6">
|
||||
{courses.length > 0 ? `Available Courses (${courses.length})` : 'No Courses Available'}
|
||||
{courses.length > 0 ? `${t('courseList.title')} (${courses.length})` : t('courseList.noCourses')}
|
||||
</h2>
|
||||
|
||||
{courses.length === 0 ? (
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="p-12 text-center">
|
||||
<BookOpen className="w-16 h-16 text-gray-600 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-bold text-gray-400 mb-2">No Active Courses</h3>
|
||||
<h3 className="text-xl font-bold text-gray-400 mb-2">{t('courseList.noActive')}</h3>
|
||||
<p className="text-gray-500 mb-6">
|
||||
Check back later for new educational content.
|
||||
{t('courseList.checkBackLater')}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -98,7 +100,7 @@ export function CourseList({ enrolledCourseIds, onEnroll }: CourseListProps) {
|
||||
</Badge>
|
||||
{isUserEnrolled && (
|
||||
<Badge className="bg-blue-500/10 text-blue-400 border-blue-500/30">
|
||||
Enrolled
|
||||
{t('courseList.enrolled')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -118,7 +120,7 @@ export function CourseList({ enrolledCourseIds, onEnroll }: CourseListProps) {
|
||||
className="flex items-center gap-1 text-green-400 hover:text-green-300"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
Course Materials
|
||||
{t('courseList.courseMaterials')}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
@@ -128,7 +130,7 @@ export function CourseList({ enrolledCourseIds, onEnroll }: CourseListProps) {
|
||||
{isUserEnrolled ? (
|
||||
<Button className="bg-blue-600 hover:bg-blue-700" disabled>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
Already Enrolled
|
||||
{t('courseList.alreadyEnrolled')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
@@ -136,7 +138,7 @@ export function CourseList({ enrolledCourseIds, onEnroll }: CourseListProps) {
|
||||
onClick={() => handleEnroll(course.id)}
|
||||
disabled={!selectedAccount}
|
||||
>
|
||||
Enroll Now
|
||||
{t('courseList.enrollNow')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -15,11 +16,12 @@ interface StudentDashboardProps {
|
||||
}
|
||||
|
||||
export function StudentDashboard({ enrollments, loading, onCourseCompleted }: StudentDashboardProps) {
|
||||
const { t } = useTranslation();
|
||||
const { api, selectedAccount } = usePezkuwi();
|
||||
|
||||
const handleComplete = async (courseId: number) => {
|
||||
if (!api || !selectedAccount) {
|
||||
toast.error('Please connect your wallet first');
|
||||
toast.error(t('studentDashboard.connectWallet'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,7 +36,7 @@ export function StudentDashboard({ enrollments, loading, onCourseCompleted }: St
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <LoadingState message="Loading your dashboard..." />;
|
||||
return <LoadingState message={t('studentDashboard.loading')} />;
|
||||
}
|
||||
|
||||
const completedCourses = enrollments.filter(e => e.is_completed).length;
|
||||
@@ -51,7 +53,7 @@ export function StudentDashboard({ enrollments, loading, onCourseCompleted }: St
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-white">{enrollments.length}</div>
|
||||
<div className="text-sm text-gray-400">Enrolled Courses</div>
|
||||
<div className="text-sm text-gray-400">{t('studentDashboard.enrolledCourses')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -64,7 +66,7 @@ export function StudentDashboard({ enrollments, loading, onCourseCompleted }: St
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-white">{completedCourses}</div>
|
||||
<div className="text-sm text-gray-400">Completed</div>
|
||||
<div className="text-sm text-gray-400">{t('studentDashboard.completed')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -77,7 +79,7 @@ export function StudentDashboard({ enrollments, loading, onCourseCompleted }: St
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-white">{totalPoints}</div>
|
||||
<div className="text-sm text-gray-400">Total Points</div>
|
||||
<div className="text-sm text-gray-400">{t('studentDashboard.totalPoints')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -86,28 +88,28 @@ export function StudentDashboard({ enrollments, loading, onCourseCompleted }: St
|
||||
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">My Courses</CardTitle>
|
||||
<CardTitle className="text-white">{t('studentDashboard.myCourses')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{enrollments.length === 0 ? (
|
||||
<p className="text-gray-400">You are not enrolled in any courses yet.</p>
|
||||
<p className="text-gray-400">{t('studentDashboard.notEnrolled')}</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{enrollments.map(enrollment => (
|
||||
<div key={enrollment.id} className="p-4 bg-gray-800 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-bold text-white">Course #{enrollment.course_id}</h4>
|
||||
<h4 className="font-bold text-white">{t('studentDashboard.courseNum', { id: enrollment.course_id })}</h4>
|
||||
<p className="text-sm text-gray-400">
|
||||
Enrolled on: {new Date(enrollment.enrolled_at).toLocaleDateString()}
|
||||
{t('studentDashboard.enrolledOn')}: {new Date(enrollment.enrolled_at).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
{enrollment.is_completed ? (
|
||||
<Badge className="bg-green-500/10 text-green-400">Completed</Badge>
|
||||
<Badge className="bg-green-500/10 text-green-400">{t('studentDashboard.completed')}</Badge>
|
||||
) : (
|
||||
<Button size="sm" onClick={() => handleComplete(enrollment.course_id)}>
|
||||
Mark as Complete
|
||||
{t('studentDashboard.markComplete')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useMemo } 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';
|
||||
@@ -21,6 +22,7 @@ interface InviteUserModalProps {
|
||||
}
|
||||
|
||||
export const InviteUserModal: React.FC<InviteUserModalProps> = ({ isOpen, onClose }) => {
|
||||
const { t } = useTranslation();
|
||||
const { peopleApi, isPeopleReady, selectedAccount } = usePezkuwi();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [inviteeAddress, setInviteeAddress] = useState('');
|
||||
@@ -122,17 +124,17 @@ export const InviteUserModal: React.FC<InviteUserModalProps> = ({ isOpen, onClos
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white flex items-center gap-2">
|
||||
<Share2 className="w-5 h-5 text-green-500" />
|
||||
Invite Friends to PezkuwiChain
|
||||
{t('invite.title')}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Share your referral link. When your friends complete KYC, you'll earn trust score points!
|
||||
{t('invite.subtitle')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 mt-4">
|
||||
{/* Referral Link Display */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">Your Referral Link</Label>
|
||||
<Label className="text-gray-300">{t('invite.yourLink')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
@@ -157,23 +159,22 @@ export const InviteUserModal: React.FC<InviteUserModalProps> = ({ isOpen, onClos
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
Anyone who signs up with this link will be counted as your referral
|
||||
{t('invite.linkDesc')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Manual Referral Initiation */}
|
||||
<div className="space-y-2 bg-blue-900/20 border border-blue-600/30 rounded-lg p-4">
|
||||
<Label className="text-blue-300">Or Pre-Register a Friend (Advanced)</Label>
|
||||
<Label className="text-blue-300">{t('invite.preRegister')}</Label>
|
||||
<p className="text-xs text-gray-400 mb-2">
|
||||
If you know your friend's wallet address, you can pre-register them on-chain.
|
||||
They must then complete KYC to finalize the referral.
|
||||
{t('invite.preRegisterDesc')}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
value={inviteeAddress}
|
||||
onChange={(e) => setInviteeAddress(e.target.value)}
|
||||
placeholder="Friend's wallet address"
|
||||
placeholder={t('invite.friendAddress')}
|
||||
className="bg-gray-800 border-gray-700 text-white font-mono text-sm placeholder:text-gray-500 placeholder:opacity-50"
|
||||
/>
|
||||
<Button
|
||||
@@ -181,11 +182,11 @@ export const InviteUserModal: React.FC<InviteUserModalProps> = ({ isOpen, onClos
|
||||
disabled={initiating || !inviteeAddress}
|
||||
className="bg-blue-600 hover:bg-blue-700 shrink-0"
|
||||
>
|
||||
{initiating ? 'Initiating...' : 'Initiate'}
|
||||
{initiating ? t('invite.initiating') : t('invite.initiate')}
|
||||
</Button>
|
||||
</div>
|
||||
{initiateSuccess && (
|
||||
<p className="text-xs text-green-400">Referral initiated successfully!</p>
|
||||
<p className="text-xs text-green-400">{t('invite.initiated')}</p>
|
||||
)}
|
||||
{initiateError && (
|
||||
<p className="text-xs text-red-400">{initiateError}</p>
|
||||
@@ -194,7 +195,7 @@ export const InviteUserModal: React.FC<InviteUserModalProps> = ({ isOpen, onClos
|
||||
|
||||
{/* Share Options */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-gray-300">Share via</Label>
|
||||
<Label className="text-gray-300">{t('invite.shareVia')}</Label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* WhatsApp */}
|
||||
<Button
|
||||
@@ -260,12 +261,12 @@ export const InviteUserModal: React.FC<InviteUserModalProps> = ({ isOpen, onClos
|
||||
|
||||
{/* Rewards Info */}
|
||||
<div className="bg-green-900/20 border border-green-600/30 rounded-lg p-4">
|
||||
<h4 className="text-green-400 font-semibold mb-2 text-sm">Referral Rewards</h4>
|
||||
<h4 className="text-green-400 font-semibold mb-2 text-sm">{t('invite.rewards')}</h4>
|
||||
<ul className="text-xs text-gray-300 space-y-1">
|
||||
<li>• 1-10 referrals: 10 points each (up to 100 points)</li>
|
||||
<li>• 11-50 referrals: 5 points each (up to 300 points)</li>
|
||||
<li>• 51-100 referrals: 4 points each (up to 500 points)</li>
|
||||
<li>• Maximum: 500 trust score points</li>
|
||||
<li>• {t('invite.reward1')}</li>
|
||||
<li>• {t('invite.reward2')}</li>
|
||||
<li>• {t('invite.reward3')}</li>
|
||||
<li>• {t('invite.maxReward')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -275,7 +276,7 @@ export const InviteUserModal: React.FC<InviteUserModalProps> = ({ isOpen, onClos
|
||||
onClick={onClose}
|
||||
className="bg-gray-800 hover:bg-gray-700 text-white"
|
||||
>
|
||||
Done
|
||||
{t('invite.done')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useReferral } from '@/contexts/ReferralContext';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@@ -11,6 +12,7 @@ import { getPendingApprovalsForReferrer, approveReferral } from '@pezkuwi/lib/ci
|
||||
import type { PendingApproval } from '@pezkuwi/lib/citizenship-workflow';
|
||||
|
||||
export const ReferralDashboard: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { stats, myReferrals, loading } = useReferral();
|
||||
const { peopleApi, isPeopleReady, selectedAccount } = usePezkuwi();
|
||||
const { toast } = useToast();
|
||||
@@ -83,10 +85,10 @@ export const ReferralDashboard: React.FC = () => {
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white flex items-center gap-2">
|
||||
<Users className="w-6 h-6 text-green-500" />
|
||||
Referral System
|
||||
{t('referral.title')}
|
||||
</h2>
|
||||
<p className="text-gray-400 mt-1">
|
||||
Invite friends to PezkuwiChain and earn trust score
|
||||
{t('referral.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -94,7 +96,7 @@ export const ReferralDashboard: React.FC = () => {
|
||||
className="bg-green-600 hover:bg-green-700 text-white"
|
||||
>
|
||||
<UserPlus className="mr-2 h-4 w-4" />
|
||||
Invite Friend
|
||||
{t('referral.inviteFriend')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -105,10 +107,10 @@ export const ReferralDashboard: React.FC = () => {
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<Users className="w-5 h-5 text-green-500" />
|
||||
Total Referrals
|
||||
{t('referral.totalReferrals')}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
Confirmed referrals (KYC completed)
|
||||
{t('referral.totalReferralsDesc')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -123,10 +125,10 @@ export const ReferralDashboard: React.FC = () => {
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<Trophy className="w-5 h-5 text-yellow-500" />
|
||||
Referral Score
|
||||
{t('referral.referralScore')}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
Score earned from referrals
|
||||
{t('referral.referralScoreDesc')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -134,7 +136,7 @@ export const ReferralDashboard: React.FC = () => {
|
||||
{stats?.referralScore ?? 0}
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
Max: 500 points
|
||||
{t('referral.maxPoints')}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -144,10 +146,10 @@ export const ReferralDashboard: React.FC = () => {
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<Clock className="w-5 h-5 text-yellow-500" />
|
||||
Pending Approvals
|
||||
{t('referral.pendingApprovals')}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
Waiting for your approval
|
||||
{t('referral.pendingApprovalsDesc')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -156,7 +158,7 @@ export const ReferralDashboard: React.FC = () => {
|
||||
</div>
|
||||
{stats?.whoInvitedMe && (
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
You were invited by {stats.whoInvitedMe.slice(0, 8)}...{stats.whoInvitedMe.slice(-6)}
|
||||
{t('referral.invitedBy')} {stats.whoInvitedMe.slice(0, 8)}...{stats.whoInvitedMe.slice(-6)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
@@ -166,28 +168,28 @@ export const ReferralDashboard: React.FC = () => {
|
||||
{/* Score Breakdown */}
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Score Calculation</CardTitle>
|
||||
<CardTitle className="text-white">{t('referral.scoreCalc')}</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
How referrals contribute to your trust score
|
||||
{t('referral.scoreCalcDesc')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 bg-gray-800/50 rounded-lg">
|
||||
<span className="text-gray-300">1-10 referrals</span>
|
||||
<span className="text-green-400 font-semibold">10 points each</span>
|
||||
<span className="text-gray-300">{t('referral.referrals1_10')}</span>
|
||||
<span className="text-green-400 font-semibold">{t('referral.points10each')}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 bg-gray-800/50 rounded-lg">
|
||||
<span className="text-gray-300">11-50 referrals</span>
|
||||
<span className="text-blue-400 font-semibold">100 + 5 points each</span>
|
||||
<span className="text-gray-300">{t('referral.referrals11_50')}</span>
|
||||
<span className="text-blue-400 font-semibold">{t('referral.points5each')}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 bg-gray-800/50 rounded-lg">
|
||||
<span className="text-gray-300">51-100 referrals</span>
|
||||
<span className="text-yellow-400 font-semibold">300 + 4 points each</span>
|
||||
<span className="text-gray-300">{t('referral.referrals51_100')}</span>
|
||||
<span className="text-yellow-400 font-semibold">{t('referral.points4each')}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 bg-gray-800/50 rounded-lg">
|
||||
<span className="text-gray-300">101+ referrals</span>
|
||||
<span className="text-red-400 font-semibold">500 points (max)</span>
|
||||
<span className="text-gray-300">{t('referral.referrals101plus')}</span>
|
||||
<span className="text-red-400 font-semibold">{t('referral.maxPointsValue')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -198,10 +200,10 @@ export const ReferralDashboard: React.FC = () => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Users className="w-5 h-5 text-green-500" />
|
||||
My Invitations ({pendingApprovals.length + myReferrals.length})
|
||||
{t('referral.myInvitations')} ({pendingApprovals.length + myReferrals.length})
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
People you invited — approve pending ones to complete step 2
|
||||
{t('referral.myInvitationsDesc')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -212,16 +214,16 @@ export const ReferralDashboard: React.FC = () => {
|
||||
) : pendingApprovals.length === 0 && myReferrals.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<Users className="w-12 h-12 text-gray-600 mx-auto mb-3" />
|
||||
<p className="text-gray-500">No invitations yet</p>
|
||||
<p className="text-gray-500">{t('referral.noInvitations')}</p>
|
||||
<p className="text-gray-600 text-sm mt-1">
|
||||
Invite friends to start building your network
|
||||
{t('referral.noInvitationsDesc')}
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => setShowInviteModal(true)}
|
||||
className="mt-4 bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
<UserPlus className="mr-2 h-4 w-4" />
|
||||
Send First Invitation
|
||||
{t('referral.sendFirst')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -242,7 +244,7 @@ export const ReferralDashboard: React.FC = () => {
|
||||
</div>
|
||||
<div className="text-xs mt-0.5">
|
||||
<Badge className="bg-yellow-500/20 text-yellow-400 border-yellow-500/30 text-[10px] px-1.5 py-0">
|
||||
Pending — Waiting Your Approval
|
||||
{t('referral.pendingApproval')}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
@@ -258,7 +260,7 @@ export const ReferralDashboard: React.FC = () => {
|
||||
) : (
|
||||
<CheckCircle className="w-4 h-4 mr-1" />
|
||||
)}
|
||||
Approve
|
||||
{t('referral.approve')}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
@@ -279,7 +281,7 @@ export const ReferralDashboard: React.FC = () => {
|
||||
</div>
|
||||
<div className="text-xs mt-0.5">
|
||||
<Badge className="bg-green-500/20 text-green-400 border-green-500/30 text-[10px] px-1.5 py-0">
|
||||
Confirmed
|
||||
{t('referral.confirmed')}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
@@ -303,9 +305,9 @@ export const ReferralDashboard: React.FC = () => {
|
||||
<Award className="w-5 h-5 text-blue-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-white font-semibold">Pending Invitation</div>
|
||||
<div className="text-white font-semibold">{t('referral.pendingInvitation')}</div>
|
||||
<div className="text-sm text-blue-300">
|
||||
Complete KYC to confirm your referral from{' '}
|
||||
{t('referral.completeKyc')}{' '}
|
||||
<span className="font-mono">
|
||||
{stats.pendingReferral.slice(0, 8)}...{stats.pendingReferral.slice(-6)}
|
||||
</span>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
@@ -18,44 +19,45 @@ interface Role {
|
||||
|
||||
const PERMISSION_CATEGORIES = {
|
||||
governance: {
|
||||
title: 'Governance',
|
||||
titleKey: 'permission.catGovernance',
|
||||
permissions: {
|
||||
create_proposal: 'Create Proposals',
|
||||
vote_proposal: 'Vote on Proposals',
|
||||
delegate_vote: 'Delegate Voting Power',
|
||||
manage_treasury: 'Manage Treasury',
|
||||
create_proposal: 'permission.createProposals',
|
||||
vote_proposal: 'permission.voteOnProposals',
|
||||
delegate_vote: 'permission.delegateVoting',
|
||||
manage_treasury: 'permission.manageTreasury',
|
||||
}
|
||||
},
|
||||
moderation: {
|
||||
title: 'Moderation',
|
||||
titleKey: 'permission.catModeration',
|
||||
permissions: {
|
||||
moderate_content: 'Moderate Content',
|
||||
ban_users: 'Ban Users',
|
||||
delete_posts: 'Delete Posts',
|
||||
pin_posts: 'Pin Posts',
|
||||
moderate_content: 'permission.moderateContent',
|
||||
ban_users: 'permission.banUsers',
|
||||
delete_posts: 'permission.deletePosts',
|
||||
pin_posts: 'permission.pinPosts',
|
||||
}
|
||||
},
|
||||
administration: {
|
||||
title: 'Administration',
|
||||
titleKey: 'permission.catAdministration',
|
||||
permissions: {
|
||||
manage_users: 'Manage Users',
|
||||
manage_roles: 'Manage Roles',
|
||||
view_analytics: 'View Analytics',
|
||||
system_settings: 'System Settings',
|
||||
manage_users: 'permission.manageUsers',
|
||||
manage_roles: 'permission.manageRoles',
|
||||
view_analytics: 'permission.viewAnalytics',
|
||||
system_settings: 'permission.systemSettings',
|
||||
}
|
||||
},
|
||||
security: {
|
||||
title: 'Security',
|
||||
titleKey: 'permission.catSecurity',
|
||||
permissions: {
|
||||
view_audit_logs: 'View Audit Logs',
|
||||
manage_sessions: 'Manage Sessions',
|
||||
configure_2fa: 'Configure 2FA',
|
||||
access_api: 'Access API',
|
||||
view_audit_logs: 'permission.viewAuditLogs',
|
||||
manage_sessions: 'permission.manageSessions',
|
||||
configure_2fa: 'permission.configure2fa',
|
||||
access_api: 'permission.accessApi',
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function PermissionEditor() {
|
||||
const { t } = useTranslation();
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -81,8 +83,8 @@ export function PermissionEditor() {
|
||||
} catch {
|
||||
if (import.meta.env.DEV) console.error('Error loading roles:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to load roles',
|
||||
title: t('common.error'),
|
||||
description: t('permission.loadFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
@@ -116,13 +118,13 @@ export function PermissionEditor() {
|
||||
if (error) throw error;
|
||||
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Permissions updated successfully',
|
||||
title: t('common.success'),
|
||||
description: t('permission.saved'),
|
||||
});
|
||||
} catch {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to save permissions',
|
||||
title: t('common.error'),
|
||||
description: t('permission.saveFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
@@ -143,7 +145,7 @@ export function PermissionEditor() {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Shield className="h-5 w-5" />
|
||||
Permission Editor
|
||||
{t('permission.title')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -171,7 +173,7 @@ export function PermissionEditor() {
|
||||
{selectedRole.is_system && (
|
||||
<Badge variant="secondary" className="mt-2">
|
||||
<Lock className="h-3 w-3 mr-1" />
|
||||
System Role (Read Only)
|
||||
{t('permission.systemRole')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -183,7 +185,7 @@ export function PermissionEditor() {
|
||||
onClick={resetPermissions}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-1" />
|
||||
Reset
|
||||
{t('permission.reset')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -191,7 +193,7 @@ export function PermissionEditor() {
|
||||
disabled={saving}
|
||||
>
|
||||
<Save className="h-4 w-4 mr-1" />
|
||||
Save Changes
|
||||
{t('permission.saveChanges')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -200,7 +202,7 @@ export function PermissionEditor() {
|
||||
<div className="space-y-6">
|
||||
{Object.entries(PERMISSION_CATEGORIES).map(([categoryKey, category]) => (
|
||||
<div key={categoryKey} className="space-y-3">
|
||||
<h4 className="font-medium text-sm">{category.title}</h4>
|
||||
<h4 className="font-medium text-sm">{t(category.titleKey)}</h4>
|
||||
<div className="space-y-2">
|
||||
{Object.entries(category.permissions).map(([permKey, permName]) => {
|
||||
const fullPerm = `${categoryKey}.${permKey}`;
|
||||
@@ -214,7 +216,7 @@ export function PermissionEditor() {
|
||||
) : (
|
||||
<Lock className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
<span className="text-sm">{permName}</span>
|
||||
<span className="text-sm">{t(permName)}</span>
|
||||
</div>
|
||||
<Switch
|
||||
checked={isEnabled}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
@@ -25,6 +26,7 @@ interface AuditLog {
|
||||
}
|
||||
|
||||
export function SecurityAudit() {
|
||||
const { t } = useTranslation();
|
||||
const [metrics, setMetrics] = useState<SecurityMetrics>({
|
||||
totalUsers: 0,
|
||||
activeUsers: 0,
|
||||
@@ -106,25 +108,25 @@ export function SecurityAudit() {
|
||||
};
|
||||
|
||||
const getScoreBadge = (score: number) => {
|
||||
if (score >= 80) return { text: 'Excellent', variant: 'default' as const };
|
||||
if (score >= 60) return { text: 'Good', variant: 'secondary' as const };
|
||||
if (score >= 40) return { text: 'Fair', variant: 'outline' as const };
|
||||
return { text: 'Poor', variant: 'destructive' as const };
|
||||
if (score >= 80) return { text: t('securityAudit.excellent'), variant: 'default' as const };
|
||||
if (score >= 60) return { text: t('securityAudit.good'), variant: 'secondary' as const };
|
||||
if (score >= 40) return { text: t('securityAudit.fair'), variant: 'outline' as const };
|
||||
return { text: t('securityAudit.poor'), variant: 'destructive' as const };
|
||||
};
|
||||
|
||||
const pieData = [
|
||||
{ name: '2FA Enabled', value: metrics.twoFactorEnabled, color: '#10b981' },
|
||||
{ name: 'No 2FA', value: metrics.totalUsers - metrics.twoFactorEnabled, color: '#ef4444' },
|
||||
{ name: t('securityAudit.pieEnabled'), value: metrics.twoFactorEnabled, color: '#10b981' },
|
||||
{ name: t('securityAudit.pieNotEnabled'), value: metrics.totalUsers - metrics.twoFactorEnabled, color: '#ef4444' },
|
||||
];
|
||||
|
||||
const activityData = [
|
||||
{ name: 'Mon', logins: 45, failures: 2 },
|
||||
{ name: 'Tue', logins: 52, failures: 3 },
|
||||
{ name: 'Wed', logins: 48, failures: 1 },
|
||||
{ name: 'Thu', logins: 61, failures: 4 },
|
||||
{ name: 'Fri', logins: 55, failures: 2 },
|
||||
{ name: 'Sat', logins: 32, failures: 1 },
|
||||
{ name: 'Sun', logins: 28, failures: 0 },
|
||||
{ name: t('securityAudit.dayMon'), logins: 45, failures: 2 },
|
||||
{ name: t('securityAudit.dayTue'), logins: 52, failures: 3 },
|
||||
{ name: t('securityAudit.dayWed'), logins: 48, failures: 1 },
|
||||
{ name: t('securityAudit.dayThu'), logins: 61, failures: 4 },
|
||||
{ name: t('securityAudit.dayFri'), logins: 55, failures: 2 },
|
||||
{ name: t('securityAudit.daySat'), logins: 32, failures: 1 },
|
||||
{ name: t('securityAudit.daySun'), logins: 28, failures: 0 },
|
||||
];
|
||||
|
||||
const scoreBadge = getScoreBadge(metrics.securityScore);
|
||||
@@ -137,7 +139,7 @@ export function SecurityAudit() {
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<Shield className="h-5 w-5" />
|
||||
Security Score
|
||||
{t('securityAudit.scoreTitle')}
|
||||
</span>
|
||||
<Badge variant={scoreBadge.variant}>{scoreBadge.text}</Badge>
|
||||
</CardTitle>
|
||||
@@ -148,7 +150,7 @@ export function SecurityAudit() {
|
||||
<div className={`text-6xl font-bold ${getScoreColor(metrics.securityScore)}`}>
|
||||
{metrics.securityScore}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-2">Out of 100</p>
|
||||
<p className="text-sm text-muted-foreground mt-2">{t('securityAudit.outOf')}</p>
|
||||
</div>
|
||||
<Progress value={metrics.securityScore} className="h-3" />
|
||||
</div>
|
||||
@@ -161,7 +163,7 @@ export function SecurityAudit() {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Total Users</p>
|
||||
<p className="text-sm text-muted-foreground">{t('securityAudit.totalUsers')}</p>
|
||||
<p className="text-2xl font-bold">{metrics.totalUsers}</p>
|
||||
</div>
|
||||
<Users className="h-8 w-8 text-blue-500" />
|
||||
@@ -173,7 +175,7 @@ export function SecurityAudit() {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">2FA Enabled</p>
|
||||
<p className="text-sm text-muted-foreground">{t('securityAudit.twoFaEnabled')}</p>
|
||||
<p className="text-2xl font-bold">{metrics.twoFactorEnabled}</p>
|
||||
</div>
|
||||
<Key className="h-8 w-8 text-green-500" />
|
||||
@@ -185,7 +187,7 @@ export function SecurityAudit() {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Active Sessions</p>
|
||||
<p className="text-sm text-muted-foreground">{t('securityAudit.activeSessions')}</p>
|
||||
<p className="text-2xl font-bold">{metrics.activeUsers}</p>
|
||||
</div>
|
||||
<Activity className="h-8 w-8 text-purple-500" />
|
||||
@@ -197,7 +199,7 @@ export function SecurityAudit() {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Suspicious</p>
|
||||
<p className="text-sm text-muted-foreground">{t('securityAudit.suspicious')}</p>
|
||||
<p className="text-2xl font-bold">{metrics.suspiciousActivities}</p>
|
||||
</div>
|
||||
<AlertTriangle className="h-8 w-8 text-orange-500" />
|
||||
@@ -210,7 +212,7 @@ export function SecurityAudit() {
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Login Activity</CardTitle>
|
||||
<CardTitle>{t('securityAudit.loginActivity')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
@@ -228,7 +230,7 @@ export function SecurityAudit() {
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>2FA Adoption</CardTitle>
|
||||
<CardTitle>{t('securityAudit.twoFaAdoption')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
@@ -257,7 +259,7 @@ export function SecurityAudit() {
|
||||
{/* Recent Security Events */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Security Events</CardTitle>
|
||||
<CardTitle>{t('securityAudit.recentEvents')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
@@ -270,7 +272,7 @@ export function SecurityAudit() {
|
||||
{log.severity === 'low' && <CheckCircle className="h-5 w-5 text-green-500" />}
|
||||
<div>
|
||||
<p className="font-medium">{log.action}</p>
|
||||
<p className="text-sm text-muted-foreground">IP: {log.ip_address}</p>
|
||||
<p className="text-sm text-muted-foreground">{t('securityAudit.ip')}: {log.ip_address}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -22,6 +23,7 @@ interface Session {
|
||||
}
|
||||
|
||||
export function SessionMonitor() {
|
||||
const { t } = useTranslation();
|
||||
const [sessions, setSessions] = useState<Session[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { toast } = useToast();
|
||||
@@ -61,32 +63,32 @@ export function SessionMonitor() {
|
||||
if (error) throw error;
|
||||
|
||||
toast({
|
||||
title: 'Session Terminated',
|
||||
description: 'The session has been successfully terminated.',
|
||||
title: t('sessionMonitor.terminated'),
|
||||
description: t('sessionMonitor.terminatedDesc'),
|
||||
});
|
||||
loadSessions();
|
||||
} catch {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to terminate session',
|
||||
title: t('common.error'),
|
||||
description: t('sessionMonitor.terminateFailed'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getDeviceInfo = (userAgent: string) => {
|
||||
if (userAgent.includes('Mobile')) return 'Mobile';
|
||||
if (userAgent.includes('Tablet')) return 'Tablet';
|
||||
return 'Desktop';
|
||||
if (userAgent.includes('Mobile')) return t('sessionMonitor.mobile');
|
||||
if (userAgent.includes('Tablet')) return t('sessionMonitor.tablet');
|
||||
return t('sessionMonitor.desktop');
|
||||
};
|
||||
|
||||
const getActivityStatus = (lastActivity: string) => {
|
||||
const diff = Date.now() - new Date(lastActivity).getTime();
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
|
||||
if (minutes < 5) return { text: 'Active', variant: 'default' as const };
|
||||
if (minutes < 30) return { text: 'Idle', variant: 'secondary' as const };
|
||||
return { text: 'Inactive', variant: 'outline' as const };
|
||||
if (minutes < 5) return { text: t('sessionMonitor.active'), variant: 'default' as const };
|
||||
if (minutes < 30) return { text: t('sessionMonitor.idle'), variant: 'secondary' as const };
|
||||
return { text: t('sessionMonitor.inactive'), variant: 'outline' as const };
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -94,7 +96,7 @@ export function SessionMonitor() {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Monitor className="h-5 w-5" />
|
||||
Active Sessions
|
||||
{t('sessionMonitor.title')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -114,15 +116,15 @@ export function SessionMonitor() {
|
||||
{session.is_active && (
|
||||
<Badge variant="default">
|
||||
<Shield className="h-3 w-3 mr-1" />
|
||||
Active
|
||||
{t('sessionMonitor.active')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground space-y-1">
|
||||
<p>IP: {session.ip_address}</p>
|
||||
<p>Device: {getDeviceInfo(session.user_agent)}</p>
|
||||
<p>Started: {format(new Date(session.created_at), 'PPp')}</p>
|
||||
<p>Last Activity: {format(new Date(session.last_activity), 'PPp')}</p>
|
||||
<p>{t('sessionMonitor.ip')}: {session.ip_address}</p>
|
||||
<p>{t('sessionMonitor.device')}: {getDeviceInfo(session.user_agent)}</p>
|
||||
<p>{t('sessionMonitor.started')}: {format(new Date(session.created_at), 'PPp')}</p>
|
||||
<p>{t('sessionMonitor.lastActivity')}: {format(new Date(session.last_activity), 'PPp')}</p>
|
||||
</div>
|
||||
</div>
|
||||
{session.is_active && (
|
||||
@@ -132,7 +134,7 @@ export function SessionMonitor() {
|
||||
onClick={() => terminateSession(session.id)}
|
||||
>
|
||||
<LogOut className="h-4 w-4 mr-1" />
|
||||
Terminate
|
||||
{t('sessionMonitor.terminate')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -142,7 +144,7 @@ export function SessionMonitor() {
|
||||
{sessions.length === 0 && !loading && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<Monitor className="h-12 w-12 mx-auto mb-2 opacity-50" />
|
||||
<p>No active sessions</p>
|
||||
<p>{t('sessionMonitor.empty')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
@@ -13,6 +14,7 @@ interface PoolCategorySelectorProps {
|
||||
const POOL_CATEGORIES = Object.values(ValidatorPoolCategory);
|
||||
|
||||
export function PoolCategorySelector({ currentCategory, onCategoryChange, disabled }: PoolCategorySelectorProps) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedCategory, setSelectedCategory] = useState<ValidatorPoolCategory>(currentCategory || POOL_CATEGORIES[0]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
@@ -23,7 +25,7 @@ export function PoolCategorySelector({ currentCategory, onCategoryChange, disabl
|
||||
<div className="space-y-4">
|
||||
<Select value={selectedCategory} onValueChange={(value) => setSelectedCategory(value as ValidatorPoolCategory)} disabled={disabled}>
|
||||
<SelectTrigger className="w-full bg-gray-800 border-gray-700 text-white">
|
||||
<SelectValue placeholder="Select a pool category..." />
|
||||
<SelectValue placeholder={t('poolCategory.placeholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{POOL_CATEGORIES.map(cat => (
|
||||
@@ -33,7 +35,7 @@ export function PoolCategorySelector({ currentCategory, onCategoryChange, disabl
|
||||
</Select>
|
||||
<Button onClick={handleSubmit} disabled={disabled || !selectedCategory} className="w-full">
|
||||
{disabled && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{currentCategory ? 'Switch Category' : 'Join Pool'}
|
||||
{currentCategory ? t('poolCategory.switchCategory') : t('poolCategory.joinPool')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { AlertCircle, CheckCircle2 } from 'lucide-react';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { web3FromAddress, web3Enable } from '@pezkuwi/extension-dapp';
|
||||
import {
|
||||
getStakingInfo,
|
||||
@@ -46,6 +47,7 @@ async function getInjectorSigner(address: string) {
|
||||
export const StakingDashboard: React.FC = () => {
|
||||
const { assetHubApi, peopleApi, selectedAccount, isAssetHubReady, isPeopleReady } = usePezkuwi();
|
||||
const { balances, refreshBalances } = useWallet();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [stakingInfo, setStakingInfo] = useState<StakingInfo | null>(null);
|
||||
const [validators, setValidators] = useState<string[]>([]);
|
||||
@@ -92,7 +94,7 @@ export const StakingDashboard: React.FC = () => {
|
||||
}
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Failed to fetch staking data:', error);
|
||||
toast.error('Failed to fetch staking information');
|
||||
toast.error(t('staking.fetchError'));
|
||||
} finally {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
@@ -139,11 +141,11 @@ export const StakingDashboard: React.FC = () => {
|
||||
}
|
||||
}, 3000);
|
||||
} else {
|
||||
toast.error(result.error || 'Failed to record trust score');
|
||||
toast.error(result.error || t('staking.recordFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Record trust score failed:', error);
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to record trust score');
|
||||
toast.error(error instanceof Error ? error.message : t('staking.recordFailed'));
|
||||
} finally {
|
||||
setIsRecordingScore(false);
|
||||
}
|
||||
@@ -169,11 +171,11 @@ export const StakingDashboard: React.FC = () => {
|
||||
}
|
||||
}, 3000);
|
||||
} else {
|
||||
toast.error(result.error || 'Failed to claim reward');
|
||||
toast.error(result.error || t('staking.claimFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Claim reward failed:', error);
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to claim reward');
|
||||
toast.error(error instanceof Error ? error.message : t('staking.claimFailed'));
|
||||
} finally {
|
||||
setIsClaimingReward(false);
|
||||
}
|
||||
@@ -188,11 +190,11 @@ export const StakingDashboard: React.FC = () => {
|
||||
|
||||
// Validate
|
||||
if (parseFloat(bondAmount) < parseFloat(minNominatorBond)) {
|
||||
throw new Error(`Minimum bond is ${minNominatorBond} HEZ`);
|
||||
throw new Error(t('staking.minBondError', { amount: minNominatorBond }));
|
||||
}
|
||||
|
||||
if (parseFloat(bondAmount) > parseFloat(balances.HEZ)) {
|
||||
throw new Error('Insufficient HEZ balance');
|
||||
throw new Error(t('staking.insufficientHez'));
|
||||
}
|
||||
|
||||
const injector = await getInjectorSigner(selectedAccount.address);
|
||||
@@ -242,7 +244,7 @@ export const StakingDashboard: React.FC = () => {
|
||||
if (!assetHubApi || !selectedAccount || selectedValidators.length === 0) return;
|
||||
|
||||
if (!stakingInfo || parseFloat(stakingInfo.bonded) === 0) {
|
||||
toast.error('You must bond tokens before nominating validators');
|
||||
toast.error(t('staking.bondBeforeNominate'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -288,7 +290,7 @@ export const StakingDashboard: React.FC = () => {
|
||||
const amount = parseAmount(unbondAmount);
|
||||
|
||||
if (!stakingInfo || parseFloat(unbondAmount) > parseFloat(stakingInfo.active)) {
|
||||
throw new Error('Insufficient staked amount');
|
||||
throw new Error(t('staking.insufficientStaked'));
|
||||
}
|
||||
|
||||
const injector = await getInjectorSigner(selectedAccount.address);
|
||||
@@ -329,7 +331,7 @@ export const StakingDashboard: React.FC = () => {
|
||||
if (!assetHubApi || !selectedAccount) return;
|
||||
|
||||
if (!stakingInfo || parseFloat(stakingInfo.redeemable) === 0) {
|
||||
toast.info('No tokens available to withdraw');
|
||||
toast.info(t('staking.noWithdraw'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -346,7 +348,7 @@ export const StakingDashboard: React.FC = () => {
|
||||
({ status, dispatchError }) => {
|
||||
if (status.isInBlock) {
|
||||
if (dispatchError) {
|
||||
let errorMessage = 'Withdrawal failed';
|
||||
let errorMessage = t('staking.withdrawFailed');
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule);
|
||||
errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
|
||||
@@ -354,7 +356,7 @@ export const StakingDashboard: React.FC = () => {
|
||||
toast.error(errorMessage);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
toast.success(`Withdrew ${stakingInfo.redeemable} HEZ`);
|
||||
toast.success(t('staking.withdrawSuccess', { amount: stakingInfo.redeemable }));
|
||||
refreshBalances();
|
||||
setTimeout(() => {
|
||||
if (assetHubApi && selectedAccount) {
|
||||
@@ -388,7 +390,7 @@ export const StakingDashboard: React.FC = () => {
|
||||
({ status, dispatchError }) => {
|
||||
if (status.isInBlock) {
|
||||
if (dispatchError) {
|
||||
let errorMessage = 'Failed to start score tracking';
|
||||
let errorMessage = t('staking.scoreTrackingFailed');
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = peopleApi.registry.findMetaError(dispatchError.asModule);
|
||||
errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
|
||||
@@ -396,7 +398,7 @@ export const StakingDashboard: React.FC = () => {
|
||||
toast.error(errorMessage);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
toast.success('Score tracking started successfully! Your staking score will now accumulate over time.');
|
||||
toast.success(t('staking.scoreTrackingSuccess'));
|
||||
// Refresh staking data after a delay
|
||||
setTimeout(() => {
|
||||
if (assetHubApi && selectedAccount) {
|
||||
@@ -410,7 +412,7 @@ export const StakingDashboard: React.FC = () => {
|
||||
);
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Start score tracking failed:', error);
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to start score tracking');
|
||||
toast.error(error instanceof Error ? error.message : t('staking.scoreTrackingFailed'));
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
@@ -422,7 +424,7 @@ export const StakingDashboard: React.FC = () => {
|
||||
} else {
|
||||
// Max 16 nominations
|
||||
if (prev.length >= 16) {
|
||||
toast.info('Maximum 16 validators can be nominated');
|
||||
toast.info(t('staking.maxValidators'));
|
||||
return prev;
|
||||
}
|
||||
return [...prev, validator];
|
||||
@@ -431,7 +433,7 @@ export const StakingDashboard: React.FC = () => {
|
||||
};
|
||||
|
||||
if (isLoadingData) {
|
||||
return <LoadingState message="Loading staking data..." />;
|
||||
return <LoadingState message={t('staking.loadingData')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -440,35 +442,35 @@ export const StakingDashboard: React.FC = () => {
|
||||
<div className={`grid grid-cols-1 md:grid-cols-2 ${pezRewards ? 'lg:grid-cols-5' : 'lg:grid-cols-4'} gap-4`}>
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-400">Total Bonded</CardTitle>
|
||||
<CardTitle className="text-sm text-gray-400">{t('staking.totalBonded')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-white">
|
||||
{stakingInfo?.bonded || '0'} HEZ
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Active: {stakingInfo?.active || '0'} HEZ
|
||||
{t('staking.activeAmount', { amount: stakingInfo?.active || '0' })}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-400">Unlocking</CardTitle>
|
||||
<CardTitle className="text-sm text-gray-400">{t('staking.unlocking')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-yellow-500">
|
||||
{stakingInfo?.unlocking.reduce((sum, u) => sum + parseFloat(u.amount), 0).toFixed(2) || '0'} HEZ
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{stakingInfo?.unlocking.length || 0} chunk(s)
|
||||
{t('staking.chunks', { count: stakingInfo?.unlocking.length || 0 })}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-400">Redeemable</CardTitle>
|
||||
<CardTitle className="text-sm text-gray-400">{t('staking.redeemable')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-500">
|
||||
@@ -480,14 +482,14 @@ export const StakingDashboard: React.FC = () => {
|
||||
disabled={!stakingInfo || parseFloat(stakingInfo.redeemable) === 0 || isLoading}
|
||||
className="mt-2 w-full"
|
||||
>
|
||||
Withdraw
|
||||
{t('staking.withdraw')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-400">Staking Score</CardTitle>
|
||||
<CardTitle className="text-sm text-gray-400">{t('staking.stakingScore')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{stakingInfo?.hasStartedScoreTracking ? (
|
||||
@@ -497,29 +499,29 @@ export const StakingDashboard: React.FC = () => {
|
||||
{stakingInfo.stakingScore}/100
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Duration: {stakingInfo.stakingDuration
|
||||
? `${Math.floor(stakingInfo.stakingDuration / (24 * 60 * 10))} days`
|
||||
: '0 days'}
|
||||
{t('staking.scoreDuration', { days: stakingInfo.stakingDuration
|
||||
? Math.floor(stakingInfo.stakingDuration / (24 * 60 * 10))
|
||||
: 0 })}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-lg font-bold text-yellow-500">Waiting for data...</div>
|
||||
<div className="text-lg font-bold text-yellow-500">{t('staking.waitingData')}</div>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Score tracking started. A noter will submit your staking data soon.
|
||||
{t('staking.scoreTrackingStartedInfo')}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<div className="text-2xl font-bold text-gray-500">Not Started</div>
|
||||
<div className="text-2xl font-bold text-gray-500">{t('staking.notStarted')}</div>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleStartScoreTracking}
|
||||
disabled={!stakingInfo || isLoading}
|
||||
className="mt-2 w-full bg-purple-600 hover:bg-purple-700"
|
||||
>
|
||||
Start Score Tracking
|
||||
{t('staking.startScoreTracking')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -531,7 +533,7 @@ export const StakingDashboard: React.FC = () => {
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-sm text-gray-400">PEZ Rewards</CardTitle>
|
||||
<CardTitle className="text-sm text-gray-400">{t('staking.pezRewards')}</CardTitle>
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${
|
||||
pezRewards.epochStatus === 'Open'
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
@@ -539,22 +541,22 @@ export const StakingDashboard: React.FC = () => {
|
||||
? 'bg-orange-500/20 text-orange-400'
|
||||
: 'bg-gray-500/20 text-gray-400'
|
||||
}`}>
|
||||
{pezRewards.epochStatus === 'Open' ? 'Open' : pezRewards.epochStatus === 'ClaimPeriod' ? 'Claim Period' : 'Closed'}
|
||||
{pezRewards.epochStatus === 'Open' ? t('staking.epochOpen') : pezRewards.epochStatus === 'ClaimPeriod' ? t('staking.epochClaimPeriod') : t('staking.epochClosed')}
|
||||
</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-gray-500">Epoch {pezRewards.currentEpoch}</p>
|
||||
<p className="text-xs text-gray-500">{t('staking.epoch', { epoch: pezRewards.currentEpoch })}</p>
|
||||
|
||||
{/* Open epoch: Record score or show recorded score */}
|
||||
{pezRewards.epochStatus === 'Open' && (
|
||||
pezRewards.hasRecordedThisEpoch ? (
|
||||
<div>
|
||||
<div className="text-lg font-bold text-green-400">
|
||||
Score: {pezRewards.userScoreCurrentEpoch}
|
||||
{t('staking.scoreLabel', { score: pezRewards.userScoreCurrentEpoch })}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">Recorded for this epoch</p>
|
||||
<p className="text-xs text-gray-500">{t('staking.recordedForEpoch')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
@@ -563,7 +565,7 @@ export const StakingDashboard: React.FC = () => {
|
||||
disabled={isRecordingScore}
|
||||
className="w-full bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
{isRecordingScore ? 'Recording...' : 'Record Trust Score'}
|
||||
{isRecordingScore ? t('staking.recording') : t('staking.recordTrustScore')}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
@@ -577,7 +579,7 @@ export const StakingDashboard: React.FC = () => {
|
||||
<div className="space-y-1">
|
||||
{pezRewards.claimableRewards.map((reward) => (
|
||||
<div key={reward.epoch} className="flex items-center justify-between">
|
||||
<span className="text-xs text-gray-400">Epoch {reward.epoch}: {reward.amount} PEZ</span>
|
||||
<span className="text-xs text-gray-400">{t('staking.epochReward', { epoch: reward.epoch, amount: reward.amount })}</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@@ -585,7 +587,7 @@ export const StakingDashboard: React.FC = () => {
|
||||
disabled={isClaimingReward}
|
||||
className="h-6 text-xs px-2 border-orange-500 text-orange-400 hover:bg-orange-500/20"
|
||||
>
|
||||
{isClaimingReward ? '...' : 'Claim'}
|
||||
{isClaimingReward ? '...' : t('staking.claim')}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
@@ -601,18 +603,18 @@ export const StakingDashboard: React.FC = () => {
|
||||
{/* Main Staking Interface */}
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl text-white">Staking</CardTitle>
|
||||
<CardTitle className="text-xl text-white">{t('staking.title')}</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
Stake HEZ to secure the network and earn rewards.
|
||||
{t('staking.description')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="stake">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="stake">Stake</TabsTrigger>
|
||||
<TabsTrigger value="nominate">Nominate</TabsTrigger>
|
||||
<TabsTrigger value="pool">Validator Pool</TabsTrigger>
|
||||
<TabsTrigger value="unstake">Unstake</TabsTrigger>
|
||||
<TabsTrigger value="stake">{t('staking.tabStake')}</TabsTrigger>
|
||||
<TabsTrigger value="nominate">{t('staking.tabNominate')}</TabsTrigger>
|
||||
<TabsTrigger value="pool">{t('staking.tabPool')}</TabsTrigger>
|
||||
<TabsTrigger value="unstake">{t('staking.tabUnstake')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* STAKE TAB */}
|
||||
@@ -620,12 +622,12 @@ export const StakingDashboard: React.FC = () => {
|
||||
<Alert className="bg-blue-900/20 border-blue-500">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription className="text-sm">
|
||||
Minimum bond: {minNominatorBond} HEZ. Bonded tokens are locked and earn rewards when nominated validators produce blocks.
|
||||
{t('staking.minBondInfo', { amount: minNominatorBond })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div>
|
||||
<Label>Amount to Bond (HEZ)</Label>
|
||||
<Label>{t('staking.amountToBond')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={`Min: ${minNominatorBond}`}
|
||||
@@ -635,12 +637,12 @@ export const StakingDashboard: React.FC = () => {
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<div className="flex justify-between mt-1 text-xs text-gray-400">
|
||||
<span>Available: {balances.HEZ} HEZ</span>
|
||||
<span>{t('staking.available', { amount: balances.HEZ })}</span>
|
||||
<button
|
||||
onClick={() => setBondAmount(balances.HEZ)}
|
||||
className="text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
Max
|
||||
{t('staking.max')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -650,7 +652,7 @@ export const StakingDashboard: React.FC = () => {
|
||||
disabled={isLoading || !bondAmount || parseFloat(bondAmount) < parseFloat(minNominatorBond)}
|
||||
className="w-full bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
{stakingInfo && parseFloat(stakingInfo.bonded) > 0 ? 'Bond Additional' : 'Bond Tokens'}
|
||||
{stakingInfo && parseFloat(stakingInfo.bonded) > 0 ? t('staking.bondAdditional') : t('staking.bondTokens')}
|
||||
</Button>
|
||||
</TabsContent>
|
||||
|
||||
@@ -659,13 +661,13 @@ export const StakingDashboard: React.FC = () => {
|
||||
<Alert className="bg-purple-900/20 border-purple-500">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription className="text-sm">
|
||||
Select up to 16 validators to nominate. Your stake will be distributed to active validators.
|
||||
{stakingInfo && parseFloat(stakingInfo.bonded) === 0 && ' You must bond tokens first.'}
|
||||
{t('staking.nominateInfo')}
|
||||
{stakingInfo && parseFloat(stakingInfo.bonded) === 0 && ` ${t('staking.bondFirst')}`}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Active Validators ({validators.length})</Label>
|
||||
<Label>{t('staking.activeValidators', { count: validators.length })}</Label>
|
||||
<div className="max-h-64 overflow-y-auto space-y-2 border border-gray-700 rounded-lg p-3 bg-gray-800">
|
||||
{validators.map((validator) => (
|
||||
<div
|
||||
@@ -687,7 +689,7 @@ export const StakingDashboard: React.FC = () => {
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-gray-400">
|
||||
Selected: {selectedValidators.length}/16
|
||||
{t('staking.selected', { count: selectedValidators.length })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -696,7 +698,7 @@ export const StakingDashboard: React.FC = () => {
|
||||
disabled={isLoading || selectedValidators.length === 0 || !stakingInfo || parseFloat(stakingInfo.bonded) === 0}
|
||||
className="w-full bg-purple-600 hover:bg-purple-700"
|
||||
>
|
||||
Nominate Validators
|
||||
{t('staking.nominateValidators')}
|
||||
</Button>
|
||||
</TabsContent>
|
||||
|
||||
@@ -710,12 +712,12 @@ export const StakingDashboard: React.FC = () => {
|
||||
<Alert className="bg-yellow-900/20 border-yellow-500">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription className="text-sm">
|
||||
Unbonded tokens will be locked for {bondingDuration} eras (~{Math.floor(bondingDuration / 4)} days) before withdrawal.
|
||||
{t('staking.unbondInfo', { eras: bondingDuration, days: Math.floor(bondingDuration / 4) })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div>
|
||||
<Label>Amount to Unbond (HEZ)</Label>
|
||||
<Label>{t('staking.amountToUnbond')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={`Max: ${stakingInfo?.active || '0'}`}
|
||||
@@ -725,24 +727,24 @@ export const StakingDashboard: React.FC = () => {
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<div className="flex justify-between mt-1 text-xs text-gray-400">
|
||||
<span>Staked: {stakingInfo?.active || '0'} HEZ</span>
|
||||
<span>{t('staking.staked', { amount: stakingInfo?.active || '0' })}</span>
|
||||
<button
|
||||
onClick={() => setUnbondAmount(stakingInfo?.active || '0')}
|
||||
className="text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
Max
|
||||
{t('staking.max')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{stakingInfo && stakingInfo.unlocking.length > 0 && (
|
||||
<div className="bg-gray-800 rounded-lg p-3 space-y-2">
|
||||
<Label className="text-sm">Unlocking Chunks</Label>
|
||||
<Label className="text-sm">{t('staking.unlockingChunks')}</Label>
|
||||
{stakingInfo.unlocking.map((chunk, i) => (
|
||||
<div key={i} className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">{chunk.amount} HEZ</span>
|
||||
<span className="text-gray-500">
|
||||
Era {chunk.era} ({chunk.blocksRemaining > 0 ? `~${Math.floor(chunk.blocksRemaining / 600)} blocks` : 'Ready'})
|
||||
{t('staking.eraInfo', { era: chunk.era })} ({chunk.blocksRemaining > 0 ? t('staking.blocksRemaining', { blocks: Math.floor(chunk.blocksRemaining / 600) }) : t('staking.ready')})
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -755,7 +757,7 @@ export const StakingDashboard: React.FC = () => {
|
||||
className="w-full bg-red-600 hover:bg-red-700"
|
||||
variant="destructive"
|
||||
>
|
||||
Unbond Tokens
|
||||
{t('staking.unbondTokens')}
|
||||
</Button>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
@@ -16,6 +17,7 @@ import { Loader2, Users, UserCheck, UserX } from 'lucide-react';
|
||||
import { PoolCategorySelector } from './PoolCategorySelector';
|
||||
|
||||
export function ValidatorPoolDashboard() {
|
||||
const { t } = useTranslation();
|
||||
const { api, selectedAccount } = usePezkuwi();
|
||||
const [poolMember, setPoolMember] = useState<ValidatorPoolCategory | null>(null);
|
||||
const [poolSize, setPoolSize] = useState(0);
|
||||
@@ -49,7 +51,7 @@ export function ValidatorPoolDashboard() {
|
||||
}
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Failed to fetch validator pool data:', error);
|
||||
toast.error('Failed to fetch pool data');
|
||||
toast.error(t('validatorPool.fetchError'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -64,7 +66,7 @@ export function ValidatorPoolDashboard() {
|
||||
setActionLoading(true);
|
||||
try {
|
||||
await joinValidatorPool(api, selectedAccount, category);
|
||||
toast.success(`Joined the ${category} pool`);
|
||||
toast.success(t('validatorPool.joinedPool', { category }));
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Join pool error:', error);
|
||||
@@ -79,7 +81,7 @@ export function ValidatorPoolDashboard() {
|
||||
setActionLoading(true);
|
||||
try {
|
||||
await leaveValidatorPool(api, selectedAccount);
|
||||
toast.success('Left the validator pool');
|
||||
toast.success(t('validatorPool.leftPool'));
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Leave pool error:', error);
|
||||
@@ -94,7 +96,7 @@ export function ValidatorPoolDashboard() {
|
||||
setActionLoading(true);
|
||||
try {
|
||||
await updateValidatorCategory(api, selectedAccount, newCategory);
|
||||
toast.success(`Switched to ${newCategory}`);
|
||||
toast.success(t('validatorPool.switchedCategory', { category: newCategory }));
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Switch category error:', error);
|
||||
@@ -109,7 +111,7 @@ export function ValidatorPoolDashboard() {
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="p-12 text-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-green-500 mx-auto mb-4" />
|
||||
<p className="text-gray-400">Loading validator pool info...</p>
|
||||
<p className="text-gray-400">{t('validatorPool.loading')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -118,22 +120,22 @@ export function ValidatorPoolDashboard() {
|
||||
return (
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Validator Pool</CardTitle>
|
||||
<CardDescription>Join a pool to support the network and earn rewards.</CardDescription>
|
||||
<CardTitle className="text-white">{t('validatorPool.title')}</CardTitle>
|
||||
<CardDescription>{t('validatorPool.description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-gray-800 rounded-lg">
|
||||
<div className="flex items-center text-gray-400 mb-2">
|
||||
<Users className="w-4 h-4 mr-2" />
|
||||
Pool Size
|
||||
{t('validatorPool.poolSize')}
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-white">{poolSize}</div>
|
||||
</div>
|
||||
<div className="p-4 bg-gray-800 rounded-lg">
|
||||
<div className="flex items-center text-gray-400 mb-2">
|
||||
<UserCheck className="w-4 h-4 mr-2" />
|
||||
Active Validators
|
||||
{t('validatorPool.activeValidators')}
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-white">{validatorCount}</div>
|
||||
</div>
|
||||
@@ -144,7 +146,7 @@ export function ValidatorPoolDashboard() {
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||
<p className="text-green-400">
|
||||
You are in the <span className="font-bold">{poolMember}</span> pool
|
||||
{t('validatorPool.inPool', { category: poolMember })}
|
||||
</p>
|
||||
</div>
|
||||
<PoolCategorySelector
|
||||
@@ -163,13 +165,13 @@ export function ValidatorPoolDashboard() {
|
||||
) : (
|
||||
<UserX className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Leave Pool
|
||||
{t('validatorPool.leavePool')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
|
||||
<p className="text-yellow-400">You are not currently in a validator pool</p>
|
||||
<p className="text-yellow-400">{t('validatorPool.notInPool')}</p>
|
||||
</div>
|
||||
<PoolCategorySelector
|
||||
onCategoryChange={handleJoin}
|
||||
@@ -179,7 +181,7 @@ export function ValidatorPoolDashboard() {
|
||||
)
|
||||
) : (
|
||||
<div className="p-6 text-center">
|
||||
<p className="text-gray-400">Please connect your wallet to manage pool membership</p>
|
||||
<p className="text-gray-400">{t('validatorPool.connectWallet')}</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { XAxis, YAxis, Tooltip, ResponsiveContainer, Area, AreaChart } from 'recharts';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
@@ -18,6 +19,7 @@ const getDisplayName = (token: string): string => {
|
||||
};
|
||||
|
||||
export const PriceChart: React.FC<PriceChartProps> = ({ fromToken, toToken, currentPrice }) => {
|
||||
const { t } = useTranslation();
|
||||
const [timeframe, setTimeframe] = useState<'1H' | '24H' | '7D' | '30D'>('24H');
|
||||
const [chartData, setChartData] = useState<Array<Record<string, number>>>([]);
|
||||
const [priceChange, setPriceChange] = useState<{ value: number; percent: number }>({ value: 0, percent: 0 });
|
||||
@@ -90,7 +92,7 @@ export const PriceChart: React.FC<PriceChartProps> = ({ fromToken, toToken, curr
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<div className="text-sm text-gray-400 mb-1">
|
||||
{getDisplayName(fromToken)}/{getDisplayName(toToken)} Price
|
||||
{t('priceChart.label', { from: getDisplayName(fromToken), to: getDisplayName(toToken) })}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl font-bold text-white">
|
||||
@@ -107,10 +109,10 @@ export const PriceChart: React.FC<PriceChartProps> = ({ fromToken, toToken, curr
|
||||
|
||||
<Tabs value={timeframe} onValueChange={(v) => setTimeframe(v as Record<string, unknown>)}>
|
||||
<TabsList className="bg-gray-800">
|
||||
<TabsTrigger value="1H" className="text-xs">1H</TabsTrigger>
|
||||
<TabsTrigger value="24H" className="text-xs">24H</TabsTrigger>
|
||||
<TabsTrigger value="7D" className="text-xs">7D</TabsTrigger>
|
||||
<TabsTrigger value="30D" className="text-xs">30D</TabsTrigger>
|
||||
<TabsTrigger value="1H" className="text-xs">{t('priceChart.1h')}</TabsTrigger>
|
||||
<TabsTrigger value="24H" className="text-xs">{t('priceChart.24h')}</TabsTrigger>
|
||||
<TabsTrigger value="7D" className="text-xs">{t('priceChart.7d')}</TabsTrigger>
|
||||
<TabsTrigger value="30D" className="text-xs">{t('priceChart.30d')}</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
@@ -160,7 +162,7 @@ export const PriceChart: React.FC<PriceChartProps> = ({ fromToken, toToken, curr
|
||||
</ResponsiveContainer>
|
||||
|
||||
<div className="mt-3 text-xs text-gray-500 text-center">
|
||||
Historical price data • Updated in real-time
|
||||
{t('priceChart.footnote')}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -6,14 +7,14 @@ import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Plus,
|
||||
Trash2,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
import {
|
||||
Plus,
|
||||
Trash2,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
AlertCircle
|
||||
} from 'lucide-react';
|
||||
|
||||
@@ -34,6 +35,7 @@ interface Milestone {
|
||||
}
|
||||
|
||||
export const FundingProposal: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [proposalTitle, setProposalTitle] = useState('');
|
||||
const [proposalDescription, setProposalDescription] = useState('');
|
||||
const [category, setCategory] = useState('');
|
||||
@@ -92,42 +94,42 @@ export const FundingProposal: React.FC = () => {
|
||||
{/* Proposal Header */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Create Funding Proposal</CardTitle>
|
||||
<CardDescription>Submit a detailed budget request for treasury funding</CardDescription>
|
||||
<CardTitle>{t('funding.createTitle')}</CardTitle>
|
||||
<CardDescription>{t('funding.createDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">Proposal Title</Label>
|
||||
<Label htmlFor="title">{t('funding.proposalTitle')}</Label>
|
||||
<Input
|
||||
id="title"
|
||||
placeholder="Enter a clear, descriptive title"
|
||||
placeholder={t('funding.titlePlaceholder')}
|
||||
value={proposalTitle}
|
||||
onChange={(e) => setProposalTitle(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">Category</Label>
|
||||
<Label htmlFor="category">{t('funding.category')}</Label>
|
||||
<Select value={category} onValueChange={setCategory}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select category" />
|
||||
<SelectValue placeholder={t('funding.selectCategory')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="development">Development</SelectItem>
|
||||
<SelectItem value="marketing">Marketing</SelectItem>
|
||||
<SelectItem value="operations">Operations</SelectItem>
|
||||
<SelectItem value="community">Community</SelectItem>
|
||||
<SelectItem value="research">Research</SelectItem>
|
||||
<SelectItem value="infrastructure">Infrastructure</SelectItem>
|
||||
<SelectItem value="development">{t('funding.catDevelopment')}</SelectItem>
|
||||
<SelectItem value="marketing">{t('funding.catMarketing')}</SelectItem>
|
||||
<SelectItem value="operations">{t('funding.catOperations')}</SelectItem>
|
||||
<SelectItem value="community">{t('funding.catCommunity')}</SelectItem>
|
||||
<SelectItem value="research">{t('funding.catResearch')}</SelectItem>
|
||||
<SelectItem value="infrastructure">{t('funding.catInfrastructure')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Label htmlFor="description">{t('funding.description')}</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="Provide a detailed description of the proposal"
|
||||
placeholder={t('funding.descPlaceholder')}
|
||||
rows={4}
|
||||
value={proposalDescription}
|
||||
onChange={(e) => setProposalDescription(e.target.value)}
|
||||
@@ -140,9 +142,9 @@ export const FundingProposal: React.FC = () => {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span>Budget Breakdown</span>
|
||||
<span>{t('funding.budgetBreakdown')}</span>
|
||||
<Badge variant="outline" className="text-lg px-3 py-1">
|
||||
Total: ${totalBudget.toLocaleString()}
|
||||
{t('funding.total', { amount: totalBudget.toLocaleString() })}
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -150,7 +152,7 @@ export const FundingProposal: React.FC = () => {
|
||||
{budgetItems.map((item, index) => (
|
||||
<div key={item.id} className="p-4 border rounded-lg space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">Item {index + 1}</span>
|
||||
<span className="font-medium">{t('funding.item', { index: index + 1 })}</span>
|
||||
{budgetItems.length > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -164,16 +166,16 @@ export const FundingProposal: React.FC = () => {
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label>Description</Label>
|
||||
<Label>{t('funding.itemDesc')}</Label>
|
||||
<Input
|
||||
placeholder="Budget item description"
|
||||
placeholder={t('funding.itemDescPlaceholder')}
|
||||
value={item.description}
|
||||
onChange={(e) => updateBudgetItem(item.id, 'description', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Amount ($)</Label>
|
||||
<Label>{t('funding.amountUsd')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="0"
|
||||
@@ -184,9 +186,9 @@ export const FundingProposal: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Justification</Label>
|
||||
<Label>{t('funding.justification')}</Label>
|
||||
<Textarea
|
||||
placeholder="Explain why this expense is necessary"
|
||||
placeholder={t('funding.justificationPlaceholder')}
|
||||
rows={2}
|
||||
value={item.justification}
|
||||
onChange={(e) => updateBudgetItem(item.id, 'justification', e.target.value)}
|
||||
@@ -197,7 +199,7 @@ export const FundingProposal: React.FC = () => {
|
||||
|
||||
<Button onClick={addBudgetItem} variant="outline" className="w-full">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Budget Item
|
||||
{t('funding.addBudgetItem')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -205,14 +207,14 @@ export const FundingProposal: React.FC = () => {
|
||||
{/* Milestones */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Milestones & Deliverables</CardTitle>
|
||||
<CardDescription>Define clear milestones with payment schedule</CardDescription>
|
||||
<CardTitle>{t('funding.milestones')}</CardTitle>
|
||||
<CardDescription>{t('funding.milestonesDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{milestones.map((milestone, index) => (
|
||||
<div key={milestone.id} className="p-4 border rounded-lg space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">Milestone {index + 1}</span>
|
||||
<span className="font-medium">{t('funding.milestone', { index: index + 1 })}</span>
|
||||
{milestones.length > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -226,16 +228,16 @@ export const FundingProposal: React.FC = () => {
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label>Title</Label>
|
||||
<Label>{t('funding.milestoneTitle')}</Label>
|
||||
<Input
|
||||
placeholder="Milestone title"
|
||||
placeholder={t('funding.milestoneTitlePlaceholder')}
|
||||
value={milestone.title}
|
||||
onChange={(e) => updateMilestone(milestone.id, 'title', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Payment Amount ($)</Label>
|
||||
<Label>{t('funding.paymentAmount')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="0"
|
||||
@@ -247,9 +249,9 @@ export const FundingProposal: React.FC = () => {
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label>Deliverables</Label>
|
||||
<Label>{t('funding.deliverables')}</Label>
|
||||
<Textarea
|
||||
placeholder="What will be delivered"
|
||||
placeholder={t('funding.deliverablesPlaceholder')}
|
||||
rows={2}
|
||||
value={milestone.deliverables}
|
||||
onChange={(e) => updateMilestone(milestone.id, 'deliverables', e.target.value)}
|
||||
@@ -257,7 +259,7 @@ export const FundingProposal: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Deadline</Label>
|
||||
<Label>{t('funding.deadline')}</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={milestone.deadline}
|
||||
@@ -270,14 +272,14 @@ export const FundingProposal: React.FC = () => {
|
||||
|
||||
<Button onClick={addMilestone} variant="outline" className="w-full">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Milestone
|
||||
{t('funding.addMilestone')}
|
||||
</Button>
|
||||
|
||||
{totalMilestoneAmount !== totalBudget && totalMilestoneAmount > 0 && (
|
||||
<div className="flex items-center gap-2 p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg text-gray-900">
|
||||
<AlertCircle className="h-5 w-5 text-yellow-600" />
|
||||
<span className="text-sm text-gray-900">
|
||||
Milestone total (${totalMilestoneAmount.toLocaleString()}) doesn't match budget total (${totalBudget.toLocaleString()})
|
||||
{t('funding.mismatchWarning', { milestoneTotal: totalMilestoneAmount.toLocaleString(), budgetTotal: totalBudget.toLocaleString() })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -286,8 +288,8 @@ export const FundingProposal: React.FC = () => {
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button variant="outline">Save Draft</Button>
|
||||
<Button>Submit Proposal</Button>
|
||||
<Button variant="outline">{t('funding.saveDraft')}</Button>
|
||||
<Button>{t('funding.submitProposal')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import {
|
||||
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
import {
|
||||
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Clock,
|
||||
Users,
|
||||
AlertTriangle,
|
||||
|
||||
|
||||
DollarSign
|
||||
} from 'lucide-react';
|
||||
|
||||
@@ -36,6 +37,7 @@ interface Approval {
|
||||
}
|
||||
|
||||
export const MultiSigApproval: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState('pending');
|
||||
|
||||
const [approvals] = useState<Approval[]>([
|
||||
@@ -117,22 +119,22 @@ export const MultiSigApproval: React.FC = () => {
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span>Deadline: {approval.deadline}</span>
|
||||
<span>{t('msApproval.deadline', { date: approval.deadline })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span>Approval Progress</span>
|
||||
<span>{t('msApproval.approvalProgress')}</span>
|
||||
<span className="font-medium">
|
||||
{approval.currentSignatures}/{approval.requiredSignatures} signatures
|
||||
{t('msApproval.signatures', { current: approval.currentSignatures, required: approval.requiredSignatures })}
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={progress} className="h-2" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">Signers</p>
|
||||
<p className="text-sm font-medium">{t('msApproval.signers')}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{approval.signers.map((signer, index) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
@@ -153,14 +155,14 @@ export const MultiSigApproval: React.FC = () => {
|
||||
<div className="flex gap-2">
|
||||
<Button className="flex-1" size="sm">
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
Approve
|
||||
{t('msApproval.approve')}
|
||||
</Button>
|
||||
<Button variant="outline" className="flex-1" size="sm">
|
||||
<XCircle className="h-4 w-4 mr-2" />
|
||||
Reject
|
||||
{t('msApproval.reject')}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm">
|
||||
View Details
|
||||
{t('msApproval.viewDetails')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -176,7 +178,7 @@ export const MultiSigApproval: React.FC = () => {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Pending Approvals</p>
|
||||
<p className="text-sm text-muted-foreground">{t('msApproval.pendingApprovals')}</p>
|
||||
<p className="text-2xl font-bold">{pendingApprovals.length}</p>
|
||||
</div>
|
||||
<Clock className="h-8 w-8 text-yellow-500" />
|
||||
@@ -188,7 +190,7 @@ export const MultiSigApproval: React.FC = () => {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Total Value</p>
|
||||
<p className="text-sm text-muted-foreground">{t('msApproval.totalValue')}</p>
|
||||
<p className="text-2xl font-bold">
|
||||
${(pendingApprovals.reduce((sum, a) => sum + a.amount, 0) / 1000).toFixed(0)}k
|
||||
</p>
|
||||
@@ -202,7 +204,7 @@ export const MultiSigApproval: React.FC = () => {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Active Signers</p>
|
||||
<p className="text-sm text-muted-foreground">{t('msApproval.activeSigners')}</p>
|
||||
<p className="text-2xl font-bold">5</p>
|
||||
</div>
|
||||
<Users className="h-8 w-8 text-blue-500" />
|
||||
@@ -214,7 +216,7 @@ export const MultiSigApproval: React.FC = () => {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Expiring Soon</p>
|
||||
<p className="text-sm text-muted-foreground">{t('msApproval.expiringSoon')}</p>
|
||||
<p className="text-2xl font-bold">2</p>
|
||||
</div>
|
||||
<AlertTriangle className="h-8 w-8 text-orange-500" />
|
||||
@@ -227,13 +229,13 @@ export const MultiSigApproval: React.FC = () => {
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="pending">
|
||||
Pending ({pendingApprovals.length})
|
||||
{t('msApproval.pending', { count: pendingApprovals.length })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="approved">
|
||||
Approved ({approvedApprovals.length})
|
||||
{t('msApproval.approved', { count: approvedApprovals.length })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="rejected">
|
||||
Rejected ({rejectedApprovals.length})
|
||||
{t('msApproval.rejected', { count: rejectedApprovals.length })}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -247,7 +249,7 @@ export const MultiSigApproval: React.FC = () => {
|
||||
{approvedApprovals.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="p-6 text-center text-muted-foreground">
|
||||
No approved proposals yet
|
||||
{t('msApproval.noApproved')}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
@@ -261,7 +263,7 @@ export const MultiSigApproval: React.FC = () => {
|
||||
{rejectedApprovals.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="p-6 text-center text-muted-foreground">
|
||||
No rejected proposals
|
||||
{t('msApproval.noRejected')}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -29,6 +30,7 @@ interface Transaction {
|
||||
}
|
||||
|
||||
export const SpendingHistory: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [filterCategory, setFilterCategory] = useState('all');
|
||||
const [filterStatus, setFilterStatus] = useState('all');
|
||||
@@ -102,11 +104,11 @@ export const SpendingHistory: React.FC = () => {
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return <Badge className="bg-green-600"><CheckCircle className="w-3 h-3 mr-1" />Completed</Badge>;
|
||||
return <Badge className="bg-green-600"><CheckCircle className="w-3 h-3 mr-1" />{t('spending.completed')}</Badge>;
|
||||
case 'pending':
|
||||
return <Badge className="bg-yellow-600"><Clock className="w-3 h-3 mr-1" />Pending</Badge>;
|
||||
return <Badge className="bg-yellow-600"><Clock className="w-3 h-3 mr-1" />{t('spending.pending')}</Badge>;
|
||||
case 'rejected':
|
||||
return <Badge className="bg-red-600"><XCircle className="w-3 h-3 mr-1" />Rejected</Badge>;
|
||||
return <Badge className="bg-red-600"><XCircle className="w-3 h-3 mr-1" />{t('spending.rejected')}</Badge>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -129,9 +131,9 @@ export const SpendingHistory: React.FC = () => {
|
||||
<div className="space-y-6">
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Treasury Spending History</CardTitle>
|
||||
<CardTitle className="text-white">{t('spending.title')}</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
Track all treasury expenditures and approved proposals
|
||||
{t('spending.description')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -141,7 +143,7 @@ export const SpendingHistory: React.FC = () => {
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-3 h-4 w-4 text-gray-500" />
|
||||
<Input
|
||||
placeholder="Search transactions..."
|
||||
placeholder={t('spending.searchPlaceholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10 bg-gray-800 border-gray-700 text-white"
|
||||
@@ -154,11 +156,11 @@ export const SpendingHistory: React.FC = () => {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-gray-800 border-gray-700">
|
||||
<SelectItem value="all">All Categories</SelectItem>
|
||||
<SelectItem value="Development">Development</SelectItem>
|
||||
<SelectItem value="Marketing">Marketing</SelectItem>
|
||||
<SelectItem value="Infrastructure">Infrastructure</SelectItem>
|
||||
<SelectItem value="Community">Community</SelectItem>
|
||||
<SelectItem value="all">{t('spending.allCategories')}</SelectItem>
|
||||
<SelectItem value="Development">{t('funding.catDevelopment')}</SelectItem>
|
||||
<SelectItem value="Marketing">{t('funding.catMarketing')}</SelectItem>
|
||||
<SelectItem value="Infrastructure">{t('funding.catInfrastructure')}</SelectItem>
|
||||
<SelectItem value="Community">{t('funding.catCommunity')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -167,16 +169,16 @@ export const SpendingHistory: React.FC = () => {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-gray-800 border-gray-700">
|
||||
<SelectItem value="all">All Status</SelectItem>
|
||||
<SelectItem value="completed">Completed</SelectItem>
|
||||
<SelectItem value="pending">Pending</SelectItem>
|
||||
<SelectItem value="rejected">Rejected</SelectItem>
|
||||
<SelectItem value="all">{t('spending.allStatus')}</SelectItem>
|
||||
<SelectItem value="completed">{t('spending.completed')}</SelectItem>
|
||||
<SelectItem value="pending">{t('spending.pending')}</SelectItem>
|
||||
<SelectItem value="rejected">{t('spending.rejected')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button className="bg-blue-600 hover:bg-blue-700">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Export
|
||||
{t('spending.export')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -184,13 +186,13 @@ export const SpendingHistory: React.FC = () => {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-gray-700 hover:bg-gray-750">
|
||||
<TableHead className="text-gray-400">Date</TableHead>
|
||||
<TableHead className="text-gray-400">Description</TableHead>
|
||||
<TableHead className="text-gray-400">Category</TableHead>
|
||||
<TableHead className="text-gray-400">Amount</TableHead>
|
||||
<TableHead className="text-gray-400">Status</TableHead>
|
||||
<TableHead className="text-gray-400">Proposal ID</TableHead>
|
||||
<TableHead className="text-gray-400">Actions</TableHead>
|
||||
<TableHead className="text-gray-400">{t('spending.date')}</TableHead>
|
||||
<TableHead className="text-gray-400">{t('spending.desc')}</TableHead>
|
||||
<TableHead className="text-gray-400">{t('spending.category')}</TableHead>
|
||||
<TableHead className="text-gray-400">{t('spending.amount')}</TableHead>
|
||||
<TableHead className="text-gray-400">{t('spending.status')}</TableHead>
|
||||
<TableHead className="text-gray-400">{t('spending.proposalId')}</TableHead>
|
||||
<TableHead className="text-gray-400">{t('spending.actions')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -210,7 +212,7 @@ export const SpendingHistory: React.FC = () => {
|
||||
<TableCell>{getStatusBadge(tx.status)}</TableCell>
|
||||
<TableCell className="text-gray-300 font-mono">{tx.proposalId}</TableCell>
|
||||
<TableCell>
|
||||
<Button variant="ghost" size="sm">View</Button>
|
||||
<Button variant="ghost" size="sm">{t('spending.view')}</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -10,7 +11,7 @@ import {
|
||||
DollarSign,
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
|
||||
|
||||
Activity,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
@@ -30,6 +31,7 @@ interface BudgetCategory {
|
||||
}
|
||||
|
||||
export const TreasuryOverview: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { metrics, proposals, loading, error } = useTreasury();
|
||||
|
||||
const [categories] = useState<BudgetCategory[]>([
|
||||
@@ -42,17 +44,17 @@ export const TreasuryOverview: React.FC = () => {
|
||||
]);
|
||||
|
||||
const getHealthStatus = (score: number) => {
|
||||
if (score >= 80) return { label: 'Excellent', color: 'text-green-500', icon: CheckCircle };
|
||||
if (score >= 60) return { label: 'Good', color: 'text-blue-500', icon: Activity };
|
||||
if (score >= 40) return { label: 'Fair', color: 'text-yellow-500', icon: AlertCircle };
|
||||
return { label: 'Critical', color: 'text-red-500', icon: AlertCircle };
|
||||
if (score >= 80) return { label: 'treasury.healthExcellent', color: 'text-green-500', icon: CheckCircle };
|
||||
if (score >= 60) return { label: 'treasury.healthGood', color: 'text-blue-500', icon: Activity };
|
||||
if (score >= 40) return { label: 'treasury.healthFair', color: 'text-yellow-500', icon: AlertCircle };
|
||||
return { label: 'treasury.healthCritical', color: 'text-red-500', icon: AlertCircle };
|
||||
};
|
||||
|
||||
const healthStatus = getHealthStatus(metrics.healthScore);
|
||||
const HealthIcon = healthStatus.icon;
|
||||
|
||||
if (loading) {
|
||||
return <LoadingState message="Loading treasury data from blockchain..." />;
|
||||
return <LoadingState message={t('treasury.loading')} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
@@ -60,7 +62,7 @@ export const TreasuryOverview: React.FC = () => {
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Failed to load treasury data: {error}
|
||||
{t('treasury.errorLoad', { error })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
@@ -72,10 +74,10 @@ export const TreasuryOverview: React.FC = () => {
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||
<Activity className="h-3 w-3 mr-1" />
|
||||
Live Blockchain Data
|
||||
{t('treasury.liveData')}
|
||||
</Badge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{proposals.length} active proposals • {metrics.totalBalance.toFixed(2)} HEZ in treasury
|
||||
{t('treasury.activeProposals', { count: proposals.length })} • {t('treasury.hezInTreasury', { amount: metrics.totalBalance.toFixed(2) })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -83,7 +85,7 @@ export const TreasuryOverview: React.FC = () => {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span>Treasury Health</span>
|
||||
<span>{t('treasury.health')}</span>
|
||||
<HealthIcon className={`h-6 w-6 ${healthStatus.color}`} />
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -91,16 +93,16 @@ export const TreasuryOverview: React.FC = () => {
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-2xl font-bold">{metrics.healthScore}%</span>
|
||||
<Badge className={healthStatus.color}>{healthStatus.label}</Badge>
|
||||
<Badge className={healthStatus.color}>{t(healthStatus.label)}</Badge>
|
||||
</div>
|
||||
<Progress value={metrics.healthScore} className="h-3" />
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-muted-foreground">Runway</p>
|
||||
<p className="font-semibold">20.8 months</p>
|
||||
<p className="text-muted-foreground">{t('treasury.runway')}</p>
|
||||
<p className="font-semibold">{t('treasury.runwayMonths', { months: '20.8' })}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Burn Rate</p>
|
||||
<p className="text-muted-foreground">{t('treasury.burnRate')}</p>
|
||||
<p className="font-semibold">$120k/month</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,11 +116,11 @@ export const TreasuryOverview: React.FC = () => {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Total Balance</p>
|
||||
<p className="text-sm text-muted-foreground">{t('treasury.totalBalance')}</p>
|
||||
<p className="text-2xl font-bold">${(metrics.totalBalance / 1000000).toFixed(2)}M</p>
|
||||
<p className="text-xs text-green-500 flex items-center mt-1">
|
||||
<ArrowUpRight className="h-3 w-3 mr-1" />
|
||||
+12.5% this month
|
||||
{t('treasury.thisMonth', { percent: '12.5' })}
|
||||
</p>
|
||||
</div>
|
||||
<DollarSign className="h-8 w-8 text-green-500" />
|
||||
@@ -130,11 +132,11 @@ export const TreasuryOverview: React.FC = () => {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Monthly Income</p>
|
||||
<p className="text-sm text-muted-foreground">{t('treasury.monthlyIncome')}</p>
|
||||
<p className="text-2xl font-bold">${(metrics.monthlyIncome / 1000).toFixed(0)}k</p>
|
||||
<p className="text-xs text-green-500 flex items-center mt-1">
|
||||
<TrendingUp className="h-3 w-3 mr-1" />
|
||||
+8.3% vs last month
|
||||
{t('treasury.vsLastMonth', { percent: '+8.3' })}
|
||||
</p>
|
||||
</div>
|
||||
<TrendingUp className="h-8 w-8 text-blue-500" />
|
||||
@@ -146,11 +148,11 @@ export const TreasuryOverview: React.FC = () => {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Monthly Expenses</p>
|
||||
<p className="text-sm text-muted-foreground">{t('treasury.monthlyExpenses')}</p>
|
||||
<p className="text-2xl font-bold">${(metrics.monthlyExpenses / 1000).toFixed(0)}k</p>
|
||||
<p className="text-xs text-red-500 flex items-center mt-1">
|
||||
<ArrowDownRight className="h-3 w-3 mr-1" />
|
||||
-5.2% vs last month
|
||||
{t('treasury.vsLastMonth', { percent: '-5.2' })}
|
||||
</p>
|
||||
</div>
|
||||
<TrendingDown className="h-8 w-8 text-red-500" />
|
||||
@@ -162,11 +164,11 @@ export const TreasuryOverview: React.FC = () => {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Pending Proposals</p>
|
||||
<p className="text-sm text-muted-foreground">{t('treasury.pendingProposals')}</p>
|
||||
<p className="text-2xl font-bold">{metrics.pendingProposals}</p>
|
||||
<p className="text-xs text-yellow-500 flex items-center mt-1">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
$450k requested
|
||||
{t('treasury.requested', { amount: '450' })}
|
||||
</p>
|
||||
</div>
|
||||
<Clock className="h-8 w-8 text-yellow-500" />
|
||||
@@ -178,8 +180,8 @@ export const TreasuryOverview: React.FC = () => {
|
||||
{/* Budget Categories */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Budget Allocation by Category</CardTitle>
|
||||
<CardDescription>Current quarter budget utilization</CardDescription>
|
||||
<CardTitle>{t('treasury.budgetAllocation')}</CardTitle>
|
||||
<CardDescription>{t('treasury.quarterUtilization')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -20,6 +21,7 @@ interface MultiSigTransaction {
|
||||
}
|
||||
|
||||
export const MultiSigWallet: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [amount, setAmount] = useState('');
|
||||
const [recipient, setRecipient] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
@@ -62,7 +64,7 @@ export const MultiSigWallet: React.FC = () => {
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-400">Wallet Balance</CardTitle>
|
||||
<CardTitle className="text-sm text-gray-400">{t('multisig.walletBalance')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-white">50,000 HEZ</div>
|
||||
@@ -72,33 +74,33 @@ export const MultiSigWallet: React.FC = () => {
|
||||
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-400">Required Signatures</CardTitle>
|
||||
<CardTitle className="text-sm text-gray-400">{t('multisig.requiredSignatures')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-white">3 of 5</div>
|
||||
<p className="text-xs text-gray-500 mt-1">Signers required</p>
|
||||
<p className="text-xs text-gray-500 mt-1">{t('multisig.signersRequired')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-400">Pending Transactions</CardTitle>
|
||||
<CardTitle className="text-sm text-gray-400">{t('multisig.pendingTransactions')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-yellow-500">2</div>
|
||||
<p className="text-xs text-gray-500 mt-1">Awaiting signatures</p>
|
||||
<p className="text-xs text-gray-500 mt-1">{t('multisig.awaitingSignatures')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle>Create Transaction</CardTitle>
|
||||
<CardTitle>{t('multisig.createTransaction')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Recipient Address</Label>
|
||||
<Label>{t('multisig.recipientAddress')}</Label>
|
||||
<Input
|
||||
placeholder="0x..."
|
||||
value={recipient}
|
||||
@@ -107,7 +109,7 @@ export const MultiSigWallet: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Amount</Label>
|
||||
<Label>{t('multisig.amount')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="0"
|
||||
@@ -118,9 +120,9 @@ export const MultiSigWallet: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Description</Label>
|
||||
<Label>{t('multisig.description')}</Label>
|
||||
<Input
|
||||
placeholder="Transaction purpose"
|
||||
placeholder={t('multisig.txPurpose')}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
className="bg-gray-800 border-gray-700"
|
||||
@@ -128,14 +130,14 @@ export const MultiSigWallet: React.FC = () => {
|
||||
</div>
|
||||
<Button onClick={handleCreateTransaction} className="bg-green-600 hover:bg-green-700">
|
||||
<Send className="w-4 h-4 mr-2" />
|
||||
Create Transaction
|
||||
{t('multisig.createTransaction')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle>Pending Transactions</CardTitle>
|
||||
<CardTitle>{t('multisig.pendingTransactions')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{transactions.map((tx) => (
|
||||
@@ -155,14 +157,14 @@ export const MultiSigWallet: React.FC = () => {
|
||||
<div className="flex items-center gap-4">
|
||||
<Progress value={(tx.currentSignatures / tx.requiredSignatures) * 100} className="w-32" />
|
||||
<span className="text-sm text-gray-400">
|
||||
{tx.currentSignatures}/{tx.requiredSignatures} signatures
|
||||
{t('multisig.signatures', { current: tx.currentSignatures, required: tx.requiredSignatures })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{tx.status === 'pending' && (
|
||||
<Button size="sm" onClick={() => handleSign(tx.id)} className="bg-blue-600 hover:bg-blue-700">
|
||||
<Key className="w-4 h-4 mr-1" />
|
||||
Sign
|
||||
{t('multisig.sign')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Send, Loader2, CheckCircle, XCircle } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -27,6 +28,7 @@ export const TransactionModal: React.FC<TransactionModalProps> = ({
|
||||
type
|
||||
}) => {
|
||||
const { signTransaction, signMessage } = useWallet();
|
||||
const { t } = useTranslation();
|
||||
const [recipient, setRecipient] = useState('');
|
||||
const [amount, setAmount] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
@@ -36,7 +38,7 @@ export const TransactionModal: React.FC<TransactionModalProps> = ({
|
||||
|
||||
const handleSendTransaction = async () => {
|
||||
if (!recipient || !amount) {
|
||||
setError('Please fill in all fields');
|
||||
setError(t('txModal.fillAllFields'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -62,7 +64,7 @@ export const TransactionModal: React.FC<TransactionModalProps> = ({
|
||||
|
||||
const handleSignMessage = async () => {
|
||||
if (!message) {
|
||||
setError('Please enter a message to sign');
|
||||
setError(t('txModal.enterMessage'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -95,14 +97,14 @@ export const TransactionModal: React.FC<TransactionModalProps> = ({
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Send className="h-5 w-5 text-kesk" />
|
||||
{type === 'send' ? 'Send HEZ' : type === 'vote' ? 'Cast Vote' : 'Delegate Voting Power'}
|
||||
{type === 'send' ? t('txModal.sendHez') : type === 'vote' ? t('txModal.castVote') : t('txModal.delegateVoting')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{type === 'send'
|
||||
? 'Send HEZ tokens to another address'
|
||||
? t('txModal.sendHezDesc')
|
||||
: type === 'vote'
|
||||
? 'Submit your vote for the proposal'
|
||||
: 'Delegate your voting power to another address'}
|
||||
? t('txModal.voteDesc')
|
||||
: t('txModal.delegateDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -111,7 +113,7 @@ export const TransactionModal: React.FC<TransactionModalProps> = ({
|
||||
{type === 'send' && (
|
||||
<>
|
||||
<div>
|
||||
<Label htmlFor="recipient">Recipient Address</Label>
|
||||
<Label htmlFor="recipient">{t('txModal.recipientAddress')}</Label>
|
||||
<Input
|
||||
id="recipient"
|
||||
placeholder="0x..."
|
||||
@@ -121,7 +123,7 @@ export const TransactionModal: React.FC<TransactionModalProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="amount">Amount (HEZ)</Label>
|
||||
<Label htmlFor="amount">{t('txModal.amountHez')}</Label>
|
||||
<Input
|
||||
id="amount"
|
||||
type="number"
|
||||
@@ -135,10 +137,10 @@ export const TransactionModal: React.FC<TransactionModalProps> = ({
|
||||
|
||||
{type === 'vote' && (
|
||||
<div>
|
||||
<Label htmlFor="message">Vote Message</Label>
|
||||
<Label htmlFor="message">{t('txModal.voteMessage')}</Label>
|
||||
<Textarea
|
||||
id="message"
|
||||
placeholder="Enter your vote reason (optional)"
|
||||
placeholder={t('txModal.votePlaceholder')}
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
/>
|
||||
@@ -161,17 +163,17 @@ export const TransactionModal: React.FC<TransactionModalProps> = ({
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Processing...
|
||||
{t('txModal.processing')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
{type === 'send' ? 'Send Transaction' : 'Sign & Submit'}
|
||||
{type === 'send' ? t('txModal.sendTransaction') : t('txModal.signSubmit')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={resetForm} disabled={loading}>
|
||||
Cancel
|
||||
{t('txModal.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,15 +182,15 @@ export const TransactionModal: React.FC<TransactionModalProps> = ({
|
||||
<Alert className="border-kesk/20">
|
||||
<CheckCircle className="h-4 w-4 text-kesk" />
|
||||
<AlertDescription>
|
||||
Transaction submitted successfully!
|
||||
{t('txModal.txSubmitted')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-sm text-muted-foreground">Transaction Hash</div>
|
||||
<div className="text-sm text-muted-foreground">{t('txModal.txHash')}</div>
|
||||
<div className="font-mono text-xs break-all">{txHash}</div>
|
||||
</div>
|
||||
<Button onClick={resetForm} className="w-full">
|
||||
Close
|
||||
{t('txModal.close')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Wallet, LogOut, AlertCircle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@@ -14,10 +15,11 @@ import { formatAddress, formatBalance } from '@pezkuwi/lib/wallet';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
export const WalletButton: React.FC = () => {
|
||||
const {
|
||||
isConnected,
|
||||
address,
|
||||
balance,
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
isConnected,
|
||||
address,
|
||||
balance,
|
||||
chainId,
|
||||
error,
|
||||
connectMetaMask,
|
||||
@@ -39,7 +41,7 @@ export const WalletButton: React.FC = () => {
|
||||
className="bg-kesk hover:bg-kesk/90 text-white"
|
||||
>
|
||||
<Wallet className="mr-2 h-4 w-4" />
|
||||
Connect Wallet
|
||||
{t('walletBtn.connectWallet')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -59,41 +61,41 @@ export const WalletButton: React.FC = () => {
|
||||
</div>
|
||||
{!isCorrectNetwork && (
|
||||
<Badge variant="destructive" className="ml-2 bg-sor">
|
||||
Wrong Network
|
||||
{t('walletBtn.wrongNetwork')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuLabel>Wallet Details</DropdownMenuLabel>
|
||||
<DropdownMenuLabel>{t('walletBtn.walletDetails')}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="px-2 py-1.5">
|
||||
<div className="text-sm text-muted-foreground">Address</div>
|
||||
<div className="text-sm text-muted-foreground">{t('walletBtn.address')}</div>
|
||||
<div className="text-sm font-mono">{formatAddress(address!)}</div>
|
||||
</div>
|
||||
<div className="px-2 py-1.5">
|
||||
<div className="text-sm text-muted-foreground">Balance</div>
|
||||
<div className="text-sm text-muted-foreground">{t('walletBtn.balance')}</div>
|
||||
<div className="text-sm font-medium">{formatBalance(balance)} HEZ</div>
|
||||
</div>
|
||||
<div className="px-2 py-1.5">
|
||||
<div className="text-sm text-muted-foreground">Network</div>
|
||||
<div className="text-sm text-muted-foreground">{t('walletBtn.network')}</div>
|
||||
<div className="text-sm font-medium">
|
||||
{isCorrectNetwork ? 'PezkuwiChain' : 'Unknown Network'}
|
||||
{isCorrectNetwork ? t('walletBtn.pezkuwiChain') : t('walletBtn.unknownNetwork')}
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
{!isCorrectNetwork && (
|
||||
<>
|
||||
<DropdownMenuItem onClick={switchNetwork} className="text-zer">
|
||||
Switch to PezkuwiChain
|
||||
{t('walletBtn.switchNetwork')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItem onClick={disconnect} className="text-sor">
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Disconnect
|
||||
{t('walletBtn.disconnect')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Wallet, Chrome, ExternalLink, Copy, Check, LogOut, Award, Users, TrendingUp, Shield } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -30,6 +31,7 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
error
|
||||
} = usePezkuwi();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [scores, setScores] = useState<UserScores>({
|
||||
trustScore: 0,
|
||||
@@ -103,12 +105,12 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Wallet className="h-5 w-5 text-purple-400" />
|
||||
{selectedAccount ? 'Wallet Connected' : 'Connect Wallet'}
|
||||
{selectedAccount ? t('walletModal.connected') : t('walletModal.connect')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{selectedAccount
|
||||
? 'Manage your Pezkuwi account'
|
||||
: 'Connect your Pezkuwi.js extension to interact with PezkuwiChain'}
|
||||
? t('walletModal.manageAccount')
|
||||
: t('walletModal.connectDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -126,7 +128,7 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
className="w-full bg-gradient-to-r from-purple-600 to-cyan-400 hover:from-purple-700 hover:to-cyan-500"
|
||||
>
|
||||
<Wallet className="mr-2 h-4 w-4" />
|
||||
Try Again
|
||||
{t('walletModal.tryAgain')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -149,13 +151,13 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
>
|
||||
<Button className="w-full bg-green-600 hover:bg-green-700">
|
||||
<Chrome className="mr-2 h-4 w-4" />
|
||||
Install from Chrome Web Store
|
||||
{t('walletModal.installChrome')}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-400 text-center">
|
||||
After installing, refresh the page and try again
|
||||
{t('walletModal.afterInstall')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -166,12 +168,12 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
{/* Account Info */}
|
||||
<div className="bg-gray-800/50 rounded-lg p-4 space-y-3">
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 mb-1">Account Name</div>
|
||||
<div className="font-medium">{selectedAccount.meta.name || 'Unnamed Account'}</div>
|
||||
<div className="text-xs text-gray-400 mb-1">{t('walletModal.accountName')}</div>
|
||||
<div className="font-medium">{selectedAccount.meta.name || t('walletModal.unnamed')}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 mb-1">Address</div>
|
||||
<div className="text-xs text-gray-400 mb-1">{t('walletModal.address')}</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<code className="text-sm font-mono text-gray-300 truncate">
|
||||
{formatAddress(selectedAccount.address)}
|
||||
@@ -192,36 +194,36 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 mb-2">Scores from Blockchain</div>
|
||||
<div className="text-xs text-gray-400 mb-2">{t('walletModal.scoresFromBlockchain')}</div>
|
||||
{loadingScores ? (
|
||||
<div className="text-sm text-gray-400">Loading scores...</div>
|
||||
<div className="text-sm text-gray-400">{t('walletModal.loadingScores')}</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="bg-gray-900/50 rounded p-2">
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<Shield className="h-3 w-3 text-purple-400" />
|
||||
<span className="text-xs text-gray-400">Trust</span>
|
||||
<span className="text-xs text-gray-400">{t('walletModal.trust')}</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-purple-400">{scores.trustScore}</span>
|
||||
</div>
|
||||
<div className="bg-gray-900/50 rounded p-2">
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<Users className="h-3 w-3 text-cyan-400" />
|
||||
<span className="text-xs text-gray-400">Referral</span>
|
||||
<span className="text-xs text-gray-400">{t('walletModal.referral')}</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-cyan-400">{scores.referralScore}</span>
|
||||
</div>
|
||||
<div className="bg-gray-900/50 rounded p-2">
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<TrendingUp className="h-3 w-3 text-green-400" />
|
||||
<span className="text-xs text-gray-400">Staking</span>
|
||||
<span className="text-xs text-gray-400">{t('walletModal.staking')}</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-green-400">{scores.stakingScore}</span>
|
||||
</div>
|
||||
<div className="bg-gray-900/50 rounded p-2">
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<Award className="h-3 w-3 text-pink-400" />
|
||||
<span className="text-xs text-gray-400">Tiki</span>
|
||||
<span className="text-xs text-gray-400">{t('walletModal.tiki')}</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-pink-400">{scores.tikiScore}</span>
|
||||
</div>
|
||||
@@ -229,7 +231,7 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
)}
|
||||
<div className="mt-2 pt-2 border-t border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-gray-400">Total Score</span>
|
||||
<span className="text-xs text-gray-400">{t('walletModal.totalScore')}</span>
|
||||
<span className="text-lg font-bold bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
|
||||
{loadingScores ? '...' : scores.totalScore}
|
||||
</span>
|
||||
@@ -238,7 +240,7 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 mb-1">Source</div>
|
||||
<div className="text-xs text-gray-400 mb-1">{t('walletModal.source')}</div>
|
||||
<div className="text-sm text-gray-300">
|
||||
{selectedAccount.meta.source || 'pezkuwi'}
|
||||
</div>
|
||||
@@ -253,7 +255,7 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
onClick={() => window.open(`https://pezkuwichain.io/explorer`, '_blank')}
|
||||
>
|
||||
<ExternalLink className="mr-2 h-4 w-4" />
|
||||
View on Explorer
|
||||
{t('walletModal.viewOnExplorer')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -261,14 +263,14 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
className="text-red-400 border-red-400/30 hover:bg-red-400/10"
|
||||
>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Disconnect
|
||||
{t('walletModal.disconnect')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Switch Account */}
|
||||
{accounts.length > 1 && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-gray-400">Switch Account</div>
|
||||
<div className="text-sm text-gray-400">{t('walletModal.switchAccount')}</div>
|
||||
<div className="space-y-2 max-h-48 overflow-y-auto">
|
||||
{accounts.map((account) => (
|
||||
<button
|
||||
@@ -281,7 +283,7 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium text-sm">
|
||||
{account.meta.name || 'Unnamed'}
|
||||
{account.meta.name || t('walletModal.unnamed')}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 font-mono">
|
||||
{formatAddress(account.address)}
|
||||
@@ -300,7 +302,7 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
{accounts.length > 0 ? (
|
||||
// Has accounts, show selection
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-gray-400">Select an account to connect:</div>
|
||||
<div className="text-sm text-gray-400">{t('walletModal.selectAccount')}</div>
|
||||
<div className="space-y-2">
|
||||
{accounts.map((account) => (
|
||||
<button
|
||||
@@ -309,7 +311,7 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
className="w-full p-4 rounded-lg border border-gray-700 bg-gray-800/50 hover:border-purple-500/50 hover:bg-gray-800 transition-all text-left"
|
||||
>
|
||||
<div className="font-medium mb-1">
|
||||
{account.meta.name || 'Unnamed Account'}
|
||||
{account.meta.name || t('walletModal.unnamed')}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400 font-mono">
|
||||
{account.address}
|
||||
@@ -326,18 +328,18 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
className="w-full bg-gradient-to-r from-purple-600 to-cyan-400 hover:from-purple-700 hover:to-cyan-500"
|
||||
>
|
||||
<Wallet className="mr-2 h-4 w-4" />
|
||||
Connect Pezkuwi.js
|
||||
{t('walletModal.connectPezkuwi')}
|
||||
</Button>
|
||||
|
||||
<div className="text-sm text-gray-400 text-center">
|
||||
Don't have Pezkuwi Wallet?{' '}
|
||||
{t('walletModal.noWallet')}{' '}
|
||||
<a
|
||||
href="https://chrome.google.com/webstore/detail/pezkuwi-wallet/fbnboicjjeebjhgnapneaeccpgjcdibn"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-purple-400 hover:underline"
|
||||
>
|
||||
Get it from Chrome Web Store
|
||||
{t('walletModal.getFromStore')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user