From 4ce52f300f560415c85c499b1655bf92434f78d3 Mon Sep 17 00:00:00 2001 From: Yannick Le Duc Date: Tue, 26 Aug 2025 23:39:58 +0200 Subject: [PATCH] redesign de la page /admin --- public/favicon.svg | 12 + public/vote-icon.svg | 12 + .../campaigns/[id]/participants/page.tsx | 176 ++---- .../campaigns/[id]/propositions/page.tsx | 58 +- src/app/admin/page.tsx | 513 ++++++++++-------- src/app/campaigns/[id]/propose/page.tsx | 4 +- src/app/globals.css | 15 + src/app/layout.tsx | 3 + src/app/p/[slug]/page.tsx | 269 ++------- src/components/AuthGuard.tsx | 17 +- src/components/Navigation.tsx | 50 +- src/components/StatusSwitch.tsx | 133 +++++ 12 files changed, 577 insertions(+), 685 deletions(-) create mode 100644 public/favicon.svg create mode 100644 public/vote-icon.svg create mode 100644 src/components/StatusSwitch.tsx diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..f497877 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/vote-icon.svg b/public/vote-icon.svg new file mode 100644 index 0000000..1965346 --- /dev/null +++ b/public/vote-icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/app/admin/campaigns/[id]/participants/page.tsx b/src/app/admin/campaigns/[id]/participants/page.tsx index 74fcdc2..e2f31d9 100644 --- a/src/app/admin/campaigns/[id]/participants/page.tsx +++ b/src/app/admin/campaigns/[id]/participants/page.tsx @@ -10,14 +10,13 @@ import DeleteParticipantModal from '@/components/DeleteParticipantModal'; import ImportFileModal from '@/components/ImportFileModal'; import SendParticipantEmailModal from '@/components/SendParticipantEmailModal'; import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; import Navigation from '@/components/Navigation'; import AuthGuard from '@/components/AuthGuard'; -import { Users, User, Calendar, Mail, Vote, Copy, Check, Upload } from 'lucide-react'; +import { User, Calendar, Mail, Vote, Copy, Check, Upload } from 'lucide-react'; export const dynamic = 'force-dynamic'; @@ -148,8 +147,7 @@ function CampaignParticipantsPageContent() { ); } - const votedCount = participants.filter(p => p.has_voted).length; - const totalBudget = participants.reduce((sum, p) => sum + (p.total_voted_amount || 0), 0); + return (
@@ -179,73 +177,14 @@ function CampaignParticipantsPageContent() {
- {/* Stats Overview */} -
- - -
-
-

Total Participants

-

{participants.length}

-
-
- -
-
-
-
- - - -
-
-

Ont voté

-

{votedCount}

-
-
- -
-
-
-
- - - -
-
-

Taux de participation

-

- {participants.length > 0 ? Math.round((votedCount / participants.length) * 100) : 0}% -

-
-
- 📊 -
-
-
-
- - - -
-
-

Budget total voté

-

{totalBudget}€

-
-
- đź’° -
-
-
-
-
+ {/* Participants List */} {participants.length === 0 ? (
- +

Aucun participant @@ -273,9 +212,6 @@ function CampaignParticipantsPageContent() { {participant.has_voted ? 'A voté' : 'N\'a pas voté'} - - {participant.email} -
-
- +
+
+
+ Lien de vote :
- - +
+ + + +
+
+
)} - - {/* Email Button */} - ))} diff --git a/src/app/admin/campaigns/[id]/propositions/page.tsx b/src/app/admin/campaigns/[id]/propositions/page.tsx index 0062c65..a1d5187 100644 --- a/src/app/admin/campaigns/[id]/propositions/page.tsx +++ b/src/app/admin/campaigns/[id]/propositions/page.tsx @@ -10,11 +10,11 @@ import DeletePropositionModal from '@/components/DeletePropositionModal'; import ImportFileModal from '@/components/ImportFileModal'; 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 AuthGuard from '@/components/AuthGuard'; -import { FileText, User, Calendar, Mail, Upload } from 'lucide-react'; +import { FileText, Calendar, Mail, Upload } from 'lucide-react'; export const dynamic = 'force-dynamic'; @@ -89,6 +89,8 @@ function CampaignPropositionsPageContent() { } }; + + const getInitials = (firstName: string, lastName: string) => { return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase(); }; @@ -161,57 +163,11 @@ function CampaignPropositionsPageContent() { + + - {/* Stats Overview */} -
- - -
-
-

Total Propositions

-

{propositions.length}

-
-
- -
-
-
-
- - - -
-
-

Auteurs uniques

-

- {new Set(propositions.map(p => p.author_email)).size} -

-
-
- -
-
-
-
- - - -
-
-

Statut Campagne

-

- {campaign.status === 'deposit' ? 'Dépôt' : - campaign.status === 'voting' ? 'Vote' : 'Terminée'} -

-
-
- 📊 -
-
-
-
-
+ {/* Propositions List */} {propositions.length === 0 ? ( diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 0efe7fa..720d9cd 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -3,18 +3,18 @@ import { useState, useEffect } from 'react'; import Link from 'next/link'; import { Campaign, CampaignWithStats } from '@/types'; import { campaignService } from '@/lib/services'; +import { authService } from '@/lib/auth'; import CreateCampaignModal from '@/components/CreateCampaignModal'; import EditCampaignModal from '@/components/EditCampaignModal'; import DeleteCampaignModal from '@/components/DeleteCampaignModal'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Progress } from '@/components/ui/progress'; -import Navigation from '@/components/Navigation'; + + import AuthGuard from '@/components/AuthGuard'; -import { FolderOpen, Users, FileText, CheckCircle, Clock, Plus, BarChart3, Settings } from 'lucide-react'; +import { FolderOpen, Users, FileText, Plus, BarChart3, Settings, Check, Copy } from 'lucide-react'; +import StatusSwitch from '@/components/StatusSwitch'; export const dynamic = 'force-dynamic'; @@ -25,7 +25,7 @@ function AdminPageContent() { const [showEditModal, setShowEditModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [selectedCampaign, setSelectedCampaign] = useState(null); - const [searchTerm, setSearchTerm] = useState(''); + const [copiedCampaignId, setCopiedCampaignId] = useState(null); useEffect(() => { @@ -68,6 +68,23 @@ function AdminPageContent() { loadCampaigns(); }; + const handleStatusChange = async (campaignId: string, newStatus: 'deposit' | 'voting' | 'closed') => { + try { + await campaignService.update(campaignId, { status: newStatus }); + // Mettre à jour l'état local sans recharger toute la page + setCampaigns(prevCampaigns => + prevCampaigns.map(campaign => + campaign.id === campaignId + ? { ...campaign, status: newStatus } + : campaign + ) + ); + } catch (error) { + console.error('Erreur lors du changement de statut:', error); + throw error; // Propager l'erreur pour que le composant puisse la gérer + } + }; + const getStatusBadge = (status: string) => { switch (status) { case 'deposit': @@ -81,9 +98,7 @@ function AdminPageContent() { } }; - const getSpendingTiersDisplay = (tiers: string) => { - return tiers.split(',').map(tier => `${tier.trim()}€`).join(', '); - }; + const copyToClipboard = async (text: string, campaignId: string) => { try { @@ -108,23 +123,14 @@ function AdminPageContent() { } }; - 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 (
-
@@ -139,233 +145,284 @@ function AdminPageContent() { return (
- - {/* Header */} -
-
-
-

Administration

-

Gérez vos campagnes de budget participatif

-
-
- - + {/* Header */} +
+
+
+
+
+
+
+
+ + + +
+
+

+ Mes Budgets Participatifs +

+

Administration

+
+
+

+ Gérez vos campagnes de budget participatif, suivez les votes et analysez les résultats +

+
+
+ +
+ + +
+
+
- {/* Stats Overview */} -
- - -
-
-

Total Campagnes

-

{stats.total}

-
-
- -
-
-
-
- - - -
-
-

En cours

-

{stats.voting}

-
-
- -
-
-
-
- - - -
-
-

Dépôt

-

{stats.deposit}

-
-
- -
-
-
-
- - - -
-
-

Terminées

-

{stats.closed}

-
-
- -
-
-
-
-
- {/* Search */} -
- setSearchTerm(e.target.value)} - className="max-w-md" - /> -
+ + {/* Campaigns List */} - {filteredCampaigns.length === 0 ? ( - - -
- -
-

- {searchTerm ? 'Aucune campagne trouvée' : 'Aucune campagne'} -

-

- {searchTerm - ? 'Aucune campagne ne correspond à votre recherche.' - : 'Commencez par créer votre première campagne de budget participatif.' - } -

- {!searchTerm && ( - - )} -
-
+ {campaigns.length === 0 ? ( +
+
+ +
+

+ Aucune campagne +

+

+ Créez votre première campagne de budget participatif pour commencer à collecter les idées de votre communauté +

+ +
) : (
- {filteredCampaigns.map((campaign) => ( - - -
-
-
- {campaign.title} - {getStatusBadge(campaign.status)} + {campaigns.map((campaign) => ( + +
+ + +
+
+
+
+
+ + {campaign.title} + +
+ + {campaign.description} + + + {/* Status Switch */} +
+ handleStatusChange(campaign.id, newStatus)} + /> +
+
+
+ + {/* Stats avec icônes modernes et boutons intégrés */} +
+

+ Cliquez sur les éléments pour les gérer +

+
+
+ + + + +
+
+ +
+
+
+ {campaign.budget_per_user}€ +
+
Budget
+
+
+
- {campaign.description} -
-
- - -
-
- - - - {/*
-
-

Propositions

-

{campaign.stats.propositions}

-
-
-

Participants

-

{campaign.stats.participants}

-
-
-

Budget/participant

-

{campaign.budget_per_user}€

-
-
-

Paliers

-

{getSpendingTiersDisplay(campaign.spending_tiers)}

-
-
*/} - - {/* Public URL for deposit campaigns */} - {campaign.status === 'deposit' && ( -
-

- Lien public pour le dépôt de propositions : -

-
- + + {/* Boutons discrets en haut Ă  droite */} +
+
- )} - - {/* Action Buttons */} -
- - - {(campaign.status === 'voting' || campaign.status === 'closed') && ( - - )} -
- + + + + {/* Section actions - mĂŞme espace pour lien public et statistiques */} +
+ {/* Lien public OU Bouton Statistiques */} + {campaign.status === 'deposit' ? ( + /* Lien public pour les campagnes en dépôt */ +
+
+
+ + + + Lien public pour déposer une proposition +
+
+
+ {`${window.location.origin}/p/${campaign.slug || 'campagne'}`} +
+ +
+
+
+ ) : (campaign.status === 'voting' || campaign.status === 'closed') ? ( + /* Bouton Statistiques pour les campagnes en vote/fermées */ +
+ +
+ ) : ( + /* Espace vide pour les autres statuts */ +
+ )} +
+
+
))}
diff --git a/src/app/campaigns/[id]/propose/page.tsx b/src/app/campaigns/[id]/propose/page.tsx index d08ca58..dfac3bc 100644 --- a/src/app/campaigns/[id]/propose/page.tsx +++ b/src/app/campaigns/[id]/propose/page.tsx @@ -192,7 +192,9 @@ export default function PublicProposePage() {

Description

-

{campaign?.description}

+
+ {campaign?.description} +
diff --git a/src/app/globals.css b/src/app/globals.css index 66a4683..7c7db3a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -187,3 +187,18 @@ @apply bg-background text-foreground; } } + +/* Motif de grille pour le header */ +.bg-grid-slate-100 { + background-image: + linear-gradient(rgba(148, 163, 184, 0.1) 1px, transparent 1px), + linear-gradient(90deg, rgba(148, 163, 184, 0.1) 1px, transparent 1px); + background-size: 20px 20px; +} + +.bg-grid-slate-800 { + background-image: + linear-gradient(rgba(148, 163, 184, 0.05) 1px, transparent 1px), + linear-gradient(90deg, rgba(148, 163, 184, 0.05) 1px, transparent 1px); + background-size: 20px 20px; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 046d185..f1be840 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -15,6 +15,9 @@ const geistMono = Geist_Mono({ export const metadata: Metadata = { title: "Mes budgets participatifs", description: "Votez pour les dépenses de votre collectif", + icons: { + icon: '/favicon.svg', + }, }; export default function RootLayout({ diff --git a/src/app/p/[slug]/page.tsx b/src/app/p/[slug]/page.tsx index 9dbd17f..f742e2f 100644 --- a/src/app/p/[slug]/page.tsx +++ b/src/app/p/[slug]/page.tsx @@ -1,285 +1,88 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useParams, useRouter } from 'next/navigation'; -import { Campaign } from '@/types'; import { campaignService } from '@/lib/services'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Textarea } from '@/components/ui/textarea'; -import { Alert, AlertDescription } from '@/components/ui/alert'; -import { Loader2, CheckCircle, AlertCircle } from 'lucide-react'; +import { Loader2 } from 'lucide-react'; // Force dynamic rendering to avoid SSR issues with Supabase export const dynamic = 'force-dynamic'; -export default function ShortProposePage() { +export default function ShortProposeRedirect() { const params = useParams(); const router = useRouter(); const slug = params.slug as string; - const [campaign, setCampaign] = useState(null); const [loading, setLoading] = useState(true); - const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(''); - const [success, setSuccess] = useState(false); - - const [formData, setFormData] = useState({ - title: '', - description: '', - author_first_name: '', - author_last_name: '', - author_email: '' - }); useEffect(() => { if (slug) { - loadCampaign(); + redirectToProposePage(); } }, [slug]); - const loadCampaign = async () => { + const redirectToProposePage = async () => { try { setLoading(true); - const campaignData = await campaignService.getBySlug(slug); - if (!campaignData) { + // Récupérer la campagne par slug + const campaign = await campaignService.getBySlug(slug); + + if (!campaign) { setError('Campagne non trouvée'); return; } - if (campaignData.status !== 'deposit') { + if (campaign.status !== 'deposit') { setError('Cette campagne n\'accepte plus de propositions'); return; } - setCampaign(campaignData); + // Rediriger vers la route avec l'ID complet + const proposeUrl = `/campaigns/${campaign.id}/propose`; + router.replace(proposeUrl); + } catch (error) { - console.error('Erreur lors du chargement de la campagne:', error); + console.error('Erreur lors de la redirection:', error); setError('Erreur lors du chargement de la campagne'); } finally { setLoading(false); } }; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!campaign) return; - - // Validation basique - if (!formData.title.trim() || !formData.description.trim() || - !formData.author_first_name.trim() || !formData.author_last_name.trim() || - !formData.author_email.trim()) { - setError('Veuillez remplir tous les champs obligatoires'); - return; - } - - // Validation email basique - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(formData.author_email)) { - setError('Veuillez saisir une adresse email valide'); - return; - } - - try { - setSubmitting(true); - setError(''); - - const { propositionService } = await import('@/lib/services'); - - await propositionService.create({ - campaign_id: campaign.id, - title: formData.title.trim(), - description: formData.description.trim(), - author_first_name: formData.author_first_name.trim(), - author_last_name: formData.author_last_name.trim(), - author_email: formData.author_email.trim() - }); - - setSuccess(true); - setFormData({ - title: '', - description: '', - author_first_name: '', - author_last_name: '', - author_email: '' - }); - - // Rediriger vers la page de succès après 3 secondes - setTimeout(() => { - router.push(`/p/${slug}/success`); - }, 3000); - - } catch (error: any) { - console.error('Erreur lors de la soumission:', error); - setError(error.message || 'Erreur lors de la soumission de la proposition'); - } finally { - setSubmitting(false); - } - }; - - const handleInputChange = (field: string, value: string) => { - setFormData(prev => ({ ...prev, [field]: value })); - if (error) setError(''); // Effacer l'erreur quand l'utilisateur commence à taper - }; - if (loading) { return ( -
+
- -

Chargement de la campagne...

+ +

Redirection vers la page de dépôt de propositions...

); } - if (error && !campaign) { + if (error) { return ( -
- - -
- -

- Erreur -

-

{error}

-
-
-
+
+
+
+ + + +

Erreur

+

{error}

+ +
+
); } - if (success) { - return ( -
- - -
- -

- Proposition soumise avec succès ! -

-

- Votre proposition a été enregistrée. Vous allez être redirigé... -

-
-
-
-
- ); - } - - return ( -
-
- - - - Dépôt de proposition - - - {campaign?.title} - - {campaign?.description && ( -

- {campaign.description} -

- )} -
- - - {error && ( - - - - {error} - - - )} - -
-
-
- - handleInputChange('author_first_name', e.target.value)} - placeholder="Votre prénom" - required - /> -
-
- - handleInputChange('author_last_name', e.target.value)} - placeholder="Votre nom" - required - /> -
-
- -
- - handleInputChange('author_email', e.target.value)} - placeholder="votre.email@exemple.com" - required - /> -
- -
- - handleInputChange('title', e.target.value)} - placeholder="Titre de votre proposition" - required - /> -
- -
- -