mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-21 23:47:56 +00:00
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:
@@ -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 />} />
|
||||
|
||||
@@ -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' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user