diff --git a/shared/lib/error-handler.ts b/shared/lib/error-handler.ts index 41d35286..7b7dc680 100644 --- a/shared/lib/error-handler.ts +++ b/shared/lib/error-handler.ts @@ -140,6 +140,92 @@ const ERROR_MESSAGES: Record = { kmr: 'Ji bo pêşniyara treasury-yê balance kêm e. Bond pêwîst e.', }, + // Welati (Elections & Governance) errors + 'welati.ElectionNotFound': { + en: 'Election not found. Please check the election ID.', + kmr: 'Hilbijartin nehat dîtin. Ji kerema xwe ID-ya hilbijartinê kontrol bike.', + }, + 'welati.ElectionNotActive': { + en: 'This election is not currently active.', + kmr: 'Ev hilbijartin niha ne çalak e.', + }, + 'welati.CandidacyPeriodExpired': { + en: 'Candidate registration period has ended.', + kmr: 'Dema qeydkirina berendaman qediya.', + }, + 'welati.VotingPeriodNotStarted': { + en: 'Voting period has not started yet. Please wait.', + kmr: 'Dema dengdanê hîn dest pê nekiriye. Ji kerema xwe bisekine.', + }, + 'welati.VotingPeriodExpired': { + en: 'Voting period has ended.', + kmr: 'Dema dengdanê qediya.', + }, + 'welati.AlreadyCandidate': { + en: 'You are already registered as a candidate in this election.', + kmr: 'We berê wekî berendam di vê hilbijartinê de tomar bûyî.', + }, + 'welati.AlreadyVoted': { + en: 'You have already voted in this election.', + kmr: 'We berê di vê hilbijartinê de deng da.', + }, + 'welati.InsufficientEndorsements': { + en: 'Insufficient endorsements. You need more citizen supporters.', + kmr: 'Piştgiriya têr tune. We piştgiriya zêdetir ji welatiyên pêwîst e.', + }, + 'welati.InsufficientTrustScore': { + en: 'Your trust score is too low for this election. Build your reputation first.', + kmr: 'Skora emîniya we ji bo vê hilbijartinê zêde kêm e. Pêşî navê xwe baş bike.', + }, + 'welati.NotACitizen': { + en: 'You must be a verified citizen (KYC approved) to participate.', + kmr: 'Divê we welatiyeke pejirandî (KYC pejirandî) bin da beşdar bibin.', + }, + 'welati.DepositRequired': { + en: 'Candidacy deposit required. Please pay the registration fee.', + kmr: 'Depozîta berendamiyê pêwîst e. Ji kerema xwe lêçûna qeydkirinê bidin.', + }, + 'welati.NotAuthorizedToNominate': { + en: 'You are not authorized to nominate officials. Minister or President only.', + kmr: 'We destûra hilbijartina karbidestan nîne. Tenê Wezîr an Serok.', + }, + 'welati.NotAuthorizedToApprove': { + en: 'Only the President can approve appointments.', + kmr: 'Tenê Serok dikare bicîhbûnan bipejirîne.', + }, + 'welati.NotAuthorizedToPropose': { + en: 'You are not authorized to submit proposals. Parliament members only.', + kmr: 'We destûra pêşniyaran pêşkêş kirinê nîne. Tenê endamên parlamentoyê.', + }, + 'welati.NotAuthorizedToVote': { + en: 'You are not authorized to vote on this proposal.', + kmr: 'We destûra dengdanê li ser vê pêşniyarê nîne.', + }, + 'welati.ProposalNotFound': { + en: 'Proposal not found. Please check the proposal ID.', + kmr: 'Pêşniyar nehat dîtin. Ji kerema xwe ID-ya pêşniyarê kontrol bike.', + }, + 'welati.ProposalNotActive': { + en: 'This proposal is not currently active or voting has ended.', + kmr: 'Ev pêşniyar niha ne çalak e an dengdan qediya.', + }, + 'welati.ProposalAlreadyVoted': { + en: 'You have already voted on this proposal.', + kmr: 'We berê li ser vê pêşniyarê deng da.', + }, + 'welati.QuorumNotMet': { + en: 'Quorum not met. Insufficient participation for this decision.', + kmr: 'Quorum nehat bidest xistin. Beşdariya têr ji bo vê biryarê tune ye.', + }, + 'welati.InvalidDistrict': { + en: 'Invalid electoral district. Please select a valid district.', + kmr: 'Qeza hilbijartinê nederbasdar e. Ji kerema xwe qezayeke derbasdar hilbijêre.', + }, + 'welati.RoleAlreadyFilled': { + en: 'This government position is already filled.', + kmr: 'Ev pozîsyona hukûmetê berê hatiye dagirtin.', + }, + // System/General errors 'system.CallFiltered': { en: 'This action is not permitted by the system filters.', @@ -339,6 +425,36 @@ export const SUCCESS_MESSAGES: Record = { en: 'Successfully removed liquidity from the pool!', kmr: 'Bi serkeftî liquidity ji pool-ê derxist!', }, + + // Welati (Elections & Governance) + 'welati.candidateRegistered': { + en: 'Successfully registered as candidate! Deposit: {{deposit}} HEZ. Good luck!', + kmr: 'Bi serkeftî wekî berendam tomar bûn! Depozît: {{deposit}} HEZ. Serkeftinê!', + }, + 'welati.voteCast': { + en: 'Your vote has been cast successfully! Thank you for participating.', + kmr: 'Deng-a we bi serkeftî hate dayîn! Spas ji bo beşdarî bûnê.', + }, + 'welati.proposalSubmitted': { + en: 'Proposal submitted successfully! Voting period: {{days}} days.', + kmr: 'Pêşniyar bi serkeftî hate şandin! Dema dengdanê: {{days}} roj.', + }, + 'welati.proposalVoted': { + en: 'Vote recorded on proposal #{{id}}. Your voice matters!', + kmr: 'Deng li ser pêşniyara #{{id}} tomar bû. Deng-a we girîng e!', + }, + 'welati.officialNominated': { + en: 'Official nominated successfully! Awaiting presidential approval.', + kmr: 'Karbides bi serkeftî hate hilbijartin! Li pejirandina serokê bisekine.', + }, + 'welati.appointmentApproved': { + en: 'Appointment approved! {{nominee}} is now {{role}}.', + kmr: 'Bicîhbûn pejirandî! {{nominee}} niha {{role}} ye.', + }, + 'welati.electionFinalized': { + en: 'Election finalized! {{winners}} elected. Turnout: {{turnout}}%', + kmr: 'Hilbijartin temam bû! {{winners}} hate hilbijartin. Beşdarî: {{turnout}}%', + }, }; /** diff --git a/shared/lib/welati.ts b/shared/lib/welati.ts new file mode 100644 index 00000000..202d1afa --- /dev/null +++ b/shared/lib/welati.ts @@ -0,0 +1,616 @@ +/** + * Welati (Elections & Governance) Pallet Integration + * + * This module provides helper functions for interacting with the Welati pallet, + * which handles: + * - Presidential and Parliamentary Elections + * - Speaker and Constitutional Court Elections + * - Official Appointments (Ministers, Diwan) + * - Collective Proposals (Parliament/Diwan voting) + */ + +import type { ApiPromise } from '@polkadot/api'; +import type { Option, Vec } from '@polkadot/types'; +import type { AccountId, BlockNumber } from '@polkadot/types/interfaces'; + +// ============================================================================ +// TYPE DEFINITIONS +// ============================================================================ + +export type ElectionType = 'Presidential' | 'Parliamentary' | 'SpeakerElection' | 'ConstitutionalCourt'; + +export type ElectionStatus = 'CandidacyPeriod' | 'CampaignPeriod' | 'VotingPeriod' | 'Completed'; + +export type VoteChoice = 'Aye' | 'Nay' | 'Abstain'; + +export type CollectiveDecisionType = + | 'ParliamentSimpleMajority' + | 'ParliamentSuperMajority' + | 'ParliamentAbsoluteMajority' + | 'ConstitutionalReview' + | 'ConstitutionalUnanimous' + | 'ExecutiveDecision'; + +export type ProposalPriority = 'Urgent' | 'High' | 'Normal' | 'Low'; + +export type ProposalStatus = 'Active' | 'Approved' | 'Rejected' | 'Expired' | 'Executed'; + +export type MinisterRole = + | 'WezireDarayiye' // Finance + | 'WezireParez' // Defense + | 'WezireDad' // Justice + | 'WezireBelaw' // Education + | 'WezireTend' // Health + | 'WezireAva' // Water Resources + | 'WezireCand'; // Culture + +export type GovernmentPosition = 'Serok' | 'SerokWeziran' | 'MeclisBaskanı'; + +export interface ElectionInfo { + electionId: number; + electionType: ElectionType; + status: ElectionStatus; + startBlock: number; + candidacyEndBlock: number; + campaignEndBlock: number; + votingEndBlock: number; + totalCandidates: number; + totalVotes: number; + turnoutPercentage: number; + districtCount?: number; +} + +export interface CandidateInfo { + account: string; + districtId?: number; + registeredAt: number; + endorsersCount: number; + voteCount: number; + depositPaid: string; +} + +export interface ElectionResult { + electionId: number; + winners: string[]; + totalVotes: number; + turnoutPercentage: number; + finalizedAt: number; + runoffRequired: boolean; +} + +export interface ParliamentMember { + account: string; + electedAt: number; + termEndsAt: number; + votesParticipated: number; + totalVotesEligible: number; + participationRate: number; + committees: string[]; +} + +export interface CollectiveProposal { + proposalId: number; + proposer: string; + title: string; + description: string; + proposedAt: number; + votingStartsAt: number; + expiresAt: number; + decisionType: CollectiveDecisionType; + status: ProposalStatus; + ayeVotes: number; + nayVotes: number; + abstainVotes: number; + threshold: number; + votesCast: number; + priority: ProposalPriority; +} + +export interface AppointmentProcess { + processId: number; + nominee: string; + role: string; + nominator: string; + justification: string; + status: 'Pending' | 'Approved' | 'Rejected'; + createdAt: number; + deadline: number; +} + +export interface GovernanceMetrics { + totalElectionsHeld: number; + activeElections: number; + parliamentSize: number; + diwanSize: number; + activeProposals: number; + totalProposalsSubmitted: number; + averageTurnout: number; +} + +// ============================================================================ +// QUERY FUNCTIONS (Read-only) +// ============================================================================ + +/** + * Get current government officials + */ +export async function getCurrentOfficials(api: ApiPromise): Promise<{ + serok?: string; + serokWeziran?: string; + meclisBaskanı?: string; +}> { + const [serok, serokWeziran, speaker] = await Promise.all([ + api.query.welati.currentOfficials('Serok'), + api.query.welati.currentOfficials('SerokWeziran'), + api.query.welati.currentOfficials('MeclisBaskanı'), + ]); + + return { + serok: serok.isSome ? serok.unwrap().toString() : undefined, + serokWeziran: serokWeziran.isSome ? serokWeziran.unwrap().toString() : undefined, + meclisBaskanı: speaker.isSome ? speaker.unwrap().toString() : undefined, + }; +} + +/** + * Get current cabinet ministers + */ +export async function getCurrentMinisters(api: ApiPromise): Promise> { + const roles: MinisterRole[] = [ + 'WezireDarayiye', + 'WezireParez', + 'WezireDad', + 'WezireBelaw', + 'WezireTend', + 'WezireAva', + 'WezireCand', + ]; + + const ministers = await Promise.all( + roles.map(role => api.query.welati.currentMinisters(role)) + ); + + const result: Record = {}; + roles.forEach((role, index) => { + result[role] = ministers[index].isSome ? ministers[index].unwrap().toString() : undefined; + }); + + return result as Record; +} + +/** + * Get parliament members list + */ +export async function getParliamentMembers(api: ApiPromise): Promise { + const members = await api.query.welati.parliamentMembers(); + + if (!members || members.isEmpty) { + return []; + } + + const memberList: ParliamentMember[] = []; + const accountIds = members.toJSON() as string[]; + + for (const accountId of accountIds) { + // In a real implementation, fetch detailed member info + // For now, return basic structure + memberList.push({ + account: accountId, + electedAt: 0, + termEndsAt: 0, + votesParticipated: 0, + totalVotesEligible: 0, + participationRate: 0, + committees: [], + }); + } + + return memberList; +} + +/** + * Get Diwan (Constitutional Court) members + */ +export async function getDiwanMembers(api: ApiPromise): Promise { + const members = await api.query.welati.diwanMembers(); + + if (!members || members.isEmpty) { + return []; + } + + return (members.toJSON() as string[]) || []; +} + +/** + * Get active elections + */ +export async function getActiveElections(api: ApiPromise): Promise { + const nextId = await api.query.welati.nextElectionId(); + const currentId = (nextId.toJSON() as number) || 0; + + const elections: ElectionInfo[] = []; + + // Query last 10 elections + for (let i = Math.max(0, currentId - 10); i < currentId; i++) { + const election = await api.query.welati.activeElections(i); + + if (election.isSome) { + const data = election.unwrap().toJSON() as any; + + elections.push({ + electionId: i, + electionType: data.electionType as ElectionType, + status: data.status as ElectionStatus, + startBlock: data.startBlock, + candidacyEndBlock: data.candidacyEndBlock, + campaignEndBlock: data.campaignEndBlock, + votingEndBlock: data.votingEndBlock, + totalCandidates: data.totalCandidates || 0, + totalVotes: data.totalVotes || 0, + turnoutPercentage: data.turnoutPercentage || 0, + districtCount: data.districtCount, + }); + } + } + + return elections.filter(e => e.status !== 'Completed'); +} + +/** + * Get election by ID + */ +export async function getElectionById(api: ApiPromise, electionId: number): Promise { + const election = await api.query.welati.activeElections(electionId); + + if (election.isNone) { + return null; + } + + const data = election.unwrap().toJSON() as any; + + return { + electionId, + electionType: data.electionType as ElectionType, + status: data.status as ElectionStatus, + startBlock: data.startBlock, + candidacyEndBlock: data.candidacyEndBlock, + campaignEndBlock: data.campaignEndBlock, + votingEndBlock: data.votingEndBlock, + totalCandidates: data.totalCandidates || 0, + totalVotes: data.totalVotes || 0, + turnoutPercentage: data.turnoutPercentage || 0, + districtCount: data.districtCount, + }; +} + +/** + * Get candidates for an election + */ +export async function getElectionCandidates( + api: ApiPromise, + electionId: number +): Promise { + const entries = await api.query.welati.electionCandidates.entries(electionId); + + const candidates: CandidateInfo[] = []; + + for (const [key, value] of entries) { + const data = value.toJSON() as any; + const account = (key.args[1] as AccountId).toString(); + + candidates.push({ + account, + districtId: data.districtId, + registeredAt: data.registeredAt, + endorsersCount: data.endorsers?.length || 0, + voteCount: data.voteCount || 0, + depositPaid: data.depositPaid?.toString() || '0', + }); + } + + return candidates.sort((a, b) => b.voteCount - a.voteCount); +} + +/** + * Check if user has voted in an election + */ +export async function hasVoted( + api: ApiPromise, + electionId: number, + voterAddress: string +): Promise { + const vote = await api.query.welati.electionVotes(electionId, voterAddress); + return vote.isSome; +} + +/** + * Get election results + */ +export async function getElectionResults( + api: ApiPromise, + electionId: number +): Promise { + const result = await api.query.welati.electionResults(electionId); + + if (result.isNone) { + return null; + } + + const data = result.unwrap().toJSON() as any; + + return { + electionId, + winners: data.winners || [], + totalVotes: data.totalVotes || 0, + turnoutPercentage: data.turnoutPercentage || 0, + finalizedAt: data.finalizedAt || 0, + runoffRequired: data.runoffRequired || false, + }; +} + +/** + * Get active proposals + */ +export async function getActiveProposals(api: ApiPromise): Promise { + const nextId = await api.query.welati.nextProposalId(); + const currentId = (nextId.toJSON() as number) || 0; + + const proposals: CollectiveProposal[] = []; + + // Query last 50 proposals + for (let i = Math.max(0, currentId - 50); i < currentId; i++) { + const proposal = await api.query.welati.activeProposals(i); + + if (proposal.isSome) { + const data = proposal.unwrap().toJSON() as any; + + proposals.push({ + proposalId: i, + proposer: data.proposer, + title: data.title, + description: data.description, + proposedAt: data.proposedAt, + votingStartsAt: data.votingStartsAt, + expiresAt: data.expiresAt, + decisionType: data.decisionType as CollectiveDecisionType, + status: data.status as ProposalStatus, + ayeVotes: data.ayeVotes || 0, + nayVotes: data.nayVotes || 0, + abstainVotes: data.abstainVotes || 0, + threshold: data.threshold || 0, + votesCast: data.votesCast || 0, + priority: data.priority as ProposalPriority, + }); + } + } + + return proposals.filter(p => p.status === 'Active').reverse(); +} + +/** + * Get proposal by ID + */ +export async function getProposalById( + api: ApiPromise, + proposalId: number +): Promise { + const proposal = await api.query.welati.activeProposals(proposalId); + + if (proposal.isNone) { + return null; + } + + const data = proposal.unwrap().toJSON() as any; + + return { + proposalId, + proposer: data.proposer, + title: data.title, + description: data.description, + proposedAt: data.proposedAt, + votingStartsAt: data.votingStartsAt, + expiresAt: data.expiresAt, + decisionType: data.decisionType as CollectiveDecisionType, + status: data.status as ProposalStatus, + ayeVotes: data.ayeVotes || 0, + nayVotes: data.nayVotes || 0, + abstainVotes: data.abstainVotes || 0, + threshold: data.threshold || 0, + votesCast: data.votesCast || 0, + priority: data.priority as ProposalPriority, + }; +} + +/** + * Check if user has voted on a proposal + */ +export async function hasVotedOnProposal( + api: ApiPromise, + proposalId: number, + voterAddress: string +): Promise { + const vote = await api.query.welati.collectiveVotes(proposalId, voterAddress); + return vote.isSome; +} + +/** + * Get user's vote on a proposal + */ +export async function getProposalVote( + api: ApiPromise, + proposalId: number, + voterAddress: string +): Promise { + const vote = await api.query.welati.collectiveVotes(proposalId, voterAddress); + + if (vote.isNone) { + return null; + } + + const data = vote.unwrap().toJSON() as any; + return data.vote as VoteChoice; +} + +/** + * Get pending appointments + */ +export async function getPendingAppointments(api: ApiPromise): Promise { + const nextId = await api.query.welati.nextAppointmentId(); + const currentId = (nextId.toJSON() as number) || 0; + + const appointments: AppointmentProcess[] = []; + + for (let i = Math.max(0, currentId - 20); i < currentId; i++) { + const appointment = await api.query.welati.appointmentProcesses(i); + + if (appointment.isSome) { + const data = appointment.unwrap().toJSON() as any; + + if (data.status === 'Pending') { + appointments.push({ + processId: i, + nominee: data.nominee, + role: data.role, + nominator: data.nominator, + justification: data.justification, + status: data.status, + createdAt: data.createdAt, + deadline: data.deadline, + }); + } + } + } + + return appointments; +} + +/** + * Get governance statistics + */ +export async function getGovernanceStats(api: ApiPromise): Promise { + const stats = await api.query.welati.governanceStats(); + + if (!stats || stats.isEmpty) { + return { + totalElectionsHeld: 0, + activeElections: 0, + parliamentSize: 0, + diwanSize: 0, + activeProposals: 0, + totalProposalsSubmitted: 0, + averageTurnout: 0, + }; + } + + const data = stats.toJSON() as any; + + return { + totalElectionsHeld: data.totalElectionsHeld || 0, + activeElections: data.activeElections || 0, + parliamentSize: data.parliamentSize || 0, + diwanSize: data.diwanSize || 0, + activeProposals: data.activeProposals || 0, + totalProposalsSubmitted: data.totalProposalsSubmitted || 0, + averageTurnout: data.averageTurnout || 0, + }; +} + +/** + * Get current block number + */ +export async function getCurrentBlock(api: ApiPromise): Promise { + const header = await api.rpc.chain.getHeader(); + return header.number.toNumber(); +} + +/** + * Calculate remaining blocks until deadline + */ +export async function getRemainingBlocks(api: ApiPromise, deadlineBlock: number): Promise { + const currentBlock = await getCurrentBlock(api); + return Math.max(0, deadlineBlock - currentBlock); +} + +/** + * Convert blocks to approximate time (6 seconds per block average) + */ +export function blocksToTime(blocks: number): { + days: number; + hours: number; + minutes: number; +} { + const seconds = blocks * 6; + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + + return { days, hours, minutes }; +} + +// ============================================================================ +// HELPER FUNCTIONS +// ============================================================================ + +/** + * Get election type label + */ +export function getElectionTypeLabel(type: ElectionType): { en: string; kmr: string } { + const labels = { + Presidential: { en: 'Presidential Election', kmr: 'Hilbijartina Serokî' }, + Parliamentary: { en: 'Parliamentary Election', kmr: 'Hilbijartina Parlamentoyê' }, + SpeakerElection: { en: 'Speaker Election', kmr: 'Hilbijartina Serokê Parlamentoyê' }, + ConstitutionalCourt: { en: 'Constitutional Court Election', kmr: 'Hilbijartina Dadgeha Destûrî' }, + }; + + return labels[type] || { en: type, kmr: type }; +} + +/** + * Get election status label + */ +export function getElectionStatusLabel(status: ElectionStatus): { en: string; kmr: string } { + const labels = { + CandidacyPeriod: { en: 'Candidate Registration Open', kmr: 'Qeydkirina Berendam Vekirî ye' }, + CampaignPeriod: { en: 'Campaign Period', kmr: 'Dema Kampanyayê' }, + VotingPeriod: { en: 'Voting Open', kmr: 'Dengdan Vekirî ye' }, + Completed: { en: 'Completed', kmr: 'Temam bû' }, + }; + + return labels[status] || { en: status, kmr: status }; +} + +/** + * Get minister role label + */ +export function getMinisterRoleLabel(role: MinisterRole): { en: string; kmr: string } { + const labels = { + WezireDarayiye: { en: 'Minister of Finance', kmr: 'Wezîrê Darayiyê' }, + WezireParez: { en: 'Minister of Defense', kmr: 'Wezîrê Parezê' }, + WezireDad: { en: 'Minister of Justice', kmr: 'Wezîrê Dadê' }, + WezireBelaw: { en: 'Minister of Education', kmr: 'Wezîrê Perwerdeyê' }, + WezireTend: { en: 'Minister of Health', kmr: 'Wezîrê Tendirustiyê' }, + WezireAva: { en: 'Minister of Water Resources', kmr: 'Wezîrê Avê' }, + WezireCand: { en: 'Minister of Culture', kmr: 'Wezîrê Çandî' }, + }; + + return labels[role] || { en: role, kmr: role }; +} + +/** + * Get proposal decision type threshold + */ +export function getDecisionTypeThreshold(type: CollectiveDecisionType, totalMembers: number): number { + switch (type) { + case 'ParliamentSimpleMajority': + return Math.floor(totalMembers / 2) + 1; // > 50% + case 'ParliamentSuperMajority': + case 'ConstitutionalReview': + return Math.ceil((totalMembers * 2) / 3); // > 66.67% + case 'ParliamentAbsoluteMajority': + return Math.ceil((totalMembers * 3) / 4); // > 75% + case 'ConstitutionalUnanimous': + return totalMembers; // 100% + default: + return Math.floor(totalMembers / 2) + 1; + } +} diff --git a/web/src/App.tsx b/web/src/App.tsx index 884996c1..526c68eb 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -10,6 +10,8 @@ import AdminPanel from '@/pages/AdminPanel'; import WalletDashboard from './pages/WalletDashboard'; import ReservesDashboardPage from './pages/ReservesDashboardPage'; import BeCitizen from './pages/BeCitizen'; +import Elections from './pages/Elections'; +import EducationPlatform from './pages/EducationPlatform'; import { AppProvider } from '@/contexts/AppContext'; import { PolkadotProvider } from '@/contexts/PolkadotContext'; import { WalletProvider } from '@/contexts/WalletContext'; @@ -66,6 +68,16 @@ function App() { } /> + + + + } /> + + + + } /> } /> diff --git a/web/src/pages/EducationPlatform.tsx b/web/src/pages/EducationPlatform.tsx new file mode 100644 index 00000000..1591412e --- /dev/null +++ b/web/src/pages/EducationPlatform.tsx @@ -0,0 +1,265 @@ +/** + * Perwerde Education Platform + * + * Decentralized education system for Digital Kurdistan + * - Browse courses + * - Enroll in courses + * - Track learning progress + * - Earn educational credentials + */ + +import React from 'react'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { + GraduationCap, + BookOpen, + Award, + Users, + Clock, + Star, + TrendingUp, + CheckCircle, + AlertCircle, + Play, +} from 'lucide-react'; + +export default function EducationPlatform() { + // Mock data - will be replaced with blockchain integration + const courses = [ + { + id: 1, + title: 'Kurdish Language & Literature', + instructor: 'Prof. Hêmin Xelîl', + students: 1247, + rating: 4.8, + duration: '8 weeks', + level: 'Beginner', + status: 'Active', + }, + { + id: 2, + title: 'Blockchain Technology Fundamentals', + instructor: 'Dr. Sara Hasan', + students: 856, + rating: 4.9, + duration: '6 weeks', + level: 'Intermediate', + status: 'Active', + }, + { + id: 3, + title: 'Kurdish History & Culture', + instructor: 'Prof. Azad Muhammed', + students: 2103, + rating: 4.7, + duration: '10 weeks', + level: 'Beginner', + status: 'Active', + }, + ]; + + return ( +
+ {/* Header */} +
+

+ + Perwerde - Education Platform +

+

+ Decentralized learning for Digital Kurdistan. Build skills, earn credentials, empower our nation. +

+
+ + {/* Integration Notice */} + + + + Blockchain Integration In Progress: This platform will connect to the Perwerde pallet + for decentralized course management, credential issuance, and educator rewards. Current data is for + demonstration purposes. + + + + {/* Stats Cards */} +
+ + +
+
+ +
+
+
127
+
Active Courses
+
+
+
+
+ + + +
+
+ +
+
+
12.4K
+
Students
+
+
+
+
+ + + +
+
+ +
+
+
342
+
Instructors
+
+
+
+
+ + + +
+
+ +
+
+
8.9K
+
Certificates Issued
+
+
+
+
+
+ + {/* Courses List */} +
+
+

Featured Courses

+ +
+ +
+ {courses.map((course) => ( + + +
+ {/* Course Info */} +
+
+

{course.title}

+ + {course.status} + +
+ +
+
+ + {course.instructor} +
+
+ + {course.students.toLocaleString()} students +
+
+ + {course.duration} +
+
+ +
+
+ + {course.rating} + (4.8/5.0) +
+ {course.level} +
+
+ + {/* Actions */} +
+ + +
+
+
+
+ ))} +
+
+ + {/* My Learning Section */} +
+

My Learning Progress

+ + + +

No Courses Enrolled Yet

+

+ Start your learning journey! Enroll in courses to track your progress and earn credentials. +

+ +
+
+
+ + {/* Blockchain Features Notice */} + + + + + Upcoming Blockchain Features + + + +
    +
  • +
    + Decentralized course creation & hosting +
  • +
  • +
    + NFT-based certificates & credentials +
  • +
  • +
    + Educator rewards in HEZ tokens +
  • +
  • +
    + Peer review & quality assurance +
  • +
  • +
    + Skill-based Tiki role assignments +
  • +
  • +
    + Decentralized governance for education +
  • +
+
+
+
+ ); +} diff --git a/web/src/pages/Elections.tsx b/web/src/pages/Elections.tsx new file mode 100644 index 00000000..38cce2de --- /dev/null +++ b/web/src/pages/Elections.tsx @@ -0,0 +1,461 @@ +/** + * Welati Elections & Governance Page + * + * Features: + * - View active elections (Presidential, Parliamentary, Speaker, Constitutional Court) + * - Register as candidate + * - Cast votes + * - View proposals & vote on them + * - See government officials + */ + +import React, { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { + Vote, + Users, + Trophy, + Clock, + FileText, + CheckCircle2, + XCircle, + AlertCircle, + Crown, + Scale, + Building, +} from 'lucide-react'; +import { usePolkadot } from '@/contexts/PolkadotContext'; +import { useAuth } from '@/contexts/AuthContext'; +import { toast } from '@/components/ui/use-toast'; +import { AsyncComponent, LoadingState } from '@pezkuwi/components/AsyncComponent'; +import { + getActiveElections, + getElectionCandidates, + getActiveProposals, + getCurrentOfficials, + getCurrentMinisters, + getElectionTypeLabel, + getElectionStatusLabel, + getMinisterRoleLabel, + blocksToTime, + getRemainingBlocks, + type ElectionInfo, + type CollectiveProposal, + type CandidateInfo, +} from '@pezkuwi/lib/welati'; +import { handleBlockchainError, handleBlockchainSuccess } from '@pezkuwi/lib/error-handler'; +import { web3FromAddress } from '@polkadot/extension-dapp'; + +export default function Elections() { + const { api, selectedAccount, isApiReady } = usePolkadot(); + const { user } = useAuth(); + + const [loading, setLoading] = useState(true); + const [elections, setElections] = useState([]); + const [proposals, setProposals] = useState([]); + const [officials, setOfficials] = useState({}); + const [ministers, setMinisters] = useState({}); + + // Fetch data + useEffect(() => { + const fetchData = async () => { + if (!api || !isApiReady) return; + + try { + setLoading(true); + const [electionsData, proposalsData, officialsData, ministersData] = await Promise.all([ + getActiveElections(api), + getActiveProposals(api), + getCurrentOfficials(api), + getCurrentMinisters(api), + ]); + + setElections(electionsData); + setProposals(proposalsData); + setOfficials(officialsData); + setMinisters(ministersData); + } catch (error) { + console.error('Failed to load elections data:', error); + toast({ + title: 'Error', + description: 'Failed to load elections data', + variant: 'destructive', + }); + } finally { + setLoading(false); + } + }; + + fetchData(); + + // Refresh every 30 seconds + const interval = setInterval(fetchData, 30000); + return () => clearInterval(interval); + }, [api, isApiReady]); + + if (loading) { + return ; + } + + return ( +
+ {/* Header */} +
+

Welati - Elections & Governance

+

+ Democratic governance for Digital Kurdistan. Vote, propose, and participate in building our nation. +

+
+ + {/* Tabs */} + + + + + Elections + + + + Proposals + + + + Government + + + + {/* Elections Tab */} + + {elections.length === 0 ? ( + + + + No active elections at this time. Check back later for upcoming elections. + + + ) : ( +
+ {elections.map((election) => ( + + ))} +
+ )} +
+ + {/* Proposals Tab */} + + {proposals.length === 0 ? ( + + + + No active proposals at this time. Parliament members can submit new proposals. + + + ) : ( +
+ {proposals.map((proposal) => ( + + ))} +
+ )} +
+ + {/* Government Tab */} + + + +
+
+ ); +} + +// ============================================================================ +// ELECTION CARD +// ============================================================================ + +function ElectionCard({ election, api }: { election: ElectionInfo; api: any }) { + const [candidates, setCandidates] = useState([]); + const [timeLeft, setTimeLeft] = useState(null); + + const typeLabel = getElectionTypeLabel(election.electionType); + const statusLabel = getElectionStatusLabel(election.status); + + useEffect(() => { + if (!api) return; + + // Load candidates + getElectionCandidates(api, election.electionId).then(setCandidates); + + // Update time left + const updateTime = async () => { + let targetBlock = election.votingEndBlock; + if (election.status === 'CandidacyPeriod') targetBlock = election.candidacyEndBlock; + else if (election.status === 'CampaignPeriod') targetBlock = election.campaignEndBlock; + + const remaining = await getRemainingBlocks(api, targetBlock); + setTimeLeft(blocksToTime(remaining)); + }; + + updateTime(); + const interval = setInterval(updateTime, 10000); + return () => clearInterval(interval); + }, [api, election]); + + return ( + + +
+
+ {typeLabel.en} + {typeLabel.kmr} +
+ + {statusLabel.en} + +
+
+ + {/* Stats */} +
+
+
+ + Candidates +
+
{election.totalCandidates}
+
+
+
+ + Votes Cast +
+
{election.totalVotes.toLocaleString()}
+
+
+
+ + Time Left +
+
+ {timeLeft ? `${timeLeft.days}d ${timeLeft.hours}h` : '-'} +
+
+
+ + {/* Top Candidates */} + {candidates.length > 0 && ( +
+

Leading Candidates

+
+ {candidates.slice(0, 5).map((candidate, idx) => ( +
+
+
+ #{idx + 1} +
+
+
+ {candidate.account.slice(0, 12)}...{candidate.account.slice(-8)} +
+
+
+
+ + {candidate.voteCount.toLocaleString()} +
+
+ ))} +
+
+ )} + + {/* Actions */} +
+ {election.status === 'CandidacyPeriod' && ( + + )} + {election.status === 'VotingPeriod' && ( + + )} + +
+
+
+ ); +} + +// ============================================================================ +// PROPOSAL CARD +// ============================================================================ + +function ProposalCard({ proposal, api }: { proposal: CollectiveProposal; api: any }) { + const [timeLeft, setTimeLeft] = useState(null); + + const totalVotes = proposal.ayeVotes + proposal.nayVotes + proposal.abstainVotes; + const ayePercent = totalVotes > 0 ? Math.round((proposal.ayeVotes / totalVotes) * 100) : 0; + const nayPercent = totalVotes > 0 ? Math.round((proposal.nayVotes / totalVotes) * 100) : 0; + + useEffect(() => { + if (!api) return; + + const updateTime = async () => { + const remaining = await getRemainingBlocks(api, proposal.expiresAt); + setTimeLeft(blocksToTime(remaining)); + }; + + updateTime(); + const interval = setInterval(updateTime, 10000); + return () => clearInterval(interval); + }, [api, proposal]); + + return ( + + +
+
+ #{proposal.proposalId} {proposal.title} + {proposal.description} +
+ + {proposal.status} + +
+
+ + {/* Vote Progress */} +
+
+ Aye ({proposal.ayeVotes}) + Nay ({proposal.nayVotes}) +
+
+
+
+
+
+ {ayePercent}% Aye + + {proposal.votesCast} / {proposal.threshold} votes cast + + {nayPercent}% Nay +
+
+ + {/* Metadata */} +
+
+ + {timeLeft && `${timeLeft.days}d ${timeLeft.hours}h remaining`} +
+ {proposal.decisionType} +
+ + {/* Actions */} + {proposal.status === 'Active' && ( +
+ + + +
+ )} + + + ); +} + +// ============================================================================ +// GOVERNMENT OFFICIALS +// ============================================================================ + +function GovernmentOfficials({ officials, ministers }: { officials: any; ministers: any }) { + return ( +
+ {/* Executive */} + + + + + Executive Branch + + + + {officials.serok && ( + + )} + {officials.serokWeziran && ( + + )} + {officials.meclisBaskanı && ( + + )} + + + + {/* Cabinet */} + + + + + Cabinet Ministers + + + + {Object.entries(ministers).map( + ([role, address]: [string, any]) => + address && ( + + ) + )} + {Object.values(ministers).every((v) => !v) && ( +
No ministers appointed yet
+ )} +
+
+
+ ); +} + +function OfficeRow({ title, address, icon: Icon }: { title: string; address: string; icon: any }) { + return ( +
+
+ + {title} +
+ + {address.slice(0, 8)}...{address.slice(-6)} + +
+ ); +}