diff --git a/client/package-lock.json b/client/package-lock.json
index e066048..4e40e71 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -27,6 +27,7 @@
"katex": "^0.16.11",
"marked": "^14.1.2",
"nanoid": "^5.1.2",
+ "qrcode.react": "^4.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-modal": "^3.16.3",
@@ -11168,6 +11169,15 @@
],
"license": "MIT"
},
+ "node_modules/qrcode.react": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
+ "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
diff --git a/client/package.json b/client/package.json
index e980a43..82f2d57 100644
--- a/client/package.json
+++ b/client/package.json
@@ -31,6 +31,7 @@
"katex": "^0.16.11",
"marked": "^14.1.2",
"nanoid": "^5.1.2",
+ "qrcode.react": "^4.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-modal": "^3.16.3",
diff --git a/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx b/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx
index 2b5a016..aca1d64 100644
--- a/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx
+++ b/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx
@@ -19,6 +19,11 @@ jest.mock('react-router-dom', () => ({
}));
jest.mock('src/pages/Teacher/ManageRoom/RoomContext');
+jest.mock('qrcode.react', () => ({
+ __esModule: true,
+ QRCodeCanvas: ({ value }: { value: string }) =>
{value}
,
+}));
+
const mockSocket = {
on: jest.fn(),
off: jest.fn(),
@@ -325,5 +330,53 @@ describe('ManageRoom', () => {
});
});
+ test("Affiche la modale QR Code lorsqu’on clique sur le bouton", async () => {
+ render();
+
+ const button = screen.getByRole('button', { name: /lien de participation/i });
+ fireEvent.click(button);
+
+ await waitFor(() => {
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
+ });
+
+ expect(screen.getByRole('heading', { name: /Rejoindre la salle/i })).toBeInTheDocument();
+ expect(screen.getByText(/Scannez ce QR code ou partagez le lien ci-dessous/i)).toBeInTheDocument();
+ expect(screen.getByTestId('qr-code')).toBeInTheDocument();
+ });
+
+ test("Ferme la modale QR Code lorsqu’on clique sur le bouton Fermer", async () => {
+ render();
+
+ fireEvent.click(screen.getByRole('button', { name: /lien de participation/i }));
+
+ await waitFor(() => {
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
+ });
+
+ fireEvent.click(screen.getByRole('button', { name: /fermer/i }));
+
+ await waitFor(() => {
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+ });
+ });
+
+ test('Affiche le bon lien de participation', () => {
+ render();
+
+ fireEvent.click(screen.getByRole('button', { name: /lien de participation/i }));
+
+ const roomUrl = `${window.location.origin}/student/join-room?roomName=Test Room`;
+ expect(screen.getByTestId('qr-code')).toHaveTextContent(roomUrl);
+ });
+
+ test('Vérifie que le QR code contient la bonne URL', () => {
+ render();
+
+ fireEvent.click(screen.getByRole('button', { name: /lien de participation/i }));
+
+ const roomUrl = `${window.location.origin}/student/join-room?roomName=Test Room`;
+ expect(screen.getByTestId('qr-code')).toHaveTextContent(roomUrl);
+ });
});
diff --git a/client/src/pages/Student/JoinRoom/JoinRoom.tsx b/client/src/pages/Student/JoinRoom/JoinRoom.tsx
index a5ee1ff..1b02104 100644
--- a/client/src/pages/Student/JoinRoom/JoinRoom.tsx
+++ b/client/src/pages/Student/JoinRoom/JoinRoom.tsx
@@ -5,7 +5,7 @@ import { ENV_VARIABLES } from 'src/constants';
import StudentModeQuiz from 'src/components/StudentModeQuiz/StudentModeQuiz';
import TeacherModeQuiz from 'src/components/TeacherModeQuiz/TeacherModeQuiz';
-import webSocketService, { AnswerSubmissionToBackendType } from '../../../services/WebsocketService';
+import webSocketService, {AnswerSubmissionToBackendType} from '../../../services/WebsocketService';
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
import './joinRoom.css';
@@ -13,9 +13,10 @@ import { QuestionType } from '../../../Types/QuestionType';
import { TextField } from '@mui/material';
import LoadingButton from '@mui/lab/LoadingButton';
-import LoginContainer from 'src/components/LoginContainer/LoginContainer'
+import LoginContainer from 'src/components/LoginContainer/LoginContainer';
-import ApiService from '../../../services/ApiService'
+import ApiService from '../../../services/ApiService';
+import { useSearchParams } from 'react-router-dom';
export type AnswerType = Array;
@@ -30,6 +31,17 @@ const JoinRoom: React.FC = () => {
const [answers, setAnswers] = useState([]);
const [connectionError, setConnectionError] = useState('');
const [isConnecting, setIsConnecting] = useState(false);
+ const [isQRCodeJoin, setIsQRCodeJoin] = useState(false);
+ const [searchParams] = useSearchParams();
+
+ useEffect(() => {
+ const roomFromUrl = searchParams.get('roomName');
+ if (roomFromUrl) {
+ setRoomName(roomFromUrl);
+ setIsQRCodeJoin(true);
+ console.log('Mode QR Code détecté, salle:', roomFromUrl);
+ }
+ }, [searchParams]);
useEffect(() => {
handleCreateSocket();
@@ -42,7 +54,7 @@ const JoinRoom: React.FC = () => {
console.log(`JoinRoom: useEffect: questions: ${JSON.stringify(questions)}`);
setAnswers(questions ? Array(questions.length).fill({} as AnswerSubmissionToBackendType) : []);
}, [questions]);
-
+
const handleCreateSocket = () => {
console.log(`JoinRoom: handleCreateSocket: ${ENV_VARIABLES.VITE_BACKEND_URL}`);
@@ -101,7 +113,7 @@ const JoinRoom: React.FC = () => {
};
const disconnect = () => {
-// localStorage.clear();
+ // localStorage.clear();
webSocketService.disconnect();
setSocket(null);
setQuestion(undefined);
@@ -198,21 +210,25 @@ const JoinRoom: React.FC = () => {
default:
return (
-
- setRoomName(e.target.value.toUpperCase())}
- placeholder="Nom de la salle"
- sx={{ marginBottom: '1rem' }}
- fullWidth={true}
- onKeyDown={handleReturnKey}
- />
+ title={isQRCodeJoin ? `Rejoindre la salle ${roomName}` : 'Rejoindre une salle'}
+ error={connectionError}
+ >
+ {/* Afficher champ salle SEULEMENT si pas de QR code */}
+ {!isQRCodeJoin && (
+ setRoomName(e.target.value.toUpperCase())}
+ placeholder="Nom de la salle"
+ sx={{ marginBottom: '1rem' }}
+ fullWidth={true}
+ onKeyDown={handleReturnKey}
+ />
+ )}
+ {/* Champ username toujours visible */}
{
onClick={handleSocket}
variant="contained"
sx={{ marginBottom: `${connectionError && '2rem'}` }}
- disabled={!username || !roomName}
- >Rejoindre
-
+ disabled={!username || (isQRCodeJoin && !roomName)}
+ >
+ {isQRCodeJoin ? 'Rejoindre avec QR Code' : 'Rejoindre'}
+
);
}
diff --git a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx
index c938f7e..132b51d 100644
--- a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx
+++ b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx
@@ -9,6 +9,7 @@ import webSocketService, {
import { QuizType } from '../../../Types/QuizType';
import GroupIcon from '@mui/icons-material/Group';
import './manageRoom.css';
+import QRCodeIcon from '@mui/icons-material/QrCode';
import { ENV_VARIABLES } from 'src/constants';
import { StudentType, Answer } from '../../../Types/StudentType';
import LoadingCircle from 'src/components/LoadingCircle/LoadingCircle';
@@ -18,8 +19,18 @@ import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
import QuestionDisplay from 'src/components/QuestionsDisplay/QuestionDisplay';
import ApiService from '../../../services/ApiService';
import { QuestionType } from 'src/Types/QuestionType';
-import { Button } from '@mui/material';
+import {
+ Button,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogContentText,
+ DialogActions
+} from '@mui/material';
import { checkIfIsCorrect } from './useRooms';
+import { QRCodeCanvas } from 'qrcode.react';
+import ContentCopyIcon from "@mui/icons-material/ContentCopy";
+
const ManageRoom: React.FC = () => {
const navigate = useNavigate();
@@ -34,6 +45,16 @@ const ManageRoom: React.FC = () => {
const [quizStarted, setQuizStarted] = useState(false);
const [formattedRoomName, setFormattedRoomName] = useState('');
const [newlyConnectedUser, setNewlyConnectedUser] = useState(null);
+ const roomUrl = `${window.location.origin}/student/join-room?roomName=${roomName}`;
+ const [showQrModal, setShowQrModal] = useState(false);
+ const [copied, setCopied] = useState(false);
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(roomUrl).then(() => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ });
+ };
// Handle the newly connected user in useEffect, because it needs state info
// not available in the socket.on() callback
@@ -77,6 +98,15 @@ const ManageRoom: React.FC = () => {
verifyLogin();
}, []);
+ useEffect(() => {
+ if (!roomName) {
+ console.error('Room name is missing!');
+ return;
+ }
+
+ console.log(`Joining room: ${roomName}`);
+ }, [roomName]);
+
useEffect(() => {
if (!roomName || !quizId) {
window.alert(
@@ -386,7 +416,62 @@ const ManageRoom: React.FC = () => {
return (
-
Salle : {formattedRoomName}
+ {/* En-tête avec titre et bouton QR code*/}
+
+
Salle : {formattedRoomName}
+
+
+
+
+ {/* Modale QR Code */}
+
= 0.8"
}
},
+ "node_modules/react": {
+ "version": "19.0.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
+ "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
diff --git a/server/package.json b/server/package.json
index 3f0cfd4..0eaf6d2 100644
--- a/server/package.json
+++ b/server/package.json
@@ -28,6 +28,7 @@
"passport-oauth2": "^1.8.0",
"passport-openidconnect": "^0.1.2",
"patch-package": "^8.0.0",
+ "qrcode.react": "^4.2.0",
"socket.io": "^4.7.2",
"socket.io-client": "^4.7.2"
},