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>
281 lines
9.1 KiB
TypeScript
281 lines
9.1 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import Link from "next/link"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { BlockchainVisualizer, BlockchainData } from "@/components/blockchain-visualizer"
|
|
import { ArrowLeft, RefreshCw } from "lucide-react"
|
|
|
|
export default function BlockchainPage() {
|
|
const [selectedElection, setSelectedElection] = useState<number | null>(null)
|
|
const [blockchainData, setBlockchainData] = useState<BlockchainData | null>(null)
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
const [isVerifying, setIsVerifying] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [elections, setElections] = useState<Array<{ id: number; name: string }>>([])
|
|
const [electionsLoading, setElectionsLoading] = useState(true)
|
|
|
|
// Fetch available elections
|
|
useEffect(() => {
|
|
const fetchElections = async () => {
|
|
try {
|
|
setElectionsLoading(true)
|
|
const token = localStorage.getItem("auth_token")
|
|
const response = await fetch("/api/elections", {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error("Impossible de charger les élections")
|
|
}
|
|
|
|
const data = await response.json()
|
|
setElections(data.elections || [])
|
|
|
|
// Select first election by default
|
|
if (data.elections && data.elections.length > 0) {
|
|
setSelectedElection(data.elections[0].id)
|
|
}
|
|
} catch (err) {
|
|
console.error("Error fetching elections:", err)
|
|
// Mock elections for demo
|
|
setElections([
|
|
{ id: 1, name: "Election Présidentielle 2025" },
|
|
{ id: 2, name: "Référendum : Réforme Constitutionnelle" },
|
|
{ id: 3, name: "Election Municipale - Île-de-France" },
|
|
])
|
|
setSelectedElection(1)
|
|
} finally {
|
|
setElectionsLoading(false)
|
|
}
|
|
}
|
|
|
|
fetchElections()
|
|
}, [])
|
|
|
|
// Fetch blockchain data
|
|
useEffect(() => {
|
|
if (!selectedElection) return
|
|
|
|
const fetchBlockchain = async () => {
|
|
try {
|
|
setIsLoading(true)
|
|
setError(null)
|
|
const token = localStorage.getItem("auth_token")
|
|
|
|
const response = await fetch(
|
|
`/api/votes/blockchain?election_id=${selectedElection}`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
}
|
|
)
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 404) {
|
|
// No blockchain yet, create empty state
|
|
setBlockchainData({
|
|
blocks: [],
|
|
verification: {
|
|
chain_valid: true,
|
|
total_blocks: 0,
|
|
total_votes: 0,
|
|
},
|
|
})
|
|
return
|
|
}
|
|
throw new Error("Impossible de charger la blockchain")
|
|
}
|
|
|
|
const data = await response.json()
|
|
setBlockchainData(data)
|
|
} catch (err) {
|
|
const errorMessage = err instanceof Error ? err.message : "Erreur inconnue"
|
|
setError(errorMessage)
|
|
// Mock blockchain for demo
|
|
setBlockchainData({
|
|
blocks: [
|
|
{
|
|
index: 0,
|
|
prev_hash: "0".repeat(64),
|
|
timestamp: Math.floor(Date.now() / 1000) - 3600,
|
|
encrypted_vote: "",
|
|
transaction_id: "genesis",
|
|
block_hash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
signature: "",
|
|
},
|
|
{
|
|
index: 1,
|
|
prev_hash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
timestamp: Math.floor(Date.now() / 1000) - 2400,
|
|
encrypted_vote: "aGVsbG8gd29ybGQgdm90ZSBl",
|
|
transaction_id: "tx-voter1-001",
|
|
block_hash: "2c26b46911185131006ba5991ab4ef3d89854e7cf44e10898fbee6d29fc80e4d",
|
|
signature: "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
|
|
},
|
|
{
|
|
index: 2,
|
|
prev_hash: "2c26b46911185131006ba5991ab4ef3d89854e7cf44e10898fbee6d29fc80e4d",
|
|
timestamp: Math.floor(Date.now() / 1000) - 1200,
|
|
encrypted_vote: "d29ybGQgaGVsbG8gdm90ZSBl",
|
|
transaction_id: "tx-voter2-001",
|
|
block_hash: "fcde2b2edba56bf408601fb721fe9b5348ccb48664c11d95d3a0e17de2d63594e",
|
|
signature: "d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3",
|
|
},
|
|
],
|
|
verification: {
|
|
chain_valid: true,
|
|
total_blocks: 3,
|
|
total_votes: 2,
|
|
},
|
|
})
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
fetchBlockchain()
|
|
}, [selectedElection])
|
|
|
|
// Verify blockchain
|
|
const handleVerifyBlockchain = async () => {
|
|
if (!selectedElection) return
|
|
|
|
try {
|
|
setIsVerifying(true)
|
|
const token = localStorage.getItem("auth_token")
|
|
|
|
const response = await fetch("/api/votes/verify-blockchain", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({ election_id: selectedElection }),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error("Erreur lors de la vérification")
|
|
}
|
|
|
|
const data = await response.json()
|
|
if (blockchainData) {
|
|
setBlockchainData({
|
|
...blockchainData,
|
|
verification: {
|
|
...blockchainData.verification,
|
|
chain_valid: data.chain_valid,
|
|
},
|
|
})
|
|
}
|
|
} catch (err) {
|
|
console.error("Verification error:", err)
|
|
} finally {
|
|
setIsVerifying(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
{/* Header */}
|
|
<div>
|
|
<div className="flex items-center gap-4 mb-4">
|
|
<Link href="/dashboard">
|
|
<Button variant="ghost" size="sm">
|
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
Retour
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
<h1 className="text-3xl font-bold">Blockchain Électorale</h1>
|
|
<p className="text-muted-foreground mt-2">
|
|
Vérifiez l'immuabilité et la transparence des votes enregistrés
|
|
</p>
|
|
</div>
|
|
|
|
{/* Election Selector */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Sélectionner une Élection</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{electionsLoading ? (
|
|
<div className="text-sm text-muted-foreground">Chargement des élections...</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
{elections.map((election) => (
|
|
<button
|
|
key={election.id}
|
|
onClick={() => setSelectedElection(election.id)}
|
|
className={`p-3 rounded-lg border transition-colors text-left ${
|
|
selectedElection === election.id
|
|
? "border-accent bg-accent/10"
|
|
: "border-border hover:border-accent/50"
|
|
}`}
|
|
>
|
|
<div className="text-sm font-medium">{election.name}</div>
|
|
<div className="text-xs text-muted-foreground mt-1">ID: {election.id}</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Error Message */}
|
|
{error && (
|
|
<Card className="border-red-500 bg-red-50 dark:bg-red-950">
|
|
<CardContent className="pt-6 flex gap-4">
|
|
<div className="flex-1">
|
|
<h3 className="font-semibold text-red-900 dark:text-red-100">Erreur</h3>
|
|
<p className="text-sm text-red-800 dark:text-red-200 mt-1">{error}</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Blockchain Visualizer */}
|
|
{blockchainData && selectedElection && (
|
|
<>
|
|
<BlockchainVisualizer
|
|
data={blockchainData}
|
|
isLoading={isLoading}
|
|
isVerifying={isVerifying}
|
|
onVerify={handleVerifyBlockchain}
|
|
/>
|
|
|
|
{/* Refresh Button */}
|
|
<div className="flex justify-center">
|
|
<Button
|
|
onClick={() => setSelectedElection(selectedElection)}
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={isLoading}
|
|
>
|
|
<RefreshCw className={`w-4 h-4 mr-2 ${isLoading ? "animate-spin" : ""}`} />
|
|
Actualiser
|
|
</Button>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Empty State */}
|
|
{blockchainData && blockchainData.blocks.length === 0 && (
|
|
<Card>
|
|
<CardContent className="pt-6 text-center py-12">
|
|
<div className="text-5xl mb-4">⛓️</div>
|
|
<h3 className="font-semibold text-lg">Aucun vote enregistré</h3>
|
|
<p className="text-sm text-muted-foreground mt-2">
|
|
Les votes pour cette élection s'afficheront ici une fois qu'ils seront soumis.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|