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

This commit is contained in:
2025-11-19 18:56:38 +03:00
parent bdf59cea47
commit 590ac521e8
21 changed files with 888 additions and 240 deletions
+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">