diff --git a/package-lock.json b/package-lock.json index 9ad2c3f..d0c5034 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,10 +14,14 @@ "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-navigation-menu": "^1.2.5", "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-progress": "^1.1.2", + "@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3", + "@radix-ui/react-tooltip": "^1.1.8", "@tailwindcss/vite": "^4.0.9", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -2245,6 +2249,62 @@ } } }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.2.tgz", + "integrity": "sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.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-radio-group": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.3.tgz", + "integrity": "sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA==", + "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-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "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-roving-focus": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", @@ -2319,6 +2379,29 @@ } } }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz", + "integrity": "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.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-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", @@ -2396,6 +2479,40 @@ } } }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz", + "integrity": "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==", + "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-dismissable-layer": "1.1.5", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-visually-hidden": "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-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", diff --git a/package.json b/package.json index 1cc6813..85094a1 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,14 @@ "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-navigation-menu": "^1.2.5", "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-progress": "^1.1.2", + "@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3", + "@radix-ui/react-tooltip": "^1.1.8", "@tailwindcss/vite": "^4.0.9", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/src/App.tsx b/src/App.tsx index 1f62543..8184980 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,7 +8,11 @@ import CandidatePage from '@/pages/AddCandidatePage'; import CreateElection from '@/pages/CreateElection'; import HomePage from '@/pages/Home'; import AdminLayout from '@/pages/admin/AdminLayout'; -import AdminVerifyVotersPage from './pages/admin/Verify-voters'; +import AdminVerifyVotersPage from '@/pages/admin/Verify-voters'; +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'; function App() { const auth = React.useContext(AuthContext); @@ -70,13 +74,19 @@ function App() { } /> } /> - election} /> + + } /> + } /> + } /> + past election} /> - } /> upcoming election} /> active election} /> + }> + } /> + } /> admin dashboard} /> } /> diff --git a/src/components/shared/admin/sidebar.tsx b/src/components/shared/admin/sidebar.tsx index fda6ab0..e2aa445 100644 --- a/src/components/shared/admin/sidebar.tsx +++ b/src/components/shared/admin/sidebar.tsx @@ -1,4 +1,4 @@ -import { Link } from 'react-router'; +import { Link, useLocation } from 'react-router'; import { BarChart, Home, Users, Vote } from 'lucide-react'; import { Button } from '@/components/ui/button'; @@ -28,8 +28,9 @@ const sidebarItems = [ ]; export function AdminSidebar() { + const location = useLocation(); return ( -
+

Admin Panel

@@ -39,8 +40,8 @@ export function AdminSidebar() { key={item.href} variant="ghost" className={cn( - 'w-full justify-start', - item.href === '/admin/verify-voters' && 'bg-accent text-accent-foreground' + 'w-full justify-start transition-colors duration-200 ease-in-out hover:bg-accent hover:text-accent-foreground', + item.href === location.pathname && 'bg-accent text-accent-foreground' )} asChild > diff --git a/src/components/shared/auth/Add-candidate-form.tsx b/src/components/shared/auth/Add-candidate-form.tsx index 17e1b0f..59f0d50 100644 --- a/src/components/shared/auth/Add-candidate-form.tsx +++ b/src/components/shared/auth/Add-candidate-form.tsx @@ -47,10 +47,7 @@ export function AddCandidateForm() { try { if (state.instance !== null) { - await state.instance.methods.addCandidate(values.name, values.slogan, electionid).send({ - from: state.account, - gas: 1000000, - }); + await state.instance.methods.addCandidate(values.name, values.slogan, electionid); toast.success('Candidate added successfully'); form.reset(); diff --git a/src/components/shared/auth/Login-form.tsx b/src/components/shared/auth/Login-form.tsx index 4135fa8..74a5f06 100644 --- a/src/components/shared/auth/Login-form.tsx +++ b/src/components/shared/auth/Login-form.tsx @@ -42,10 +42,7 @@ export function LoginForm() { } if (state.instance !== null) { - await state.instance.addVoter(state.account, values.name).send({ - from: state.account, - gas: 1000000, - }); + await state.instance.addVoter(state.account, values.name); dispatch({ type: 'REGISTER', diff --git a/src/components/shared/candidate-card.tsx b/src/components/shared/candidate-card.tsx new file mode 100644 index 0000000..14bfb3b --- /dev/null +++ b/src/components/shared/candidate-card.tsx @@ -0,0 +1,62 @@ +import { Link } from 'react-router'; + +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Progress } from '@/components/ui/progress'; + +interface Candidate { + id: number; + name: string; + slogan: string; + votes?: number; + percentage?: number; + position?: number; + winner?: boolean; +} + +interface CandidateCardProps { + candidate: Candidate; + electionId: number; + showResults?: boolean; +} + +export function CandidateCard({ candidate, electionId, showResults = false }: CandidateCardProps) { + return ( + + +
+
+ {candidate.name} + {candidate.position && ( +
+ Rank: {candidate.position} + {candidate.winner && ( + Winner + )} +
+ )} +
+
+
+ +

{candidate.slogan}

+ + {showResults && candidate.votes !== undefined && candidate.percentage !== undefined && ( +
+
+ Votes: {candidate.votes} + {candidate.percentage}% +
+ +
+ )} +
+ + + +
+ ); +} diff --git a/src/components/shared/election/create-election.tsx b/src/components/shared/election/create-election.tsx index c926e2b..5b45b33 100644 --- a/src/components/shared/election/create-election.tsx +++ b/src/components/shared/election/create-election.tsx @@ -52,10 +52,7 @@ export function CreateElectionForm() { } } - await state.instance.createElection(values.purpose).send({ - from: state.account, - gas: 1000000, - }); + await state.instance.createElection(values.purpose); toast.message('Election created successfully', { description: 'A new election has been created.', @@ -63,7 +60,6 @@ export function CreateElectionForm() { } } catch (error) { console.error(`Error: ${error}`); - toast.error('Error occurred while creating the election'); } }); }; diff --git a/src/components/shared/election/page.tsx b/src/components/shared/election/page.tsx new file mode 100644 index 0000000..bbc4ec7 --- /dev/null +++ b/src/components/shared/election/page.tsx @@ -0,0 +1,275 @@ +import { Link } from 'react-router'; +import { CalendarClock, Clock, ExternalLink, Info, Shield, Timer, Users, Vote } from 'lucide-react'; + +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Progress } from '@/components/ui/progress'; +import { Separator } from '@/components/ui/separator'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { CandidateCard } from '@/components/shared/candidate-card'; + +const election = { + id: 3, + title: 'City Council Representative Election', + description: + "Vote for your district's representative on the city council for the upcoming term. The elected representative will serve for a period of two years and will be responsible for representing the interests of residents in district meetings, proposing and voting on local ordinances, and addressing community concerns.", + startDate: '2023-11-15T09:00:00', + endDate: '2023-12-15T18:00:00', + organizerName: 'City Electoral Commission', + organizerDescription: + 'The City Electoral Commission is responsible for conducting fair and transparent elections within the city limits. The commission ensures that all eligible citizens have the opportunity to vote and that the electoral process adheres to established regulations.', + totalVoters: 2500, + totalVotes: 1243, + voterParticipation: 49.7, + status: 'active', + electionType: 'Single Choice', + blockchainAddress: '0x7a8b9c0d1e2f3g4h5i6j7k8l9m0n1o2p3q4r5s6t7u', + candidates: [ + { + id: 1, + name: 'Rebecca Johnson', + slogan: 'Sustainable development and affordable housing for all', + votes: 487, + percentage: 39.2, + }, + { + id: 2, + name: 'Michael Chen', + slogan: 'Economic growth, public safety, and efficient city services', + votes: 356, + percentage: 28.6, + }, + { + id: 3, + name: 'Sophia Rodriguez', + slogan: 'Better public spaces and youth programs for our community', + votes: 289, + percentage: 23.3, + }, + { + id: 4, + name: 'David Washington', + slogan: 'Improving infrastructure and environmental initiatives', + votes: 111, + percentage: 8.9, + }, + ], +}; + +function formatDate(dateString: string) { + const options: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }; + return new Date(dateString).toLocaleDateString(undefined, options); +} + +function getDaysRemaining(endDate: string) { + const end = new Date(endDate); + const now = new Date(); + const diffTime = end.getTime() - now.getTime(); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + return diffDays; +} + +export default function ElectionDetailPage() { + const daysRemaining = getDaysRemaining(election.endDate); + const isActive = + new Date() >= new Date(election.startDate) && new Date() <= new Date(election.endDate); + + return ( +
+
+
+
+
+
+ + {election.electionType} + + {isActive ? ( + + Active + + ) : ( + + Upcoming + + )} +
+ +

{election.title}

+ +

{election.description}

+ +
+
+ + Start: {formatDate(election.startDate)} +
+
+ + End: {formatDate(election.endDate)} +
+
+ + {election.totalVotes} votes cast +
+
+ + {isActive && ( +
+ + +
+ )} +
+ +
+
+

Candidates

+
+ {election.candidates.map((candidate) => ( + + ))} +
+
+ +
+

About the Organizer

+ + + {election.organizerName} + + +

{election.organizerDescription}

+
+
+
+
+
+ +
+
+ + + Election Status + + + {isActive ? ( +
+
Voting Open
+
+ + {daysRemaining > 0 ? `${daysRemaining} days remaining` : 'Ending today'} +
+
+ ) : ( +
+
+ Upcoming Election +
+
+ + Starts {formatDate(election.startDate)} +
+
+ )} + +
+
+ Voter Participation + {election.voterParticipation}% +
+ +
+ {election.totalVotes} out of {election.totalVoters} registered voters +
+
+ + + +
+
+ Election Type + {election.electionType} +
+
+ Total Candidates + {election.candidates.length} +
+
+
+
+ + + + + + Blockchain Verification + + + +
+

+ This election is secured by blockchain technology, ensuring transparency and + immutability of all votes. +

+
+ Contract Address + + + + + {election.blockchainAddress.substring(0, 10)}... + + + + +

{election.blockchainAddress}

+
+
+
+
+ +
+
+
+ + + + Share This Election + + +
+ +
+
+
+
+
+
+
+
+ ); +} diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx new file mode 100644 index 0000000..10af7e6 --- /dev/null +++ b/src/components/ui/progress.tsx @@ -0,0 +1,29 @@ +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +function Progress({ + className, + value, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { Progress } diff --git a/src/components/ui/radio-group.tsx b/src/components/ui/radio-group.tsx new file mode 100644 index 0000000..ae6f99f --- /dev/null +++ b/src/components/ui/radio-group.tsx @@ -0,0 +1,43 @@ +import * as React from "react" +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" +import { CircleIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function RadioGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function RadioGroupItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { RadioGroup, RadioGroupItem } diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 0000000..3cf4f89 --- /dev/null +++ b/src/components/ui/separator.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@/lib/utils" + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Separator } diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..ee7ae86 --- /dev/null +++ b/src/components/ui/tooltip.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +function TooltipProvider({ + delayDuration = 0, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function Tooltip({ + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function TooltipTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function TooltipContent({ + className, + sideOffset = 0, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + ) +} + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/src/pages/CreateElection.tsx b/src/pages/CreateElection.tsx index 04e54a2..68beadc 100644 --- a/src/pages/CreateElection.tsx +++ b/src/pages/CreateElection.tsx @@ -1,11 +1,194 @@ -import { CreateElectionForm } from '@/components/shared/election/create-election'; +import type React from 'react'; +import { useState } from 'react'; +import { Link } from 'react-router'; +import { Info, Plus, Trash2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Switch } from '@/components/ui/switch'; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; + +export default function CreateElectionPage() { + const [purpose, setPurpose] = useState(''); + + const [candidates, setCandidates] = useState([{ id: 1, name: '', slogan: '' }]); + const [successDialogOpen, setSuccessDialogOpen] = useState(false); + + const addCandidate = () => { + setCandidates([...candidates, { id: candidates.length + 1, name: '', slogan: '' }]); + }; + + const removeCandidate = (id: number) => { + if (candidates.length > 1) { + setCandidates(candidates.filter((candidate) => candidate.id !== id)); + } + }; + + const updateCandidate = (id: number, field: string, value: string) => { + setCandidates( + candidates.map((candidate) => + candidate.id === id ? { ...candidate, [field]: value } : candidate + ) + ); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setSuccessDialogOpen(true); + }; -const CreateElection = () => { return ( -
- -
- ); -}; +
+
+

Create a New Election

+

+ Set up a secure blockchain-based election in minutes. +

+
-export default CreateElection; +
+ + + Election Details + Enter the basic information about this election + + +
+ +