@@ -12,17 +12,19 @@ import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input' ;
import { Progress } from '@/components/ui/progress' ;
import Navigation from '@/components/Navigation' ;
import AuthGuard from '@/components/AuthGuard' ;
import { FolderOpen , Users , FileText , CheckCircle , Clock , Plus } from 'lucide-react' ;
export const dynamic = 'force-dynamic' ;
export default function AdminPage() {
function AdminPageContent () {
const [ campaigns , setCampaigns ] = useState < CampaignWithStats [ ] > ( [ ] ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ showCreateModal , setShowCreateModal ] = useState ( false ) ;
const [ showEditModal , setShowEditModal ] = useState ( false ) ;
const [ showDeleteModal , setShowDeleteModal ] = useState ( false ) ;
const [ selectedCampaign , setSelectedCampaign ] = useState < Campaign | null > ( null ) ;
const [ copiedCampaignId , setCopiedCampaignId ] = useState < string | null > ( null ) ;
const [ searchTerm , setSearchTerm ] = useState ( '' ) ;
useEffect ( ( ) = > {
loadCampaigns ( ) ;
@@ -65,23 +67,39 @@ export default function AdminPage() {
} ;
const getStatusBadge = ( status : string ) = > {
const statusConfig = {
deposit : { label : 'Dépôt de propositions' , variant : 'secondary' as const } ,
voting : { label : 'En cours de vote' , variant: 'default' as const } ,
closed : { label : 'Terminée' , variant : 'destructive' as const }
} ;
const config = statusConfig [ status as keyof typeof statusConfig ] ;
return < Badge variant = { config . variant } > { config . label } < / Badge > ;
switch ( status ) {
case 'deposit' :
return < Badge variant= "secondary" > Dépôt de propositions < / Badge > ;
case 'voting' :
return < Badge variant = "default" > En cours de vote < / Badge > ;
case 'closed' :
return < Badge variant = "outline" > Terminée < / Badge > ;
default :
return < Badge variant = "outline" > { status } < / Badge > ;
}
} ;
const getSpendingTiersDisplay = ( tiers : string ) = > {
return tiers . split ( ',' ) . map ( tier = > tier . trim ( ) ) . join ( '€ , ' ) + '€' ;
return tiers . split ( ',' ) . map ( tier = > ` ${ tier . trim ( ) } € ` ) . join ( ', ' ) ;
} ;
const filteredCampaigns = campaigns . filter ( campaign = >
campaign . title . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ||
campaign . description . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) )
) ;
const stats = {
total : campaigns.length ,
deposit : campaigns.filter ( c = > c . status === 'deposit' ) . length ,
voting : campaigns.filter ( c = > c . status === 'voting' ) . length ,
closed : campaigns.filter ( c = > c . status === 'closed' ) . length ,
} ;
if ( loading ) {
return (
< div className = "min-h-screen bg-slate-50 dark:bg-slate-900" >
< div className = "container mx-auto px-4 py-8" >
< Navigation / >
< div className = "flex items-center justify-center h-64" >
< div className = "text-center" >
< div className = "animate-spin rounded-full h-12 w-12 border-b-2 border-slate-900 dark:border-slate-100 mx-auto mb-4" > < / div >
@@ -102,15 +120,12 @@ export default function AdminPage() {
< div className = "mb-8" >
< div className = "flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4" >
< div >
< h1 className = "text-3xl font-bold text-slate-900 dark:text-slate-100" >
Administration
< / h1 >
< p className = "text-slate-600 dark:text-slate-300 mt-2" >
Gérez vos campagnes de budget participatif
< / p >
< h1 className = "text-3xl font-bold text-slate-900 dark:text-slate-100" > Administration < / h1 >
< p className = "text-slate-600 dark:text-slate-300 mt-2" > Gérez vos campagnes de budget participatif < / p >
< / div >
< Button onClick = { ( ) = > setShowCreateModal ( true ) } size = "lg" >
✨ Nouvelle campagne
< Plus className = "w-4 h-4 mr-2" / >
Nouvelle campagne
< / Button >
< / div >
< / div >
@@ -122,11 +137,11 @@ export default function AdminPage() {
< div className = "flex items-center justify-between" >
< div >
< p className = "text-sm font-medium text-slate-600 dark:text-slate-300" > Total Campagnes < / p >
< p className = "text-2xl font-bold text-slate-900 dark:text-slate-100" > { campaigns . length } < / p >
< / div >
< div className = "w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center" >
< span className = "text-blue-600 dark:text-blue-300" > 📊 < / span >
< p className = "text-2xl font-bold text-slate-900 dark:text-slate-100" > { stats . total } < / p >
< / div >
< div className = "w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center" >
< FolderOpen className = "w-4 h-4 text-blue-600 dark:text-blue-300" / >
< / div >
< / div >
< / CardContent >
< / Card >
@@ -136,12 +151,10 @@ export default function AdminPage() {
< div className = "flex items-center justify-between" >
< div >
< p className = "text-sm font-medium text-slate-600 dark:text-slate-300" > En cours < / p >
< p className = "text-2xl font-bold text-slate-900 dark:text-slate-100" >
{ campaigns . filter ( c = > c . status === 'voting' ) . length }
< / p >
< p className = "text-2xl font-bold text-slate-900 dark:text-slate-100" > { stats . voting } < / p >
< / div >
< div className = "w-8 h-8 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center" >
< span className = "text-green-600 dark:text-green-300" > 🗳 ️ < / span >
< Clock className = "w-4 h-4 text-green-600 dark:text-green-300" / >
< / div >
< / div >
< / CardContent >
@@ -152,12 +165,10 @@ export default function AdminPage() {
< div className = "flex items-center justify-between" >
< div >
< p className = "text-sm font-medium text-slate-600 dark:text-slate-300" > Dépôt < / p >
< p className = "text-2xl font-bold text-slate-900 dark:text-slate-100" >
{ campaigns . filter ( c = > c . status === 'deposit' ) . length }
< / p >
< p className = "text-2xl font-bold text-slate-900 dark:text-slate-100" > { stats . deposit } < / p >
< / div >
< div className = "w-8 h-8 bg-yellow-100 dark:bg-yellow-900 rounded-lg flex items-center justify-center" >
< span className = "text-yellow-600 dark:text-yellow-300" > 📝 < / span >
< FileText className = "w-4 h-4 text-yellow-600 dark:text-yellow-300" / >
< / div >
< / div >
< / CardContent >
@@ -168,39 +179,54 @@ export default function AdminPage() {
< div className = "flex items-center justify-between" >
< div >
< p className = "text-sm font-medium text-slate-600 dark:text-slate-300" > Terminées < / p >
< p className = "text-2xl font-bold text-slate-900 dark:text-slate-100" >
{ campaigns . filter ( c = > c . status === 'closed' ) . length }
< / p >
< p className = "text-2xl font-bold text-slate-900 dark:text-slate-100" > { stats . closed } < / p >
< / div >
< div className = "w-8 h-8 bg-purple-100 dark:bg-purple-900 rounded-lg flex items-center justify-center" >
< span className = "text-purple-600 dark:text-purple-300" > ✅ < / span >
< CheckCircle className = "w-4 h-4 text-purple-600 dark:text-purple-300" / >
< / div >
< / div >
< / CardContent >
< / Card >
< / div >
{ /* Search */ }
< div className = "mb-6" >
< Input
type = "text"
placeholder = "Rechercher une campagne..."
value = { searchTerm }
onChange = { ( e ) = > setSearchTerm ( e . target . value ) }
className = "max-w-md"
/ >
< / div >
{ /* Campaigns List */ }
{ c ampaigns. length === 0 ? (
{ filteredC ampaigns. length === 0 ? (
< Card className = "border-dashed" >
< CardContent className = "p-12 text-center" >
< div className = "w-16 h-16 bg-slate-100 dark:bg-slate-800 rounded-full flex items-center justify-center mx-auto mb-4" >
< spa n className = "text-2xl" > 📋 < / span >
< / div >
< div className = "w-16 h-16 bg-slate-100 dark:bg-slate-800 rounded-full flex items-center justify-center mx-auto mb-4" >
< FolderOpe n className = "w-8 h-8 text-slate-400" / >
< / div >
< h3 className = "text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2" >
Aucune campagne créée
{ searchTerm ? ' Aucune campagne trouvée' : 'Aucune campagne' }
< / h3 >
< p className = "text-slate-600 dark:text-slate-300 mb-6" >
Commencez par créer votre première campagne de budget participatif
{ searchTerm
? 'Aucune campagne ne correspond à votre recherche.'
: 'Commencez par créer votre première campagne de budget participatif.'
}
< / p >
< Button onClick = { ( ) = > setShowCreateModal ( true ) } >
Créer une campagne
< / Button >
{ ! searchTerm && (
< Button onClick = { ( ) = > setShowCreateModal ( true ) } >
< Plus className = "w-4 h-4 mr-2" / >
Créer une campagne
< / Button >
) }
< / CardContent >
< / Card >
) : (
< div className = "grid gap-6" >
{ c ampaigns. map ( ( campaign ) = > (
{ filteredC ampaigns. map ( ( campaign ) = > (
< Card key = { campaign . id } className = "hover:shadow-lg transition-shadow duration-200" >
< CardHeader >
< div className = "flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4" >
@@ -209,9 +235,7 @@ export default function AdminPage() {
< CardTitle className = "text-xl" > { campaign . title } < / CardTitle >
{ getStatusBadge ( campaign . status ) }
< / div >
< CardDescription className = "text-base" >
{ campaign . description }
< / CardDescription >
< CardDescription className = "text-base" > { campaign . description } < / CardDescription >
< / div >
< div className = "flex flex-col sm:flex-row gap-2" >
< Button
@@ -240,10 +264,6 @@ export default function AdminPage() {
< CardContent >
< div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4" >
< div className = "text-center p-3 bg-slate-50 dark:bg-slate-800 rounded-lg" >
< p className = "text-sm text-slate-600 dark:text-slate-300" > Budget par utilisateur < / p >
< p className = "text-lg font-semibold text-slate-900 dark:text-slate-100" > { campaign . budget_per_user } € < / p >
< / div >
< div className = "text-center p-3 bg-slate-50 dark:bg-slate-800 rounded-lg" >
< p className = "text-sm text-slate-600 dark:text-slate-300" > Propositions < / p >
< p className = "text-lg font-semibold text-slate-900 dark:text-slate-100" > { campaign . stats . propositions } < / p >
@@ -253,56 +273,54 @@ export default function AdminPage() {
< p className = "text-lg font-semibold text-slate-900 dark:text-slate-100" > { campaign . stats . participants } < / p >
< / div >
< div className = "text-center p-3 bg-slate-50 dark:bg-slate-800 rounded-lg" >
< p className = "text-sm text-slate-600 dark:text-slate-300" > Paliers de dépense < / p >
< p className = "text-sm text-slate-600 dark:text-slate-300" > Budget / participant < / p >
< p className = "text-lg font-semibold text-slate-900 dark:text-slate-100" > { campaign . budget_per_user } € < / p >
< / div >
< div className = "text-center p-3 bg-slate-50 dark:bg-slate-800 rounded-lg" >
< p className = "text-sm text-slate-600 dark:text-slate-300" > Paliers < / p >
< p className = "text-sm font-semibold text-slate-900 dark:text-slate-100" > { getSpendingTiersDisplay ( campaign . spending_tiers ) } < / p >
< / div >
< / div >
{ /* Public URL for deposit campaigns */ }
{ campaign . status === 'deposit' && (
< Card className = "bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800" >
< CardContent className = "p-4 " >
< div className = "flex items-center justify-between" >
< div className = "flex-1" >
< h4 className = "text-sm font-medium text-blue-900 dark:text-blue-100 mb-1 " >
Lien public pour déposer des propositions
< / h4 >
< div className = "flex items-center space-x-2" >
< Input
type = "text "
readOnly
value = { ` ${ window . location . origin } /campaigns/ ${ campaign . id } /propose ` }
className = "flex-1 text-xs bg-white dark:bg-slate-800 border-blue-300 dark:border-blue-600 text-blue-700 dark:text-blue-300 font-mono "
/ >
< Button
variant = "outline"
size = "sm"
onClick = { ( ) = > {
navigator . clipboard . writeText ( ` ${ window . location . origin } /campaigns/ ${ campaign . id } /propose ` ) ;
setCopiedCampaignId ( campaign . id ) ;
setTimeout ( ( ) = > setCopiedCampaignId ( null ) , 2000 ) ;
} }
className = "text-xs"
>
{ copiedCampaignId === campaign . id ? 'Copié !' : '📋 Copier' }
< / Button >
< / div >
< / div >
< / div >
< / CardContent >
< / Card >
< div className = "mb-4 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg " >
< h4 className = "text-sm font-medium text-blue-900 dark:text-blue-100 mb-2 " >
Lien public pour le dépôt de propositions :
< / h4 >
< div className = "flex items-center space-x-2 " >
< Input
type = "text"
readOnly
value = { ` ${ window . location . origin } /campaigns/ ${ campaign . id } /propose ` }
className = "flex-1 text-sm bg-white dark:bg-slate-800 border-blue-300 dark:border-blue-600 text-blue-700 dark:text-blue-300 font-mono "
/ >
< Button
variant = "outline "
size = "sm"
onClick = { ( ) = > {
navigator . clipboard . writeText ( ` ${ window . location . origin } /campaigns/ ${ campaign . id } /propose ` ) ;
} }
className = "text-xs"
>
Copier
< / Button >
< / div >
< / div >
) }
{ /* Action Buttons */ }
< div className = "flex flex-col sm:flex-row gap-2 mt-4" >
< Button asChild variant = "outline" className = "flex-1" >
< Link href = { ` /admin/campaigns/ ${ campaign . id } /propositions ` } >
📝 Propositions ( { campaign . stats . propositions } )
< FileText className = "w-4 h-4 mr-2" / >
Propositions ( { campaign . stats . propositions } )
< / Link >
< / Button >
< Button asChild variant = "outline" className = "flex-1" >
< Link href = { ` /admin/campaigns/ ${ campaign . id } /participants ` } >
👥 Votants ( { campaign . stats . participants } )
< Users className = "w-4 h-4 mr-2" / >
Votants ( { campaign . stats . participants } )
< / Link >
< / Button >
< / div >
@@ -313,30 +331,22 @@ export default function AdminPage() {
) }
{ /* Modals */ }
< CreateCampaignModal
isOpen = { showCreateModal }
onClose = { ( ) = > setShowCreateModal ( false ) }
onSuccess = { handleCampaignCreated }
/ >
< CreateCampaignModal isOpen = { showCreateModal } onClose = { ( ) = > setShowCreateModal ( false ) } onSuccess = { handleCampaignCreated } / >
{ selectedCampaign && (
< EditCampaignModal
isOpen = { showEditModal }
onClose = { ( ) = > setShowEditModal ( false ) }
onSuccess = { handleCampaignEdited }
campaign = { selectedCampaign }
/ >
< EditCampaignModal isOpen = { showEditModal } onClose = { ( ) = > setShowEditModal ( false ) } onSuccess = { handleCampaignEdited } campaign = { selectedCampaign } / >
) }
{ selectedCampaign && (
< DeleteCampaignModal
isOpen = { showDeleteModal }
onClose = { ( ) = > setShowDeleteModal ( false ) }
onSuccess = { handleCampaignDeleted }
campaign = { selectedCampaign }
/ >
< DeleteCampaignModal isOpen = { showDeleteModal } onClose = { ( ) = > setShowDeleteModal ( false ) } onSuccess = { handleCampaignDeleted } campaign = { selectedCampaign } / >
) }
< / div >
< / div >
) ;
}
export default function AdminPage() {
return (
< AuthGuard >
< AdminPageContent / >
< / AuthGuard >
) ;
}