feat: add governance pages (Assembly, Justice, Polls) to web

- Add AssemblyPage with members tab (7 members) and sessions tab
- Add JusticePage with dispute cases, expandable cards, status stats
- Add PollsPage with interactive voting and results progress bars
- Wire routes /governance/assembly, /governance/justice, /governance/polls in App.tsx
- Activate assembly, justice, polls buttons in MobileHomeLayout (remove comingSoon)
This commit is contained in:
2026-04-09 04:02:40 +03:00
parent 3b9b7c2643
commit c5f369776c
5 changed files with 498 additions and 3 deletions
+6
View File
@@ -66,6 +66,9 @@ const Subdomains = lazy(() => import('@/pages/Subdomains'));
const Messaging = lazy(() => import('@/pages/Messaging'));
const TaxZekatPage = lazy(() => import('@/pages/finance/TaxZekatPage'));
const BankPage = lazy(() => import('@/pages/finance/BankPage'));
const AssemblyPage = lazy(() => import('@/pages/governance/AssemblyPage'));
const JusticePage = lazy(() => import('@/pages/governance/JusticePage'));
const PollsPage = lazy(() => import('@/pages/governance/PollsPage'));
// Network pages
const Mainnet = lazy(() => import('@/pages/networks/Mainnet'));
@@ -230,6 +233,9 @@ function App() {
} />
<Route path="/finance/zekat" element={<TaxZekatPage />} />
<Route path="/finance/bank" element={<BankPage />} />
<Route path="/governance/assembly" element={<AssemblyPage />} />
<Route path="/governance/justice" element={<JusticePage />} />
<Route path="/governance/polls" element={<PollsPage />} />
<Route path="/presale" element={<Presale />} />
<Route path="/launchpad" element={<PresaleList />} />
<Route path="/launchpad/:id" element={<PresaleDetail />} />
+3 -3
View File
@@ -67,12 +67,12 @@ const APP_SECTIONS: AppSection[] = [
borderColor: 'border-l-red-500',
apps: [
{ title: 'mobile.app.president', icon: '👑', route: '/elections', requiresAuth: true },
{ title: 'mobile.app.assembly', icon: '🏛️', route: '/citizens/government', comingSoon: true },
{ title: 'mobile.app.assembly', icon: '🏛️', route: '/governance/assembly' },
{ title: 'mobile.app.vote', icon: '🗳️', route: '/elections', requiresAuth: true },
{ title: 'mobile.app.validators', icon: '🛡️', route: '/wallet' },
{ title: 'mobile.app.justice', icon: '⚖️', route: '/citizens/government', comingSoon: true },
{ title: 'mobile.app.justice', icon: '⚖️', route: '/governance/justice' },
{ title: 'mobile.app.proposals', icon: '📜', route: '/citizens/government' },
{ title: 'mobile.app.polls', icon: '📊', route: '/citizens/government', comingSoon: true },
{ title: 'mobile.app.polls', icon: '📊', route: '/governance/polls' },
{ title: 'mobile.app.identity', icon: '🆔', route: '/identity' },
],
},
+132
View File
@@ -0,0 +1,132 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
interface Member {
id: string;
name: string;
role: string;
roleKu: string;
emoji: string;
region: string;
since: string;
}
interface Session {
id: string;
titleKu: string;
title: string;
date: string;
status: 'upcoming' | 'completed' | 'in-session';
agenda: string;
}
const MEMBERS: Member[] = [
{ id: '1', name: 'Azad Kurdo', role: 'Speaker', roleKu: 'Serokê Meclîsê', emoji: '👨‍⚖️', region: 'Amed', since: '2025-06' },
{ id: '2', name: 'Rozerin Xan', role: 'Deputy Speaker', roleKu: 'Cîgirê Serokê Meclîsê', emoji: '👩‍⚖️', region: 'Hewler', since: '2025-06' },
{ id: '3', name: 'Serhat Demirtash', role: 'Finance Committee', roleKu: 'Komîteya Darayî', emoji: '👨‍💼', region: 'Diyarbekir', since: '2025-08' },
{ id: '4', name: 'Jîn Bakir', role: 'Technology Committee', roleKu: 'Komîteya Teknolojiyê', emoji: '👩‍💻', region: 'Silêmanî', since: '2025-07' },
{ id: '5', name: 'Kawa Zana', role: 'Education Committee', roleKu: 'Komîteya Perwerdê', emoji: '👨‍🎓', region: 'Wan', since: '2025-09' },
{ id: '6', name: 'Berfîn Shêx', role: 'Social Affairs', roleKu: 'Karên Civakî', emoji: '👩‍🏫', region: 'Kerkûk', since: '2025-08' },
{ id: '7', name: 'Dilovan Ehmed', role: 'Foreign Relations', roleKu: 'Têkiliyên Derve', emoji: '🧑‍💼', region: 'Qamişlo', since: '2025-10' },
];
const SESSIONS: Session[] = [
{ id: 's1', titleKu: 'Civîna Budceya Q2 2026', title: 'Q2 2026 Budget Session', date: '2026-04-15', status: 'upcoming', agenda: 'Treasury allocation, staking rewards adjustment, education fund.' },
{ id: 's2', titleKu: 'Civîna Yasadanînê #12', title: 'Legislative Session #12', date: '2026-04-10', status: 'upcoming', agenda: 'Cross-chain bridge proposal, fee structure revision.' },
{ id: 's3', titleKu: 'Civîna Awarte ya Ewlehiyê', title: 'Emergency Security Session', date: '2026-03-28', status: 'completed', agenda: 'Network security audit results, validator requirements update.' },
{ id: 's4', titleKu: 'Civîna Yasadanînê #11', title: 'Legislative Session #11', date: '2026-03-15', status: 'completed', agenda: 'Citizenship criteria, NFT standards, community grants.' },
];
const STATUS_CONFIG = {
upcoming: { label: 'Tê / Upcoming', cls: 'bg-blue-900/50 text-blue-400' },
'in-session': { label: 'Niha / In Session', cls: 'bg-green-900/50 text-green-400' },
completed: { label: 'Qediya / Completed', cls: 'bg-gray-800 text-gray-400' },
};
type Tab = 'members' | 'sessions';
export default function AssemblyPage() {
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState<Tab>('members');
return (
<div className="min-h-screen bg-gray-950 text-white">
{/* Header */}
<div className="bg-green-700 px-4 pt-4 pb-6">
<div className="flex items-center gap-3 mb-4">
<button onClick={() => navigate(-1)} className="text-white/80 hover:text-white text-xl"></button>
<span className="text-sm text-white/70">Governance</span>
</div>
<div className="text-center">
<span className="text-5xl block mb-2">🏛</span>
<h1 className="text-2xl font-bold">Meclîsa Kurdistanê</h1>
<p className="text-white/70 text-sm mt-0.5">Kurdistan Digital Assembly</p>
</div>
<div className="mt-4 flex bg-white/10 rounded-2xl overflow-hidden">
{[
{ val: MEMBERS.length, label: 'Endam / Members' },
{ val: 4, label: 'Komîte / Committees' },
{ val: 12, label: 'Civîn / Sessions' },
].map((stat, i) => (
<div key={i} className={`flex-1 py-3 text-center ${i > 0 ? 'border-l border-white/20' : ''}`}>
<p className="text-xl font-bold text-white">{stat.val}</p>
<p className="text-[10px] text-white/60 mt-0.5">{stat.label}</p>
</div>
))}
</div>
</div>
{/* Tabs */}
<div className="px-4 pt-4">
<div className="flex bg-gray-900 rounded-xl p-1">
{(['members', 'sessions'] as Tab[]).map(tab => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`flex-1 py-2.5 rounded-lg text-sm font-semibold transition-all ${
activeTab === tab ? 'bg-green-600 text-white' : 'text-gray-400'
}`}
>
{tab === 'members' ? 'Endam / Members' : 'Civîn / Sessions'}
</button>
))}
</div>
</div>
<div className="px-4 py-4 space-y-3">
{activeTab === 'members' && MEMBERS.map(m => (
<div key={m.id} className="bg-gray-900 rounded-xl p-4 flex items-center gap-4">
<span className="text-4xl">{m.emoji}</span>
<div className="flex-1 min-w-0">
<p className="font-bold text-white">{m.name}</p>
<p className="text-green-400 text-sm font-medium">{m.roleKu}</p>
<p className="text-gray-500 text-xs">{m.role}</p>
<div className="flex gap-3 mt-1.5 text-xs text-gray-500">
<span>📍 {m.region}</span>
<span>Ji {m.since}</span>
</div>
</div>
</div>
))}
{activeTab === 'sessions' && SESSIONS.map(s => {
const cfg = STATUS_CONFIG[s.status];
return (
<div key={s.id} className="bg-gray-900 rounded-xl p-4">
<div className="flex items-center justify-between mb-2">
<span className={`text-xs font-semibold px-2.5 py-1 rounded-full ${cfg.cls}`}>
{cfg.label}
</span>
<span className="text-xs text-gray-500">{s.date}</span>
</div>
<p className="font-bold text-white">{s.titleKu}</p>
<p className="text-gray-400 text-sm mb-2">{s.title}</p>
<p className="text-gray-500 text-xs leading-relaxed">{s.agenda}</p>
</div>
);
})}
</div>
<div className="h-10" />
</div>
);
}
+151
View File
@@ -0,0 +1,151 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
interface DisputeCase {
id: string;
caseNumber: string;
titleKu: string;
title: string;
description: string;
status: 'open' | 'in-review' | 'resolved';
category: string;
filedDate: string;
resolvedDate?: string;
resolution?: string;
}
const CASES: DisputeCase[] = [
{
id: '1', caseNumber: 'DKR-2026-001',
titleKu: 'Nakokiya Dravdana Token', title: 'Token Transaction Dispute',
description: 'Dispute over a failed transaction of 500 HEZ between two parties. Sender claims tokens were deducted but receiver did not receive them.',
status: 'open', category: 'Transaction', filedDate: '2026-04-02',
},
{
id: '2', caseNumber: 'DKR-2026-002',
titleKu: 'Binpêkirina Peymana Zîrek', title: 'Smart Contract Violation',
description: 'A DeFi protocol allegedly failed to distribute staking rewards as specified in its smart contract terms.',
status: 'in-review', category: 'Smart Contract', filedDate: '2026-03-28',
},
{
id: '3', caseNumber: 'DKR-2025-047',
titleKu: 'Destavêtina Nasnameya Dijîtal', title: 'Digital Identity Fraud',
description: 'A citizen reported unauthorized use of their digital identity credentials to access governance voting.',
status: 'resolved', category: 'Identity', filedDate: '2026-02-15',
resolvedDate: '2026-03-10',
resolution: 'Identity credentials were revoked and reissued. Fraudulent votes were invalidated. Perpetrator account suspended.',
},
{
id: '4', caseNumber: 'DKR-2025-039',
titleKu: 'Nakokiya NFT ya Milkiyetê', title: 'NFT Ownership Dispute',
description: 'Two parties claim ownership of the same NFT certificate. Investigation revealed a minting error in the original smart contract.',
status: 'resolved', category: 'NFT / Ownership', filedDate: '2026-01-20',
resolvedDate: '2026-02-28',
resolution: 'Both parties received compensatory NFTs. Smart contract was patched to prevent duplicate minting.',
},
];
const STATUS_CONFIG = {
'open': { label: 'Vekirî / Open', cls: 'bg-red-900/50 text-red-400', dot: 'bg-red-400' },
'in-review': { label: 'Di lêkolînê de / In Review', cls: 'bg-yellow-900/50 text-yellow-400', dot: 'bg-yellow-400' },
'resolved': { label: 'Çareserkirî / Resolved', cls: 'bg-green-900/50 text-green-400', dot: 'bg-green-400' },
};
export default function JusticePage() {
const navigate = useNavigate();
const [expanded, setExpanded] = useState<string | null>(null);
const counts = {
open: CASES.filter(c => c.status === 'open').length,
'in-review': CASES.filter(c => c.status === 'in-review').length,
resolved: CASES.filter(c => c.status === 'resolved').length,
};
return (
<div className="min-h-screen bg-gray-950 text-white">
{/* Header */}
<div className="bg-green-700 px-4 pt-4 pb-5">
<div className="flex items-center gap-3 mb-4">
<button onClick={() => navigate(-1)} className="text-white/80 hover:text-white text-xl"></button>
<span className="text-sm text-white/70">Governance</span>
</div>
<div className="text-center">
<span className="text-5xl block mb-2"></span>
<h1 className="text-2xl font-bold">Dadwerî</h1>
<p className="text-white/70 text-sm mt-0.5">Justice & Dispute Resolution</p>
</div>
</div>
<div className="px-4 py-4 space-y-3 max-w-lg mx-auto">
{/* Stats */}
<div className="grid grid-cols-3 gap-3">
{[
{ label: 'Vekirî\nOpen', val: counts['open'], color: 'border-l-red-500' },
{ label: 'Lêkolîn\nIn Review', val: counts['in-review'], color: 'border-l-yellow-500' },
{ label: 'Çareser\nResolved', val: counts['resolved'], color: 'border-l-green-500' },
].map((s, i) => (
<div key={i} className={`bg-gray-900 rounded-xl p-4 text-center border-l-4 ${s.color}`}>
<p className="text-2xl font-bold text-white">{s.val}</p>
<p className="text-[10px] text-gray-400 mt-1 whitespace-pre-line leading-tight">{s.label}</p>
</div>
))}
</div>
{/* Info */}
<div className="bg-gray-900 rounded-xl p-4 border-l-4 border-l-green-600">
<p className="font-bold text-white text-sm mb-2">Çareserkirina Nakokiyan / Dispute Resolution</p>
<p className="text-xs text-gray-400 leading-relaxed">
Sîstema dadweriya dijîtal a Kurdistanê nakokiyên di navbera welatiyên dijîtal de bi awayekî adil û zelal çareser dike. Hemû biryar li ser blockchain tên tomarkirin.
</p>
<p className="text-xs text-gray-500 mt-2 leading-relaxed italic">
Kurdistan's digital justice system resolves disputes between digital citizens fairly and transparently. All decisions are recorded on the blockchain.
</p>
</div>
{/* Cases */}
<h2 className="font-bold text-white text-base pt-1">Dozên Dawî / Recent Cases</h2>
{CASES.map(c => {
const cfg = STATUS_CONFIG[c.status];
const isOpen = expanded === c.id;
return (
<div key={c.id} className="bg-gray-900 rounded-xl p-4">
<div className="flex items-center justify-between mb-2">
<span className="text-blue-400 text-xs font-semibold">{c.caseNumber}</span>
<span className={`text-xs font-semibold px-2.5 py-1 rounded-full ${cfg.cls}`}>
{cfg.label}
</span>
</div>
<p className="font-bold text-white">{c.titleKu}</p>
<p className="text-gray-400 text-sm mb-2">{c.title}</p>
<div className="flex gap-4 text-xs text-gray-500 mb-3">
<span>📁 {c.category}</span>
<span>📅 {c.filedDate}</span>
</div>
{isOpen && (
<div className="border-t border-gray-800 pt-3 space-y-3">
<p className="text-sm text-gray-300 leading-relaxed">{c.description}</p>
{c.resolution && (
<div className="bg-green-900/20 border border-green-800/50 rounded-xl p-3">
<p className="text-green-400 text-xs font-bold mb-1">Biryar / Resolution:</p>
<p className="text-gray-300 text-xs leading-relaxed">{c.resolution}</p>
<p className="text-gray-500 text-xs mt-2">Dîroka çareseriyê: {c.resolvedDate}</p>
</div>
)}
</div>
)}
<button
onClick={() => setExpanded(isOpen ? null : c.id)}
className="mt-2 text-green-400 text-xs font-medium w-full text-center"
>
{isOpen ? ' Kêmtir' : ' Bêtir'}
</button>
</div>
);
})}
</div>
<div className="h-10" />
</div>
);
}
+206
View File
@@ -0,0 +1,206 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
interface PollOption {
id: string;
text: string;
votes: number;
}
interface Poll {
id: string;
title: string;
titleKu: string;
description: string;
status: 'active' | 'ended';
totalVotes: number;
endsAt: string;
options: PollOption[];
userVoted: string | null;
}
const INITIAL_POLLS: Poll[] = [
{
id: '1',
titleKu: 'Taybetmendiya nû ya paşîn çi be?',
title: 'What should the next new feature be?',
description: 'Vote for the feature you want to see next in PWAP.',
status: 'active', totalVotes: 342, endsAt: '2026-04-20',
options: [
{ id: '1a', text: 'NFT Marketplace', votes: 128 },
{ id: '1b', text: 'DeFi Lending', votes: 97 },
{ id: '1c', text: 'DAO Voting System', votes: 72 },
{ id: '1d', text: 'Cross-chain Bridge', votes: 45 },
],
userVoted: null,
},
{
id: '2',
titleKu: 'Karmasiyonên torê kêm bibin?',
title: 'Should network fees be reduced?',
description: 'Proposal to reduce transaction fees by 50% for the next quarter.',
status: 'active', totalVotes: 521, endsAt: '2026-04-15',
options: [
{ id: '2a', text: 'Ere / Yes', votes: 389 },
{ id: '2b', text: 'Na / No', votes: 87 },
{ id: '2c', text: 'Bêalî / Abstain', votes: 45 },
],
userVoted: null,
},
{
id: '3',
titleKu: 'Bernameya bursê ji bo perwerdehiyê?',
title: 'Scholarship program for education?',
description: 'Allocate 5% of treasury funds for a community education scholarship.',
status: 'active', totalVotes: 198, endsAt: '2026-04-25',
options: [
{ id: '3a', text: 'Ere, 5% / Yes, 5%', votes: 112 },
{ id: '3b', text: 'Ere, 3% / Yes, 3%', votes: 48 },
{ id: '3c', text: 'Na / No', votes: 23 },
{ id: '3d', text: 'Bêalî / Abstain', votes: 15 },
],
userVoted: null,
},
{
id: '4',
titleKu: 'Logoya nû ya PWAP?',
title: 'New PWAP logo design?',
description: 'Community voted on the new logo. Results are final.',
status: 'ended', totalVotes: 876, endsAt: '2026-03-30',
options: [
{ id: '4a', text: 'Design A - Modern', votes: 412 },
{ id: '4b', text: 'Design B - Classic', votes: 298 },
{ id: '4c', text: 'Design C - Minimal', votes: 166 },
],
userVoted: '4a',
},
];
export default function PollsPage() {
const navigate = useNavigate();
const [polls, setPolls] = useState<Poll[]>(INITIAL_POLLS);
const handleVote = (pollId: string, optionId: string) => {
setPolls(prev =>
prev.map(poll => {
if (poll.id !== pollId || poll.userVoted) return poll;
return {
...poll,
totalVotes: poll.totalVotes + 1,
userVoted: optionId,
options: poll.options.map(opt =>
opt.id === optionId ? { ...opt, votes: opt.votes + 1 } : opt
),
};
})
);
};
const pct = (votes: number, total: number) =>
total === 0 ? 0 : Math.round((votes / total) * 100);
const activePolls = polls.filter(p => p.status === 'active');
const endedPolls = polls.filter(p => p.status === 'ended');
const renderPoll = (poll: Poll) => {
const showResults = poll.userVoted !== null || poll.status === 'ended';
const maxVotes = Math.max(...poll.options.map(o => o.votes));
return (
<div key={poll.id} className="bg-gray-900 rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<span className={`text-xs font-semibold px-2.5 py-1 rounded-full ${
poll.status === 'active' ? 'bg-green-900/50 text-green-400' : 'bg-gray-800 text-gray-400'
}`}>
{poll.status === 'active' ? 'Çalak / Active' : 'Qediya / Ended'}
</span>
<span className="text-xs text-gray-500">{poll.totalVotes} deng</span>
</div>
<p className="font-bold text-white mb-0.5">{poll.titleKu}</p>
<p className="text-gray-400 text-sm mb-1">{poll.title}</p>
<p className="text-gray-500 text-xs leading-relaxed mb-4">{poll.description}</p>
<div className="space-y-2">
{poll.options.map(option => {
const percentage = pct(option.votes, poll.totalVotes);
const isSelected = poll.userVoted === option.id;
const isWinner = poll.status === 'ended' && option.votes === maxVotes;
return (
<button
key={option.id}
onClick={() => { if (!showResults && poll.status === 'active') handleVote(poll.id, option.id); }}
disabled={showResults}
className={`w-full relative rounded-xl border-2 overflow-hidden text-left transition-all ${
isSelected ? 'border-green-500' :
isWinner ? 'border-yellow-500' :
showResults ? 'border-gray-800 cursor-default' :
'border-gray-700 hover:border-gray-500 active:scale-[0.99]'
}`}
>
{showResults && (
<div
className="absolute inset-y-0 left-0 rounded-xl transition-all"
style={{
width: `${percentage}%`,
backgroundColor: isSelected ? 'rgba(0,169,79,0.2)' : isWinner ? 'rgba(255,215,0,0.15)' : 'rgba(255,255,255,0.04)',
}}
/>
)}
<div className="relative flex items-center justify-between px-4 py-3">
<span className={`text-sm font-medium ${isSelected ? 'text-green-400' : 'text-white'}`}>
{isSelected && '✓ '}{option.text}
</span>
{showResults && (
<span className={`text-sm font-bold ml-2 ${isSelected ? 'text-green-400' : isWinner ? 'text-yellow-400' : 'text-gray-400'}`}>
{percentage}%
</span>
)}
</div>
</button>
);
})}
</div>
<p className="text-xs text-gray-600 mt-3 text-right">
{poll.status === 'active' ? `Dawî: ${poll.endsAt}` : `Qediya: ${poll.endsAt}`}
</p>
</div>
);
};
return (
<div className="min-h-screen bg-gray-950 text-white">
{/* Header */}
<div className="bg-green-700 px-4 pt-4 pb-5">
<div className="flex items-center gap-3 mb-4">
<button onClick={() => navigate(-1)} className="text-white/80 hover:text-white text-xl"></button>
<span className="text-sm text-white/70">Governance</span>
</div>
<div className="text-center">
<span className="text-5xl block mb-2">📊</span>
<h1 className="text-2xl font-bold">Rapirsî</h1>
<p className="text-white/70 text-sm mt-0.5">Community Polls</p>
<p className="text-yellow-400 text-sm font-semibold mt-2">{activePolls.length} çalak / active</p>
</div>
</div>
<div className="px-4 py-4 space-y-4 max-w-lg mx-auto">
{activePolls.length > 0 && (
<div className="space-y-3">
<h2 className="font-bold text-white text-base">Rapirsiyên Çalak / Active Polls</h2>
{activePolls.map(renderPoll)}
</div>
)}
{endedPolls.length > 0 && (
<div className="space-y-3">
<h2 className="font-bold text-white text-base">Rapirsiyên Qediyayî / Ended Polls</h2>
{endedPolls.map(renderPoll)}
</div>
)}
</div>
<div className="h-10" />
</div>
);
}