-
+
{activeVotes.length}
Votes Actifs
-
Voir â
+
Voir â
-
-
-
+
{futureVotes.length}
Ă Venir
-
Voir â
+
Voir â
-
-
-
+
{historyVotes.length}
Votes Terminés
-
Voir â
+
Voir â
-
-
-
+
{userVotes.length}
Votes Effectués
@@ -111,39 +103,10 @@ export default function DashboardPage({ voter }) {
- {/* Navigation Tabs */}
-
-
-
-
-
-
-
- {/* Action Required Section - Only for Active Votes */}
- {filter === 'all' && activeVotes.length > 0 && (
-
-
⥠Action Requise
-
Votes en attente de votre participation
+ {activeVotes.length > 0 && (
+
+
⥠Votes Actifs
+
Votes en cours - Participez maintenant!
{activeVotes.slice(0, 2).map(vote => (
{activeVotes.length > 2 && (
- Voir tous les votes actifs
+ Voir tous les votes actifs ({activeVotes.length})
)}
)}
- {/* Votes Display */}
-
-
- {filter === 'all' && 'Tous les votes'}
- {filter === 'actifs' && 'Votes Actifs'}
- {filter === 'futurs' && 'Votes Ă Venir'}
- {filter === 'historique' && 'Mon Historique'}
-
+ {futureVotes.length > 0 && (
+
+
đź Votes Ă Venir
+
Ălections qui dĂ©marreront bientĂŽt
+
+ {futureVotes.slice(0, 2).map(vote => (
+ setSelectedElectionId(id)}
+ />
+ ))}
+
+ {futureVotes.length > 2 && (
+
+ Voir tous les votes Ă venir ({futureVotes.length})
+
+ )}
+
+ )}
- {(() => {
- let displayVotes = votes;
- if (filter === 'actifs') displayVotes = activeVotes;
- if (filter === 'futurs') displayVotes = futureVotes;
- if (filter === 'historique') displayVotes = historyVotes;
+ {historyVotes.length > 0 && (
+
+
đ Mon Historique
+
Vos 2 derniers votes
+
+ {historyVotes.slice(0, 2).map(vote => (
+ v.election_id === vote.id)?.choix}
+ showResult={true}
+ context="historique"
+ onShowDetails={(id) => setSelectedElectionId(id)}
+ />
+ ))}
+
+ {historyVotes.length > 2 && (
+
+ Voir tout mon historique ({historyVotes.length})
+
+ )}
+
+ )}
- if (displayVotes.length === 0) {
- return (
-
-
đ
-
Aucun vote
-
- {filter === 'all' && 'Il n\'y a pas encore de votes disponibles.'}
- {filter === 'actifs' && 'Aucun vote actif pour le moment.'}
- {filter === 'futurs' && 'Aucun vote Ă venir.'}
- {filter === 'historique' && 'Vous n\'avez pas encore voté.'}
-
-
- );
- }
+ {activeVotes.length === 0 && futureVotes.length === 0 && historyVotes.length === 0 && (
+
+
+
đ
+
Aucun vote disponible
+
Il n'y a pas encore de votes disponibles.
+
+
+ )}
- return (
-
- {displayVotes.map(vote => (
- v.election_id === vote.id)?.choix}
- showResult={filter === 'historique' || vote.status === 'ferme'}
- onVote={(id) => window.location.href = `/vote/${id}`}
- />
- ))}
-
- );
- })()}
-
+
setSelectedElectionId(null)}
+ voter={voter}
+ type="futur"
+ />
);
diff --git a/e-voting-system/frontend/src/pages/ElectionDetailsPage.css b/e-voting-system/frontend/src/pages/ElectionDetailsPage.css
new file mode 100644
index 0000000..17511a3
--- /dev/null
+++ b/e-voting-system/frontend/src/pages/ElectionDetailsPage.css
@@ -0,0 +1,332 @@
+.election-details-page {
+ padding: 40px 20px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ min-height: calc(100vh - 200px);
+}
+
+.election-details-page .container {
+ max-width: 900px;
+ margin: 0 auto;
+}
+
+.btn-back {
+ margin-bottom: 30px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: white;
+ background: rgba(255, 255, 255, 0.2);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ transition: all 0.3s ease;
+}
+
+.btn-back:hover {
+ background: rgba(255, 255, 255, 0.3);
+ border-color: rgba(255, 255, 255, 0.5);
+}
+
+.details-header {
+ background: white;
+ padding: 30px;
+ border-radius: 12px;
+ margin-bottom: 30px;
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+}
+
+.details-header h1 {
+ font-size: 32px;
+ font-weight: bold;
+ margin: 0 0 15px 0;
+ color: #333;
+}
+
+.status-badge {
+ display: inline-block;
+ padding: 8px 16px;
+ border-radius: 20px;
+ font-weight: 600;
+ font-size: 14px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.status-active {
+ background: #4CAF50;
+ color: white;
+}
+
+.status-closed {
+ background: #9E9E9E;
+ color: white;
+}
+
+.status-future {
+ background: #2196F3;
+ color: white;
+}
+
+.details-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 25px;
+ margin-bottom: 40px;
+}
+
+@media (max-width: 768px) {
+ .details-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+.details-card {
+ background: white;
+ padding: 25px;
+ border-radius: 12px;
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
+ animation: slideUp 0.3s ease-out;
+}
+
+@keyframes slideUp {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.details-card h2 {
+ font-size: 18px;
+ font-weight: bold;
+ margin: 0 0 20px 0;
+ color: #333;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.details-card .description {
+ color: #666;
+ margin-bottom: 20px;
+ line-height: 1.6;
+ font-size: 15px;
+}
+
+.info-section {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+}
+
+.info-item {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ padding: 12px;
+ background: #f5f5f5;
+ border-radius: 8px;
+ color: #666;
+}
+
+.info-item svg {
+ color: #667eea;
+ flex-shrink: 0;
+ margin-top: 2px;
+}
+
+.info-item div label {
+ display: block;
+ font-size: 12px;
+ font-weight: 600;
+ color: #999;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-bottom: 4px;
+}
+
+.info-item div p {
+ margin: 0;
+ font-size: 14px;
+ font-weight: 500;
+}
+
+/* Candidats */
+.candidates-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.candidate-item {
+ display: flex;
+ align-items: flex-start;
+ gap: 15px;
+ padding: 12px;
+ background: #f8f9ff;
+ border-left: 3px solid #667eea;
+ border-radius: 6px;
+ transition: all 0.3s ease;
+}
+
+.candidate-item:hover {
+ background: #f0f2ff;
+ transform: translateX(5px);
+}
+
+.candidate-number {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ background: #667eea;
+ color: white;
+ border-radius: 50%;
+ font-weight: bold;
+ font-size: 14px;
+ flex-shrink: 0;
+}
+
+.candidate-info h3 {
+ margin: 0 0 5px 0;
+ font-size: 15px;
+ font-weight: 600;
+ color: #333;
+}
+
+.candidate-info p {
+ margin: 0;
+ font-size: 13px;
+ color: #999;
+}
+
+/* Résultats */
+.results-card {
+ grid-column: 1 / -1;
+}
+
+.results-section {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+}
+
+.total-votes {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 14px;
+ color: #666;
+ padding: 10px;
+ background: #f0f7ff;
+ border-radius: 6px;
+ margin-bottom: 10px;
+}
+
+.total-votes svg {
+ color: #667eea;
+}
+
+.total-votes strong {
+ color: #667eea;
+ font-size: 16px;
+}
+
+.results-list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.result-item {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.result-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.result-name {
+ font-weight: 500;
+ color: #333;
+ font-size: 14px;
+}
+
+.result-percentage {
+ font-weight: 600;
+ color: #667eea;
+ font-size: 13px;
+}
+
+.result-bar {
+ height: 24px;
+ background: #e0e0e0;
+ border-radius: 12px;
+ overflow: hidden;
+ background: linear-gradient(90deg, #f0f0f0 0%, #e8e8e8 100%);
+}
+
+.result-bar-fill {
+ height: 100%;
+ background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
+ border-radius: 12px;
+ transition: width 0.3s ease;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ padding-right: 8px;
+}
+
+.result-count {
+ font-size: 12px;
+ color: #999;
+ text-align: right;
+}
+
+.no-results,
+.info-message {
+ text-align: center;
+ color: #999;
+ padding: 20px;
+ font-size: 14px;
+ background: #f5f5f5;
+ border-radius: 8px;
+ margin: 0;
+}
+
+.info-message {
+ background: #f0f7ff;
+ color: #667eea;
+}
+
+/* Responsive */
+@media (max-width: 480px) {
+ .election-details-page {
+ padding: 20px 15px;
+ }
+
+ .details-header {
+ padding: 20px;
+ }
+
+ .details-header h1 {
+ font-size: 24px;
+ }
+
+ .details-card {
+ padding: 20px;
+ }
+
+ .details-card h2 {
+ font-size: 16px;
+ }
+}
diff --git a/e-voting-system/frontend/src/pages/ElectionDetailsPage.jsx b/e-voting-system/frontend/src/pages/ElectionDetailsPage.jsx
new file mode 100644
index 0000000..bfcf485
--- /dev/null
+++ b/e-voting-system/frontend/src/pages/ElectionDetailsPage.jsx
@@ -0,0 +1,253 @@
+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
;
+
+ if (error) {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ if (!election) {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ const status = getElectionStatus();
+ const statusColor = status === 'Terminée' ? 'closed' : status === 'En cours' ? 'active' : 'future';
+
+ return (
+
+
+
+
+
+
+
{election.name}
+ {status}
+
+
+
+
+ {/* Section Informations */}
+
+
đ Informations
+
{election.description}
+
+
+
+
+
+
+
{formatDate(election.start_date)}
+
+
+
+
+
+
+
{formatDate(election.end_date)}
+
+
+
+
+
+ {/* Section Candidats */}
+
+
đ„ Candidats ({candidates.length})
+
+ {candidates.map((candidate, index) => (
+
+
{candidate.order || index + 1}
+
+
{candidate.name}
+ {candidate.description &&
{candidate.description}
}
+
+
+ ))}
+
+
+
+ {/* Section Résultats */}
+ {results && election.results_published && (
+
+
đ RĂ©sultats
+
+ {results.results && results.results.length > 0 ? (
+ <>
+
+
+ Total: {results.total_votes} vote(s)
+
+
+ {results.results.map((result, index) => (
+
+
+
+ {userVote && userVote.candidate_name === result.candidate_name && (
+
+ )}
+ {result.candidate_name}
+
+ {result.percentage.toFixed(1)}%
+
+
+
{result.vote_count} vote(s)
+
+ ))}
+
+ >
+ ) : (
+
Aucun résultat disponible
+ )}
+
+
+ )}
+
+ {!election.results_published && status === 'Terminée' && (
+
+
+ đ Les rĂ©sultats de cette Ă©lection n'ont pas encore Ă©tĂ© publiĂ©s.
+
+
+ )}
+
+ {status !== 'Terminée' && (
+
+
+ ⳠLes résultats seront disponibles une fois l'élection terminée.
+
+
+ )}
+
+
+
+ );
+}
diff --git a/e-voting-system/frontend/src/pages/HistoriquePage.css b/e-voting-system/frontend/src/pages/HistoriquePage.css
new file mode 100644
index 0000000..b9ef5aa
--- /dev/null
+++ b/e-voting-system/frontend/src/pages/HistoriquePage.css
@@ -0,0 +1,198 @@
+.historique-page {
+ min-height: 100vh;
+ padding: 40px 20px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.historique-page .container {
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.historique-header {
+ margin-bottom: 40px;
+ position: relative;
+}
+
+.back-btn {
+ display: inline-block;
+ margin-bottom: 20px;
+ padding: 10px 20px;
+ background-color: rgba(255, 255, 255, 0.2);
+ color: white;
+ border: 2px solid white;
+ border-radius: 8px;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.back-btn:hover {
+ background-color: rgba(255, 255, 255, 0.3);
+ transform: translateX(-4px);
+}
+
+.historique-header h1 {
+ color: white;
+ font-size: 2.5rem;
+ margin: 10px 0 5px 0;
+ font-weight: 700;
+}
+
+.historique-header p {
+ color: rgba(255, 255, 255, 0.9);
+ font-size: 1.1rem;
+ margin: 0;
+}
+
+.stats-bar {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 20px;
+ margin-bottom: 40px;
+ background: rgba(255, 255, 255, 0.95);
+ padding: 30px;
+ border-radius: 12px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
+}
+
+.stat {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+}
+
+.stat-label {
+ font-size: 0.95rem;
+ color: #666;
+ margin-bottom: 10px;
+ font-weight: 500;
+}
+
+.stat-value {
+ font-size: 2.5rem;
+ color: #667eea;
+ font-weight: 700;
+}
+
+.votes-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+ gap: 20px;
+ margin-bottom: 40px;
+}
+
+.vote-card {
+ background: white;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+ border-left: 4px solid #667eea;
+}
+
+.vote-card:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
+}
+
+.vote-card.historique {
+ cursor: default;
+}
+
+.vote-card-header {
+ padding: 20px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ display: flex;
+ justify-content: space-between;
+ align-items: start;
+}
+
+.vote-card-header h3 {
+ margin: 0;
+ font-size: 1.2rem;
+ flex: 1;
+}
+
+.status-badge {
+ display: inline-block;
+ padding: 4px 12px;
+ border-radius: 20px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ white-space: nowrap;
+ background: rgba(255, 255, 255, 0.2);
+ border: 1px solid rgba(255, 255, 255, 0.4);
+}
+
+.status-badge.active {
+ background: #ff6b6b;
+ border-color: #ff5252;
+}
+
+.status-badge.ferme {
+ background: #51cf66;
+ border-color: #40c057;
+}
+
+.vote-card-body {
+ padding: 20px;
+}
+
+.vote-card-body p {
+ margin: 10px 0;
+ color: #333;
+ font-size: 0.95rem;
+}
+
+.vote-card-body strong {
+ color: #667eea;
+}
+
+.empty-state {
+ background: rgba(255, 255, 255, 0.95);
+ padding: 60px 40px;
+ border-radius: 12px;
+ text-align: center;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
+}
+
+.empty-icon {
+ font-size: 4rem;
+ margin-bottom: 20px;
+ display: block;
+}
+
+.empty-state h3 {
+ color: #333;
+ font-size: 1.5rem;
+ margin: 0 0 10px 0;
+}
+
+.empty-state p {
+ color: #666;
+ margin: 0;
+}
+
+@media (max-width: 768px) {
+ .historique-header h1 {
+ font-size: 1.8rem;
+ }
+
+ .historique-header p {
+ font-size: 0.95rem;
+ }
+
+ .stats-bar {
+ grid-template-columns: 1fr;
+ gap: 15px;
+ }
+
+ .votes-grid {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/e-voting-system/frontend/src/pages/HistoriquePage.jsx b/e-voting-system/frontend/src/pages/HistoriquePage.jsx
new file mode 100644
index 0000000..f3cb99c
--- /dev/null
+++ b/e-voting-system/frontend/src/pages/HistoriquePage.jsx
@@ -0,0 +1,114 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import VoteCard from '../components/VoteCard';
+import ElectionDetailsModal from '../components/ElectionDetailsModal';
+import LoadingSpinner from '../components/LoadingSpinner';
+import './HistoriquePage.css';
+
+export default function HistoriquePage({ voter }) {
+ const [elections, setElections] = useState([]);
+ const [userVotes, setUserVotes] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [selectedElectionId, setSelectedElectionId] = useState(null);
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ const fetchData = async () => {
+ try {
+ const token = localStorage.getItem('token');
+
+ // Fetch user's votes (c'est la source de vérité)
+ const votesResponse = await fetch('http://localhost:8000/api/votes/history', {
+ headers: { 'Authorization': `Bearer ${token}` },
+ });
+
+ if (!votesResponse.ok) throw new Error('Erreur de chargement de l\'historique');
+
+ const votesData = await votesResponse.json();
+
+ // Filtrer SEULEMENT les votes pour les Ă©lections TERMINĂES (status: "closed")
+ const closedVotes = votesData.filter(vote => vote.election_status === 'closed');
+ setUserVotes(closedVotes);
+
+ } catch (err) {
+ console.error('Erreur:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Les votes retournés par /api/votes/history contiennent déjà les informations nécessaires
+ const filteredElections = userVotes;
+
+ if (loading) return
;
+
+ return (
+
+
+
+
+
đ Mon Historique de Votes
+
Toutes les élections passées auxquelles vous avez participé
+
+
+ {filteredElections.length === 0 ? (
+
+
đ
+
Aucun vote enregistré
+
Vous n'avez pas encore participé à des élections terminées.
+
+ ) : (
+ <>
+
+
+ Total de votes
+ {filteredElections.length}
+
+
+ Ălections auxquelles vous avez participĂ©
+ {filteredElections.length}
+
+
+
+
+ {filteredElections.map(vote => (
+
+
+
{vote.election_name}
+
+ â
Terminée
+
+
+
+
Votre choix : {vote.candidate_name}
+
Date du vote : {new Date(vote.vote_date).toLocaleDateString('fr-FR')}
+
+
+
+ ))}
+
+ >
+ )}
+
+ {/* Election Details Modal */}
+
setSelectedElectionId(null)}
+ voter={voter}
+ type="historique"
+ />
+
+
+ );
+}
diff --git a/e-voting-system/frontend/src/pages/LoginPage.jsx b/e-voting-system/frontend/src/pages/LoginPage.jsx
new file mode 100644
index 0000000..be7f8e8
--- /dev/null
+++ b/e-voting-system/frontend/src/pages/LoginPage.jsx
@@ -0,0 +1,138 @@
+import React, { useState } from 'react';
+import { useNavigate, Link } from 'react-router-dom';
+import { Mail, Lock, LogIn } from 'lucide-react';
+import Alert from '../components/Alert';
+import { API_ENDPOINTS } from '../config/api';
+import './AuthPage.css';
+
+export default function LoginPage({ onLogin }) {
+ console.log('đŽ LoginPage MONTĂE!');
+ const navigate = useNavigate();
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = async (e) => {
+ console.log('đŽ handleSubmit APPELĂ!');
+ e.preventDefault();
+ setError('');
+ setLoading(true);
+
+ try {
+ const response = await fetch(API_ENDPOINTS.LOGIN, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email, password }),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.detail || 'Email ou mot de passe incorrect');
+ }
+
+ const data = await response.json();
+ console.log('â
Data reçue:', data);
+
+ const voterData = {
+ id: data.id,
+ email: data.email,
+ first_name: data.first_name,
+ last_name: data.last_name,
+ };
+ console.log('â
voterData préparé:', voterData);
+
+ localStorage.setItem('voter', JSON.stringify(voterData));
+ console.log('â
localStorage voter set');
+ localStorage.setItem('token', data.access_token);
+ console.log('â
localStorage token set');
+
+ console.log('â
Appel onLogin');
+ onLogin(voterData);
+ console.log('â
onLogin appelé, navigation...');
+ navigate('/dashboard');
+ console.log('â
navigate appelé');
+ } catch (err) {
+ console.error('â CATCH ERROR:', err);
+ console.error('â Error message:', err.message);
+ setError(err.message || 'Erreur de connexion');
+ } finally {
+ console.log('â
Finally: setLoading(false)');
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
Se Connecter
+
Accédez à votre tableau de bord
+
+
+ {error && (
+
setError('')} />
+ )}
+
+
+
+ ou
+
+
+ Pas encore de compte ? S'inscrire
+
+
+
+
+
+
đłïž
+
Bienvenue
+
Votez en toute confiance sur notre plateforme sécurisée
+
+
+
+
+ );
+}
diff --git a/e-voting-system/frontend/src/pages/UpcomingVotesPage.css b/e-voting-system/frontend/src/pages/UpcomingVotesPage.css
new file mode 100644
index 0000000..b1d82bc
--- /dev/null
+++ b/e-voting-system/frontend/src/pages/UpcomingVotesPage.css
@@ -0,0 +1,155 @@
+.upcoming-votes-page {
+ min-height: 100vh;
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+ padding: 2rem;
+}
+
+.upcoming-votes-page .container {
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.upcoming-votes-page .page-header {
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(10px);
+ border-radius: 12px;
+ padding: 2rem;
+ margin-bottom: 2rem;
+ color: white;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+.upcoming-votes-page .page-header h1 {
+ margin: 0.5rem 0 0.5rem 0;
+ font-size: 2.5rem;
+}
+
+.upcoming-votes-page .page-header p {
+ margin: 0;
+ opacity: 0.9;
+}
+
+.upcoming-votes-page .back-btn {
+ background: rgba(255, 255, 255, 0.2);
+ color: white;
+ border: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 8px;
+ cursor: pointer;
+ margin-bottom: 1rem;
+ transition: all 0.3s ease;
+}
+
+.upcoming-votes-page .back-btn:hover {
+ background: rgba(255, 255, 255, 0.3);
+ transform: translateX(-2px);
+}
+
+.upcoming-votes-page .stats-bar {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 1.5rem;
+ margin-bottom: 2rem;
+}
+
+.upcoming-votes-page .stat {
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(10px);
+ border-radius: 12px;
+ padding: 1.5rem;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ color: white;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.upcoming-votes-page .stat-label {
+ font-size: 0.9rem;
+ opacity: 0.8;
+}
+
+.upcoming-votes-page .stat-value {
+ font-size: 2rem;
+ font-weight: bold;
+}
+
+.upcoming-votes-page .elections-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 1.5rem;
+}
+
+.upcoming-votes-page .election-card {
+ background: white;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+ cursor: pointer;
+}
+
+.upcoming-votes-page .election-card:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
+}
+
+.upcoming-votes-page .election-card-header {
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+ color: white;
+ padding: 1.5rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 1rem;
+}
+
+.upcoming-votes-page .election-card-header h3 {
+ margin: 0;
+ font-size: 1.2rem;
+ flex: 1;
+}
+
+.upcoming-votes-page .election-card-body {
+ padding: 1.5rem;
+}
+
+.upcoming-votes-page .election-card-body p {
+ margin: 0.5rem 0;
+ color: #333;
+}
+
+.upcoming-votes-page .status-badge {
+ display: inline-block;
+ padding: 0.4rem 0.8rem;
+ border-radius: 20px;
+ font-size: 0.85rem;
+ font-weight: 600;
+ white-space: nowrap;
+}
+
+.upcoming-votes-page .status-badge.upcoming {
+ background: rgba(255, 255, 255, 0.2);
+ color: white;
+ border: 1px solid rgba(255, 255, 255, 0.4);
+}
+
+.upcoming-votes-page .empty-state {
+ text-align: center;
+ padding: 3rem 1rem;
+ color: white;
+}
+
+.upcoming-votes-page .empty-icon {
+ font-size: 4rem;
+ margin-bottom: 1rem;
+}
+
+.upcoming-votes-page .empty-state h3 {
+ font-size: 1.5rem;
+ margin: 1rem 0;
+}
+
+.upcoming-votes-page .empty-state p {
+ opacity: 0.9;
+}
diff --git a/e-voting-system/frontend/src/pages/UpcomingVotesPage.jsx b/e-voting-system/frontend/src/pages/UpcomingVotesPage.jsx
new file mode 100644
index 0000000..b200fde
--- /dev/null
+++ b/e-voting-system/frontend/src/pages/UpcomingVotesPage.jsx
@@ -0,0 +1,101 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import LoadingSpinner from '../components/LoadingSpinner';
+import ElectionDetailsModal from '../components/ElectionDetailsModal';
+import './UpcomingVotesPage.css';
+
+export default function UpcomingVotesPage({ voter }) {
+ const [upcomingElections, setUpcomingElections] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [selectedElectionId, setSelectedElectionId] = useState(null);
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ const fetchData = async () => {
+ try {
+ const token = localStorage.getItem('token');
+
+ // Fetch upcoming elections
+ const electionsResponse = await fetch('http://localhost:8000/api/elections/upcoming', {
+ headers: { 'Authorization': `Bearer ${token}` },
+ });
+
+ if (!electionsResponse.ok) throw new Error('Erreur de chargement');
+
+ const electionsData = await electionsResponse.json();
+ setUpcomingElections(electionsData || []);
+
+ } catch (err) {
+ console.error('Erreur:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (loading) return
;
+
+ return (
+
+
+
+
+
âł Votes Ă Venir
+
Ălections qui arriveront prochainement
+
+
+ {upcomingElections.length === 0 ? (
+
+
đ
+
Aucun vote Ă venir
+
Aucune élection prévue pour le moment.
+
+ ) : (
+ <>
+
+
+ Ălections Ă venir
+ {upcomingElections.length}
+
+
+
+
+ {upcomingElections.map(election => (
+
+
+
{election.name}
+
+ âł Ă venir
+
+
+
+
Description : {election.description || 'N/A'}
+
Début : {new Date(election.start_date).toLocaleDateString('fr-FR')}
+
+
+
+ ))}
+
+ >
+ )}
+
+
setSelectedElectionId(null)}
+ voter={voter}
+ type="futur"
+ />
+
+
+ );
+}
diff --git a/e-voting-system/frontend/src/pages/VotingPage.jsx b/e-voting-system/frontend/src/pages/VotingPage.jsx
index 8d4d188..8acadac 100644
--- a/e-voting-system/frontend/src/pages/VotingPage.jsx
+++ b/e-voting-system/frontend/src/pages/VotingPage.jsx
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
-import { ArrowLeft, CheckCircle, AlertCircle } from 'lucide-react';
+import { ArrowLeft, CheckCircle, AlertCircle, BarChart3 } from 'lucide-react';
import Alert from '../components/Alert';
import Modal from '../components/Modal';
import LoadingSpinner from '../components/LoadingSpinner';
@@ -9,7 +9,10 @@ import './VotingPage.css';
export default function VotingPage() {
const { id } = useParams();
const navigate = useNavigate();
- const [vote, setVote] = useState(null);
+ const [election, setElection] = useState(null);
+ const [candidates, setCandidates] = useState([]);
+ const [userVote, setUserVote] = useState(null);
+ const [results, setResults] = useState(null);
const [loading, setLoading] = useState(true);
const [selectedOption, setSelectedOption] = useState('');
const [submitted, setSubmitted] = useState(false);
@@ -19,20 +22,56 @@ export default function VotingPage() {
const [voting, setVoting] = useState(false);
useEffect(() => {
- fetchVote();
+ fetchElectionData();
}, [id]);
- const fetchVote = async () => {
+ const fetchElectionData = async () => {
try {
const token = localStorage.getItem('token');
- const response = await fetch(`http://localhost:8000/elections/${id}`, {
+
+ // Fetch election details
+ const electionResponse = await fetch(`http://localhost:8000/elections/${id}`, {
headers: { 'Authorization': `Bearer ${token}` },
});
- if (!response.ok) throw new Error('Vote non trouvé');
+ if (!electionResponse.ok) throw new Error('Ălection non trouvĂ©e');
+
+ const electionData = await electionResponse.json();
+ setElection(electionData);
- const data = await response.json();
- setVote(data);
+ // Fetch candidates for this election
+ const candidatesResponse = await fetch(`http://localhost:8000/elections/${id}/candidates`, {
+ headers: { 'Authorization': `Bearer ${token}` },
+ });
+
+ if (candidatesResponse.ok) {
+ const candidatesData = await candidatesResponse.json();
+ setCandidates(candidatesData);
+ }
+
+ // Fetch user's vote if it exists
+ const userVoteResponse = await fetch(`http://localhost:8000/votes/my-votes?election_id=${id}`, {
+ headers: { 'Authorization': `Bearer ${token}` },
+ });
+
+ if (userVoteResponse.ok) {
+ const userVoteData = await userVoteResponse.json();
+ if (Array.isArray(userVoteData) && userVoteData.length > 0) {
+ setUserVote(userVoteData[0]);
+ }
+ }
+
+ // Fetch results if election is closed
+ if (electionData.results_published) {
+ const resultsResponse = await fetch(`http://localhost:8000/elections/${id}/results`, {
+ headers: { 'Authorization': `Bearer ${token}` },
+ });
+
+ if (resultsResponse.ok) {
+ const resultsData = await resultsResponse.json();
+ setResults(resultsData);
+ }
+ }
} catch (err) {
setError(err.message);
} finally {
@@ -51,14 +90,14 @@ export default function VotingPage() {
try {
const token = localStorage.getItem('token');
- const response = await fetch('http://localhost:8000/votes/submit', {
+ const response = await fetch('http://localhost:8000/votes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
- election_id: vote.id,
+ election_id: election.id,
choix: selectedOption,
}),
});
@@ -79,11 +118,11 @@ export default function VotingPage() {
if (loading) return
;
- if (!vote) {
+ if (!election) {
return (
-
+