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

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>
)
}