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
+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}