-
-
{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}
-
-
- )}
-
-
-
-
-
-
- );
+ return null;
}
diff --git a/src/components/AuthGuard.tsx b/src/components/AuthGuard.tsx
index 02f8513..ef57ec6 100644
--- a/src/components/AuthGuard.tsx
+++ b/src/components/AuthGuard.tsx
@@ -96,6 +96,8 @@ export default function AuthGuard({ children, requireSuperAdmin = false }: AuthG
}
};
+
+
if (isLoading) {
return (
@@ -218,21 +220,6 @@ export default function AuthGuard({ children, requireSuperAdmin = false }: AuthG
return (
- {/* Barre de navigation admin */}
-
-
-
- Administration
-
-
-
-
{children}
);
diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx
index d7adcd5..9170c9d 100644
--- a/src/components/Navigation.tsx
+++ b/src/components/Navigation.tsx
@@ -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 (
@@ -30,36 +25,23 @@ export default function Navigation({ showBackButton = false, backUrl = '/' }: Na
)}
-
-
-
-
-
-
+
+ Mes Budgets Participatifs - Admin
+
-
- Mes Budgets Participatifs
-
+
+
diff --git a/src/components/StatusSwitch.tsx b/src/components/StatusSwitch.tsx
new file mode 100644
index 0000000..a1e539d
--- /dev/null
+++ b/src/components/StatusSwitch.tsx
@@ -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;
+ 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 (
+
+ {/* Notification de succès */}
+ {showSuccess && (
+
+
+
+ Statut mis Ă jour !
+
+
+ )}
+
+
+ {(['deposit', 'voting', 'closed'] as const).map((status, index) => {
+ const config = statusConfig[status];
+ const Icon = config.icon;
+ const isActive = localStatus === status;
+
+ return (
+
+ );
+ })}
+
+
+ {/* Effet de brillance au survol */}
+
+
+ );
+}