Compare commits

..

13 commits

Author SHA1 Message Date
Christopher (Cris) Fuhrman
5a5897d25c
Merge pull request #290 from ets-cfuhrman-pfe/feature/qr-code-url-generation
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Failing after 58s
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Failing after 1m0s
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Failing after 19s
Tests / lint-and-tests (client) (push) Failing after 1m32s
Tests / lint-and-tests (server) (push) Failing after 57s
Rejoindre une salle via un code QR
2025-04-07 20:14:10 -04:00
C. Fuhrman
20bfb8774f Merge remote-tracking branch 'origin/main' into feature/qr-code-url-generation 2025-04-07 20:08:33 -04:00
NouhailaAater
1de68418e0 correction 2025-04-06 23:19:34 -04:00
NouhailaAater
7c7fe031f8 Résolution des conflits avec main 2025-03-24 00:08:34 -04:00
NouhailaAater
a3e0ba70f9 Tests 2025-03-19 14:41:59 -04:00
NouhailaAater
a4d36389c7 Bouton Copier link 2025-03-18 16:36:16 -04:00
NouhailaAater
dc7fb82d97 Affichage nom de salle 2025-03-17 14:39:43 -04:00
NouhailaAater
fb41980f29 correction qrCode to qrcodecanvas 2025-03-13 16:09:04 -04:00
NouhailaAater
0243270fdb correction tests 2025-03-13 15:56:02 -04:00
NouhailaAater
241854159b update qrcode.react 2025-03-12 15:23:36 -04:00
NouhailaAater
312ccd0b52 update package 2025-03-12 15:00:00 -04:00
NouhailaAater
ef192eb784 Merge branch 'main' of https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir into feature/qr-code-url-generation 2025-03-12 14:42:27 -04:00
NouhailaAater
01deaee487 Add QR code URL generation feature 2025-03-12 14:25:17 -04:00
7 changed files with 211 additions and 24 deletions

View file

@ -27,6 +27,7 @@
"katex": "^0.16.11", "katex": "^0.16.11",
"marked": "^14.1.2", "marked": "^14.1.2",
"nanoid": "^5.1.2", "nanoid": "^5.1.2",
"qrcode.react": "^4.2.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-modal": "^3.16.3", "react-modal": "^3.16.3",
@ -11168,6 +11169,15 @@
], ],
"license": "MIT" "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": { "node_modules/querystringify": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",

View file

@ -31,6 +31,7 @@
"katex": "^0.16.11", "katex": "^0.16.11",
"marked": "^14.1.2", "marked": "^14.1.2",
"nanoid": "^5.1.2", "nanoid": "^5.1.2",
"qrcode.react": "^4.2.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-modal": "^3.16.3", "react-modal": "^3.16.3",

View file

@ -19,6 +19,11 @@ jest.mock('react-router-dom', () => ({
})); }));
jest.mock('src/pages/Teacher/ManageRoom/RoomContext'); jest.mock('src/pages/Teacher/ManageRoom/RoomContext');
jest.mock('qrcode.react', () => ({
__esModule: true,
QRCodeCanvas: ({ value }: { value: string }) => <div data-testid="qr-code">{value}</div>,
}));
const mockSocket = { const mockSocket = {
on: jest.fn(), on: jest.fn(),
off: jest.fn(), off: jest.fn(),
@ -325,5 +330,53 @@ describe('ManageRoom', () => {
}); });
}); });
test("Affiche la modale QR Code lorsquon clique sur le bouton", async () => {
render(<MemoryRouter><ManageRoom /></MemoryRouter>);
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 lorsquon clique sur le bouton Fermer", async () => {
render(<MemoryRouter><ManageRoom /></MemoryRouter>);
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(<MemoryRouter><ManageRoom /></MemoryRouter>);
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(<MemoryRouter><ManageRoom /></MemoryRouter>);
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);
});
}); });

View file

@ -5,7 +5,7 @@ import { ENV_VARIABLES } from 'src/constants';
import StudentModeQuiz from 'src/components/StudentModeQuiz/StudentModeQuiz'; import StudentModeQuiz from 'src/components/StudentModeQuiz/StudentModeQuiz';
import TeacherModeQuiz from 'src/components/TeacherModeQuiz/TeacherModeQuiz'; 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 DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
import './joinRoom.css'; import './joinRoom.css';
@ -13,9 +13,10 @@ import { QuestionType } from '../../../Types/QuestionType';
import { TextField } from '@mui/material'; import { TextField } from '@mui/material';
import LoadingButton from '@mui/lab/LoadingButton'; 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<string | number | boolean>; export type AnswerType = Array<string | number | boolean>;
@ -30,6 +31,17 @@ const JoinRoom: React.FC = () => {
const [answers, setAnswers] = useState<AnswerSubmissionToBackendType[]>([]); const [answers, setAnswers] = useState<AnswerSubmissionToBackendType[]>([]);
const [connectionError, setConnectionError] = useState<string>(''); const [connectionError, setConnectionError] = useState<string>('');
const [isConnecting, setIsConnecting] = useState<boolean>(false); const [isConnecting, setIsConnecting] = useState<boolean>(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(() => { useEffect(() => {
handleCreateSocket(); handleCreateSocket();
@ -101,7 +113,7 @@ const JoinRoom: React.FC = () => {
}; };
const disconnect = () => { const disconnect = () => {
// localStorage.clear(); // localStorage.clear();
webSocketService.disconnect(); webSocketService.disconnect();
setSocket(null); setSocket(null);
setQuestion(undefined); setQuestion(undefined);
@ -198,9 +210,11 @@ const JoinRoom: React.FC = () => {
default: default:
return ( return (
<LoginContainer <LoginContainer
title='Rejoindre une salle' title={isQRCodeJoin ? `Rejoindre la salle ${roomName}` : 'Rejoindre une salle'}
error={connectionError}> error={connectionError}
>
{/* Afficher champ salle SEULEMENT si pas de QR code */}
{!isQRCodeJoin && (
<TextField <TextField
type="text" type="text"
label="Nom de la salle" label="Nom de la salle"
@ -212,7 +226,9 @@ const JoinRoom: React.FC = () => {
fullWidth={true} fullWidth={true}
onKeyDown={handleReturnKey} onKeyDown={handleReturnKey}
/> />
)}
{/* Champ username toujours visible */}
<TextField <TextField
label="Nom d'utilisateur" label="Nom d'utilisateur"
variant="outlined" variant="outlined"
@ -229,9 +245,10 @@ const JoinRoom: React.FC = () => {
onClick={handleSocket} onClick={handleSocket}
variant="contained" variant="contained"
sx={{ marginBottom: `${connectionError && '2rem'}` }} sx={{ marginBottom: `${connectionError && '2rem'}` }}
disabled={!username || !roomName} disabled={!username || (isQRCodeJoin && !roomName)}
>Rejoindre</LoadingButton> >
{isQRCodeJoin ? 'Rejoindre avec QR Code' : 'Rejoindre'}
</LoadingButton>
</LoginContainer> </LoginContainer>
); );
} }

View file

@ -9,6 +9,7 @@ import webSocketService, {
import { QuizType } from '../../../Types/QuizType'; import { QuizType } from '../../../Types/QuizType';
import GroupIcon from '@mui/icons-material/Group'; import GroupIcon from '@mui/icons-material/Group';
import './manageRoom.css'; import './manageRoom.css';
import QRCodeIcon from '@mui/icons-material/QrCode';
import { ENV_VARIABLES } from 'src/constants'; import { ENV_VARIABLES } from 'src/constants';
import { StudentType, Answer } from '../../../Types/StudentType'; import { StudentType, Answer } from '../../../Types/StudentType';
import LoadingCircle from 'src/components/LoadingCircle/LoadingCircle'; 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 QuestionDisplay from 'src/components/QuestionsDisplay/QuestionDisplay';
import ApiService from '../../../services/ApiService'; import ApiService from '../../../services/ApiService';
import { QuestionType } from 'src/Types/QuestionType'; 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 { checkIfIsCorrect } from './useRooms';
import { QRCodeCanvas } from 'qrcode.react';
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
const ManageRoom: React.FC = () => { const ManageRoom: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -34,6 +45,16 @@ const ManageRoom: React.FC = () => {
const [quizStarted, setQuizStarted] = useState<boolean>(false); const [quizStarted, setQuizStarted] = useState<boolean>(false);
const [formattedRoomName, setFormattedRoomName] = useState(''); const [formattedRoomName, setFormattedRoomName] = useState('');
const [newlyConnectedUser, setNewlyConnectedUser] = useState<StudentType | null>(null); const [newlyConnectedUser, setNewlyConnectedUser] = useState<StudentType | null>(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 // Handle the newly connected user in useEffect, because it needs state info
// not available in the socket.on() callback // not available in the socket.on() callback
@ -77,6 +98,15 @@ const ManageRoom: React.FC = () => {
verifyLogin(); verifyLogin();
}, []); }, []);
useEffect(() => {
if (!roomName) {
console.error('Room name is missing!');
return;
}
console.log(`Joining room: ${roomName}`);
}, [roomName]);
useEffect(() => { useEffect(() => {
if (!roomName || !quizId) { if (!roomName || !quizId) {
window.alert( window.alert(
@ -386,7 +416,62 @@ const ManageRoom: React.FC = () => {
return ( return (
<div className="room"> <div className="room">
<h1>Salle : {formattedRoomName}</h1> {/* En-tête avec titre et bouton QR code*/}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '20px'
}}
>
<h1 style={{ margin: 0 }}>Salle : {formattedRoomName}</h1>
<Button
variant="contained"
color="primary"
onClick={() => setShowQrModal(true)}
startIcon={<QRCodeIcon />}
>
Lien de participation
</Button>
</div>
{/* Modale QR Code */}
<Dialog
open={showQrModal}
onClose={() => setShowQrModal(false)}
aria-labelledby="qr-modal-title"
>
<DialogTitle id="qr-modal-title">Rejoindre la salle: {formattedRoomName}</DialogTitle>
<DialogContent>
<DialogContentText>
Scannez ce QR code ou partagez le lien ci-dessous pour rejoindre la salle :
</DialogContentText>
<div style={{ textAlign: 'center', margin: '20px 0' }}>
<QRCodeCanvas value={roomUrl} size={256} />
</div>
<div style={{ wordBreak: 'break-all', textAlign: 'center' }}>
<h3>URL de participation :</h3>
<p>{roomUrl}</p>
<Button
variant="contained"
startIcon={<ContentCopyIcon />}
onClick={handleCopy}
style={{ marginTop: '10px' }}
>
{copied ? "Copié !" : "Copier le lien"}
</Button>
</div>
</DialogContent>
<DialogActions>
<Button onClick={() => setShowQrModal(false)} color="primary">
Fermer
</Button>
</DialogActions>
</Dialog>
<div className="roomHeader"> <div className="roomHeader">
<DisconnectButton <DisconnectButton
onReturn={handleReturn} onReturn={handleReturn}

View file

@ -24,6 +24,7 @@
"passport-oauth2": "^1.8.0", "passport-oauth2": "^1.8.0",
"passport-openidconnect": "^0.1.2", "passport-openidconnect": "^0.1.2",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"qrcode.react": "^4.2.0",
"socket.io": "^4.7.2", "socket.io": "^4.7.2",
"socket.io-client": "^4.7.2" "socket.io-client": "^4.7.2"
}, },
@ -5850,6 +5851,15 @@
} }
] ]
}, },
"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/qs": { "node_modules/qs": {
"version": "6.13.0", "version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
@ -5894,6 +5904,16 @@
"node": ">= 0.8" "node": ">= 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": { "node_modules/react-is": {
"version": "18.2.0", "version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",

View file

@ -28,6 +28,7 @@
"passport-oauth2": "^1.8.0", "passport-oauth2": "^1.8.0",
"passport-openidconnect": "^0.1.2", "passport-openidconnect": "^0.1.2",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"qrcode.react": "^4.2.0",
"socket.io": "^4.7.2", "socket.io": "^4.7.2",
"socket.io-client": "^4.7.2" "socket.io-client": "^4.7.2"
}, },