redesign de la page /admin
This commit is contained in:
@@ -96,6 +96,8 @@ export default function AuthGuard({ children, requireSuperAdmin = false }: AuthG
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
@@ -218,21 +220,6 @@ export default function AuthGuard({ children, requireSuperAdmin = false }: AuthG
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Barre de navigation admin */}
|
||||
<div className="bg-white border-b px-4 py-2 flex justify-between items-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Lock className="h-4 w-4 text-blue-600" />
|
||||
<span className="font-medium text-sm">Administration</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
Déconnexion
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Home, Settings, Users, FileText, ArrowLeft } from 'lucide-react';
|
||||
import { Settings, ArrowLeft } from 'lucide-react';
|
||||
|
||||
interface NavigationProps {
|
||||
showBackButton?: boolean;
|
||||
@@ -11,11 +11,6 @@ interface NavigationProps {
|
||||
}
|
||||
|
||||
export default function Navigation({ showBackButton = false, backUrl = '/' }: NavigationProps) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const isActive = (path: string) => {
|
||||
return pathname === path;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="mb-6 border-0 shadow-sm">
|
||||
@@ -30,36 +25,23 @@ export default function Navigation({ showBackButton = false, backUrl = '/' }: Na
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="flex items-center space-x-1">
|
||||
<Button
|
||||
asChild
|
||||
variant={isActive('/') ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
>
|
||||
<Link href="/">
|
||||
<Home className="w-4 h-4 mr-2" />
|
||||
Accueil
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
asChild
|
||||
variant={isActive('/admin') ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
>
|
||||
<Link href="/admin">
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
Administration
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<h1 className="text-xl font-semibold text-slate-900 dark:text-slate-100">
|
||||
Mes Budgets Participatifs - Admin
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="text-sm text-slate-600 dark:text-slate-300">
|
||||
Mes Budgets Participatifs
|
||||
</div>
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<Link href="/admin/settings">
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
Paramètres
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<Link href="/api/auth/signout">
|
||||
Déconnexion
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
133
src/components/StatusSwitch.tsx
Normal file
133
src/components/StatusSwitch.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { FileText, Vote, CheckCircle, Check } from 'lucide-react';
|
||||
|
||||
interface StatusSwitchProps {
|
||||
currentStatus: 'deposit' | 'voting' | 'closed';
|
||||
onStatusChange: (newStatus: 'deposit' | 'voting' | 'closed') => Promise<void>;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const statusConfig = {
|
||||
deposit: {
|
||||
label: 'Dépôt',
|
||||
icon: FileText,
|
||||
color: 'bg-blue-500',
|
||||
hoverColor: 'hover:bg-blue-600',
|
||||
activeColor: 'bg-blue-600',
|
||||
textColor: 'text-blue-600',
|
||||
bgColor: 'bg-blue-50',
|
||||
borderColor: 'border-blue-200'
|
||||
},
|
||||
voting: {
|
||||
label: 'Vote',
|
||||
icon: Vote,
|
||||
color: 'bg-orange-500',
|
||||
hoverColor: 'hover:bg-orange-600',
|
||||
activeColor: 'bg-orange-600',
|
||||
textColor: 'text-orange-600',
|
||||
bgColor: 'bg-orange-50',
|
||||
borderColor: 'border-orange-200'
|
||||
},
|
||||
closed: {
|
||||
label: 'Terminée',
|
||||
icon: CheckCircle,
|
||||
color: 'bg-green-500',
|
||||
hoverColor: 'hover:bg-green-600',
|
||||
activeColor: 'bg-green-600',
|
||||
textColor: 'text-green-600',
|
||||
bgColor: 'bg-green-50',
|
||||
borderColor: 'border-green-200'
|
||||
}
|
||||
};
|
||||
|
||||
export default function StatusSwitch({ currentStatus, onStatusChange, disabled = false }: StatusSwitchProps) {
|
||||
const [localStatus, setLocalStatus] = useState(currentStatus);
|
||||
const [isChanging, setIsChanging] = useState(false);
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
|
||||
// Synchroniser l'état local avec les props
|
||||
useEffect(() => {
|
||||
setLocalStatus(currentStatus);
|
||||
}, [currentStatus]);
|
||||
|
||||
const handleStatusChange = async (newStatus: 'deposit' | 'voting' | 'closed') => {
|
||||
if (disabled || isChanging || newStatus === localStatus) return;
|
||||
|
||||
setIsChanging(true);
|
||||
try {
|
||||
// Mettre à jour l'état local immédiatement pour un feedback visuel instantané
|
||||
setLocalStatus(newStatus);
|
||||
|
||||
// Appeler la fonction de mise à jour
|
||||
await onStatusChange(newStatus);
|
||||
|
||||
// Afficher la notification de succès
|
||||
setShowSuccess(true);
|
||||
setTimeout(() => setShowSuccess(false), 2000);
|
||||
|
||||
} catch (error) {
|
||||
// En cas d'erreur, revenir à l'état précédent
|
||||
setLocalStatus(currentStatus);
|
||||
console.error('Erreur lors du changement de statut:', error);
|
||||
} finally {
|
||||
setIsChanging(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Notification de succès */}
|
||||
{showSuccess && (
|
||||
<div className="absolute -top-12 left-1/2 transform -translate-x-1/2 z-10">
|
||||
<div className="bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 animate-in slide-in-from-top-2 duration-300">
|
||||
<Check className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">Statut mis à jour !</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center bg-slate-100 dark:bg-slate-800 rounded-xl p-1 shadow-inner">
|
||||
{(['deposit', 'voting', 'closed'] as const).map((status, index) => {
|
||||
const config = statusConfig[status];
|
||||
const Icon = config.icon;
|
||||
const isActive = localStatus === status;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={status}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={disabled || isChanging}
|
||||
onClick={() => handleStatusChange(status)}
|
||||
className={`
|
||||
relative flex-1 h-10 px-3 rounded-lg transition-all duration-300 ease-out
|
||||
${isActive
|
||||
? `${config.activeColor} text-white shadow-lg transform scale-105`
|
||||
: `${config.hoverColor} ${config.textColor} hover:text-white`
|
||||
}
|
||||
${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
|
||||
${isChanging ? 'animate-pulse' : ''}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className={`w-4 h-4 transition-transform duration-300 ${isActive ? 'scale-110' : ''}`} />
|
||||
<span className="text-sm font-medium">{config.label}</span>
|
||||
</div>
|
||||
|
||||
{/* Indicateur de progression */}
|
||||
{isActive && (
|
||||
<div className="absolute inset-0 rounded-lg bg-gradient-to-r from-transparent via-white/20 to-transparent animate-pulse" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Effet de brillance au survol */}
|
||||
<div className="absolute inset-0 rounded-xl bg-gradient-to-r from-transparent via-white/10 to-transparent opacity-0 hover:opacity-100 transition-opacity duration-300 pointer-events-none" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user