From 9f26dae7252b66e724ae29f8f64360a545e5f463 Mon Sep 17 00:00:00 2001 From: shishiro Date: Fri, 7 Mar 2025 00:53:44 +0530 Subject: [PATCH 1/2] added election page --- src/App.tsx | 2 + src/components/shared/admin/sidebar.tsx | 4 +- src/pages/CreateElection.tsx | 80 +++++++- src/pages/admin/AdminLayout.tsx | 4 +- src/pages/admin/Election.tsx | 2 +- src/pages/election/ElectionPage.tsx | 252 ++++++++++++++++++++++++ 6 files changed, 334 insertions(+), 10 deletions(-) create mode 100644 src/pages/election/ElectionPage.tsx diff --git a/src/App.tsx b/src/App.tsx index 8184980..a483636 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,6 +13,7 @@ import AdminElectionsPage from '@/pages/admin/Election'; import ElectionDetailPage from '@/components/shared/election/page'; import VotePage from '@/pages/election/VotePage'; import ElectionResultsPage from '@/pages/election/ResultPage'; +import ElectionDetails from '@/pages/election/ElectionPage'; function App() { const auth = React.useContext(AuthContext); @@ -85,6 +86,7 @@ function App() { }> + } /> } /> } /> admin dashboard} /> diff --git a/src/components/shared/admin/sidebar.tsx b/src/components/shared/admin/sidebar.tsx index e2aa445..c3b1f46 100644 --- a/src/components/shared/admin/sidebar.tsx +++ b/src/components/shared/admin/sidebar.tsx @@ -30,8 +30,8 @@ const sidebarItems = [ export function AdminSidebar() { const location = useLocation(); return ( -
-
+
+

Admin Panel

diff --git a/src/pages/CreateElection.tsx b/src/pages/CreateElection.tsx index 68beadc..da5c08e 100644 --- a/src/pages/CreateElection.tsx +++ b/src/pages/CreateElection.tsx @@ -1,5 +1,5 @@ import type React from 'react'; -import { useState } from 'react'; +import { useContext, useState } from 'react'; import { Link } from 'react-router'; import { Info, Plus, Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; @@ -24,12 +24,14 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; +import { toast } from 'sonner'; +import AuthContext from '@/context/AuthContext'; export default function CreateElectionPage() { const [purpose, setPurpose] = useState(''); - const [candidates, setCandidates] = useState([{ id: 1, name: '', slogan: '' }]); const [successDialogOpen, setSuccessDialogOpen] = useState(false); + const { state } = useContext(AuthContext); const addCandidate = () => { setCandidates([...candidates, { id: candidates.length + 1, name: '', slogan: '' }]); @@ -49,9 +51,75 @@ export default function CreateElectionPage() { ); }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - setSuccessDialogOpen(true); + + if (candidates.length < 2) { + toast.error('You need to have at least 2 candidates'); + return; + } + + if (!state.is_admin) { + toast.error('You do not have permission to create an election'); + return; + } + + let totalElections = 0; + let is_operation_successful = false; + + const createElectionPromise = new Promise((resolve, reject) => { + (async () => { + try { + totalElections = await state.instance!.noOfElections(); + + for (let i = 1; i <= totalElections; i++) { + const electionData = await state.instance!.getElection(i); + if ( + electionData.purpose && + electionData.purpose.toLowerCase() === purpose.toLowerCase() + ) { + throw new Error('This election is already created'); + } + } + + const tx = await state.instance!.createElection(purpose); + await tx.wait(); + + totalElections = await state.instance!.noOfElections(); + + for (const candidate of candidates) { + console.log('Adding candidate:', candidate); + const tx = await state.instance!.addCandidate( + candidate.name, + candidate.slogan, + totalElections + ); + await tx.wait(); + } + is_operation_successful = true; + resolve('Election created successfully'); + } catch (error) { + reject(error); + } + })(); + }); + + toast.promise(createElectionPromise, { + loading: 'Creating election...', + success: 'Election created successfully', + error: (error) => { + if (error.message.includes('user rejected transaction')) { + return 'Error: User rejected the transaction'; + } + return `Error while creating the election`; + }, + }); + + createElectionPromise.then(() => { + if (is_operation_successful) { + setSuccessDialogOpen(true); + } + }); }; return ( @@ -148,7 +216,9 @@ export default function CreateElectionPage() { - + diff --git a/src/pages/admin/AdminLayout.tsx b/src/pages/admin/AdminLayout.tsx index 15c8859..552cc91 100644 --- a/src/pages/admin/AdminLayout.tsx +++ b/src/pages/admin/AdminLayout.tsx @@ -3,9 +3,9 @@ import { AdminSidebar } from '@/components/shared/admin/sidebar'; const AdminLayout = () => { return ( -
+
-
+
diff --git a/src/pages/admin/Election.tsx b/src/pages/admin/Election.tsx index f3b5253..7c1492b 100644 --- a/src/pages/admin/Election.tsx +++ b/src/pages/admin/Election.tsx @@ -76,7 +76,7 @@ export default function AdminElectionsPage() {

Elections

+ )} + + {electionStatus === 'Active' && ( + + )} +
+
+ + + +
+ Election Summary + + {electionStatus} + +
+
+ +
+
+

Election ID

+

{election?.id}

+
+
+

Election Purpose

+

{election?.purpose}

+
+
+

Total Votes

+

{election?.totalVotes}

+
+
+
+
+ +

Candidates

+
+ {candidates?.map((candidate) => ( + + ))} +
+ +
+ ); +} + +interface CandidateCardProps { + name: string; + id: string; + slogan: string; + voteCount: number; +} + +function CandidateCard({ name, id, slogan, voteCount }: CandidateCardProps) { + return ( + + + {name} + Candidate ID: {id} + + +
+

Slogan

+

{slogan}

+
+ +
+

Vote Count

+

{voteCount}

+
+
+
+ ); +} From 7239e692dd56e851b7319750c802ce126e9e1dab Mon Sep 17 00:00:00 2001 From: shishiro Date: Fri, 7 Mar 2025 19:42:43 +0530 Subject: [PATCH 2/2] added pages --- package-lock.json | 29 +++ package.json | 1 + src/App.tsx | 2 + src/components/shared/Navbar.tsx | 14 +- src/components/ui/alert-dialog.tsx | 155 ++++++++++++++++ src/components/ui/button.tsx | 46 ++--- src/pages/CreateElection.tsx | 2 +- src/pages/election/ElectionPage.tsx | 4 +- src/pages/election/ResultsPage.tsx | 264 ++++++++++++++++++++++++++++ 9 files changed, 487 insertions(+), 30 deletions(-) create mode 100644 src/components/ui/alert-dialog.tsx create mode 100644 src/pages/election/ResultsPage.tsx diff --git a/package-lock.json b/package-lock.json index d0c5034..9e94734 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@hookform/resolvers": "^4.1.3", + "@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", @@ -1767,6 +1768,34 @@ "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", "license": "MIT" }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz", + "integrity": "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dialog": "1.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz", diff --git a/package.json b/package.json index 85094a1..698d1dc 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@hookform/resolvers": "^4.1.3", + "@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", diff --git a/src/App.tsx b/src/App.tsx index a483636..cf64d98 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,6 +14,7 @@ import ElectionDetailPage from '@/components/shared/election/page'; import VotePage from '@/pages/election/VotePage'; import ElectionResultsPage from '@/pages/election/ResultPage'; import ElectionDetails from '@/pages/election/ElectionPage'; +import ResultsPage from './pages/election/ResultsPage'; function App() { const auth = React.useContext(AuthContext); @@ -91,6 +92,7 @@ function App() { } /> admin dashboard
} /> } /> + } /> } /> not found return 404
} /> diff --git a/src/components/shared/Navbar.tsx b/src/components/shared/Navbar.tsx index 36a5093..d66d452 100644 --- a/src/components/shared/Navbar.tsx +++ b/src/components/shared/Navbar.tsx @@ -70,11 +70,13 @@ export default function Navbar() { - - - Register as voter - - + {state.is_admin ? null : ( + + + Register as voter + + + )} About @@ -86,7 +88,7 @@ export default function Navbar() {
{state.is_admin ? ( diff --git a/src/pages/election/ElectionPage.tsx b/src/pages/election/ElectionPage.tsx index 2653008..d00fc89 100644 --- a/src/pages/election/ElectionPage.tsx +++ b/src/pages/election/ElectionPage.tsx @@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Play, Square } from 'lucide-react'; import { Separator } from '@/components/ui/separator'; -import { useParams } from 'react-router'; +import { useNavigate, useParams } from 'react-router'; import AuthContext from '@/context/AuthContext'; import { toast } from 'sonner'; @@ -27,6 +27,7 @@ export default function ElectionDetails() { const [election, setElection] = React.useState(null); const [candidates, setCandidates] = React.useState(null); const { state } = useContext(AuthContext); + const navigate = useNavigate(); const params = useParams(); @@ -79,6 +80,7 @@ export default function ElectionDetails() { loading: 'Ending election...', success: () => { setElectionStatus('Ended'); + navigate('/admin/results'); return 'Election ended successfully'; }, error: (error) => { diff --git a/src/pages/election/ResultsPage.tsx b/src/pages/election/ResultsPage.tsx new file mode 100644 index 0000000..eec06f2 --- /dev/null +++ b/src/pages/election/ResultsPage.tsx @@ -0,0 +1,264 @@ +import React from 'react'; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogFooter, + DialogClose, +} from '@/components/ui/dialog'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog'; +import { Trophy, AlertTriangle } from 'lucide-react'; +import AuthContext from '@/context/AuthContext'; +import { BigNumber } from 'ethers'; +import { toast } from 'sonner'; + +interface ResultProps { + name: string; + votes: number; +} + +export default function ResultsPage() { + const [openDialogId, setOpenDialogId] = React.useState(null); + const [alertDialogOpen, setAlertDialogOpen] = React.useState(null); + const [loading, setLoading] = React.useState(false); + const { state } = React.useContext(AuthContext); + const [elections, setElections] = React.useState< + | { id: number; title: string; status: string; totalVotes: number; candidate_id: number[] }[] + | null + >(null); + const [result, setResult] = React.useState(null); + + const handleDeleteElection = async (id: number) => { + toast.promise( + state.instance!.deleteElection(id).then(() => { + setElections((prevElections) => + prevElections ? prevElections.filter((election) => election.id !== id) : [] + ); + }), + { + loading: 'Deleting election...', + success: 'Election deleted successfully!', + error: 'Failed to delete election.', + } + ); + }; + + React.useEffect(() => { + const getElections = async () => { + if (!state.instance) return; + + setLoading(true); + + try { + const election_count = await state.instance!.noOfElections(); + const elections_ = []; + + for (let i = 1; i <= election_count.toNumber(); i++) { + const result = await state.instance!.getElection(i); + + if (result.purpose !== '') { + const statusNumber = result.status.toNumber(); + const status = + statusNumber === 1 ? 'upcoming' : statusNumber === 2 ? 'active' : 'completed'; + console.log('result', result); + const candidates = result.candidatesids; + elections_.push({ + id: i, + title: result.purpose, + candidate_id: candidates.map((c: BigNumber) => c.toNumber()), + status, + totalVotes: result.totalVotes.toNumber(), + }); + } + } + + setElections(elections_.filter((election) => election.status === 'completed')); + } catch (error) { + console.error('Error fetching elections:', error); + } finally { + setLoading(false); + } + }; + + getElections(); + }, [state]); + + const handleResultAction = async (id: number) => { + if (!elections) { + console.error('No elections found.'); + return; + } + + const election = elections.find((e) => e.id === id); + if (!election) { + console.error('Election not found.'); + return; + } + + const candidates = election.candidate_id; + console.log('candidates', candidates); + + if (!candidates || candidates.length === 0) { + toast.error('No candidates found for this election.'); + return; + } + + toast.promise( + Promise.all( + candidates.map(async (i) => { + const candidate = await state.instance!.getCandidate(i); + return { + name: candidate.name, + votes: candidate.voteCount.toNumber(), + }; + }) + ).then((res) => { + res.sort((a, b) => b.votes - a.votes); + setResult(res); + setOpenDialogId(id); + }), + { + loading: 'Fetching election results...', + success: 'Election results fetched successfully!', + error: 'Failed to fetch election results.', + } + ); + }; + + if (loading) { + return <>loading....; + } + return ( +
+
+
+
+

Election Results

+

View the final results of completed elections

+
+
+ + {elections?.map((election) => ( + + +
+ Election Summary + Ended +
+
+ +
+

Election ID

+

{election.id}

+
+
+

Election Purpose

+

{election.title}

+
+
+

Election Status

+

{election.status}

+
+
+

Total Votes

+

{election.totalVotes}

+
+
+ + setOpenDialogId(isOpen ? election.id : null)} + > + + + + + + Election Result + + {election.title} + + +
+
+ + DRAW + +
+

+ There is a draw between the following candidates with each getting 0 votes. +

+
+ {result?.map((r) => ( +
+ {r.name} + {r.votes} votes +
+ ))} +
+
+ + + + + +
+
+ + setAlertDialogOpen(isOpen ? election.id : null)} + > + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete the election and + all associated data including votes and candidate information. + + + + Cancel + handleDeleteElection(election.id)} + className="bg-destructive text-destructive-foreground" + > + Delete + + + + +
+
+ ))} +
+
+ ); +}