refactor(core): Apply various updates and fixes across components

This commit is contained in:
2025-11-19 18:56:38 +03:00
parent e75beebebe
commit d4ce6bf8de
21 changed files with 888 additions and 240 deletions
+5 -2
View File
@@ -24,6 +24,7 @@ import { WebSocketProvider } from '@/contexts/WebSocketContext';
import { IdentityProvider } from '@/contexts/IdentityContext';
import { AuthProvider } from '@/contexts/AuthContext';
import { DashboardProvider } from '@/contexts/DashboardContext';
import { ReferralProvider } from '@/contexts/ReferralContext';
import { ProtectedRoute } from '@/components/ProtectedRoute';
import NotFound from '@/pages/NotFound';
import { Toaster } from '@/components/ui/toaster';
@@ -42,7 +43,8 @@ function App() {
<WebSocketProvider>
<IdentityProvider>
<DashboardProvider>
<Router>
<ReferralProvider>
<Router>
<Routes>
<Route path="/login" element={<Login />} />
@@ -100,7 +102,8 @@ function App() {
} />
<Route path="*" element={<NotFound />} />
</Routes>
</Router>
</Router>
</ReferralProvider>
</DashboardProvider>
</IdentityProvider>
</WebSocketProvider>
+7 -7
View File
@@ -10,13 +10,13 @@ import type { TikiInfo } from '@pezkuwi/lib/citizenship-workflow';
const getTikiIcon = (role: string) => {
const roleLower = role.toLowerCase();
if (roleLower.includes('hemwelatî') || roleLower.includes('welati') || roleLower.includes('citizen')) {
if (roleLower.includes('welati') || roleLower.includes('citizen')) {
return <Shield className="w-6 h-6 text-cyan-500" />;
}
if (roleLower.includes('leader') || roleLower.includes('chief')) {
if (roleLower.includes('serok') || roleLower.includes('leader') || roleLower.includes('chief')) {
return <Crown className="w-6 h-6 text-yellow-500" />;
}
if (roleLower.includes('elder') || roleLower.includes('wise')) {
if (roleLower.includes('axa') || roleLower.includes('hekem') || roleLower.includes('elder') || roleLower.includes('wise')) {
return <Award className="w-6 h-6 text-purple-500" />;
}
return <Users className="w-6 h-6 text-green-500" />;
@@ -26,13 +26,13 @@ const getTikiIcon = (role: string) => {
const getRoleBadgeColor = (role: string) => {
const roleLower = role.toLowerCase();
if (roleLower.includes('hemwelatî') || roleLower.includes('welati') || roleLower.includes('citizen')) {
if (roleLower.includes('welati') || roleLower.includes('citizen')) {
return 'bg-cyan-500/10 text-cyan-500 border-cyan-500/30';
}
if (roleLower.includes('leader') || roleLower.includes('chief')) {
if (roleLower.includes('serok') || roleLower.includes('leader') || roleLower.includes('chief')) {
return 'bg-yellow-500/10 text-yellow-500 border-yellow-500/30';
}
if (roleLower.includes('elder') || roleLower.includes('wise')) {
if (roleLower.includes('axa') || roleLower.includes('hekem') || roleLower.includes('elder') || roleLower.includes('wise')) {
return 'bg-purple-500/10 text-purple-500 border-purple-500/30';
}
return 'bg-green-500/10 text-green-500 border-green-500/30';
@@ -149,7 +149,7 @@ export const NftList: React.FC = () => {
Tiki #{tiki.id}
</h3>
<Badge className={getRoleBadgeColor(tiki.role)}>
{tiki.role === 'Hemwelatî' ? 'Welati' : tiki.role}
{tiki.role}
</Badge>
</div>
+83 -8
View File
@@ -1,23 +1,85 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import { Loader2 } from 'lucide-react';
import { usePolkadot } from '@/contexts/PolkadotContext';
import { Loader2, Wallet } from 'lucide-react';
import { Button } from '@/components/ui/button';
interface ProtectedRouteProps {
children: React.ReactNode;
requireAdmin?: boolean;
}
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
children,
requireAdmin = false
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
children,
requireAdmin = false
}) => {
const { user, loading, isAdmin } = useAuth();
const { selectedAccount, connectWallet } = usePolkadot();
const [walletRestoreChecked, setWalletRestoreChecked] = useState(false);
const [forceUpdate, setForceUpdate] = useState(0);
if (loading) {
// Listen for wallet changes
useEffect(() => {
const handleWalletChange = () => {
setForceUpdate(prev => prev + 1);
};
window.addEventListener('walletChanged', handleWalletChange);
return () => window.removeEventListener('walletChanged', handleWalletChange);
}, []);
// Wait for wallet restoration (max 3 seconds)
useEffect(() => {
const timeout = setTimeout(() => {
setWalletRestoreChecked(true);
}, 3000);
// If wallet restored earlier, clear timeout
if (selectedAccount) {
setWalletRestoreChecked(true);
clearTimeout(timeout);
}
return () => clearTimeout(timeout);
}, [selectedAccount, forceUpdate]);
// Show loading while:
// 1. Auth is loading, OR
// 2. Wallet restoration not checked yet
if (loading || !walletRestoreChecked) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-900">
<Loader2 className="w-8 h-8 animate-spin text-green-500" />
<div className="text-center">
<Loader2 className="w-8 h-8 animate-spin text-green-500 mx-auto mb-4" />
<p className="text-gray-400">
{!walletRestoreChecked ? 'Restoring wallet connection...' : 'Loading...'}
</p>
</div>
</div>
);
}
// For admin routes, require wallet connection
if (requireAdmin && !selectedAccount) {
const handleConnect = async () => {
await connectWallet();
// Event is automatically dispatched by handleSetSelectedAccount wrapper
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-900">
<div className="text-center max-w-md">
<Wallet className="w-16 h-16 text-green-500 mx-auto mb-4" />
<h2 className="text-2xl font-bold text-white mb-2">Connect Your Wallet</h2>
<p className="text-gray-400 mb-6">
Admin panel requires wallet authentication. Please connect your wallet to continue.
</p>
<Button onClick={handleConnect} size="lg" className="bg-green-600 hover:bg-green-700">
<Wallet className="mr-2 h-5 w-5" />
Connect Wallet
</Button>
</div>
</div>
);
}
@@ -27,7 +89,20 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
}
if (requireAdmin && !isAdmin) {
return <Navigate to="/" replace />;
return (
<div className="min-h-screen flex items-center justify-center bg-gray-900">
<div className="text-center max-w-md">
<div className="text-red-500 text-6xl mb-4"></div>
<h2 className="text-2xl font-bold text-white mb-2">Access Denied</h2>
<p className="text-gray-400 mb-4">
Your wallet ({selectedAccount?.address.slice(0, 8)}...) does not have admin privileges.
</p>
<p className="text-sm text-gray-500">
Only founder and commission members can access the admin panel.
</p>
</div>
</div>
);
}
return <>{children}</>;
+4 -4
View File
@@ -863,8 +863,8 @@ const TokenSwap = () => {
type="number"
value={fromAmount}
onChange={(e) => setFromAmount(e.target.value)}
placeholder="0.0"
className="text-2xl font-bold border-0 bg-transparent text-white placeholder:text-gray-600"
placeholder="Amount"
className="text-2xl font-bold border-0 bg-transparent text-white placeholder:text-gray-500 placeholder:opacity-50"
disabled={!selectedAccount}
/>
<Select
@@ -934,8 +934,8 @@ const TokenSwap = () => {
type="text"
value={toAmount}
readOnly
placeholder="0.0"
className="text-2xl font-bold border-0 bg-transparent text-white placeholder:text-gray-600"
placeholder="Amount"
className="text-2xl font-bold border-0 bg-transparent text-white placeholder:text-gray-500 placeholder:opacity-50"
/>
<Select
value={toToken}
+4 -4
View File
@@ -269,8 +269,8 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, s
id="recipient"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
className="bg-gray-800 border-gray-700 text-white mt-2"
placeholder="Recipient address"
className="bg-gray-800 border-gray-700 text-white mt-2 placeholder:text-gray-500 placeholder:opacity-50"
disabled={isTransferring}
/>
</div>
@@ -283,8 +283,8 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, s
step={selectedToken === 'HEZ' || selectedToken === 'PEZ' ? '0.0001' : '0.000001'}
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.0000"
className="bg-gray-800 border-gray-700 text-white mt-2"
placeholder="Amount"
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">
+6 -6
View File
@@ -214,8 +214,8 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
type="number"
value={depositAmount}
onChange={(e) => setDepositAmount(e.target.value)}
placeholder="0.00"
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="Amount"
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}
/>
</div>
@@ -279,9 +279,9 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
type="number"
value={withdrawAmount}
onChange={(e) => setWithdrawAmount(e.target.value)}
placeholder="0.00"
placeholder="Amount"
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"
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}
/>
<button
@@ -300,8 +300,8 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
type="text"
value={withdrawAddress}
onChange={(e) => setWithdrawAddress(e.target.value)}
placeholder="Enter bank account or crypto address"
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="Bank account or crypto address"
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}
/>
</div>
@@ -391,10 +391,10 @@ export function CommissionSetupTab() {
</div>
<div className="flex flex-col gap-2">
<textarea
placeholder="Paste addresses, one per line&#10;Example:&#10;5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty&#10;5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y"
placeholder="Member addresses, one per line"
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]"
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"
/>
<Button
onClick={handleAddMember}
@@ -13,10 +13,11 @@ import { NewCitizenApplication } from './NewCitizenApplication';
interface CitizenshipModalProps {
isOpen: boolean;
onClose: () => void;
referrerAddress?: string | null;
}
export const CitizenshipModal: React.FC<CitizenshipModalProps> = ({ isOpen, onClose }) => {
const [activeTab, setActiveTab] = useState<'existing' | 'new'>('existing');
export const CitizenshipModal: React.FC<CitizenshipModalProps> = ({ isOpen, onClose, referrerAddress }) => {
const [activeTab, setActiveTab] = useState<'existing' | 'new'>(referrerAddress ? 'new' : 'existing');
return (
<Dialog open={isOpen} onOpenChange={onClose}>
@@ -26,7 +27,9 @@ export const CitizenshipModal: React.FC<CitizenshipModalProps> = ({ isOpen, onCl
🏛 Digital Kurdistan Citizenship
</DialogTitle>
<DialogDescription>
Join the Digital Kurdistan State as a citizen or authenticate your existing citizenship
{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'}
</DialogDescription>
</DialogHeader>
@@ -41,7 +44,7 @@ export const CitizenshipModal: React.FC<CitizenshipModalProps> = ({ isOpen, onCl
</TabsContent>
<TabsContent value="new" className="mt-6">
<NewCitizenApplication onClose={onClose} />
<NewCitizenApplication onClose={onClose} referrerAddress={referrerAddress} />
</TabsContent>
</Tabs>
</DialogContent>
@@ -6,7 +6,7 @@ import { Alert, AlertDescription } from '@/components/ui/alert';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Loader2, CheckCircle, AlertTriangle, Shield } from 'lucide-react';
import { usePolkadot } from '@/contexts/PolkadotContext';
import { verifyNftOwnership } from '@pezkuwi/lib/citizenship-workflow';
import { verifyCitizenNumber } from '@pezkuwi/lib/tiki';
import { generateAuthChallenge, signChallenge, verifySignature, saveCitizenSession } from '@pezkuwi/lib/citizenship-workflow';
import type { AuthChallenge } from '@pezkuwi/lib/citizenship-workflow';
@@ -17,7 +17,7 @@ interface ExistingCitizenAuthProps {
export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClose }) => {
const { api, isApiReady, selectedAccount, connectWallet } = usePolkadot();
const [tikiNumber, setTikiNumber] = useState('');
const [citizenNumber, setCitizenNumber] = useState('');
const [step, setStep] = useState<'input' | 'verifying' | 'signing' | 'success' | 'error'>('input');
const [error, setError] = useState<string | null>(null);
const [challenge, setChallenge] = useState<AuthChallenge | null>(null);
@@ -28,8 +28,8 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
return;
}
if (!tikiNumber.trim()) {
setError('Please enter your Welati Tiki NFT number');
if (!citizenNumber.trim()) {
setError('Please enter your Citizen Number');
return;
}
@@ -37,22 +37,22 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
setStep('verifying');
try {
// Verify NFT ownership
const ownsNFT = await verifyNftOwnership(api, tikiNumber, selectedAccount.address);
// Verify Citizen Number
const isValid = await verifyCitizenNumber(api, citizenNumber, selectedAccount.address);
if (!ownsNFT) {
setError(`NFT #${tikiNumber} not found in your wallet or not a Welati Tiki`);
if (!isValid) {
setError(`Invalid Citizen Number or it doesn't match your wallet`);
setStep('error');
return;
}
// Generate challenge for signature
const authChallenge = generateAuthChallenge(tikiNumber);
const authChallenge = generateAuthChallenge(citizenNumber);
setChallenge(authChallenge);
setStep('signing');
} catch (err) {
console.error('Verification error:', err);
setError('Failed to verify NFT ownership');
setError('Failed to verify Citizen Number');
setStep('error');
}
};
@@ -80,7 +80,7 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
// Save session
const session = {
tikiNumber,
tikiNumber: citizenNumber,
walletAddress: selectedAccount.address,
sessionToken: signature, // In production, use proper JWT
lastAuthenticated: Date.now(),
@@ -91,11 +91,10 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
setStep('success');
// Redirect to citizen dashboard after 2 seconds
// Redirect to citizens page after 2 seconds
setTimeout(() => {
// TODO: Navigate to citizen dashboard
onClose();
window.location.href = '/dashboard'; // Or use router.push('/dashboard')
window.location.href = '/citizens';
}, 2000);
} catch (err) {
console.error('Signature error:', err);
@@ -121,24 +120,24 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
Authenticate as Citizen
</CardTitle>
<CardDescription>
Enter your Welati Tiki NFT number to authenticate
Enter your Citizen Number from your Dashboard to authenticate
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Step 1: Enter NFT Number */}
{/* Step 1: Enter Citizen Number */}
{step === 'input' && (
<>
<div className="space-y-2">
<Label htmlFor="tikiNumber">Welati Tiki NFT Number</Label>
<Label htmlFor="citizenNumber">Citizen Number</Label>
<Input
id="tikiNumber"
placeholder="e.g., 12345"
value={tikiNumber}
onChange={(e) => setTikiNumber(e.target.value)}
id="citizenNumber"
placeholder="e.g., #42-0-123456"
value={citizenNumber}
onChange={(e) => setCitizenNumber(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleVerifyNFT()}
/>
<p className="text-xs text-muted-foreground">
This is your unique citizen ID number received after KYC approval
Enter your full Citizen Number from your Dashboard (format: #CollectionID-ItemID-6digits)
</p>
</div>
@@ -148,7 +147,7 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
</Button>
) : (
<Button onClick={handleVerifyNFT} className="w-full">
Verify NFT Ownership
Verify Citizen Number
</Button>
)}
</>
@@ -158,7 +157,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 NFT ownership on blockchain...</p>
<p className="text-sm text-muted-foreground">Verifying Citizen Number on blockchain...</p>
</div>
)}
@@ -191,7 +190,7 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
<CheckCircle className="h-16 w-16 text-green-500" />
<h3 className="text-lg font-semibold">Authentication Successful!</h3>
<p className="text-sm text-muted-foreground text-center">
Welcome back, Citizen #{tikiNumber}
Welcome back, Citizen #{citizenNumber}
</p>
<p className="text-xs text-muted-foreground">
Redirecting to citizen dashboard...
@@ -8,7 +8,7 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
import { Loader2, AlertTriangle, CheckCircle, User, Users as UsersIcon, MapPin, Briefcase, Mail, Clock } from 'lucide-react';
import { Loader2, AlertTriangle, CheckCircle, User, Users as UsersIcon, MapPin, Briefcase, Mail, Clock, Check, X, AlertCircle } from 'lucide-react';
import { usePolkadot } from '@/contexts/PolkadotContext';
import type { CitizenshipData, Region, MaritalStatus } from '@pezkuwi/lib/citizenship-workflow';
import { FOUNDER_ADDRESS, submitKycApplication, subscribeToKycApproval, getKycStatus } from '@pezkuwi/lib/citizenship-workflow';
@@ -16,11 +16,12 @@ import { generateCommitmentHash, generateNullifierHash, encryptData, saveLocalCi
interface NewCitizenApplicationProps {
onClose: () => void;
referrerAddress?: string | null;
}
type FormData = Omit<CitizenshipData, 'walletAddress' | 'timestamp'>;
export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ onClose }) => {
export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ onClose, referrerAddress }) => {
const { api, isApiReady, selectedAccount, connectWallet } = usePolkadot();
const { register, handleSubmit, watch, setValue, formState: { errors } } = useForm<FormData>();
@@ -31,10 +32,80 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
const [error, setError] = useState<string | null>(null);
const [agreed, setAgreed] = useState(false);
const [checkingStatus, setCheckingStatus] = useState(false);
const [confirming, setConfirming] = useState(false);
const [applicationHash, setApplicationHash] = useState<string>('');
const maritalStatus = watch('maritalStatus');
const childrenCount = watch('childrenCount');
const handleApprove = async () => {
if (!api || !selectedAccount) {
setError('Please connect your wallet first');
return;
}
setConfirming(true);
try {
const { web3FromAddress } = await import('@polkadot/extension-dapp');
const injector = await web3FromAddress(selectedAccount.address);
console.log('Confirming citizenship application (self-confirmation)...');
// Call confirm_citizenship() extrinsic - self-confirmation for Welati Tiki
const tx = api.tx.identityKyc.confirmCitizenship();
await tx.signAndSend(selectedAccount.address, { signer: injector.signer }, ({ status, events, dispatchError }) => {
if (dispatchError) {
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule);
console.error(`${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`);
setError(`${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`);
} else {
console.error(dispatchError.toString());
setError(dispatchError.toString());
}
setConfirming(false);
return;
}
if (status.isInBlock || status.isFinalized) {
console.log('✅ Citizenship confirmed successfully!');
console.log('Block hash:', status.asInBlock || status.asFinalized);
// Check for CitizenshipConfirmed event
events.forEach(({ event }) => {
if (event.section === 'identityKyc' && event.method === 'CitizenshipConfirmed') {
console.log('📢 CitizenshipConfirmed event detected');
setKycApproved(true);
setWaitingForApproval(false);
// Redirect to citizen dashboard after 2 seconds
setTimeout(() => {
onClose();
window.location.href = '/dashboard';
}, 2000);
}
});
setConfirming(false);
}
});
} catch (err: any) {
console.error('Approval error:', err);
setError(err.message || 'Failed to approve application');
setConfirming(false);
}
};
const handleReject = async () => {
// Cancel/withdraw the application - simply close modal and go back
// No blockchain interaction needed - application will remain Pending until confirmed or admin-rejected
console.log('Canceling citizenship application (no blockchain interaction)');
onClose();
window.location.href = '/';
};
// Check KYC status on mount
useEffect(() => {
const checkKycStatus = async () => {
@@ -139,6 +210,13 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
return;
}
// Note: Referral initiation must be done by the REFERRER before the referee does KYC
// The referrer calls api.tx.referral.initiateReferral(refereeAddress) from InviteUserModal
// Here we just use the referrerAddress in the citizenship data if provided
if (referrerAddress) {
console.log(`KYC application with referrer: ${referrerAddress}`);
}
// Prepare complete citizenship data
const citizenshipData: CitizenshipData = {
...data,
@@ -193,6 +271,11 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
console.log('✅ KYC application submitted to blockchain');
console.log('Block hash:', result.blockHash);
// Save block hash for display
if (result.blockHash) {
setApplicationHash(result.blockHash.slice(0, 16) + '...');
}
// Move to waiting for approval state
setSubmitted(true);
setSubmitting(false);
@@ -238,59 +321,107 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
);
}
// Waiting for approval - Loading state
// Waiting for self-confirmation
if (waitingForApproval) {
return (
<Card>
<CardContent className="pt-6 flex flex-col items-center justify-center py-8 space-y-6">
{/* Animated Loader with Halos */}
<div className="relative flex items-center justify-center">
{/* Outer halo */}
<div className="absolute w-32 h-32 border-4 border-cyan-500/20 rounded-full animate-ping"></div>
{/* Middle halo */}
<div className="absolute w-24 h-24 border-4 border-purple-500/30 rounded-full animate-pulse"></div>
{/* Inner spinning sun */}
<div className="relative w-16 h-16 flex items-center justify-center">
<Loader2 className="w-16 h-16 text-cyan-500 animate-spin" />
<Clock className="absolute w-8 h-8 text-yellow-400 animate-pulse" />
{/* Icon */}
<div className="relative">
<div className="h-24 w-24 rounded-full border-4 border-primary/20 flex items-center justify-center">
<CheckCircle className="h-10 w-10 text-primary" />
</div>
</div>
<div className="text-center space-y-2">
<h3 className="text-lg font-semibold">Waiting for Admin Approval</h3>
<h3 className="text-lg font-semibold">Confirm Your Citizenship Application</h3>
<p className="text-sm text-muted-foreground max-w-md">
Your application has been submitted to the blockchain and is waiting for admin approval.
This page will automatically update when your citizenship is approved.
Your application has been submitted to the blockchain. Please review and confirm your identity to mint your Citizen NFT (Welati Tiki).
</p>
</div>
{/* Status steps */}
<div className="w-full max-w-md space-y-3 pt-4">
<div className="flex items-center gap-3 text-sm">
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0" />
<span className="text-muted-foreground">Application encrypted and stored on IPFS</span>
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-green-100 dark:bg-green-900">
<Check className="h-5 w-5 text-green-600 dark:text-green-400" />
</div>
<div className="flex-1">
<p className="text-sm font-medium">Data Encrypted</p>
<p className="text-xs text-muted-foreground">Your KYC data has been encrypted and stored on IPFS</p>
</div>
</div>
<div className="flex items-center gap-3 text-sm">
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0" />
<span className="text-muted-foreground">Transaction submitted to blockchain</span>
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-green-100 dark:bg-green-900">
<Check className="h-5 w-5 text-green-600 dark:text-green-400" />
</div>
<div className="flex-1">
<p className="text-sm font-medium">Blockchain Submitted</p>
<p className="text-xs text-muted-foreground">Transaction hash: {applicationHash || 'Processing...'}</p>
</div>
</div>
<div className="flex items-center gap-3 text-sm">
<Loader2 className="h-5 w-5 text-cyan-500 animate-spin flex-shrink-0" />
<span className="font-medium">Waiting for admin to approve KYC...</span>
</div>
<div className="flex items-center gap-3 text-sm opacity-50">
<Clock className="h-5 w-5 text-gray-400 flex-shrink-0" />
<span className="text-muted-foreground">Receive Welati Tiki NFT</span>
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900">
<AlertCircle className="h-5 w-5 text-blue-600 dark:text-blue-400" />
</div>
<div className="flex-1">
<p className="text-sm font-medium">Awaiting Your Confirmation</p>
<p className="text-xs text-muted-foreground">Confirm or reject your application below</p>
</div>
</div>
</div>
{/* Info */}
<Alert className="bg-cyan-500/10 border-cyan-500/30">
<AlertDescription className="text-xs">
<strong>Note:</strong> Do not close this page. The system is monitoring the blockchain
for approval events in real-time. You will be automatically redirected once approved.
</AlertDescription>
</Alert>
{/* Action buttons */}
<div className="flex gap-3 w-full max-w-md pt-4">
<Button
onClick={handleApprove}
disabled={confirming}
className="flex-1 bg-green-600 hover:bg-green-700"
>
{confirming ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Confirming...
</>
) : (
<>
<Check className="h-4 w-4 mr-2" />
Approve
</>
)}
</Button>
<Button
onClick={handleReject}
disabled={confirming}
variant="destructive"
className="flex-1"
>
{confirming ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Rejecting...
</>
) : (
<>
<X className="h-4 w-4 mr-2" />
Reject
</>
)}
</Button>
</div>
{error && (
<Alert variant="destructive" className="w-full max-w-md">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Button variant="outline" onClick={onClose} className="mt-2">
Close
</Button>
</CardContent>
</Card>
);
@@ -493,7 +624,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
</CardDescription>
</CardHeader>
<CardContent>
<Input {...register('referralCode')} placeholder="Optional - Leave empty to be auto-assigned to Founder" />
<Input {...register('referralCode')} placeholder="Referral code (optional)" 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)
</p>
+17 -12
View File
@@ -172,13 +172,14 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
</div>
<div>
<Label htmlFor="amountCrypto">Amount ({token})</Label>
<Input
id="amountCrypto"
type="number"
<Input
id="amountCrypto"
type="number"
step="0.01"
value={amountCrypto}
value={amountCrypto}
onChange={e => setAmountCrypto(e.target.value)}
placeholder="10.00"
placeholder="Amount"
className="placeholder:text-gray-500 placeholder:opacity-50"
/>
</div>
</div>
@@ -202,13 +203,14 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
</div>
<div>
<Label htmlFor="fiatAmount">Total Amount ({fiatCurrency})</Label>
<Input
id="fiatAmount"
type="number"
<Input
id="fiatAmount"
type="number"
step="0.01"
value={fiatAmount}
value={fiatAmount}
onChange={e => setFiatAmount(e.target.value)}
placeholder="1000.00"
placeholder="Amount"
className="placeholder:text-gray-500 placeholder:opacity-50"
/>
</div>
</div>
@@ -254,6 +256,7 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
value={paymentDetails[field] || ''}
onChange={(e) => handlePaymentDetailChange(field, e.target.value)}
placeholder={placeholder}
className="placeholder:text-gray-500 placeholder:opacity-50"
/>
</div>
))}
@@ -270,7 +273,8 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
step="0.01"
value={minOrderAmount}
onChange={e => setMinOrderAmount(e.target.value)}
placeholder={`Min ${token} per trade`}
placeholder="Minimum amount (optional)"
className="placeholder:text-gray-500 placeholder:opacity-50"
/>
</div>
<div>
@@ -281,7 +285,8 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
step="0.01"
value={maxOrderAmount}
onChange={e => setMaxOrderAmount(e.target.value)}
placeholder={`Max ${token} per trade`}
placeholder="Maximum amount (optional)"
className="placeholder:text-gray-500 placeholder:opacity-50"
/>
</div>
</div>
+2 -2
View File
@@ -114,8 +114,8 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
step="0.01"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder={`Enter amount (max ${offer.remaining_amount})`}
className="bg-gray-800 border-gray-700 text-white"
placeholder="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">
+51 -18
View File
@@ -96,42 +96,75 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
// Check active sessions and sets the user
supabase.auth.getSession().then(({ data: { session } }) => {
setUser(session?.user ?? null);
if (session?.user) {
checkAdminStatus();
}
checkAdminStatus(); // Check admin status regardless of Supabase session
setLoading(false);
}).catch(() => {
// If Supabase is not available, continue without auth
// If Supabase is not available, still check wallet-based admin
checkAdminStatus();
setLoading(false);
});
// Listen for changes on auth state
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
setUser(session?.user ?? null);
if (session?.user) {
checkAdminStatus();
}
checkAdminStatus(); // Check admin status on auth change
setLoading(false);
});
return () => subscription.unsubscribe();
// Listen for wallet changes (from PolkadotContext)
const handleWalletChange = () => {
checkAdminStatus();
};
window.addEventListener('walletChanged', handleWalletChange);
return () => {
subscription.unsubscribe();
window.removeEventListener('walletChanged', handleWalletChange);
};
}, []);
const checkAdminStatus = async () => {
// Admin wallet whitelist (blockchain-based auth)
const ADMIN_WALLETS = [
'5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', // Founder (original)
'5DFwqK698vL4gXHEcanaewnAqhxJ2rjhAogpSTHw3iwGDwd3', // Founder delegate (initial KYC member)
'5GgTgG9sRmPQAYU1RsTejZYnZRjwzKZKWD3awtuqjHioki45', // Founder (current dev wallet)
];
try {
// PRIMARY: Check wallet-based admin (blockchain auth)
const connectedWallet = localStorage.getItem('selectedWallet');
console.log('🔍 Admin check - Connected wallet:', connectedWallet);
console.log('🔍 Admin check - Whitelist:', ADMIN_WALLETS);
if (connectedWallet && ADMIN_WALLETS.includes(connectedWallet)) {
console.log('✅ Admin access granted (wallet-based)');
setIsAdmin(true);
return true;
}
// SECONDARY: Check Supabase admin_roles (if wallet not in whitelist)
const { data: { user } } = await supabase.auth.getUser();
if (!user) return false;
if (user) {
const { data, error } = await supabase
.from('admin_roles')
.select('role')
.eq('user_id', user.id)
.maybeSingle();
const { data, error } = await supabase
.from('admin_roles')
.select('role')
.eq('user_id', user.id)
.maybeSingle();
if (!error && data && ['admin', 'super_admin'].includes(data.role)) {
console.log('✅ Admin access granted (Supabase-based)');
setIsAdmin(true);
return true;
}
}
const adminStatus = !error && data && ['admin', 'super_admin'].includes(data.role);
setIsAdmin(adminStatus);
return adminStatus;
} catch {
console.log('❌ Admin access denied');
setIsAdmin(false);
return false;
} catch (err) {
console.error('Admin check error:', err);
setIsAdmin(false);
return false;
}
};
+62 -11
View File
@@ -33,6 +33,19 @@ export const PolkadotProvider: React.FC<PolkadotProviderProps> = ({
const [selectedAccount, setSelectedAccount] = useState<InjectedAccountWithMeta | null>(null);
const [error, setError] = useState<string | null>(null);
// Wrapper to trigger events when wallet changes
const handleSetSelectedAccount = (account: InjectedAccountWithMeta | null) => {
setSelectedAccount(account);
if (account) {
localStorage.setItem('selectedWallet', account.address);
console.log('💾 Wallet saved:', account.address);
window.dispatchEvent(new Event('walletChanged'));
} else {
localStorage.removeItem('selectedWallet');
window.dispatchEvent(new Event('walletChanged'));
}
};
// Initialize Polkadot API
useEffect(() => {
const initApi = async () => {
@@ -76,35 +89,73 @@ export const PolkadotProvider: React.FC<PolkadotProviderProps> = ({
};
}, [endpoint]);
// Auto-restore wallet on page load
useEffect(() => {
const restoreWallet = async () => {
const savedAddress = localStorage.getItem('selectedWallet');
if (!savedAddress) return;
try {
// Enable extension
const extensions = await web3Enable('PezkuwiChain');
if (extensions.length === 0) return;
// Get accounts
const allAccounts = await web3Accounts();
if (allAccounts.length === 0) return;
// Find saved account
const savedAccount = allAccounts.find(acc => acc.address === savedAddress);
if (savedAccount) {
setAccounts(allAccounts);
handleSetSelectedAccount(savedAccount);
console.log('✅ Wallet restored:', savedAddress.slice(0, 8) + '...');
}
} catch (err) {
console.error('Failed to restore wallet:', err);
}
};
restoreWallet();
}, []);
// Connect wallet (Polkadot.js extension)
const connectWallet = async () => {
try {
setError(null);
// Enable extension
const extensions = await web3Enable('PezkuwiChain');
if (extensions.length === 0) {
setError('Please install Polkadot.js extension');
window.open('https://polkadot.js.org/extension/', '_blank');
return;
}
console.log('✅ Polkadot.js extension enabled');
// Get accounts
const allAccounts = await web3Accounts();
if (allAccounts.length === 0) {
setError('No accounts found. Please create an account in Polkadot.js extension');
return;
}
setAccounts(allAccounts);
setSelectedAccount(allAccounts[0]); // Auto-select first account
// Try to restore previously selected account, otherwise use first
const savedAddress = localStorage.getItem('selectedWallet');
const accountToSelect = savedAddress
? allAccounts.find(acc => acc.address === savedAddress) || allAccounts[0]
: allAccounts[0];
// Use wrapper to trigger events
handleSetSelectedAccount(accountToSelect);
console.log(`✅ Found ${allAccounts.length} account(s)`);
} catch (err) {
console.error('❌ Wallet connection failed:', err);
setError('Failed to connect wallet');
@@ -114,7 +165,7 @@ export const PolkadotProvider: React.FC<PolkadotProviderProps> = ({
// Disconnect wallet
const disconnectWallet = () => {
setAccounts([]);
setSelectedAccount(null);
handleSetSelectedAccount(null);
console.log('🔌 Wallet disconnected');
};
@@ -124,7 +175,7 @@ export const PolkadotProvider: React.FC<PolkadotProviderProps> = ({
isConnected: isApiReady, // Alias for backward compatibility
accounts,
selectedAccount,
setSelectedAccount,
setSelectedAccount: handleSetSelectedAccount,
connectWallet,
disconnectWallet,
error,
+4 -3
View File
@@ -38,9 +38,10 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
const connectionAttempts = useRef(0);
const ENDPOINTS = [
'wss://ws.pezkuwichain.io', // Production WebSocket
'ws://localhost:9944', // Local development node
'ws://127.0.0.1:9944', // Alternative local address
'ws://localhost:8082', // Local Vite dev server
'ws://127.0.0.1:9944', // Local development node (primary)
'ws://localhost:9944', // Local development node (alternative)
'wss://ws.pezkuwichain.io', // Production WebSocket (fallback)
];
const connect = useCallback((endpointIndex: number = 0) => {
+50 -23
View File
@@ -27,6 +27,10 @@ import {
import { SessionMonitor } from '@/components/security/SessionMonitor';
import { PermissionEditor } from '@/components/security/PermissionEditor';
import { SecurityAudit } from '@/components/security/SecurityAudit';
import { KycApprovalTab } from '@/components/admin/KycApprovalTab';
import { CommissionVotingTab } from '@/components/admin/CommissionVotingTab';
import { CommissionSetupTab } from '@/components/admin/CommissionSetupTab';
export default function AdminPanel() {
const navigate = useNavigate();
const [users, setUsers] = useState<any[]>([]);
@@ -149,38 +153,58 @@ export default function AdminPanel() {
</button>
<h1 className="text-3xl font-bold mb-8">Admin Panel</h1>
<Tabs defaultValue="users" className="space-y-4">
<TabsList className="grid w-full grid-cols-7">
<TabsTrigger value="users">
<Users className="mr-2 h-4 w-4" />
Users
<Tabs defaultValue="setup" className="space-y-4">
<TabsList className="grid w-full grid-cols-10 h-auto">
<TabsTrigger value="setup" className="flex-col h-auto py-3">
<Shield className="h-4 w-4 mb-1" />
<span className="text-xs leading-tight">Commission<br/>Setup</span>
</TabsTrigger>
<TabsTrigger value="roles">
<Shield className="mr-2 h-4 w-4" />
Roles
<TabsTrigger value="kyc" className="flex-col h-auto py-3">
<Users className="h-4 w-4 mb-1" />
<span className="text-xs leading-tight">KYC<br/>Approvals</span>
</TabsTrigger>
<TabsTrigger value="sessions">
<Monitor className="mr-2 h-4 w-4" />
Sessions
<TabsTrigger value="voting" className="flex-col h-auto py-3">
<Activity className="h-4 w-4 mb-1" />
<span className="text-xs leading-tight">Commission<br/>Voting</span>
</TabsTrigger>
<TabsTrigger value="permissions">
<Lock className="mr-2 h-4 w-4" />
Permissions
<TabsTrigger value="users" className="flex-col h-auto py-3">
<Users className="h-4 w-4 mb-1" />
<span className="text-xs leading-tight">Users</span>
</TabsTrigger>
<TabsTrigger value="security">
<AlertTriangle className="mr-2 h-4 w-4" />
Security
<TabsTrigger value="roles" className="flex-col h-auto py-3">
<Shield className="h-4 w-4 mb-1" />
<span className="text-xs leading-tight">Roles</span>
</TabsTrigger>
<TabsTrigger value="activity">
<Activity className="mr-2 h-4 w-4" />
Activity
<TabsTrigger value="sessions" className="flex-col h-auto py-3">
<Monitor className="h-4 w-4 mb-1" />
<span className="text-xs leading-tight">Sessions</span>
</TabsTrigger>
<TabsTrigger value="settings">
<Settings className="mr-2 h-4 w-4" />
Settings
<TabsTrigger value="permissions" className="flex-col h-auto py-3">
<Lock className="h-4 w-4 mb-1" />
<span className="text-xs leading-tight">Permissions</span>
</TabsTrigger>
<TabsTrigger value="security" className="flex-col h-auto py-3">
<AlertTriangle className="h-4 w-4 mb-1" />
<span className="text-xs leading-tight">Security</span>
</TabsTrigger>
<TabsTrigger value="activity" className="flex-col h-auto py-3">
<Activity className="h-4 w-4 mb-1" />
<span className="text-xs leading-tight">Activity</span>
</TabsTrigger>
<TabsTrigger value="settings" className="flex-col h-auto py-3">
<Settings className="h-4 w-4 mb-1" />
<span className="text-xs leading-tight">Settings</span>
</TabsTrigger>
</TabsList>
<TabsContent value="kyc">
<KycApprovalTab />
</TabsContent>
<TabsContent value="voting">
<CommissionVotingTab />
</TabsContent>
<TabsContent value="users">
<Card>
<CardHeader>
@@ -323,6 +347,9 @@ export default function AdminPanel() {
</CardContent>
</Card>
</TabsContent>
<TabsContent value="setup">
<CommissionSetupTab />
</TabsContent>
</Tabs>
</div>
);
+87 -58
View File
@@ -1,79 +1,101 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import React, { useState, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { CitizenshipModal } from '@/components/citizenship/CitizenshipModal';
import { Shield, Users, Award, Globe, ChevronRight, ArrowLeft, Home } from 'lucide-react';
import { InviteUserModal } from '@/components/referral/InviteUserModal';
import { Shield, Users, Award, Globe, ChevronRight, ArrowLeft, UserPlus } from 'lucide-react';
const BeCitizen: React.FC = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [isModalOpen, setIsModalOpen] = useState(false);
const [isInviteModalOpen, setIsInviteModalOpen] = useState(false);
const [referrerAddress, setReferrerAddress] = useState<string | null>(null);
// Check for referral parameter on mount
useEffect(() => {
const ref = searchParams.get('ref');
if (ref) {
setReferrerAddress(ref);
// Auto-open modal if coming from referral link
setIsModalOpen(true);
}
}, [searchParams]);
return (
<div className="min-h-screen bg-gradient-to-br from-purple-900 via-indigo-900 to-blue-900">
<div className="min-h-screen bg-gradient-to-br from-green-700 via-white to-red-600">
<div className="container mx-auto px-4 py-16">
{/* Back to Home Button */}
<div className="mb-8">
{/* Back to Home Button and Invite Friend */}
<div className="mb-8 flex justify-between items-center">
<Button
onClick={() => navigate('/')}
variant="outline"
className="bg-white/10 hover:bg-white/20 border-white/30 text-white"
className="bg-red-600 hover:bg-red-700 border-yellow-400 border-2 text-white font-semibold shadow-lg"
>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Home
</Button>
<Button
onClick={() => setIsInviteModalOpen(true)}
className="bg-green-600 hover:bg-green-700 border-yellow-400 border-2 text-white font-semibold shadow-lg"
>
<UserPlus className="mr-2 h-4 w-4" />
Invite Friend
</Button>
</div>
{/* Hero Section */}
<div className="text-center mb-16">
<h1 className="text-5xl md:text-6xl font-bold text-white mb-6">
<h1 className="text-5xl md:text-6xl font-bold text-red-700 mb-6 drop-shadow-lg">
🏛 Digital Kurdistan
</h1>
<h2 className="text-3xl md:text-4xl font-semibold text-cyan-300 mb-4">
<h2 className="text-3xl md:text-4xl font-semibold text-green-700 mb-4 drop-shadow-lg">
Bibe Welati / Be a Citizen
</h2>
<p className="text-xl text-gray-200 max-w-3xl mx-auto">
<p className="text-xl text-gray-800 font-semibold max-w-3xl mx-auto drop-shadow-md">
Join the Digital Kurdistan State as a sovereign citizen. Receive your Welati Tiki NFT and unlock governance, trust scoring, and community benefits.
</p>
</div>
{/* Benefits Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
<Card className="bg-white/10 backdrop-blur-md border-cyan-500/30 hover:border-cyan-500 transition-all">
<Card className="bg-red-50/90 backdrop-blur-md border-red-600/50 hover:border-red-600 transition-all shadow-lg">
<CardHeader>
<Shield className="h-12 w-12 text-cyan-400 mb-3" />
<CardTitle className="text-white">Privacy Protected</CardTitle>
<CardDescription className="text-gray-300">
<Shield className="h-12 w-12 text-red-600 mb-3" />
<CardTitle className="text-red-700 font-bold">Privacy Protected</CardTitle>
<CardDescription className="text-gray-700 font-medium">
Your data is encrypted with ZK-proofs. Only hashes are stored on-chain.
</CardDescription>
</CardHeader>
</Card>
<Card className="bg-white/10 backdrop-blur-md border-purple-500/30 hover:border-purple-500 transition-all">
<Card className="bg-yellow-50/90 backdrop-blur-md border-yellow-600/50 hover:border-yellow-600 transition-all shadow-lg">
<CardHeader>
<Award className="h-12 w-12 text-purple-400 mb-3" />
<CardTitle className="text-white">Welati Tiki NFT</CardTitle>
<CardDescription className="text-gray-300">
<Award className="h-12 w-12 text-yellow-700 mb-3" />
<CardTitle className="text-yellow-800 font-bold">Welati Tiki NFT</CardTitle>
<CardDescription className="text-gray-700 font-medium">
Receive your unique soulbound citizenship NFT after KYC approval.
</CardDescription>
</CardHeader>
</Card>
<Card className="bg-white/10 backdrop-blur-md border-green-500/30 hover:border-green-500 transition-all">
<Card className="bg-green-50/90 backdrop-blur-md border-green-600/50 hover:border-green-600 transition-all shadow-lg">
<CardHeader>
<Users className="h-12 w-12 text-green-400 mb-3" />
<CardTitle className="text-white">Trust Scoring</CardTitle>
<CardDescription className="text-gray-300">
<Users className="h-12 w-12 text-green-600 mb-3" />
<CardTitle className="text-green-700 font-bold">Trust Scoring</CardTitle>
<CardDescription className="text-gray-700 font-medium">
Build trust through referrals, staking, and community contributions.
</CardDescription>
</CardHeader>
</Card>
<Card className="bg-white/10 backdrop-blur-md border-yellow-500/30 hover:border-yellow-500 transition-all">
<Card className="bg-red-50/90 backdrop-blur-md border-red-600/50 hover:border-red-600 transition-all shadow-lg">
<CardHeader>
<Globe className="h-12 w-12 text-yellow-400 mb-3" />
<CardTitle className="text-white">Governance Access</CardTitle>
<CardDescription className="text-gray-300">
<Globe className="h-12 w-12 text-red-600 mb-3" />
<CardTitle className="text-red-700 font-bold">Governance Access</CardTitle>
<CardDescription className="text-gray-700 font-medium">
Participate in on-chain governance and shape the future of Digital Kurdistan.
</CardDescription>
</CardHeader>
@@ -82,12 +104,12 @@ const BeCitizen: React.FC = () => {
{/* CTA Section */}
<div className="max-w-4xl mx-auto">
<Card className="bg-white/5 backdrop-blur-lg border-cyan-500/50">
<Card className="bg-gradient-to-r from-yellow-400 via-yellow-300 to-yellow-400 backdrop-blur-lg border-red-600 border-4 shadow-2xl">
<CardContent className="pt-8 pb-8">
<div className="text-center space-y-6">
<div>
<h3 className="text-2xl font-bold text-white mb-3">Ready to Join?</h3>
<p className="text-gray-300 mb-6">
<h3 className="text-2xl font-bold text-red-700 mb-3">Ready to Join?</h3>
<p className="text-gray-800 font-medium mb-6">
Whether you're already a citizen or want to become one, start your journey here.
</p>
</div>
@@ -95,25 +117,25 @@ const BeCitizen: React.FC = () => {
<Button
onClick={() => setIsModalOpen(true)}
size="lg"
className="bg-gradient-to-r from-cyan-500 to-purple-600 hover:from-cyan-600 hover:to-purple-700 text-white font-semibold px-8 py-6 text-lg group"
className="bg-gradient-to-r from-red-600 to-green-700 hover:from-red-700 hover:to-green-800 text-white font-bold px-8 py-6 text-lg group shadow-xl border-2 border-yellow-300"
>
<span>Start Citizenship Process</span>
<ChevronRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
</Button>
<div className="flex flex-col md:flex-row gap-4 justify-center items-center text-sm text-gray-400 pt-4">
<div className="flex flex-col md:flex-row gap-4 justify-center items-center text-sm text-gray-700 font-medium pt-4">
<div className="flex items-center gap-2">
<Shield className="h-4 w-4" />
<Shield className="h-4 w-4 text-green-700" />
<span>Secure ZK-Proof Authentication</span>
</div>
<div className="hidden md:block"></div>
<div className="hidden md:block text-red-600"></div>
<div className="flex items-center gap-2">
<Award className="h-4 w-4" />
<Award className="h-4 w-4 text-red-600" />
<span>Soulbound NFT Citizenship</span>
</div>
<div className="hidden md:block"></div>
<div className="hidden md:block text-red-600"></div>
<div className="flex items-center gap-2">
<Globe className="h-4 w-4" />
<Globe className="h-4 w-4 text-green-700" />
<span>Decentralized Identity</span>
</div>
</div>
@@ -124,18 +146,18 @@ const BeCitizen: React.FC = () => {
{/* Process Overview */}
<div className="mt-16 max-w-5xl mx-auto">
<h3 className="text-3xl font-bold text-white text-center mb-8">How It Works</h3>
<h3 className="text-3xl font-bold text-red-700 text-center mb-8 drop-shadow-lg">How It Works</h3>
<div className="grid md:grid-cols-3 gap-8">
{/* Existing Citizens */}
<Card className="bg-white/5 backdrop-blur-md border-cyan-500/30">
<Card className="bg-red-50/90 backdrop-blur-md border-red-600/50 shadow-lg">
<CardHeader>
<div className="bg-cyan-500/20 w-12 h-12 rounded-full flex items-center justify-center mb-4">
<span className="text-2xl font-bold text-cyan-400">1</span>
<div className="bg-red-600 w-12 h-12 rounded-full flex items-center justify-center mb-4">
<span className="text-2xl font-bold text-white">1</span>
</div>
<CardTitle className="text-white">Already a Citizen?</CardTitle>
<CardTitle className="text-red-700 font-bold">Already a Citizen?</CardTitle>
</CardHeader>
<CardContent className="text-gray-300 space-y-2 text-sm">
<CardContent className="text-gray-700 font-medium space-y-2 text-sm">
<p> Enter your Welati Tiki NFT number</p>
<p> Verify NFT ownership on-chain</p>
<p> Sign authentication challenge</p>
@@ -144,14 +166,14 @@ const BeCitizen: React.FC = () => {
</Card>
{/* New Citizens */}
<Card className="bg-white/5 backdrop-blur-md border-purple-500/30">
<Card className="bg-yellow-50/90 backdrop-blur-md border-yellow-600/50 shadow-lg">
<CardHeader>
<div className="bg-purple-500/20 w-12 h-12 rounded-full flex items-center justify-center mb-4">
<span className="text-2xl font-bold text-purple-400">2</span>
<div className="bg-yellow-600 w-12 h-12 rounded-full flex items-center justify-center mb-4">
<span className="text-2xl font-bold text-white">2</span>
</div>
<CardTitle className="text-white">New to Citizenship?</CardTitle>
<CardTitle className="text-yellow-800 font-bold">New to Citizenship?</CardTitle>
</CardHeader>
<CardContent className="text-gray-300 space-y-2 text-sm">
<CardContent className="text-gray-700 font-medium space-y-2 text-sm">
<p> Fill detailed KYC application</p>
<p> Data encrypted with ZK-proofs</p>
<p> Submit for admin approval</p>
@@ -160,14 +182,14 @@ const BeCitizen: React.FC = () => {
</Card>
{/* After Citizenship */}
<Card className="bg-white/5 backdrop-blur-md border-green-500/30">
<Card className="bg-green-50/90 backdrop-blur-md border-green-600/50 shadow-lg">
<CardHeader>
<div className="bg-green-500/20 w-12 h-12 rounded-full flex items-center justify-center mb-4">
<span className="text-2xl font-bold text-green-400">3</span>
<div className="bg-green-600 w-12 h-12 rounded-full flex items-center justify-center mb-4">
<span className="text-2xl font-bold text-white">3</span>
</div>
<CardTitle className="text-white">Citizen Benefits</CardTitle>
<CardTitle className="text-green-700 font-bold">Citizen Benefits</CardTitle>
</CardHeader>
<CardContent className="text-gray-300 space-y-2 text-sm">
<CardContent className="text-gray-700 font-medium space-y-2 text-sm">
<p> Trust score calculation enabled</p>
<p> Governance voting rights</p>
<p> Referral tree participation</p>
@@ -179,13 +201,13 @@ const BeCitizen: React.FC = () => {
{/* Security Notice */}
<div className="mt-12 max-w-3xl mx-auto">
<Card className="bg-yellow-500/10 border-yellow-500/30">
<Card className="bg-yellow-50/90 border-yellow-600/50 shadow-lg">
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<Shield className="h-6 w-6 text-yellow-400 mt-1 flex-shrink-0" />
<div className="text-sm text-gray-200">
<p className="font-semibold text-yellow-400 mb-2">Privacy & Security</p>
<p>
<Shield className="h-6 w-6 text-yellow-700 mt-1 flex-shrink-0" />
<div className="text-sm text-gray-700">
<p className="font-bold text-yellow-800 mb-2">Privacy & Security</p>
<p className="font-medium">
Your personal data is encrypted using AES-GCM with your wallet-derived keys.
Only commitment hashes are stored on the blockchain. Encrypted data is stored
on IPFS and locally on your device. No personal information is ever publicly visible.
@@ -198,7 +220,14 @@ const BeCitizen: React.FC = () => {
</div>
{/* Citizenship Modal */}
<CitizenshipModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
<CitizenshipModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
referrerAddress={referrerAddress}
/>
{/* Invite Friend Modal */}
<InviteUserModal isOpen={isInviteModalOpen} onClose={() => setIsInviteModalOpen(false)} />
</div>
);
};
+254 -3
View File
@@ -7,10 +7,14 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useAuth } from '@/contexts/AuthContext';
import { usePolkadot } from '@/contexts/PolkadotContext';
import { supabase } from '@/lib/supabase';
import { User, Mail, Phone, Globe, MapPin, Calendar, Shield, AlertCircle, ArrowLeft, Award, Users, TrendingUp } from 'lucide-react';
import { User, Mail, Phone, Globe, MapPin, Calendar, Shield, AlertCircle, ArrowLeft, Award, Users, TrendingUp, UserMinus } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import { fetchUserTikis, calculateTikiScore, getPrimaryRole, getTikiDisplayName, getTikiColor, getTikiEmoji, getUserRoleCategories } from '@pezkuwi/lib/tiki';
import { fetchUserTikis, calculateTikiScore, getPrimaryRole, getTikiDisplayName, getTikiColor, getTikiEmoji, getUserRoleCategories, getAllTikiNFTDetails, generateCitizenNumber, type TikiNFTDetails } from '@pezkuwi/lib/tiki';
import { getAllScores, type UserScores } from '@pezkuwi/lib/scores';
import { getKycStatus } from '@pezkuwi/lib/kyc';
import { ReferralDashboard } from '@/components/referral/ReferralDashboard';
// Commission proposals card removed - no longer using notary system for KYC approval
// import { CommissionProposalsCard } from '@/components/dashboard/CommissionProposalsCard';
export default function Dashboard() {
const { user } = useAuth();
@@ -28,6 +32,13 @@ export default function Dashboard() {
totalScore: 0
});
const [loadingScores, setLoadingScores] = useState(false);
const [kycStatus, setKycStatus] = useState<string>('NotStarted');
const [renouncingCitizenship, setRenouncingCitizenship] = useState(false);
const [nftDetails, setNftDetails] = useState<{ citizenNFT: TikiNFTDetails | null; roleNFTs: TikiNFTDetails[]; totalNFTs: number }>({
citizenNFT: null,
roleNFTs: [],
totalNFTs: 0
});
useEffect(() => {
fetchProfile();
@@ -107,6 +118,14 @@ export default function Dashboard() {
// Also fetch tikis separately for role display (needed for role details)
const userTikis = await fetchUserTikis(api, selectedAccount.address);
setTikis(userTikis);
// Fetch NFT details including collection/item IDs
const details = await getAllTikiNFTDetails(api, selectedAccount.address);
setNftDetails(details);
// Fetch KYC status to determine if user is a citizen
const status = await getKycStatus(api, selectedAccount.address);
setKycStatus(status);
} catch (error) {
console.error('Error fetching scores and tikis:', error);
} finally {
@@ -180,6 +199,98 @@ export default function Dashboard() {
}
};
const handleRenounceCitizenship = async () => {
if (!api || !selectedAccount) {
toast({
title: "Error",
description: "Please connect your wallet first",
variant: "destructive"
});
return;
}
if (kycStatus !== 'Approved') {
toast({
title: "Error",
description: "Only citizens can renounce citizenship",
variant: "destructive"
});
return;
}
// Confirm action
const confirmed = window.confirm(
'Are you sure you want to renounce your citizenship? This will:\n' +
'• Burn your Citizen (Welati) NFT\n' +
'• Reset your KYC status to NotStarted\n' +
'• Remove all associated citizen privileges\n\n' +
'You can always reapply later if you change your mind.'
);
if (!confirmed) return;
setRenouncingCitizenship(true);
try {
const { web3FromAddress } = await import('@polkadot/extension-dapp');
const injector = await web3FromAddress(selectedAccount.address);
console.log('Renouncing citizenship...');
const tx = api.tx.identityKyc.renounceCitizenship();
await tx.signAndSend(selectedAccount.address, { signer: injector.signer }, ({ status, events, dispatchError }) => {
if (dispatchError) {
let errorMessage = 'Transaction failed';
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule);
errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
} else {
errorMessage = dispatchError.toString();
}
console.error(errorMessage);
toast({
title: "Renunciation Failed",
description: errorMessage,
variant: "destructive"
});
setRenouncingCitizenship(false);
return;
}
if (status.isInBlock || status.isFinalized) {
console.log('✅ Citizenship renounced successfully');
// Check for CitizenshipRenounced event
events.forEach(({ event }) => {
if (event.section === 'identityKyc' && event.method === 'CitizenshipRenounced') {
console.log('📢 CitizenshipRenounced event detected');
toast({
title: "Citizenship Renounced",
description: "Your citizenship has been successfully renounced. You can reapply anytime."
});
// Refresh data after a short delay
setTimeout(() => {
fetchScoresAndTikis();
}, 2000);
}
});
setRenouncingCitizenship(false);
}
});
} catch (err: any) {
console.error('Renunciation error:', err);
toast({
title: "Error",
description: err.message || 'Failed to renounce citizenship',
variant: "destructive"
});
setRenouncingCitizenship(false);
}
};
const getRoleDisplay = (): string => {
if (loadingScores) return 'Loading...';
if (!selectedAccount) return 'Member';
@@ -342,6 +453,7 @@ export default function Dashboard() {
<TabsList>
<TabsTrigger value="profile">Profile</TabsTrigger>
<TabsTrigger value="roles">Roles & Tikis</TabsTrigger>
<TabsTrigger value="referrals">Referrals</TabsTrigger>
<TabsTrigger value="security">Security</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
</TabsList>
@@ -436,7 +548,7 @@ export default function Dashboard() {
No roles assigned yet
</p>
<p className="text-sm text-muted-foreground">
Complete KYC to become a Citizen (Hemwelatî)
Complete KYC to become a Citizen (Welati)
</p>
</div>
)}
@@ -475,18 +587,157 @@ export default function Dashboard() {
</div>
</div>
{nftDetails.totalNFTs > 0 && (
<div className="border-t pt-4">
<h4 className="font-medium mb-3">NFT Details ({nftDetails.totalNFTs})</h4>
<div className="space-y-3">
{nftDetails.citizenNFT && (
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
<div className="flex items-center justify-between mb-2">
<span className="font-medium text-blue-900 dark:text-blue-100">
{nftDetails.citizenNFT.tikiEmoji} Citizen NFT
</span>
<Badge variant="outline" className="text-blue-700 border-blue-300">
Primary
</Badge>
</div>
{/* NFT Number and Citizen Number - Side by Side */}
<div className="grid grid-cols-2 gap-3 mb-3">
{/* NFT Number */}
<div className="p-2 bg-white dark:bg-blue-950 rounded border border-blue-300 dark:border-blue-700">
<span className="text-xs text-blue-600 dark:text-blue-400 font-medium">NFT Number:</span>
<div className="font-mono text-lg font-bold text-blue-900 dark:text-blue-100">
#{nftDetails.citizenNFT.collectionId}-{nftDetails.citizenNFT.itemId}
</div>
</div>
{/* Citizen Number = NFT Number + 6 digits */}
<div className="p-2 bg-white dark:bg-green-950 rounded border border-green-300 dark:border-green-700">
<span className="text-xs text-green-600 dark:text-green-400 font-medium">Citizen Number:</span>
<div className="font-mono text-lg font-bold text-green-900 dark:text-green-100">
#{nftDetails.citizenNFT.collectionId}-{nftDetails.citizenNFT.itemId}-{generateCitizenNumber(
nftDetails.citizenNFT.owner,
nftDetails.citizenNFT.collectionId,
nftDetails.citizenNFT.itemId
)}
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-2 text-sm">
<div>
<span className="text-blue-700 dark:text-blue-300 font-medium">Collection ID:</span>
<span className="ml-2 font-mono text-blue-900 dark:text-blue-100">
{nftDetails.citizenNFT.collectionId}
</span>
</div>
<div>
<span className="text-blue-700 dark:text-blue-300 font-medium">Item ID:</span>
<span className="ml-2 font-mono text-blue-900 dark:text-blue-100">
{nftDetails.citizenNFT.itemId}
</span>
</div>
<div className="col-span-2">
<span className="text-blue-700 dark:text-blue-300 font-medium">Role:</span>
<span className="ml-2 text-blue-900 dark:text-blue-100">
{nftDetails.citizenNFT.tikiDisplayName}
</span>
</div>
<div className="col-span-2">
<span className="text-blue-700 dark:text-blue-300 font-medium">Tiki Type:</span>
<span className="ml-2 font-semibold text-purple-600 dark:text-purple-400">
{nftDetails.citizenNFT.tikiRole}
</span>
</div>
</div>
</div>
)}
{nftDetails.roleNFTs.length > 0 && (
<div className="space-y-2">
<p className="text-sm text-muted-foreground font-medium">Additional Role NFTs:</p>
{nftDetails.roleNFTs.map((nft, index) => (
<div
key={`${nft.collectionId}-${nft.itemId}`}
className="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-3"
>
<div className="flex items-center justify-between mb-2">
<span className="font-medium">
{nft.tikiEmoji} {nft.tikiDisplayName}
</span>
<Badge variant="outline" className={nft.tikiColor}>
Score: {nft.tikiScore}
</Badge>
</div>
<div className="grid grid-cols-2 gap-2 text-sm text-muted-foreground">
<div>
<span className="font-medium">Collection:</span>
<span className="ml-2 font-mono">{nft.collectionId}</span>
</div>
<div>
<span className="font-medium">Item:</span>
<span className="ml-2 font-mono">{nft.itemId}</span>
</div>
<div className="col-span-2">
<span className="font-medium">Tiki Type:</span>
<span className="ml-2 font-semibold text-purple-600 dark:text-purple-400">{nft.tikiRole}</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
)}
<div className="border-t pt-4 bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h4 className="font-medium mb-2">Blockchain Address</h4>
<p className="text-sm text-muted-foreground font-mono break-all">
{selectedAccount.address}
</p>
</div>
{kycStatus === 'Approved' && (
<div className="border-t pt-4">
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
<h4 className="font-medium mb-2 text-yellow-800 dark:text-yellow-200 flex items-center gap-2">
<UserMinus className="h-4 w-4" />
Renounce Citizenship
</h4>
<p className="text-sm text-yellow-700 dark:text-yellow-300 mb-3">
You can voluntarily renounce your citizenship at any time. This will:
</p>
<ul className="text-sm text-yellow-700 dark:text-yellow-300 mb-3 list-disc list-inside space-y-1">
<li>Burn your Citizen (Welati) NFT</li>
<li>Reset your KYC status</li>
<li>Remove citizen privileges</li>
</ul>
<p className="text-xs text-yellow-600 dark:text-yellow-400 mb-3">
Note: You can always reapply for citizenship later if you change your mind.
</p>
<Button
variant="destructive"
size="sm"
onClick={handleRenounceCitizenship}
disabled={renouncingCitizenship}
>
{renouncingCitizenship ? 'Renouncing...' : 'Renounce Citizenship'}
</Button>
</div>
</div>
)}
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="referrals" className="space-y-4">
<ReferralDashboard />
</TabsContent>
<TabsContent value="security" className="space-y-4">
<Card>
<CardHeader>
+2 -2
View File
@@ -320,8 +320,8 @@ const Login: React.FC = () => {
<Input
id="referral-code"
type="text"
placeholder={t('login.enterReferralCode', 'Enter referral code')}
className="pl-10 bg-gray-800 border-gray-700 text-white"
placeholder={t('login.enterReferralCode', 'Referral code (optional)')}
className="pl-10 bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 placeholder:opacity-50"
value={signupData.referralCode}
onChange={(e) => setSignupData({...signupData, referralCode: e.target.value})}
/>
+46 -7
View File
@@ -163,6 +163,13 @@ export default function CitizensIssues() {
if (!api || !isApiReady) return;
try {
// Check if welati pallet exists
if (!api.query.welati) {
console.log('Welati pallet not available yet');
setIssues([]);
return;
}
const issueCountResult = await api.query.welati.issueCount();
const issueCount = issueCountResult.toNumber();
@@ -188,11 +195,7 @@ export default function CitizensIssues() {
setIssues(fetchedIssues.reverse());
} catch (error) {
console.error('Error fetching issues:', error);
toast({
title: 'Xeletî (Error)',
description: 'Pirsgirêk di barkirina pirsan de (Error loading issues)',
variant: 'destructive'
});
setIssues([]);
}
};
@@ -200,6 +203,13 @@ export default function CitizensIssues() {
if (!api || !isApiReady || !selectedAccount) return;
try {
// Check if welati pallet exists
if (!api.query.welati) {
console.log('Welati pallet not available yet');
setUserVotes(new Map());
return;
}
const votesEntries = await api.query.welati.issueVotes.entries(selectedAccount.address);
const votes = new Map<number, boolean>();
@@ -212,6 +222,7 @@ export default function CitizensIssues() {
setUserVotes(votes);
} catch (error) {
console.error('Error fetching user votes:', error);
setUserVotes(new Map());
}
};
@@ -304,6 +315,13 @@ export default function CitizensIssues() {
if (!api || !isApiReady) return;
try {
// Check if welati pallet exists
if (!api.query.welati) {
console.log('Welati pallet not available yet');
setParliamentCandidates([]);
return;
}
const candidatesEntries = await api.query.welati.parliamentCandidates.entries();
const candidates: ParliamentCandidate[] = [];
@@ -333,6 +351,7 @@ export default function CitizensIssues() {
}
} catch (error) {
console.error('Error fetching parliament candidates:', error);
setParliamentCandidates([]);
}
};
@@ -450,6 +469,13 @@ export default function CitizensIssues() {
if (!api || !isApiReady) return;
try {
// Check if welati pallet exists
if (!api.query.welati) {
console.log('Welati pallet not available yet');
setPresidentCandidates([]);
return;
}
const candidatesEntries = await api.query.welati.presidentCandidates.entries();
const candidates: PresidentCandidate[] = [];
@@ -479,6 +505,7 @@ export default function CitizensIssues() {
}
} catch (error) {
console.error('Error fetching president candidates:', error);
setPresidentCandidates([]);
}
};
@@ -596,6 +623,14 @@ export default function CitizensIssues() {
if (!api || !isApiReady) return;
try {
// Check if welati pallet exists
if (!api.query.welati) {
console.log('Welati pallet not available yet');
setLegislationProposals([]);
setUserLegislationVotes(new Map());
return;
}
const proposalsEntries = await api.query.welati.legislationProposals.entries();
const proposals: LegislationProposal[] = [];
@@ -631,6 +666,8 @@ export default function CitizensIssues() {
}
} catch (error) {
console.error('Error fetching legislation proposals:', error);
setLegislationProposals([]);
setUserLegislationVotes(new Map());
}
};
@@ -1085,9 +1122,10 @@ export default function CitizensIssues() {
</DialogHeader>
<div className="space-y-4 py-4">
<Input
placeholder="5Abc123... (Substrate Address)"
placeholder="Candidate address"
value={nominateParliamentAddress}
onChange={(e) => setNominateParliamentAddress(e.target.value)}
className="placeholder:text-gray-500 placeholder:opacity-50"
/>
<Button
onClick={handleNominateParliament}
@@ -1196,9 +1234,10 @@ export default function CitizensIssues() {
</DialogHeader>
<div className="space-y-4 py-4">
<Input
placeholder="5Abc123... (Substrate Address)"
placeholder="Candidate address"
value={nominatePresidentAddress}
onChange={(e) => setNominatePresidentAddress(e.target.value)}
className="placeholder:text-gray-500 placeholder:opacity-50"
/>
<Button
onClick={handleNominatePresident}