mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
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
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
This commit is contained in:
commit
5a5897d25c
7 changed files with 211 additions and 24 deletions
10
client/package-lock.json
generated
10
client/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 }) => <div data-testid="qr-code">{value}</div>,
|
||||
}));
|
||||
|
||||
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(<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 lorsqu’on 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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string | number | boolean>;
|
||||
|
||||
|
|
@ -30,6 +31,17 @@ const JoinRoom: React.FC = () => {
|
|||
const [answers, setAnswers] = useState<AnswerSubmissionToBackendType[]>([]);
|
||||
const [connectionError, setConnectionError] = useState<string>('');
|
||||
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(() => {
|
||||
handleCreateSocket();
|
||||
|
|
@ -198,9 +210,11 @@ const JoinRoom: React.FC = () => {
|
|||
default:
|
||||
return (
|
||||
<LoginContainer
|
||||
title='Rejoindre une salle'
|
||||
error={connectionError}>
|
||||
|
||||
title={isQRCodeJoin ? `Rejoindre la salle ${roomName}` : 'Rejoindre une salle'}
|
||||
error={connectionError}
|
||||
>
|
||||
{/* Afficher champ salle SEULEMENT si pas de QR code */}
|
||||
{!isQRCodeJoin && (
|
||||
<TextField
|
||||
type="text"
|
||||
label="Nom de la salle"
|
||||
|
|
@ -212,7 +226,9 @@ const JoinRoom: React.FC = () => {
|
|||
fullWidth={true}
|
||||
onKeyDown={handleReturnKey}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Champ username toujours visible */}
|
||||
<TextField
|
||||
label="Nom d'utilisateur"
|
||||
variant="outlined"
|
||||
|
|
@ -229,9 +245,10 @@ const JoinRoom: React.FC = () => {
|
|||
onClick={handleSocket}
|
||||
variant="contained"
|
||||
sx={{ marginBottom: `${connectionError && '2rem'}` }}
|
||||
disabled={!username || !roomName}
|
||||
>Rejoindre</LoadingButton>
|
||||
|
||||
disabled={!username || (isQRCodeJoin && !roomName)}
|
||||
>
|
||||
{isQRCodeJoin ? 'Rejoindre avec QR Code' : 'Rejoindre'}
|
||||
</LoadingButton>
|
||||
</LoginContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<boolean>(false);
|
||||
const [formattedRoomName, setFormattedRoomName] = useState('');
|
||||
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
|
||||
// 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 (
|
||||
<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">
|
||||
<DisconnectButton
|
||||
onReturn={handleReturn}
|
||||
|
|
|
|||
20
server/package-lock.json
generated
20
server/package-lock.json
generated
|
|
@ -24,6 +24,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"
|
||||
},
|
||||
|
|
@ -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": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
|
|
@ -5894,6 +5904,16 @@
|
|||
"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": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue