Alexis Bruneteau dde0164b27 feat: Implement Phase 4 - Blockchain Visualization
Add comprehensive blockchain viewer with:
- BlockchainViewer component: Display blocks in expandable cards
- Hash visualization: Show SHA-256 hashes for each block
- Chain verification: Visual integrity status and verification button
- Block details: Expand to see full block information
  - Index, timestamp, previous hash, block hash
  - Encrypted vote data, transaction ID
  - Digital signatures
- Election selector: View blockchain for different elections
- Mock data: Demo blockchain included for testing
- Responsive design: Works on mobile and desktop

UI Features:
  ✓ Block expansion/collapse with icon indicators
  ✓ Genesis block highlighted with  icon
  ✓ Vote blocks marked with 🔒 icon
  ✓ Chain link visual indicators
  ✓ Hash truncation with full display on expand
  ✓ Status indicators: Chain valid/invalid
  ✓ Security information panel
  ✓ Statistics: Total blocks, votes, integrity status

Integration:
  ✓ Fetch elections list from API
  ✓ Fetch blockchain state for selected election
  ✓ Verify blockchain integrity
  ✓ Handle empty blockchain state
  ✓ Error handling with user feedback
  ✓ Loading states during API calls

Routes:
  ✓ /dashboard/blockchain - Main blockchain viewer
  ✓ Added to sidebar navigation
  ✓ 13 total routes now (added 1 new)

Frontend Build:
  ✓ No TypeScript errors
  ✓ Zero unused imports
  ✓ Production build successful
  ✓ All routes prerendered

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 01:59:46 +01:00

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 { BlockchainViewer, BlockchainData } from "@/components/blockchain-viewer"
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("access_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("access_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("access_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 Viewer */}
{blockchainData && selectedElection && (
<>
<BlockchainViewer
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>
)
}