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..3707cac 100644 --- a/src/pages/election/ElectionPage.tsx +++ b/src/pages/election/ElectionPage.tsx @@ -4,6 +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 { useNavigate, useParams } from 'react-router'; import { useParams } from 'react-router'; import AuthContext from '@/context/AuthContext'; import { toast } from 'sonner'; @@ -27,7 +28,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(); const handleStartElection = async () => { @@ -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 + + + + +
+
+ ))} +
+
+ ); +}