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>
122 lines
4.2 KiB
TypeScript
122 lines
4.2 KiB
TypeScript
"use client"
|
||
|
||
import Link from "next/link"
|
||
import { useRouter } from "next/navigation"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Menu, LogOut, User as UserIcon } from "lucide-react"
|
||
import { useState } from "react"
|
||
import { useAuth } from "@/lib/auth-context"
|
||
import { ProtectedRoute } from "@/components/protected-route"
|
||
|
||
export default function DashboardLayout({
|
||
children,
|
||
}: {
|
||
children: React.ReactNode
|
||
}) {
|
||
const router = useRouter()
|
||
const { user, logout } = useAuth()
|
||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||
|
||
const handleLogout = () => {
|
||
logout()
|
||
router.push("/auth/login")
|
||
}
|
||
|
||
const navItems = [
|
||
{ href: "/dashboard", label: "Tableau de Bord", icon: "📊" },
|
||
{ href: "/dashboard/votes/active", label: "Votes Actifs", icon: "🗳️" },
|
||
{ href: "/dashboard/votes/upcoming", label: "Votes à Venir", icon: "📅" },
|
||
{ href: "/dashboard/votes/history", label: "Historique", icon: "📜" },
|
||
{ href: "/dashboard/votes/archives", label: "Archives", icon: "🗂️" },
|
||
{ href: "/dashboard/blockchain", label: "Blockchain", icon: "⛓️" },
|
||
{ href: "/dashboard/profile", label: "Profil", icon: "👤" },
|
||
]
|
||
|
||
return (
|
||
<div className="min-h-screen bg-background flex">
|
||
{/* Sidebar */}
|
||
<aside
|
||
className={`fixed inset-y-0 left-0 z-40 w-64 bg-card border-r border-border transition-transform duration-300 ${
|
||
sidebarOpen ? "translate-x-0" : "-translate-x-full"
|
||
} lg:static lg:translate-x-0`}
|
||
>
|
||
<div className="flex flex-col h-full">
|
||
{/* Logo */}
|
||
<div className="flex items-center gap-2 p-6 border-b border-border">
|
||
<span className="text-2xl">🗳️</span>
|
||
<span className="font-bold text-lg text-accent">E-Voting</span>
|
||
</div>
|
||
|
||
{/* Navigation */}
|
||
<nav className="flex-1 p-4 space-y-2">
|
||
{navItems.map((item) => (
|
||
<Link
|
||
key={item.href}
|
||
href={item.href}
|
||
className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-muted transition-colors text-foreground hover:text-accent"
|
||
onClick={() => setSidebarOpen(false)}
|
||
>
|
||
<span className="text-xl">{item.icon}</span>
|
||
<span className="text-sm font-medium">{item.label}</span>
|
||
</Link>
|
||
))}
|
||
</nav>
|
||
|
||
{/* Footer */}
|
||
<div className="p-4 border-t border-border space-y-2">
|
||
<Link href="/dashboard/profile">
|
||
<Button variant="ghost" className="w-full justify-start gap-2">
|
||
<UserIcon className="w-4 h-4" />
|
||
Mon Profil
|
||
</Button>
|
||
</Link>
|
||
<Button
|
||
onClick={handleLogout}
|
||
variant="ghost"
|
||
className="w-full justify-start gap-2 text-destructive hover:text-destructive"
|
||
>
|
||
<LogOut className="w-4 h-4" />
|
||
Déconnexion
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
{/* Main Content */}
|
||
<div className="flex-1 flex flex-col">
|
||
{/* Top Bar */}
|
||
<header className="sticky top-0 z-30 border-b border-border bg-card/50 backdrop-blur-sm">
|
||
<div className="flex items-center justify-between px-6 py-4">
|
||
<button
|
||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||
className="lg:hidden p-2 hover:bg-muted rounded-lg"
|
||
>
|
||
<Menu className="w-5 h-5" />
|
||
</button>
|
||
<div className="ml-auto flex items-center gap-4">
|
||
{user && (
|
||
<span className="text-sm text-muted-foreground">
|
||
Bienvenue, {user.first_name} {user.last_name}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
{/* Content Area */}
|
||
<main className="flex-1 overflow-auto p-6 max-w-7xl w-full mx-auto">
|
||
<ProtectedRoute>{children}</ProtectedRoute>
|
||
</main>
|
||
</div>
|
||
|
||
{/* Mobile Overlay */}
|
||
{sidebarOpen && (
|
||
<div
|
||
className="fixed inset-0 bg-black/50 z-30 lg:hidden"
|
||
onClick={() => setSidebarOpen(false)}
|
||
/>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|