mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 11:18:01 +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>
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
{
|
||||
"nav": {
|
||||
"home": "الرئيسية",
|
||||
"governance": "الحوكمة",
|
||||
"identity": "الهوية",
|
||||
"wallet": "المحفظة",
|
||||
"docs": "الوثائق",
|
||||
"connectWallet": "ربط المحفظة",
|
||||
"disconnect": "قطع الاتصال"
|
||||
},
|
||||
"hero": {
|
||||
"title": "الحوكمة اللامركزية لكردستان",
|
||||
"subtitle": "بناء مستقبل شفاف وديمقراطي من خلال تقنية البلوكشين",
|
||||
"exploreGovernance": "استكشف الحوكمة",
|
||||
"learnMore": "اعرف المزيد",
|
||||
"stats": {
|
||||
"activeProposals": "المقترحات النشطة",
|
||||
"totalVoters": "إجمالي الناخبين",
|
||||
"tokensStaked": "الرموز المحجوزة",
|
||||
"trustScore": "نقاط الثقة"
|
||||
}
|
||||
},
|
||||
"governance": {
|
||||
"title": "الحوكمة على السلسلة",
|
||||
"subtitle": "شارك في صنع القرار الديمقراطي",
|
||||
"overview": "نظرة عامة",
|
||||
"proposals": "المقترحات",
|
||||
"elections": "الانتخابات",
|
||||
"createProposal": "إنشاء مقترح",
|
||||
"vote": "صوت",
|
||||
"delegate": "تفويض",
|
||||
"status": {
|
||||
"active": "نشط",
|
||||
"passed": "تم القبول",
|
||||
"rejected": "مرفوض",
|
||||
"pending": "قيد الانتظار"
|
||||
}
|
||||
},
|
||||
"identity": {
|
||||
"title": "الهوية الرقمية",
|
||||
"verification": "التحقق",
|
||||
"reputation": "السمعة",
|
||||
"verifyIdentity": "التحقق من الهوية",
|
||||
"trustScore": "نقاط الثقة",
|
||||
"badges": "الشارات",
|
||||
"roles": "الأدوار",
|
||||
"privacy": "إعدادات الخصوصية"
|
||||
},
|
||||
"tokenomics": {
|
||||
"title": "اقتصاد الرمز",
|
||||
"totalSupply": "العرض الإجمالي",
|
||||
"circulating": "العرض المتداول",
|
||||
"staked": "محجوز",
|
||||
"rewards": "مجمع المكافآت"
|
||||
},
|
||||
"team": {
|
||||
"title": "أبطالنا",
|
||||
"subtitle": "تكريم القادة الأكراد ومقاتلي الحرية"
|
||||
},
|
||||
"footer": {
|
||||
"about": "حول",
|
||||
"governance": "الحوكمة",
|
||||
"developers": "المطورون",
|
||||
"community": "المجتمع",
|
||||
"rights": "جميع الحقوق محفوظة"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,67 +0,0 @@
|
||||
{
|
||||
"nav": {
|
||||
"home": "ماڵەوە",
|
||||
"governance": "حوکمڕانی",
|
||||
"identity": "ناسنامە",
|
||||
"wallet": "جزدان",
|
||||
"docs": "بەڵگەنامە",
|
||||
"connectWallet": "جزدان ببەستە",
|
||||
"disconnect": "دابڕان"
|
||||
},
|
||||
"hero": {
|
||||
"title": "حوکمڕانی لامەرکەزی بۆ کوردستان",
|
||||
"subtitle": "دروستکردنی داهاتوویەکی شەفاف و دیموکراتی لە ڕێگەی تەکنەلۆژیای بلۆکچەین",
|
||||
"exploreGovernance": "حوکمڕانی بکۆڵەرەوە",
|
||||
"learnMore": "زیاتر فێربە",
|
||||
"stats": {
|
||||
"activeProposals": "پێشنیارە چالاکەکان",
|
||||
"totalVoters": "کۆی دەنگدەران",
|
||||
"tokensStaked": "تۆکنە داناوەکان",
|
||||
"trustScore": "نمرەی متمانە"
|
||||
}
|
||||
},
|
||||
"governance": {
|
||||
"title": "حوکمڕانی سەر زنجیرە",
|
||||
"subtitle": "بەشداری لە بڕیاردانی دیموکراتی بکە",
|
||||
"overview": "تێڕوانینی گشتی",
|
||||
"proposals": "پێشنیارەکان",
|
||||
"elections": "هەڵبژاردنەکان",
|
||||
"createProposal": "پێشنیار دروست بکە",
|
||||
"vote": "دەنگ بدە",
|
||||
"delegate": "نوێنەرایەتی",
|
||||
"status": {
|
||||
"active": "چالاک",
|
||||
"passed": "پەسەند کرا",
|
||||
"rejected": "ڕەت کرایەوە",
|
||||
"pending": "چاوەڕوان"
|
||||
}
|
||||
},
|
||||
"identity": {
|
||||
"title": "ناسنامەی دیجیتاڵ",
|
||||
"verification": "پشتڕاستکردنەوە",
|
||||
"reputation": "ناوبانگ",
|
||||
"verifyIdentity": "ناسنامە پشتڕاست بکەرەوە",
|
||||
"trustScore": "نمرەی متمانە",
|
||||
"badges": "نیشانەکان",
|
||||
"roles": "ڕۆڵەکان",
|
||||
"privacy": "ڕێکخستنەکانی تایبەتمەندی"
|
||||
},
|
||||
"tokenomics": {
|
||||
"title": "تۆکنۆمیکس",
|
||||
"totalSupply": "کۆی دابینکراو",
|
||||
"circulating": "دابینکراوی بازنەیی",
|
||||
"staked": "داناو",
|
||||
"rewards": "حەوزی پاداشتەکان"
|
||||
},
|
||||
"team": {
|
||||
"title": "پاڵەوانەکانمان",
|
||||
"subtitle": "ڕێزگرتن لە سەرکردە کوردەکان و شەڕڤانانی ئازادی"
|
||||
},
|
||||
"footer": {
|
||||
"about": "دەربارە",
|
||||
"governance": "حوکمڕانی",
|
||||
"developers": "گەشەپێدەران",
|
||||
"community": "کۆمەڵگە",
|
||||
"rights": "هەموو مافەکان پارێزراون"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,263 +0,0 @@
|
||||
{
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"governance": "Governance",
|
||||
"identity": "Identity",
|
||||
"wallet": "Wallet",
|
||||
"docs": "Documentation",
|
||||
"connectWallet": "Connect Wallet",
|
||||
"disconnect": "Disconnect",
|
||||
"profile": "Profile",
|
||||
"proposals": "Proposals",
|
||||
"delegation": "Delegation",
|
||||
"forum": "Forum",
|
||||
"moderation": "Moderation"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Decentralized Governance for Kurdistan",
|
||||
"subtitle": "Building a transparent, democratic future through blockchain technology",
|
||||
"exploreGovernance": "Explore Governance",
|
||||
"learnMore": "Learn More",
|
||||
"stats": {
|
||||
"activeProposals": "Active Proposals",
|
||||
"totalVoters": "Total Voters",
|
||||
"tokensStaked": "Tokens Staked",
|
||||
"trustScore": "Trust Score"
|
||||
}
|
||||
},
|
||||
"governance": {
|
||||
"title": "On-Chain Governance",
|
||||
"subtitle": "Participate in democratic decision-making",
|
||||
"overview": "Overview",
|
||||
"proposals": "Proposals",
|
||||
"elections": "Elections",
|
||||
"createProposal": "Create Proposal",
|
||||
"vote": "Vote",
|
||||
"delegate": "Delegate",
|
||||
"status": {
|
||||
"active": "Active",
|
||||
"passed": "Passed",
|
||||
"rejected": "Rejected",
|
||||
"pending": "Pending"
|
||||
}
|
||||
},
|
||||
"identity": {
|
||||
"title": "Digital Identity",
|
||||
"verification": "Verification",
|
||||
"reputation": "Reputation",
|
||||
"verifyIdentity": "Verify Identity",
|
||||
"trustScore": "Trust Score",
|
||||
"badges": "Badges",
|
||||
"roles": "Roles",
|
||||
"privacy": "Privacy Settings"
|
||||
},
|
||||
"tokenomics": {
|
||||
"title": "Tokenomics",
|
||||
"totalSupply": "Total Supply",
|
||||
"circulating": "Circulating Supply",
|
||||
"staked": "Staked",
|
||||
"rewards": "Rewards Pool"
|
||||
},
|
||||
"team": {
|
||||
"title": "Our Heroes",
|
||||
"subtitle": "Honoring Kurdish leaders and freedom fighters"
|
||||
},
|
||||
"footer": {
|
||||
"about": "About",
|
||||
"governance": "Governance",
|
||||
"developers": "Developers",
|
||||
"community": "Community",
|
||||
"rights": "All rights reserved"
|
||||
},
|
||||
"profile": {
|
||||
"edit": "Edit Profile",
|
||||
"settings": "Settings",
|
||||
"follow": "Follow",
|
||||
"unfollow": "Unfollow",
|
||||
"share": "Share",
|
||||
"reputation": "Reputation",
|
||||
"level": "Level",
|
||||
"followers": "Followers",
|
||||
"following": "Following",
|
||||
"proposalsCreated": "Proposals Created",
|
||||
"votesCast": "Votes Cast",
|
||||
"delegationsReceived": "Delegations Received",
|
||||
"successRate": "Success Rate",
|
||||
"activity": "Activity",
|
||||
"achievements": "Achievements",
|
||||
"achievementsTitle": "Your Achievements",
|
||||
"recentActivity": "Recent Activity",
|
||||
"connections": "Connections",
|
||||
"viewProfile": "View Profile",
|
||||
"customizeProfile": "Customize Profile",
|
||||
"basic": "Basic Info",
|
||||
"theme": "Theme",
|
||||
"privacy": "Privacy",
|
||||
"notifications": "Notifications",
|
||||
"changeAvatar": "Change Avatar",
|
||||
"name": "Name",
|
||||
"username": "Username",
|
||||
"bio": "Bio",
|
||||
"location": "Location",
|
||||
"socialLinks": "Social Links",
|
||||
"chooseTheme": "Choose Theme",
|
||||
"privacySettings": "Privacy Settings",
|
||||
"showEmail": "Show email publicly",
|
||||
"showActivity": "Show activity feed",
|
||||
"allowMessages": "Allow direct messages",
|
||||
"notificationSettings": "Notification Settings",
|
||||
"proposalUpdates": "Proposal updates",
|
||||
"voteReminders": "Vote reminders",
|
||||
"newFollowers": "New followers"
|
||||
},
|
||||
"proposals": {
|
||||
"templates": {
|
||||
"treasury": "Treasury",
|
||||
"treasuryDesc": "Request funding for projects and initiatives",
|
||||
"technical": "Technical",
|
||||
"technicalDesc": "Propose protocol upgrades and technical changes",
|
||||
"community": "Community",
|
||||
"communityDesc": "Community initiatives and social proposals"
|
||||
},
|
||||
"steps": {
|
||||
"template": "Template",
|
||||
"basics": "Basic Info",
|
||||
"details": "Details",
|
||||
"impact": "Impact",
|
||||
"review": "Review"
|
||||
},
|
||||
"wizard": {
|
||||
"selectTemplate": "Select a template for your proposal",
|
||||
"enterBasics": "Enter basic information about your proposal",
|
||||
"provideDetails": "Provide detailed specifications",
|
||||
"defineImpact": "Define impact and success metrics",
|
||||
"reviewSubmit": "Review and submit your proposal",
|
||||
"readyToSubmit": "Your proposal is ready to submit!"
|
||||
},
|
||||
"fields": {
|
||||
"title": "Title",
|
||||
"category": "Category",
|
||||
"summary": "Summary",
|
||||
"description": "Description",
|
||||
"budget": "Budget",
|
||||
"timeline": "Timeline",
|
||||
"milestones": "Milestones",
|
||||
"impact": "Expected Impact",
|
||||
"metrics": "Success Metrics",
|
||||
"risks": "Risks & Mitigation"
|
||||
},
|
||||
"placeholders": {
|
||||
"title": "Enter a clear, descriptive title",
|
||||
"category": "Select a category",
|
||||
"summary": "Brief summary of your proposal (max 200 characters)",
|
||||
"description": "Detailed description of what you're proposing",
|
||||
"impact": "How will this benefit the community?",
|
||||
"metrics": "How will success be measured?",
|
||||
"risks": "What are the potential risks?"
|
||||
}
|
||||
},
|
||||
"delegation": {
|
||||
"title": "Vote Delegation",
|
||||
"description": "Delegate your voting power to trusted representatives",
|
||||
"activeDelegates": "Active Delegates",
|
||||
"totalDelegated": "Total Delegated",
|
||||
"avgSuccessRate": "Avg Success Rate",
|
||||
"yourDelegated": "Your Delegated",
|
||||
"explore": "Explore Delegates",
|
||||
"myDelegations": "My Delegations",
|
||||
"becomeDelegate": "Become a Delegate",
|
||||
"topDelegates": "Top Delegates",
|
||||
"delegateTo": "Delegate to",
|
||||
"amount": "Amount",
|
||||
"period": "Period",
|
||||
"categories": "Categories",
|
||||
"confirmDelegation": "Confirm Delegation",
|
||||
"yourDelegations": "Your Delegations",
|
||||
"modify": "Modify",
|
||||
"revoke": "Revoke",
|
||||
"becomeDelegateDesc": "Accept delegations and represent your community",
|
||||
"delegateRequirements": "To become a delegate, you need to maintain a good reputation and actively participate in governance",
|
||||
"buildReputation": "Build Reputation",
|
||||
"buildReputationDesc": "Participate actively in governance",
|
||||
"earnTrust": "Earn Trust",
|
||||
"earnTrustDesc": "Gain community confidence",
|
||||
"getRewards": "Get Rewards",
|
||||
"getRewardsDesc": "Earn for your contributions",
|
||||
"delegateStatement": "Delegate Statement",
|
||||
"statementPlaceholder": "Explain why you should be trusted as a delegate...",
|
||||
"expertise": "Areas of Expertise",
|
||||
"commitments": "Commitments",
|
||||
"commitmentsPlaceholder": "What do you commit to as a delegate?",
|
||||
"website": "Website",
|
||||
"twitter": "Twitter",
|
||||
"minDelegation": "Min Delegation",
|
||||
"maxDelegation": "Max Delegation",
|
||||
"acceptingDelegations": "Accepting Delegations",
|
||||
"acceptingDesc": "Toggle to start/stop accepting new delegations",
|
||||
"activateDelegate": "Activate Delegate Profile",
|
||||
"yourDelegateProfile": "Your Delegate Profile",
|
||||
"delegateActive": "You are now an active delegate! Community members can delegate their voting power to you.",
|
||||
"delegators": "Delegators",
|
||||
"totalReceived": "Total Received",
|
||||
"successRate": "Success Rate",
|
||||
"yourStatement": "Your Statement",
|
||||
"yourExpertise": "Your Expertise",
|
||||
"delegationLimits": "Delegation Limits",
|
||||
"editProfile": "Edit Profile",
|
||||
"pauseDelegations": "Pause Delegations"
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"next": "Next",
|
||||
"back": "Back",
|
||||
"submit": "Submit",
|
||||
"backToHome": "Back to Home",
|
||||
"loading": "Loading...",
|
||||
"error": "Error",
|
||||
"success": "Success"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"markAllRead": "Mark all read",
|
||||
"settings": "Settings",
|
||||
"pushNotifications": "Push Notifications",
|
||||
"mentions": "Mentions",
|
||||
"replies": "Replies",
|
||||
"votes": "Vote Updates",
|
||||
"badges": "New Badges",
|
||||
"proposals": "Proposal Updates",
|
||||
"noNotifications": "No notifications",
|
||||
"newMention": "You were mentioned",
|
||||
"newReply": "New reply to your comment",
|
||||
"voteUpdate": "Vote count updated",
|
||||
"newBadge": "You earned a new badge",
|
||||
"proposalUpdate": "Proposal status changed"
|
||||
},
|
||||
"websocket": {
|
||||
"connected": "Connected",
|
||||
"disconnected": "Disconnected",
|
||||
"reconnecting": "Reconnecting...",
|
||||
"liveUpdates": "Live updates enabled",
|
||||
"offlineMode": "Offline mode"
|
||||
},
|
||||
"chainSpecs": {
|
||||
"title": "Chain Specifications",
|
||||
"subtitle": "Multiple network environments for development, testing, and production",
|
||||
"services": "Services",
|
||||
"subdomainsTitle": "Subdomains",
|
||||
"availableServices": "{{count}} available services",
|
||||
"viewExplorer": "View Explorer",
|
||||
"connectionExample": "Connection Example",
|
||||
"networkStats": "Network Stats",
|
||||
"blockTime": "Block Time",
|
||||
"finality": "Finality",
|
||||
"consensus": "Consensus",
|
||||
"runtime": "Runtime",
|
||||
"websocketEndpoint": "WebSocket Endpoint",
|
||||
"chainId": "Chain ID",
|
||||
"features": "Features",
|
||||
"availableSubdomains": "Available Subdomains"
|
||||
}
|
||||
}
|
||||
|
||||
+3361
-1
File diff suppressed because it is too large
Load Diff
@@ -1,67 +0,0 @@
|
||||
{
|
||||
"nav": {
|
||||
"home": "خانه",
|
||||
"governance": "حکمرانی",
|
||||
"identity": "هویت",
|
||||
"wallet": "کیف پول",
|
||||
"docs": "مستندات",
|
||||
"connectWallet": "اتصال کیف پول",
|
||||
"disconnect": "قطع اتصال"
|
||||
},
|
||||
"hero": {
|
||||
"title": "حکمرانی غیرمتمرکز برای کردستان",
|
||||
"subtitle": "ساخت آیندهای شفاف و دموکراتیک با فناوری بلاکچین",
|
||||
"exploreGovernance": "کشف حکمرانی",
|
||||
"learnMore": "بیشتر بدانید",
|
||||
"stats": {
|
||||
"activeProposals": "پیشنهادات فعال",
|
||||
"totalVoters": "مجموع رأی دهندگان",
|
||||
"tokensStaked": "توکنهای استیک شده",
|
||||
"trustScore": "امتیاز اعتماد"
|
||||
}
|
||||
},
|
||||
"governance": {
|
||||
"title": "حکمرانی روی زنجیره",
|
||||
"subtitle": "در تصمیمگیری دموکراتیک شرکت کنید",
|
||||
"overview": "نمای کلی",
|
||||
"proposals": "پیشنهادات",
|
||||
"elections": "انتخابات",
|
||||
"createProposal": "ایجاد پیشنهاد",
|
||||
"vote": "رأی دهید",
|
||||
"delegate": "نمایندگی",
|
||||
"status": {
|
||||
"active": "فعال",
|
||||
"passed": "تصویب شد",
|
||||
"rejected": "رد شد",
|
||||
"pending": "در انتظار"
|
||||
}
|
||||
},
|
||||
"identity": {
|
||||
"title": "هویت دیجیتال",
|
||||
"verification": "تأیید",
|
||||
"reputation": "اعتبار",
|
||||
"verifyIdentity": "تأیید هویت",
|
||||
"trustScore": "امتیاز اعتماد",
|
||||
"badges": "نشانها",
|
||||
"roles": "نقشها",
|
||||
"privacy": "تنظیمات حریم خصوصی"
|
||||
},
|
||||
"tokenomics": {
|
||||
"title": "اقتصاد توکن",
|
||||
"totalSupply": "عرضه کل",
|
||||
"circulating": "عرضه در گردش",
|
||||
"staked": "استیک شده",
|
||||
"rewards": "مخزن پاداش"
|
||||
},
|
||||
"team": {
|
||||
"title": "قهرمانان ما",
|
||||
"subtitle": "احترام به رهبران کرد و مبارزان آزادی"
|
||||
},
|
||||
"footer": {
|
||||
"about": "درباره",
|
||||
"governance": "حکمرانی",
|
||||
"developers": "توسعهدهندگان",
|
||||
"community": "جامعه",
|
||||
"rights": "تمامی حقوق محفوظ است"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user