- Migrate from React CRA to Next.js 15 with modern architecture - Implement comprehensive shadcn/ui component library - Create complete dashboard system with layouts and navigation - Build authentication pages (login, register) with proper forms - Implement vote management pages (active, upcoming, history, archives) - Add user profile management with security settings - Configure Tailwind CSS with custom dark theme (accent: #e8704b) - Setup TypeScript with strict type checking - Backup old React-based frontend to .backups/frontend-old - All pages compile successfully and build passes linting Pages created: - Home page with hero section and features - Authentication (login/register) - Dashboard with stats and vote cards - Vote management (active, upcoming, history, archives) - User profile with form validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
254 lines
8.6 KiB
JavaScript
254 lines
8.6 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import { ArrowLeft, Calendar, Users, BarChart3, CheckCircle } from 'lucide-react';
|
|
import LoadingSpinner from '../components/LoadingSpinner';
|
|
import Alert from '../components/Alert';
|
|
import './ElectionDetailsPage.css';
|
|
|
|
export default function ElectionDetailsPage({ voter = null, type = 'archives' }) {
|
|
const { id } = useParams();
|
|
const navigate = useNavigate();
|
|
const [election, setElection] = useState(null);
|
|
const [results, setResults] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
const [candidates, setCandidates] = useState([]);
|
|
const [userVote, setUserVote] = useState(null);
|
|
|
|
useEffect(() => {
|
|
fetchElectionDetails();
|
|
}, [id]);
|
|
|
|
const fetchElectionDetails = async () => {
|
|
try {
|
|
setLoading(true);
|
|
setError('');
|
|
|
|
// Récupérer les détails de l'élection
|
|
const electionResponse = await fetch(`http://localhost:8000/api/elections/${id}`);
|
|
if (!electionResponse.ok) {
|
|
throw new Error('Élection non trouvée');
|
|
}
|
|
const electionData = await electionResponse.json();
|
|
setElection(electionData);
|
|
setCandidates(electionData.candidates || []);
|
|
|
|
// Récupérer le vote de l'utilisateur si connecté et type = historique
|
|
if (voter && type === 'historique') {
|
|
try {
|
|
const token = localStorage.getItem('token');
|
|
const userVoteResponse = await fetch(`http://localhost:8000/api/votes/election/${id}`, {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
if (userVoteResponse.ok) {
|
|
const userVoteData = await userVoteResponse.json();
|
|
setUserVote(userVoteData);
|
|
}
|
|
} catch (err) {
|
|
console.warn('Impossible de récupérer le vote utilisateur');
|
|
}
|
|
}
|
|
|
|
// Récupérer les résultats si l'élection est terminée
|
|
if (electionData.results_published) {
|
|
try {
|
|
const resultsResponse = await fetch(`http://localhost:8000/api/elections/${id}/results`);
|
|
if (resultsResponse.ok) {
|
|
const resultsData = await resultsResponse.json();
|
|
setResults(resultsData);
|
|
}
|
|
} catch (err) {
|
|
console.warn('Résultats non disponibles');
|
|
}
|
|
}
|
|
} catch (err) {
|
|
setError(err.message || 'Erreur de chargement');
|
|
console.error('Erreur:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const formatDate = (dateString) => {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('fr-FR', {
|
|
day: 'numeric',
|
|
month: 'long',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
};
|
|
|
|
const getElectionStatus = () => {
|
|
if (!election) return '';
|
|
const now = new Date();
|
|
const start = new Date(election.start_date);
|
|
const end = new Date(election.end_date);
|
|
|
|
if (now < start) return 'À venir';
|
|
if (now > end) return 'Terminée';
|
|
return 'En cours';
|
|
};
|
|
|
|
if (loading) return <LoadingSpinner fullscreen />;
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="election-details-page">
|
|
<div className="container">
|
|
<button className="btn btn-ghost" onClick={() => navigate(-1)}>
|
|
<ArrowLeft size={20} />
|
|
Retour
|
|
</button>
|
|
<Alert type="error" title="Erreur" message={error} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!election) {
|
|
return (
|
|
<div className="election-details-page">
|
|
<div className="container">
|
|
<button className="btn btn-ghost" onClick={() => navigate(-1)}>
|
|
<ArrowLeft size={20} />
|
|
Retour
|
|
</button>
|
|
<Alert type="error" title="Erreur" message="Élection non trouvée" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const status = getElectionStatus();
|
|
const statusColor = status === 'Terminée' ? 'closed' : status === 'En cours' ? 'active' : 'future';
|
|
|
|
return (
|
|
<div className="election-details-page">
|
|
<div className="container">
|
|
<button
|
|
className="btn btn-ghost btn-back"
|
|
onClick={() => {
|
|
if (type === 'historique') {
|
|
navigate('/dashboard/historique');
|
|
} else if (type === 'futur') {
|
|
navigate('/dashboard/futurs');
|
|
} else {
|
|
navigate('/archives');
|
|
}
|
|
}}
|
|
>
|
|
<ArrowLeft size={20} />
|
|
Retour
|
|
</button>
|
|
|
|
<div className="details-header">
|
|
<div>
|
|
<h1>{election.name}</h1>
|
|
<span className={`status-badge status-${statusColor}`}>{status}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="details-grid">
|
|
{/* Section Informations */}
|
|
<div className="details-card">
|
|
<h2>📋 Informations</h2>
|
|
<p className="description">{election.description}</p>
|
|
|
|
<div className="info-section">
|
|
<div className="info-item">
|
|
<Calendar size={20} />
|
|
<div>
|
|
<label>Ouverture</label>
|
|
<p>{formatDate(election.start_date)}</p>
|
|
</div>
|
|
</div>
|
|
<div className="info-item">
|
|
<Calendar size={20} />
|
|
<div>
|
|
<label>Fermeture</label>
|
|
<p>{formatDate(election.end_date)}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Section Candidats */}
|
|
<div className="details-card">
|
|
<h2>👥 Candidats ({candidates.length})</h2>
|
|
<div className="candidates-list">
|
|
{candidates.map((candidate, index) => (
|
|
<div key={candidate.id} className="candidate-item">
|
|
<div className="candidate-number">{candidate.order || index + 1}</div>
|
|
<div className="candidate-info">
|
|
<h3>{candidate.name}</h3>
|
|
{candidate.description && <p>{candidate.description}</p>}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Section Résultats */}
|
|
{results && election.results_published && (
|
|
<div className="details-card results-card">
|
|
<h2>📊 Résultats</h2>
|
|
<div className="results-section">
|
|
{results.results && results.results.length > 0 ? (
|
|
<>
|
|
<p className="total-votes">
|
|
<Users size={18} />
|
|
Total: <strong>{results.total_votes} vote(s)</strong>
|
|
</p>
|
|
<div className="results-list">
|
|
{results.results.map((result, index) => (
|
|
<div key={index} className="result-item">
|
|
<div className="result-header">
|
|
<span className="result-name">
|
|
{userVote && userVote.candidate_name === result.candidate_name && (
|
|
<CheckCircle size={16} style={{ display: 'inline', marginRight: '8px', color: '#4CAF50' }} />
|
|
)}
|
|
{result.candidate_name}
|
|
</span>
|
|
<span className="result-percentage">{result.percentage.toFixed(1)}%</span>
|
|
</div>
|
|
<div className="result-bar">
|
|
<div
|
|
className="result-bar-fill"
|
|
style={{ width: `${result.percentage}%` }}
|
|
></div>
|
|
</div>
|
|
<span className="result-count">{result.vote_count} vote(s)</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
) : (
|
|
<p className="no-results">Aucun résultat disponible</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{!election.results_published && status === 'Terminée' && (
|
|
<div className="details-card">
|
|
<p className="info-message">
|
|
📊 Les résultats de cette élection n'ont pas encore été publiés.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{status !== 'Terminée' && (
|
|
<div className="details-card">
|
|
<p className="info-message">
|
|
⏳ Les résultats seront disponibles une fois l'élection terminée.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|