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:
2026-02-22 04:48:20 +03:00
parent df22c9ba10
commit d282f609aa
129 changed files with 22442 additions and 4186 deletions
+50 -60
View File
@@ -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>
+26 -24
View File
@@ -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>
)}