ajout de la possibilité de partager publiquement la page statistiques d'une campagne

This commit is contained in:
Yannick Le Duc
2025-09-21 20:53:50 +02:00
parent ae753dab4e
commit 8274722518
5 changed files with 642 additions and 328 deletions

173
src/app/stats/[id]/page.tsx Normal file
View File

@@ -0,0 +1,173 @@
'use client';
import { useState, useEffect } from 'react';
import { useParams } from 'next/navigation';
import { Campaign, Proposition, Participant, Vote } from '@/types';
import { campaignService, propositionService, participantService, voteService } from '@/lib/services';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { BarChart3 } from 'lucide-react';
import { StatsDisplay } from '@/components/StatsDisplay';
import { useStatsCalculation } from '@/hooks/useStatsCalculation';
import Footer from '@/components/Footer';
import VersionDisplay from '@/components/VersionDisplay';
export const dynamic = 'force-dynamic';
function PublicStatsPageContent() {
const params = useParams();
const campaignId = params.id as string;
const [campaign, setCampaign] = useState<Campaign | null>(null);
const [participants, setParticipants] = useState<Participant[]>([]);
const [propositions, setPropositions] = useState<Proposition[]>([]);
const [votes, setVotes] = useState<Vote[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { propositionStats } = useStatsCalculation(campaign, participants, propositions, votes);
useEffect(() => {
// Vérifier la configuration Supabase
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
// Si pas de configuration ou valeurs par défaut, rediriger vers setup
if (!supabaseUrl || !supabaseAnonKey ||
supabaseUrl === 'https://placeholder.supabase.co' ||
supabaseAnonKey === 'your-anon-key') {
console.log('🔧 Configuration Supabase manquante, redirection vers /setup');
window.location.href = '/setup';
return;
}
if (campaignId) {
loadData();
}
}, [campaignId]);
const loadData = async () => {
try {
setLoading(true);
setError(null);
const [campaignData, participantsData, propositionsData, votesData] = await Promise.all([
campaignService.getById(campaignId),
participantService.getByCampaign(campaignId),
propositionService.getByCampaign(campaignId),
voteService.getByCampaign(campaignId)
]);
if (!campaignData) {
throw new Error('Campagne non trouvée');
}
// Vérifier que la campagne est en cours de vote ou terminée pour permettre l'accès public
if (campaignData.status !== 'voting' && campaignData.status !== 'closed') {
throw new Error('Les statistiques ne sont pas encore disponibles pour cette campagne');
}
setCampaign(campaignData);
setParticipants(participantsData);
setPropositions(propositionsData);
setVotes(votesData);
} catch (error) {
console.error('Erreur lors du chargement des données:', error);
setError(error instanceof Error ? error.message : 'Une erreur est survenue');
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
<div className="container mx-auto px-4 py-8">
<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 statistiques...</p>
</div>
</div>
</div>
</div>
);
}
if (error || !campaign) {
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
<div className="container mx-auto px-4 py-8">
<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">
{error || 'Campagne introuvable'}
</h3>
<p className="text-slate-600 dark:text-slate-300 mb-6">
{error === 'Campagne non trouvée'
? 'La campagne que vous recherchez n\'existe pas ou a été supprimée.'
: error === 'Les statistiques ne sont pas encore disponibles pour cette campagne'
? 'Cette campagne n\'a pas encore commencé ou les statistiques ne sont pas encore disponibles.'
: 'Une erreur est survenue lors du chargement des données.'}
</p>
</CardContent>
</Card>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
<div className="container mx-auto px-4 py-8">
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-4 mb-4">
<Badge variant={campaign.status === 'voting' ? 'default' : 'secondary'}>
{campaign.status === 'voting' ? 'En cours de vote' : 'Terminée'}
</Badge>
</div>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-slate-100 flex items-center gap-3">
<BarChart3 className="w-8 h-8 text-blue-600" />
Statistiques publiques
</h1>
<p className="text-slate-600 dark:text-slate-300 mt-2">
{campaign.title}
</p>
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1">
{campaign.description}
</p>
</div>
</div>
</div>
{/* Stats Display */}
<StatsDisplay
campaign={campaign}
participants={participants}
propositions={propositions}
votes={votes}
propositionStats={propositionStats}
showSorting={true}
showExportButton={false}
/>
{/* Footer */}
<Footer />
{/* Version Display */}
<VersionDisplay />
</div>
</div>
);
}
export default function PublicStatsPage() {
return <PublicStatsPageContent />;
}