CIA/e-voting-system/frontend/components/voting-interface.tsx
Alexis Bruneteau f825a2392c feat: Implement dark theme for frontend with toggle
Changes:
- Add next-themes dependency for theme management
- Create ThemeProvider wrapper for app root layout
- Set dark mode as default theme
- Create ThemeToggle component with Sun/Moon icons
- Add theme toggle to home page navigation
- Add theme toggle to dashboard header
- App now starts in dark mode with ability to switch to light mode

Styling uses existing Tailwind dark mode variables configured in
tailwind.config.ts and globals.css. All existing components automatically
support dark theme.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 16:35:44 +01:00

369 lines
12 KiB
TypeScript

"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { AlertCircle, CheckCircle, Loader2 } from "lucide-react"
import { createSignedBallot, PublicKeysResponse } from "@/lib/crypto-client"
export interface Candidate {
id: number
name: string
description?: string
}
export interface VotingInterfaceProps {
electionId: number
candidates: Candidate[]
publicKeys?: PublicKeysResponse
onVoteSubmitted?: (success: boolean, transactionId?: string) => void
}
type VotingStep = "select" | "confirm" | "submitting" | "success" | "error"
/**
* Voting Interface Component
* Handles ballot creation, encryption, and submission
*/
export function VotingInterface({
electionId,
candidates,
publicKeys,
onVoteSubmitted
}: VotingInterfaceProps) {
const [step, setStep] = useState<VotingStep>("select")
const [selectedCandidate, setSelectedCandidate] = useState<number | null>(null)
const [error, setError] = useState<string>("")
const [transactionId, setTransactionId] = useState<string>("")
const [loading, setLoading] = useState(false)
/**
* Handle candidate selection
*/
const handleSelectCandidate = (candidateId: number) => {
setSelectedCandidate(candidateId)
setStep("confirm")
setError("")
}
/**
* Handle back button to reselect
*/
const handleBack = () => {
setSelectedCandidate(null)
setStep("select")
setError("")
}
/**
* Handle vote submission with encryption and signing
*/
const handleSubmitVote = async () => {
if (selectedCandidate === null) {
setError("Veuillez sélectionner un candidat")
return
}
setLoading(true)
setError("")
try {
// 1. Get voter context from API
const voterResponse = await fetch("/api/auth/profile", {
headers: {
"Authorization": `Bearer ${localStorage.getItem("auth_token")}`
}
})
if (!voterResponse.ok) {
throw new Error("Impossible de récupérer le profil du votant")
}
const voterData = await voterResponse.json()
const voterId = voterData.id.toString()
// 2. Get or use provided public keys
let keysToUse = publicKeys
if (!keysToUse) {
// Fetch public keys from server
const keysResponse = await fetch(`/api/votes/public-keys?election_id=${electionId}`)
if (!keysResponse.ok) {
throw new Error("Impossible de récupérer les clés publiques")
}
keysToUse = await keysResponse.json()
}
if (!keysToUse?.elgamal_pubkey) {
throw new Error("Clés publiques non disponibles")
}
// 3. Create signed ballot with client-side encryption
// For MVP: Use simple vote encoding (0 or 1)
// Selected candidate = 1, others = 0
const voteValue = selectedCandidate ? 1 : 0
const ballot = createSignedBallot(
voteValue,
voterId,
keysToUse.elgamal_pubkey,
"" // Empty private key for signing in MVP
)
// 4. Submit encrypted ballot
setStep("submitting")
const submitResponse = await fetch("/api/votes/submit", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${localStorage.getItem("auth_token")}`
},
body: JSON.stringify({
election_id: electionId,
candidate_id: selectedCandidate,
encrypted_vote: ballot.encrypted_vote,
zkp_proof: ballot.zkp_proof,
signature: ballot.signature,
timestamp: ballot.timestamp
})
})
if (!submitResponse.ok) {
const errorData = await submitResponse.json()
throw new Error(errorData.detail || "Erreur lors de la soumission du vote")
}
const result = await submitResponse.json()
// 5. Success
setTransactionId(result.transaction_id || result.id)
setStep("success")
// Notify parent component
if (onVoteSubmitted) {
onVoteSubmitted(true, result.transaction_id || result.id)
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Erreur inconnue"
setError(errorMessage)
setStep("error")
if (onVoteSubmitted) {
onVoteSubmitted(false)
}
} finally {
setLoading(false)
}
}
/**
* Reset to allow new vote (in testing mode only)
*/
const handleReset = () => {
setSelectedCandidate(null)
setStep("select")
setError("")
setTransactionId("")
}
return (
<div className="w-full max-w-2xl mx-auto">
{/* Selection Step */}
{step === "select" && (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold">Sélectionnez votre vote</h2>
<p className="text-muted-foreground mt-2">
Choisissez le candidat ou l'option pour lequel vous souhaitez voter
</p>
</div>
<div className="grid gap-4">
{candidates.map((candidate) => (
<Card
key={candidate.id}
className="cursor-pointer hover:border-accent hover:bg-accent/5 transition-colors"
onClick={() => handleSelectCandidate(candidate.id)}
>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold">{candidate.name}</h3>
{candidate.description && (
<p className="text-sm text-muted-foreground mt-1">
{candidate.description}
</p>
)}
</div>
<div className="h-6 w-6 rounded-full border-2 border-muted-foreground" />
</div>
</CardContent>
</Card>
))}
</div>
{error && (
<Card className="border-red-500 bg-red-50">
<CardContent className="pt-6 flex gap-4">
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-800">{error}</p>
</CardContent>
</Card>
)}
</div>
)}
{/* Confirmation Step */}
{step === "confirm" && selectedCandidate !== null && (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold">Confirmez votre vote</h2>
<p className="text-muted-foreground mt-2">
Veuillez vérifier votre sélection avant de soumettre
</p>
</div>
<Card className="bg-accent/10 border-accent">
<CardHeader>
<CardTitle>Vote sélectionné</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<p className="font-semibold">
{candidates.find((c) => c.id === selectedCandidate)?.name}
</p>
<p className="text-sm text-muted-foreground">
{candidates.find((c) => c.id === selectedCandidate)?.description}
</p>
</div>
</CardContent>
</Card>
<Card className="bg-blue-50 border-blue-200">
<CardContent className="pt-6">
<p className="text-sm text-blue-900">
<strong>Sécurité du vote:</strong> Votre bulletin sera chiffré avec
ElGamal homomorphe avant transmission. Seul le décompte final sera connu,
pas votre vote individuel.
</p>
</CardContent>
</Card>
<div className="flex gap-3">
<Button variant="outline" onClick={handleBack} disabled={loading}>
Retour
</Button>
<Button
onClick={handleSubmitVote}
disabled={loading}
className="flex-1"
>
{loading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Envoi en cours...
</>
) : (
"Confirmer et voter"
)}
</Button>
</div>
{error && (
<Card className="border-red-500 bg-red-50">
<CardContent className="pt-6 flex gap-4">
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-800">{error}</p>
</CardContent>
</Card>
)}
</div>
)}
{/* Submitting Step */}
{step === "submitting" && (
<Card>
<CardContent className="pt-6 flex flex-col items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-accent mb-4" />
<h3 className="font-semibold">Soumission du vote en cours...</h3>
<p className="text-sm text-muted-foreground mt-2">
Votre bulletin est en cours de chiffrement et d'enregistrement
</p>
</CardContent>
</Card>
)}
{/* Success Step */}
{step === "success" && (
<div className="space-y-6">
<Card className="bg-green-50 border-green-200">
<CardContent className="pt-6">
<div className="flex gap-4">
<CheckCircle className="h-6 w-6 text-green-600 flex-shrink-0 mt-0.5" />
<div>
<h3 className="font-semibold text-green-900">Vote enregistré avec succès</h3>
<p className="text-sm text-green-800 mt-1">
Votre bulletin chiffré a é ajouté à la blockchain électorale
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Détails du vote</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div>
<label className="text-xs font-medium text-muted-foreground">
Identifiant de transaction
</label>
<p className="font-mono text-sm break-all bg-muted p-2 rounded mt-1">
{transactionId}
</p>
</div>
<div className="text-sm text-muted-foreground">
<p> Bulletin chiffré avec ElGamal</p>
<p> Signature Dilithium appliquée</p>
<p> Enregistré dans la blockchain</p>
</div>
</CardContent>
</Card>
<Button className="w-full" onClick={handleReset}>
Fermer
</Button>
</div>
)}
{/* Error Step */}
{step === "error" && (
<div className="space-y-6">
<Card className="border-red-500 bg-red-50">
<CardContent className="pt-6">
<div className="flex gap-4">
<AlertCircle className="h-6 w-6 text-red-600 flex-shrink-0 mt-0.5" />
<div>
<h3 className="font-semibold text-red-900">Erreur lors de la soumission</h3>
<p className="text-sm text-red-800 mt-1">{error}</p>
</div>
</div>
</CardContent>
</Card>
<div className="flex gap-3">
<Button variant="outline" onClick={handleBack}>
Retour
</Button>
<Button onClick={handleReset} className="flex-1">
Réessayer
</Button>
</div>
</div>
)}
</div>
)
}