+ {/* Toast notification pour les messages en attente */}
+ {queuedNotification && (
+
+
+
+
{queuedNotification}
+
+
+
+ )}
+
{/* Sidebar - List of Conversations */}
diff --git a/frontend/pages/LearningTools.tsx b/frontend/pages/LearningTools.tsx
index 37a82d17eb17089fe7d9d420f86d9831a65b594c..27338b591e79d62fda52192e76a53d68604cbeb7 100644
--- a/frontend/pages/LearningTools.tsx
+++ b/frontend/pages/LearningTools.tsx
@@ -63,6 +63,9 @@ const LearningTools: React.FC = () => {
navigate('/tools/coloring');
} else if (toolId === 'calc') {
navigate('/tools/calculator');
+ } else if (toolId === 'chem') {
+ // Ouvrir le laboratoire chimique déployé sur Vercel
+ window.open('https://virtual-labo-chimique.vercel.app/', '_blank');
} else {
alert(`Lancement de l'outil : ${toolTitle} (Version démo)`);
}
diff --git a/frontend/pages/MentorDashboard.tsx b/frontend/pages/MentorDashboard.tsx
index 3bfed73a36dadd3417193d5ca8e38989e17cc314..e2b4870f2920371439154f68efdb506fc5108404 100644
--- a/frontend/pages/MentorDashboard.tsx
+++ b/frontend/pages/MentorDashboard.tsx
@@ -8,7 +8,7 @@ import {
Clock,
CheckCircle,
XCircle,
- Video,
+ MessageSquare as ChatIcon,
MoreHorizontal,
Star,
Award,
@@ -24,29 +24,7 @@ import { UserRole, Mentor } from '../types';
import EditProfileModal from '../components/EditProfileModal';
import ManageAvailabilityModal from '../components/ManageAvailabilityModal';
import { mentorService, bookingsService } from '../services';
-
-interface Booking {
- id: string;
- student: {
- id: string;
- profile?: {
- name: string;
- avatar?: string;
- university?: string;
- country?: string;
- };
- email: string;
- };
- mentor: any;
- date: string;
- time: string;
- status: 'PENDING' | 'CONFIRMED' | 'REJECTED' | 'COMPLETED' | 'CANCELLED';
- status_label: string;
- domains: string[];
- expectation: string;
- main_question: string;
- created_at: string;
-}
+import type { Booking } from '../services';
const MentorDashboard: React.FC = () => {
const { user, isAuthenticated } = useAuth();
@@ -182,14 +160,25 @@ const MentorDashboard: React.FC = () => {
};
const STATS = [
- { label: 'Sessions totales', value: stats.totalSessions.toString(), icon: , color: 'bg-blue-500' },
+ { label: 'Sessions totales', value: stats.totalSessions.toString(), icon: , color: 'bg-blue-500' },
{ label: 'Étudiants aidés', value: stats.studentsHelped.toString(), icon: , color: 'bg-green-500' },
{ label: 'Note moyenne', value: stats.averageRating.toFixed(1), icon: , color: 'bg-yellow-500' },
{ label: 'Heures ce mois', value: `${stats.monthlyHours}h`, icon: , color: 'bg-purple-500' },
];
const upcomingBookings = bookings
- .filter(b => b.status === 'CONFIRMED' && new Date(b.date) >= new Date())
+ .filter(b => {
+ if (b.status !== 'CONFIRMED') return false;
+
+ // Parse YYYY-MM-DD manually to avoid timezone issues
+ const [year, month, day] = b.date.split('-').map(Number);
+ const bookingDate = new Date(year, month - 1, day);
+
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+
+ return bookingDate >= today;
+ })
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
.slice(0, 5);
@@ -313,6 +302,48 @@ const MentorDashboard: React.FC = () => {
);
+ const renderConfirmedCard = (booking: Booking) => (
+
+
+
+
+
+
+ {booking.student.profile?.name || booking.student.email}
+
+
+
+ Confirmé
+
+ •
+ {booking.domains[0] || "Session"}
+
+
+
+
+
{formatDate(booking.date)}
+
{formatTime(booking.time)}
+
+
+
+
+
+
+ Ouvrir le chat
+
+
+
+ );
+
const renderOverview = () => (
{/* Stats Grid */}
@@ -331,23 +362,61 @@ const MentorDashboard: React.FC = () => {
- {/* Pending Requests Preview */}
-
-
-
Demandes récentes
-
-
+ {/* Main Content Column */}
+
+
+ {/* Pending Requests Section */}
+
+
+
+
Demandes en attente
+ {pendingRequests.length > 0 && (
+
+ {pendingRequests.length}
+
+ )}
+
+
+
-
- {pendingRequests.slice(0, 3).map(req => renderRequestCard(req))}
- {pendingRequests.length === 0 && (
-
-
-
+
+ {pendingRequests.slice(0, 2).map(req => renderRequestCard(req))}
+ {pendingRequests.length === 0 && (
+
+
+
+
+
Aucune nouvelle demande.
-
Aucune demande en attente pour le moment.
+ )}
+
+
+
+ {/* Confirmed Sessions Section */}
+
+
+
+
Séances confirmées
+ {upcomingBookings.length > 0 && (
+
+ {upcomingBookings.length}
+
+ )}
- )}
+
+
+
+
+ {upcomingBookings.slice(0, 4).map(booking => renderConfirmedCard(booking))}
+ {upcomingBookings.length === 0 && (
+
+
+
+
+
Aucune séance confirmée à venir.
+
+ )}
+
diff --git a/frontend/pages/QuestionDetail.tsx b/frontend/pages/QuestionDetail.tsx
index a3b0906d70bcbe7edfae49e0eb701b816a7ae44b..8b34b75d522277a7cff72be9c15144588cadba0a 100644
--- a/frontend/pages/QuestionDetail.tsx
+++ b/frontend/pages/QuestionDetail.tsx
@@ -185,16 +185,23 @@ const QuestionDetail: React.FC = () => {
{q.tags && q.tags.length > 0 && (
- {q.tags.slice(0, 2).map(tag => (
-
- {tag}
-
- ))}
+ {q.tags.slice(0, 2).map((tag, index) => {
+ const isLevel = ['université', 'lycée', 'collège', 'licence', 'master', 'doctorat', 'primaire', 'l1', 'l2', 'l3', 'm1', 'm2', '6ème', '5ème', '4ème', '3ème', '2nde', '1ère', 'tle', 'terminale'].includes(tag.toLowerCase());
+ return (
+
+ {tag}
+
+ );
+ })}
)}
+
))}
diff --git a/frontend/services/api.ts b/frontend/services/api.ts
index dd0b353ad2faa3866a94292afa10a2f4e2e744ee..2a6e8d493f664926ab4d5e7c5a90d2bcd3b8a792 100644
--- a/frontend/services/api.ts
+++ b/frontend/services/api.ts
@@ -11,6 +11,14 @@ const api = axios.create({
},
});
+// Instance pour les requêtes publiques (sans token)
+export const publicApi = axios.create({
+ baseURL: API_URL,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
// Intercepteur pour ajouter le token JWT à chaque requête
api.interceptors.request.use(
(config) => {
@@ -30,21 +38,21 @@ api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
-
+
// Si erreur 401 (Non autorisé) et qu'on n'a pas déjà essayé de rafraîchir
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
-
+
try {
const refreshToken = localStorage.getItem('refresh_token');
if (refreshToken) {
const response = await axios.post(`${API_URL}token/refresh/`, {
refresh: refreshToken
});
-
+
const { access } = response.data;
localStorage.setItem('access_token', access);
-
+
// Réessayer la requête originale avec le nouveau token
originalRequest.headers['Authorization'] = `Bearer ${access}`;
return api(originalRequest);
@@ -53,7 +61,8 @@ api.interceptors.response.use(
// Si le refresh échoue, on déconnecte l'utilisateur
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
- window.location.href = '/login';
+ // Ne pas rediriger automatiquement ici pour éviter les boucles sur les pages publiques
+ // window.location.href = '/login';
}
}
return Promise.reject(error);
@@ -61,3 +70,4 @@ api.interceptors.response.use(
);
export default api;
+
diff --git a/frontend/services/bookings.ts b/frontend/services/bookings.ts
index 57b3990e3d4517e2ef4330d1c24d5f1749a6e8e4..963b9cc6b6bd86e2ee27fcb9856f1a8ea7ef4da8 100644
--- a/frontend/services/bookings.ts
+++ b/frontend/services/bookings.ts
@@ -11,9 +11,10 @@ export interface Booking {
date: string;
time: string;
status: 'PENDING' | 'CONFIRMED' | 'REJECTED' | 'COMPLETED' | 'CANCELLED';
- domains?: string[];
- expectation?: string;
- main_question?: string;
+ status_label: string;
+ domains: string[];
+ expectation: string;
+ main_question: string;
created_at: string;
}
@@ -26,28 +27,39 @@ const getAvatarUrl = (avatar: string | null | undefined, name: string) => {
};
// Helper to map backend booking to frontend booking
-const mapBooking = (b: any): Booking => ({
- id: b.id ? b.id.toString() : '',
- student: {
- ...b.student,
- id: b.student?.id ? b.student.id.toString() : '',
- name: b.student?.profile?.name || 'Étudiant',
- avatar: getAvatarUrl(b.student?.profile?.avatar, b.student?.profile?.name),
- },
- mentor: {
- ...b.mentor,
- id: b.mentor?.id ? b.mentor.id.toString() : '',
- name: b.mentor?.profile?.name || 'Mentor',
- avatar: getAvatarUrl(b.mentor?.profile?.avatar, b.mentor?.profile?.name),
- },
- date: b.date,
- time: b.time,
- status: b.status,
- domains: Array.isArray(b.domains) ? b.domains : [],
- expectation: b.expectation || '',
- main_question: b.main_question || '',
- created_at: b.created_at
-});
+const mapBooking = (b: any): Booking => {
+ const statusLabels: Record
= {
+ 'PENDING': 'En attente',
+ 'CONFIRMED': 'Confirmé',
+ 'REJECTED': 'Refusé',
+ 'COMPLETED': 'Terminé',
+ 'CANCELLED': 'Annulé'
+ };
+
+ return {
+ id: b.id ? b.id.toString() : '',
+ student: {
+ ...b.student,
+ id: b.student?.id ? b.student.id.toString() : '',
+ name: b.student?.profile?.name || 'Étudiant',
+ avatar: getAvatarUrl(b.student?.profile?.avatar, b.student?.profile?.name),
+ },
+ mentor: {
+ ...b.mentor,
+ id: b.mentor?.id ? b.mentor.id.toString() : '',
+ name: b.mentor?.profile?.name || 'Mentor',
+ avatar: getAvatarUrl(b.mentor?.profile?.avatar, b.mentor?.profile?.name),
+ },
+ date: b.date,
+ time: b.time,
+ status: b.status,
+ status_label: statusLabels[b.status] || b.status,
+ domains: Array.isArray(b.domains) ? b.domains : [],
+ expectation: b.expectation || '',
+ main_question: b.main_question || '',
+ created_at: b.created_at
+ };
+};
export const bookingsService = {
// Récupérer mes réservations
diff --git a/frontend/services/encryption.ts b/frontend/services/encryption.ts
index 0e990268a2742e2b7973f7c5e89efe0c83430d46..985f8521594fe4c618a027f3e33e3114f405df1d 100644
--- a/frontend/services/encryption.ts
+++ b/frontend/services/encryption.ts
@@ -41,24 +41,49 @@ export const encryptionService = {
return null;
},
- // Initialiser le chiffrement (Générer si inexistant, et uploader la clé publique)
+ // Initialiser le chiffrement (Utiliser les clés du backend ou générer si nécessaire)
initializeEncryption: async (): Promise => {
+ // 1. Vérifier si on a des clés en localStorage
let keys = encryptionService.getLocalKeys();
- if (!keys) {
+ if (keys) {
+ return keys;
+ }
- keys = await encryptionService.generateKeyPair();
- encryptionService.saveKeysLocally(keys);
+ // 2. Récupérer l'utilisateur pour voir s'il a des clés sur le backend
+ try {
+ const user = await authService.getCurrentUser() as any;
+
+ if (user.profile?.public_key && user.profile?.encrypted_private_key) {
+ // Utiliser les clés du backend
+ keys = {
+ publicKey: user.profile.public_key,
+ privateKey: user.profile.encrypted_private_key
+ };
+
+ // Les sauvegarder localement pour les prochaines fois
+ encryptionService.saveKeysLocally(keys);
+ console.log('Clés de chiffrement récupérées du backend');
+ return keys;
+ }
+ } catch (error) {
+ console.error('Erreur lors de la récupération des clés du backend', error);
+ }
- // Upload public key to server
- try {
- await authService.updateProfile({
- public_key: keys.publicKey
- });
+ // 3. Si pas de clés du tout, générer localement (fallback)
+ console.warn('Génération de clés côté client (fallback)');
+ keys = await encryptionService.generateKeyPair();
+ encryptionService.saveKeysLocally(keys);
- } catch (error) {
- console.error('Erreur lors de l\'envoi de la clé publique', error);
- }
+ // Uploader la clé publique au serveur
+ try {
+ await authService.updateProfile({
+ public_key: keys.publicKey,
+ encrypted_private_key: keys.privateKey
+ });
+ console.log('Clés générées et envoyées au backend');
+ } catch (error) {
+ console.error('Erreur lors de l\'envoi des clés au backend', error);
}
return keys;
diff --git a/frontend/services/index.ts b/frontend/services/index.ts
index 9c0f811d6add120216eab797cae2a266bda211c8..f7d1c4b47b405c09a73332837078ee6140358de9 100644
--- a/frontend/services/index.ts
+++ b/frontend/services/index.ts
@@ -5,6 +5,7 @@ export { authService } from './auth';
export { forumService } from './forum';
export { mentorService } from './mentors';
export { bookingsService } from './bookings';
+export type { Booking } from './bookings';
export { opportunityService } from './opportunities';
export { notificationService } from './notifications';
export { messagingService } from './messaging';
diff --git a/frontend/services/messaging.ts b/frontend/services/messaging.ts
index 03739c94a7085040f9e0a9f75435259016ba062b..5c0c966829927e9fb586e2a26cae1e99bbab09bc 100644
--- a/frontend/services/messaging.ts
+++ b/frontend/services/messaging.ts
@@ -38,6 +38,7 @@ export interface Conversation {
name: string;
avatar: string | null;
public_key?: string;
+ encrypted_private_key?: string;
};
}>;
last_message: Message | null;
@@ -62,7 +63,8 @@ const mapConversation = (c: any): Conversation => ({
profile: {
...p.profile,
avatar: getAvatarUrl(p.profile?.avatar, p.profile?.name),
- public_key: p.profile?.public_key
+ public_key: p.profile?.public_key,
+ encrypted_private_key: p.profile?.encrypted_private_key
}
})),
last_message: c.last_message ? mapMessage(c.last_message) : null
diff --git a/frontend/services/opportunities.ts b/frontend/services/opportunities.ts
index a6303427486d4768b434e85c7fb22e7b36c86369..393812e0c1f2dd27d4fe001c0fb72e984de0feaa 100644
--- a/frontend/services/opportunities.ts
+++ b/frontend/services/opportunities.ts
@@ -1,4 +1,4 @@
-import api from './api';
+import api, { publicApi } from './api';
import { Opportunity, OpportunityType } from '../types';
// Service Opportunities
@@ -33,7 +33,7 @@ export const opportunityService = {
type?: string;
search?: string;
}): Promise<{ count: number; results: Opportunity[] }> => {
- const response = await api.get('opportunities/', { params });
+ const response = await publicApi.get('opportunities/', { params });
return {
count: response.data.count,
results: response.data.results.map(mapOpportunity)
@@ -42,7 +42,8 @@ export const opportunityService = {
// Récupérer une opportunité par ID
getOpportunity: async (id: string): Promise => {
- const response = await api.get(`opportunities/${id}/`);
+ const response = await publicApi.get(`opportunities/${id}/`);
return mapOpportunity(response.data);
}
};
+
diff --git a/frontend/services/socials.ts b/frontend/services/socials.ts
index c65e84c74a8087845d8dc0f41a177efc90ac4e05..99982442b2d2b019f94292f40c447a439b2bd43d 100644
--- a/frontend/services/socials.ts
+++ b/frontend/services/socials.ts
@@ -1,5 +1,4 @@
-
-import api from './api';
+import api, { publicApi } from './api';
export interface SocialLink {
id: number;
@@ -12,7 +11,8 @@ export interface SocialLink {
export const socialLinkService = {
getSocialLinks: async (): Promise => {
- const response = await api.get('social-links/');
+ const response = await publicApi.get('social-links/');
return response.data.results || response.data;
}
};
+
diff --git a/frontend/services/stats.ts b/frontend/services/stats.ts
index aabbb4347e6075efb63350cd127f2338a266470d..ca7d56d6d392b0259552933a8a9acd55192efdcb 100644
--- a/frontend/services/stats.ts
+++ b/frontend/services/stats.ts
@@ -1,4 +1,4 @@
-import api from './api';
+import api, { publicApi } from './api';
export interface PlatformStats {
active_students: number;
@@ -21,7 +21,7 @@ export const statsService = {
* Récupérer les statistiques de la plateforme
*/
getPlatformStats: async (): Promise => {
- const response = await api.get('stats/');
+ const response = await publicApi.get('stats/');
return response.data;
},
@@ -29,8 +29,9 @@ export const statsService = {
* Récupérer les statistiques d'impact configurables
*/
getImpactStats: async (): Promise => {
- const response = await api.get('impact-stats/');
+ const response = await publicApi.get('impact-stats/');
// L'API retourne une liste paginée par défaut avec DRF, donc results
return response.data.results || response.data;
}
};
+
diff --git a/frontend/services/testimonials.ts b/frontend/services/testimonials.ts
index 09259ea1ad255716aae619e21d47d00ec7457f99..5a4990efd8170191f650eae767996a310d1e713b 100644
--- a/frontend/services/testimonials.ts
+++ b/frontend/services/testimonials.ts
@@ -1,5 +1,4 @@
-
-import api from './api';
+import api, { publicApi } from './api';
export interface Testimonial {
id: number;
@@ -13,7 +12,8 @@ export interface Testimonial {
export const testimonialService = {
getTestimonials: async (): Promise => {
- const response = await api.get('testimonials/');
+ const response = await publicApi.get('testimonials/');
return response.data.results || response.data;
}
};
+
diff --git a/frontend/services/tools.ts b/frontend/services/tools.ts
index 615c643d2d6ec45dc934f7e96f97d3f283bfca47..6219a6fcdf0ea108f676c2688a67b0348c7f75d4 100644
--- a/frontend/services/tools.ts
+++ b/frontend/services/tools.ts
@@ -1,5 +1,4 @@
-
-import api from './api';
+import api, { publicApi } from './api';
export interface LearningTool {
id: number;
@@ -19,7 +18,8 @@ export interface LearningTool {
export const toolsService = {
getTools: async (): Promise => {
- const response = await api.get('learning-tools/');
+ const response = await publicApi.get('learning-tools/');
return response.data.results || response.data;
}
};
+
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000000000000000000000000000000000000..f651c6c9f1531a747a37b37247275af470ca294e
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,41 @@
+server {
+ listen 7860;
+ server_name localhost;
+
+ # Frontend - Fichiers statiques
+ location / {
+ root /app/frontend_dist;
+ index index.html;
+ try_files $uri $uri/ /index.html;
+ }
+
+ # Backend API
+ location /api {
+ proxy_pass http://127.0.0.1:8000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ }
+
+ # Django Admin et Static (Backend)
+ location /admin {
+ proxy_pass http://127.0.0.1:8000;
+ proxy_set_header Host $host;
+ }
+
+ location /static/ {
+ alias /app/backend/staticfiles/;
+ }
+
+ location /media/ {
+ alias /app/backend/media/;
+ }
+
+ # WebSockets
+ location /ws {
+ proxy_pass http://127.0.0.1:8000;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_set_header Host $host;
+ }
+}
diff --git a/start.sh b/start.sh
new file mode 100644
index 0000000000000000000000000000000000000000..9e408b487db25ad5e024618f676ad9746111ea91
--- /dev/null
+++ b/start.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+# Aller dans le dossier backend
+cd /app/backend
+
+# Appliquer les migrations
+echo "Applying migrations..."
+python manage.py migrate --noinput
+
+# Collecter les fichiers statiques de Django (Admin, etc.)
+echo "Collecting static files..."
+python manage.py collectstatic --noinput
+
+# Initialiser les données si nécessaire (optionnel)
+# python init_tools.py
+
+# Lancer supervisor
+echo "Starting all services via Supervisor..."
+exec supervisord -c /app/supervisord.conf
diff --git a/supervisord.conf b/supervisord.conf
new file mode 100644
index 0000000000000000000000000000000000000000..1a1cebb3c64a9986ddcd71d9252beaaf9c9c09f5
--- /dev/null
+++ b/supervisord.conf
@@ -0,0 +1,35 @@
+[supervisord]
+nodaemon=true
+user=root
+logfile=/tmp/supervisord.log
+pidfile=/tmp/supervisord.pid
+
+[program:redis]
+command=redis-server --port 6379
+autostart=true
+autorestart=true
+stdout_logfile=/tmp/redis.stdout.log
+stderr_logfile=/tmp/redis.stderr.log
+
+[program:backend]
+command=daphne -b 127.0.0.1 -p 8000 educonnect.asgi:application
+directory=/app/backend
+autostart=true
+autorestart=true
+stdout_logfile=/tmp/backend.stdout.log
+stderr_logfile=/tmp/backend.stderr.log
+
+[program:celery]
+command=celery -A educonnect worker -l info
+directory=/app/backend
+autostart=true
+autorestart=true
+stdout_logfile=/tmp/celery.stdout.log
+stderr_logfile=/tmp/celery.stderr.log
+
+[program:nginx]
+command=nginx -g "daemon off;"
+autostart=true
+autorestart=true
+stdout_logfile=/tmp/nginx.stdout.log
+stderr_logfile=/tmp/nginx.stderr.log