add supabase authentication

This commit is contained in:
Yannick Le Duc
2025-08-25 16:02:57 +02:00
parent 6ad24b36dc
commit ec681cfd13
5 changed files with 347 additions and 106 deletions

View File

@@ -13,11 +13,12 @@ import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import Navigation from '@/components/Navigation'; import Navigation from '@/components/Navigation';
import AuthGuard from '@/components/AuthGuard';
import { Users, User, Calendar, Mail, Vote, Copy, Check } from 'lucide-react'; import { Users, User, Calendar, Mail, Vote, Copy, Check } from 'lucide-react';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
export default function CampaignParticipantsPage() { function CampaignParticipantsPageContent() {
const params = useParams(); const params = useParams();
const campaignId = params.id as string; const campaignId = params.id as string;
const [campaign, setCampaign] = useState<Campaign | null>(null); const [campaign, setCampaign] = useState<Campaign | null>(null);
@@ -369,3 +370,11 @@ export default function CampaignParticipantsPage() {
</div> </div>
); );
} }
export default function CampaignParticipantsPage() {
return (
<AuthGuard>
<CampaignParticipantsPageContent />
</AuthGuard>
);
}

View File

@@ -12,11 +12,12 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import Navigation from '@/components/Navigation'; import Navigation from '@/components/Navigation';
import AuthGuard from '@/components/AuthGuard';
import { FileText, User, Calendar, Mail } from 'lucide-react'; import { FileText, User, Calendar, Mail } from 'lucide-react';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
export default function CampaignPropositionsPage() { function CampaignPropositionsPageContent() {
const params = useParams(); const params = useParams();
const campaignId = params.id as string; const campaignId = params.id as string;
const [campaign, setCampaign] = useState<Campaign | null>(null); const [campaign, setCampaign] = useState<Campaign | null>(null);
@@ -295,3 +296,11 @@ export default function CampaignPropositionsPage() {
</div> </div>
); );
} }
export default function CampaignPropositionsPage() {
return (
<AuthGuard>
<CampaignPropositionsPageContent />
</AuthGuard>
);
}

View File

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

View File

@@ -22,7 +22,7 @@ export default function HomePage() {
<div className="flex flex-col sm:flex-row gap-4 justify-center"> <div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button asChild size="lg" className="text-lg px-8 py-6"> <Button asChild size="lg" className="text-lg px-8 py-6">
<Link href="/admin"> <Link href="/admin">
🛠️ Espace Administration 🔐 Espace Administration
</Link> </Link>
</Button> </Button>
<Button asChild variant="outline" size="lg" className="text-lg px-8 py-6"> <Button asChild variant="outline" size="lg" className="text-lg px-8 py-6">

View File

@@ -0,0 +1,213 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { supabase } from '@/lib/supabase';
import { User } from '@supabase/supabase-js';
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 { AlertCircle, Mail, Lock, Loader2 } from 'lucide-react';
interface AuthGuardProps {
children: React.ReactNode;
}
export default function AuthGuard({ children }: AuthGuardProps) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [authMode, setAuthMode] = useState<'signin' | 'signup'>('signin');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [authLoading, setAuthLoading] = useState(false);
const [error, setError] = useState('');
const [message, setMessage] = useState('');
const router = useRouter();
useEffect(() => {
// Vérifier l'état de l'authentification au chargement
const checkUser = async () => {
const { data: { user } } = await supabase.auth.getUser();
setUser(user);
setLoading(false);
};
checkUser();
// Écouter les changements d'authentification
const { data: { subscription } } = supabase.auth.onAuthStateChange(
async (event, session) => {
setUser(session?.user ?? null);
setLoading(false);
}
);
return () => subscription.unsubscribe();
}, []);
const handleAuth = async (e: React.FormEvent) => {
e.preventDefault();
setAuthLoading(true);
setError('');
setMessage('');
try {
if (authMode === 'signin') {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) throw error;
} else {
const { error } = await supabase.auth.signUp({
email,
password,
});
if (error) throw error;
setMessage('Vérifiez votre email pour confirmer votre inscription.');
}
} catch (error: any) {
setError(error.message);
} finally {
setAuthLoading(false);
}
};
const handleSignOut = async () => {
await supabase.auth.signOut();
router.push('/');
};
if (loading) {
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-900 flex items-center justify-center">
<div className="text-center">
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-4 text-slate-600 dark:text-slate-300" />
<p className="text-slate-600 dark:text-slate-300">Chargement...</p>
</div>
</div>
);
}
if (!user) {
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-900 flex items-center justify-center p-4">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<CardTitle className="text-2xl">Administration</CardTitle>
<CardDescription>
{authMode === 'signin'
? 'Connectez-vous pour accéder à l\'administration'
: 'Créez un compte pour accéder à l\'administration'
}
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleAuth} className="space-y-4">
{error && (
<div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<div className="flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-red-600 dark:text-red-400" />
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
</div>
</div>
)}
{message && (
<div className="p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
<p className="text-sm text-green-600 dark:text-green-400">{message}</p>
</div>
)}
<div className="space-y-2">
<Label htmlFor="email" className="flex items-center gap-2">
<Mail className="w-4 h-4" />
Email
</Label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="admin@example.com"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="password" className="flex items-center gap-2">
<Lock className="w-4 h-4" />
Mot de passe
</Label>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
/>
</div>
<Button type="submit" className="w-full" disabled={authLoading}>
{authLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
{authMode === 'signin' ? 'Connexion...' : 'Inscription...'}
</>
) : (
authMode === 'signin' ? 'Se connecter' : 'S\'inscrire'
)}
</Button>
</form>
<div className="mt-4 text-center">
<Button
variant="link"
onClick={() => setAuthMode(authMode === 'signin' ? 'signup' : 'signin')}
className="text-sm"
>
{authMode === 'signin'
? 'Pas de compte ? S\'inscrire'
: 'Déjà un compte ? Se connecter'
}
</Button>
</div>
<div className="mt-4 text-center">
<Button variant="ghost" asChild className="text-sm">
<a href="/">Retour à l'accueil</a>
</Button>
</div>
</CardContent>
</Card>
</div>
);
}
return (
<div>
{/* Header avec bouton de déconnexion */}
<div className="bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700">
<div className="container mx-auto px-4 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-sm text-slate-600 dark:text-slate-300">
Connecté en tant que :
</span>
<span className="text-sm font-medium text-slate-900 dark:text-slate-100">
{user.email}
</span>
</div>
<Button variant="outline" size="sm" onClick={handleSignOut}>
Se déconnecter
</Button>
</div>
</div>
</div>
{/* Contenu protégé */}
{children}
</div>
);
}