Meilleur design via Shadcn/ui
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
@@ -8,14 +7,18 @@ import { campaignService, propositionService } from '@/lib/services';
|
||||
import AddPropositionModal from '@/components/AddPropositionModal';
|
||||
import EditPropositionModal from '@/components/EditPropositionModal';
|
||||
import DeletePropositionModal from '@/components/DeletePropositionModal';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
import Navigation from '@/components/Navigation';
|
||||
import { FileText, User, Calendar, Mail } from 'lucide-react';
|
||||
|
||||
// Force dynamic rendering to avoid SSR issues with Supabase
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default function CampaignPropositionsPage() {
|
||||
const params = useParams();
|
||||
const campaignId = params.id as string;
|
||||
|
||||
const [campaign, setCampaign] = useState<Campaign | null>(null);
|
||||
const [propositions, setPropositions] = useState<Proposition[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -25,19 +28,18 @@ export default function CampaignPropositionsPage() {
|
||||
const [selectedProposition, setSelectedProposition] = useState<Proposition | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (campaignId) {
|
||||
loadData();
|
||||
}
|
||||
loadData();
|
||||
}, [campaignId]);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [campaignData, propositionsData] = await Promise.all([
|
||||
campaignService.getAll().then(campaigns => campaigns.find(c => c.id === campaignId)),
|
||||
const [campaigns, propositionsData] = await Promise.all([
|
||||
campaignService.getAll(),
|
||||
propositionService.getByCampaign(campaignId)
|
||||
]);
|
||||
|
||||
const campaignData = campaigns.find(c => c.id === campaignId);
|
||||
setCampaign(campaignData || null);
|
||||
setPropositions(propositionsData);
|
||||
} catch (error) {
|
||||
@@ -54,22 +56,29 @@ export default function CampaignPropositionsPage() {
|
||||
|
||||
const handlePropositionEdited = () => {
|
||||
setShowEditModal(false);
|
||||
setSelectedProposition(null);
|
||||
loadData();
|
||||
};
|
||||
|
||||
const handlePropositionDeleted = () => {
|
||||
setShowDeleteModal(false);
|
||||
setSelectedProposition(null);
|
||||
loadData();
|
||||
};
|
||||
|
||||
const getInitials = (firstName: string, lastName: string) => {
|
||||
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Chargement des propositions...</p>
|
||||
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<Navigation showBackButton backUrl="/admin" />
|
||||
<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>
|
||||
<p className="text-slate-600 dark:text-slate-300">Chargement des propositions...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -77,173 +86,212 @@ export default function CampaignPropositionsPage() {
|
||||
|
||||
if (!campaign) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-semibold text-gray-900">Campagne non trouvée</h2>
|
||||
<p className="mt-2 text-gray-600">La campagne demandée n'existe pas.</p>
|
||||
<Link
|
||||
href="/admin"
|
||||
className="mt-4 inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
Retour à l'administration
|
||||
</Link>
|
||||
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<Navigation showBackButton backUrl="/admin" />
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="p-12 text-center">
|
||||
<div className="w-16 h-16 bg-red-100 dark:bg-red-900 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<span className="text-2xl">❌</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2">
|
||||
Campagne introuvable
|
||||
</h3>
|
||||
<p className="text-slate-600 dark:text-slate-300 mb-6">
|
||||
La campagne que vous recherchez n'existe pas ou a été supprimée.
|
||||
</p>
|
||||
<Button asChild>
|
||||
<Link href="/admin">Retour à l'administration</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<Navigation showBackButton backUrl="/admin" />
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link
|
||||
href="/admin"
|
||||
className="inline-flex items-center px-3 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
Retour
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Propositions</h1>
|
||||
<p className="mt-2 text-gray-600">
|
||||
Campagne : <span className="font-medium">{campaign.title}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-slate-900 dark:text-slate-100">
|
||||
Propositions
|
||||
</h1>
|
||||
<p className="text-slate-600 dark:text-slate-300 mt-2">
|
||||
{campaign.title}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Ajouter une proposition
|
||||
</button>
|
||||
<Button onClick={() => setShowAddModal(true)} size="lg">
|
||||
✨ Nouvelle proposition
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Propositions */}
|
||||
{/* Stats Overview */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-600 dark:text-slate-300">Total Propositions</p>
|
||||
<p className="text-2xl font-bold text-slate-900 dark:text-slate-100">{propositions.length}</p>
|
||||
</div>
|
||||
<div className="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center">
|
||||
<FileText className="w-4 h-4 text-blue-600 dark:text-blue-300" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-600 dark:text-slate-300">Auteurs uniques</p>
|
||||
<p className="text-2xl font-bold text-slate-900 dark:text-slate-100">
|
||||
{new Set(propositions.map(p => p.author_email)).size}
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-8 h-8 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center">
|
||||
<User className="w-4 h-4 text-green-600 dark:text-green-300" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-600 dark:text-slate-300">Statut Campagne</p>
|
||||
<p className="text-2xl font-bold text-slate-900 dark:text-slate-100">
|
||||
{campaign.status === 'deposit' ? 'Dépôt' :
|
||||
campaign.status === 'voting' ? 'Vote' : 'Terminée'}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Propositions List */}
|
||||
{propositions.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<h3 className="mt-2 text-sm font-medium text-gray-900">Aucune proposition</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">Commencez par ajouter votre première proposition.</p>
|
||||
<div className="mt-6">
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Ajouter une proposition
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||
<div className="px-4 py-5 sm:px-6 border-b border-gray-200">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||
Propositions ({propositions.length})
|
||||
<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">
|
||||
<FileText 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 proposition
|
||||
</h3>
|
||||
<p className="mt-1 max-w-2xl text-sm text-gray-500">
|
||||
Liste de toutes les propositions pour cette campagne
|
||||
<p className="text-slate-600 dark:text-slate-300 mb-6">
|
||||
Aucune proposition n'a encore été soumise pour cette campagne.
|
||||
</p>
|
||||
</div>
|
||||
<ul className="divide-y divide-gray-200">
|
||||
{propositions.map((proposition) => (
|
||||
<li key={proposition.id} className="px-4 py-6 sm:px-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-lg font-medium text-gray-900 mb-2">
|
||||
{proposition.title}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600 mb-3">
|
||||
<Button onClick={() => setShowAddModal(true)}>
|
||||
Ajouter une proposition
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-6">
|
||||
{propositions.map((proposition) => (
|
||||
<Card key={proposition.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">
|
||||
<div className="flex-1">
|
||||
<CardTitle className="text-xl mb-2">{proposition.title}</CardTitle>
|
||||
<CardDescription className="text-base">
|
||||
{proposition.description}
|
||||
</p>
|
||||
<div className="flex items-center space-x-4 text-xs text-gray-500">
|
||||
<span>
|
||||
<strong>Auteur :</strong> {proposition.author_first_name} {proposition.author_last_name}
|
||||
</span>
|
||||
<span>
|
||||
<strong>Email :</strong> {proposition.author_email}
|
||||
</span>
|
||||
<span>
|
||||
<strong>Créée le :</strong> {new Date(proposition.created_at).toLocaleDateString('fr-FR')}
|
||||
</span>
|
||||
</div>
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 ml-6">
|
||||
<button
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedProposition(proposition);
|
||||
setShowEditModal(true);
|
||||
}}
|
||||
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
Modifier
|
||||
</button>
|
||||
<button
|
||||
✏️ Modifier
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedProposition(proposition);
|
||||
setShowDeleteModal(true);
|
||||
}}
|
||||
className="inline-flex items-center px-3 py-2 border border-red-300 shadow-sm text-sm leading-4 font-medium rounded-md text-red-700 bg-white hover:bg-red-50"
|
||||
>
|
||||
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
Supprimer
|
||||
</button>
|
||||
🗑️ Supprimer
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<Avatar className="w-10 h-10">
|
||||
<AvatarFallback className="bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300">
|
||||
{getInitials(proposition.author_first_name, proposition.author_last_name)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-slate-900 dark:text-slate-100">
|
||||
{proposition.author_first_name} {proposition.author_last_name}
|
||||
</p>
|
||||
<div className="flex items-center gap-4 text-sm text-slate-600 dark:text-slate-300">
|
||||
<div className="flex items-center gap-1">
|
||||
<Mail className="w-3 h-3" />
|
||||
{proposition.author_email}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="w-3 h-3" />
|
||||
{new Date(proposition.created_at).toLocaleDateString('fr-FR')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
{showAddModal && (
|
||||
{/* Modals */}
|
||||
<AddPropositionModal
|
||||
isOpen={showAddModal}
|
||||
onClose={() => setShowAddModal(false)}
|
||||
onSuccess={handlePropositionAdded}
|
||||
campaignId={campaignId}
|
||||
campaignTitle={campaign.title}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showEditModal && selectedProposition && (
|
||||
<EditPropositionModal
|
||||
isOpen={showEditModal}
|
||||
onClose={() => setShowEditModal(false)}
|
||||
onSuccess={handlePropositionEdited}
|
||||
proposition={selectedProposition}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showDeleteModal && selectedProposition && (
|
||||
<DeletePropositionModal
|
||||
isOpen={showDeleteModal}
|
||||
onClose={() => setShowDeleteModal(false)}
|
||||
onSuccess={handlePropositionDeleted}
|
||||
proposition={selectedProposition}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedProposition && (
|
||||
<EditPropositionModal
|
||||
isOpen={showEditModal}
|
||||
onClose={() => setShowEditModal(false)}
|
||||
onSuccess={handlePropositionEdited}
|
||||
proposition={selectedProposition}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedProposition && (
|
||||
<DeletePropositionModal
|
||||
isOpen={showDeleteModal}
|
||||
onClose={() => setShowDeleteModal(false)}
|
||||
onSuccess={handlePropositionDeleted}
|
||||
proposition={selectedProposition}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user