Meilleur design via Shadcn/ui

This commit is contained in:
Yannick Le Duc
2025-08-25 15:50:16 +02:00
parent 4e8b592feb
commit 46993d2c9f
31 changed files with 3536 additions and 1664 deletions

View File

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