mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
Merge branch 'main' into dev-percent-display
This commit is contained in:
commit
e03da0c808
43 changed files with 3221 additions and 2254 deletions
5
.github/workflows/tests.yml
vendored
5
.github/workflows/tests.yml
vendored
|
|
@ -35,6 +35,11 @@ jobs:
|
||||||
working-directory: ${{ matrix.directory }}
|
working-directory: ${{ matrix.directory }}
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
run: |
|
run: |
|
||||||
|
echo "Installing dependencies..."
|
||||||
|
npm install
|
||||||
|
echo "Running ESLint..."
|
||||||
|
npx eslint .
|
||||||
|
echo "Running tests..."
|
||||||
echo "::group::Installing dependencies for ${{ matrix.directory }}"
|
echo "::group::Installing dependencies for ${{ matrix.directory }}"
|
||||||
npm ci
|
npm ci
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import react from "eslint-plugin-react";
|
||||||
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
||||||
import typescriptParser from "@typescript-eslint/parser";
|
import typescriptParser from "@typescript-eslint/parser";
|
||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
import pluginJs from "@eslint/js";
|
|
||||||
import jest from "eslint-plugin-jest";
|
import jest from "eslint-plugin-jest";
|
||||||
import reactRefresh from "eslint-plugin-react-refresh";
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
import unusedImports from "eslint-plugin-unused-imports";
|
import unusedImports from "eslint-plugin-unused-imports";
|
||||||
|
|
|
||||||
3085
client/package-lock.json
generated
3085
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -4,7 +4,7 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host",
|
"dev": "cross-env MODE=development VITE_BACKEND_URL=http://localhost:4400 vite --host",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
|
@ -12,65 +12,65 @@
|
||||||
"test:watch": "jest --watch"
|
"test:watch": "jest --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.3",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@mui/icons-material": "^6.4.1",
|
"@mui/icons-material": "^6.4.6",
|
||||||
"@mui/lab": "^5.0.0-alpha.153",
|
"@mui/lab": "^5.0.0-alpha.153",
|
||||||
"@mui/material": "^6.1.0",
|
"@mui/material": "^6.4.6",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.8.1",
|
||||||
"dompurify": "^3.2.3",
|
"dompurify": "^3.2.3",
|
||||||
"esbuild": "^0.23.1",
|
"esbuild": "^0.25.0",
|
||||||
"gift-pegjs": "^2.0.0-beta.1",
|
"gift-pegjs": "^2.0.0-beta.1",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"katex": "^0.16.11",
|
"katex": "^0.16.11",
|
||||||
"marked": "^14.1.2",
|
"marked": "^14.1.2",
|
||||||
"nanoid": "^5.0.2",
|
"nanoid": "^5.1.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-modal": "^3.16.1",
|
"react-modal": "^3.16.3",
|
||||||
"react-router-dom": "^6.26.2",
|
"react-router-dom": "^6.26.2",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"socket.io-client": "^4.7.2",
|
"socket.io-client": "^4.7.2",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"vite-plugin-checker": "^0.8.0"
|
"vite-plugin-checker": "^0.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-env": "^7.23.3",
|
"@babel/preset-env": "^7.26.9",
|
||||||
"@babel/preset-react": "^7.23.3",
|
"@babel/preset-react": "^7.26.3",
|
||||||
"@babel/preset-typescript": "^7.23.3",
|
"@babel/preset-typescript": "^7.23.3",
|
||||||
"@eslint/js": "^9.18.0",
|
"@eslint/js": "^9.21.0",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.5.0",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.0.1",
|
"@testing-library/react": "^16.2.0",
|
||||||
"@types/jest": "^29.5.13",
|
"@types/jest": "^29.5.13",
|
||||||
"@types/node": "^22.5.5",
|
"@types/node": "^22.13.5",
|
||||||
"@types/react": "^18.2.15",
|
"@types/react": "^18.2.15",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@types/react-latex": "^2.0.3",
|
"@types/react-latex": "^2.0.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.5.0",
|
"@typescript-eslint/eslint-plugin": "^8.25.0",
|
||||||
"@typescript-eslint/parser": "^8.5.0",
|
"@typescript-eslint/parser": "^8.25.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.7.2",
|
"@vitejs/plugin-react-swc": "^3.8.0",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.21.0",
|
||||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||||
"eslint-plugin-jest": "^28.11.0",
|
"eslint-plugin-jest": "^28.11.0",
|
||||||
"eslint-plugin-react": "^7.37.3",
|
"eslint-plugin-react": "^7.37.3",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0-rc-206df66e-20240912",
|
"eslint-plugin-react-hooks": "^5.1.0-rc-206df66e-20240912",
|
||||||
"eslint-plugin-react-refresh": "^0.4.12",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"eslint-plugin-unused-imports": "^4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"globals": "^15.14.0",
|
"globals": "^15.14.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"ts-jest": "^29.1.1",
|
"ts-jest": "^29.2.6",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.7.3",
|
||||||
"typescript-eslint": "^8.19.1",
|
"typescript-eslint": "^8.25.0",
|
||||||
"vite": "^5.4.5",
|
"vite": "^6.2.0",
|
||||||
"vite-plugin-environment": "^1.1.3"
|
"vite-plugin-environment": "^1.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ const App: React.FC = () => {
|
||||||
element={isTeacherAuthenticated ? <QuizForm /> : <Navigate to="/login" />}
|
element={isTeacherAuthenticated ? <QuizForm /> : <Navigate to="/login" />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/teacher/manage-room/:id"
|
path="/teacher/manage-room/:quizId/:roomName"
|
||||||
element={isTeacherAuthenticated ? <ManageRoom /> : <Navigate to="/login" />}
|
element={isTeacherAuthenticated ? <ManageRoom /> : <Navigate to="/login" />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
6
client/src/Types/RoomType.tsx
Normal file
6
client/src/Types/RoomType.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export interface RoomType {
|
||||||
|
_id: string;
|
||||||
|
userId: string;
|
||||||
|
title: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
17
client/src/__tests__/Types/RoomType.test.tsx
Normal file
17
client/src/__tests__/Types/RoomType.test.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { RoomType } from "../../Types/RoomType";
|
||||||
|
|
||||||
|
const room: RoomType = {
|
||||||
|
_id: '123',
|
||||||
|
userId: '456',
|
||||||
|
title: 'Test Room',
|
||||||
|
created_at: '2025-02-21T00:00:00Z'
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('RoomType', () => {
|
||||||
|
test('creates a room with _id, userId, title, and created_at', () => {
|
||||||
|
expect(room._id).toBe('123');
|
||||||
|
expect(room.userId).toBe('456');
|
||||||
|
expect(room.title).toBe('Test Room');
|
||||||
|
expect(room.created_at).toBe('2025-02-21T00:00:00Z');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -88,10 +88,10 @@ describe('LiveResultsTable', () => {
|
||||||
//50% because only one of the two questions have been answered (getALLByText, because there are a value 50% for the %reussite de la question
|
//50% because only one of the two questions have been answered (getALLByText, because there are a value 50% for the %reussite de la question
|
||||||
// and a second one for the student grade)
|
// and a second one for the student grade)
|
||||||
const gradeElements = screen.getAllByText('50 %');
|
const gradeElements = screen.getAllByText('50 %');
|
||||||
expect(gradeElements.length).toBe(2);
|
expect(gradeElements).toHaveLength(2);
|
||||||
|
|
||||||
const gradeElements2 = screen.getAllByText('0 %');
|
const gradeElements2 = screen.getAllByText('0 %');
|
||||||
expect(gradeElements2.length).toBe(2); });
|
expect(gradeElements2).toHaveLength(2); });
|
||||||
|
|
||||||
test('calculates and displays class average', () => {
|
test('calculates and displays class average', () => {
|
||||||
render(
|
render(
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,6 @@ describe('LiveResultsTableBody', () => {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getAllByText('******').length).toBe(2);
|
expect(screen.getAllByText('******')).toHaveLength(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -10,14 +10,15 @@ describe('StudentWaitPage Component', () => {
|
||||||
{ id: '1', name: 'User1', answers: new Array<Answer>() },
|
{ id: '1', name: 'User1', answers: new Array<Answer>() },
|
||||||
{ id: '2', name: 'User2', answers: new Array<Answer>() },
|
{ id: '2', name: 'User2', answers: new Array<Answer>() },
|
||||||
{ id: '3', name: 'User3', answers: new Array<Answer>() },
|
{ id: '3', name: 'User3', answers: new Array<Answer>() },
|
||||||
];
|
];
|
||||||
|
|
||||||
const mockProps = {
|
const mockProps = {
|
||||||
students: mockUsers,
|
students: mockUsers,
|
||||||
launchQuiz: jest.fn(),
|
launchQuiz: jest.fn(),
|
||||||
roomName: 'Test Room',
|
roomName: 'Test Room',
|
||||||
setQuizMode: jest.fn(),
|
setQuizMode: jest.fn(),
|
||||||
};
|
setIsRoomSelectionVisible: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
test('renders StudentWaitPage with correct content', () => {
|
test('renders StudentWaitPage with correct content', () => {
|
||||||
render(<StudentWaitPage {...mockProps} />);
|
render(<StudentWaitPage {...mockProps} />);
|
||||||
|
|
@ -28,16 +29,15 @@ describe('StudentWaitPage Component', () => {
|
||||||
expect(launchButton).toBeInTheDocument();
|
expect(launchButton).toBeInTheDocument();
|
||||||
|
|
||||||
mockUsers.forEach((user) => {
|
mockUsers.forEach((user) => {
|
||||||
expect(screen.getByText(user.name)).toBeInTheDocument();
|
expect(screen.getByText(user.name)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('clicking on "Lancer" button opens LaunchQuizDialog', () => {
|
test('clicking on "Lancer" button opens LaunchQuizDialog', () => {
|
||||||
render(<StudentWaitPage {...mockProps} />);
|
render(<StudentWaitPage {...mockProps} />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /Lancer/i }));
|
fireEvent.click(screen.getByRole('button', { name: /Lancer/i }));
|
||||||
|
|
||||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
})
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { QuizType } from 'src/Types/QuizType';
|
||||||
import webSocketService, { AnswerReceptionFromBackendType } from 'src/services/WebsocketService';
|
import webSocketService, { AnswerReceptionFromBackendType } from 'src/services/WebsocketService';
|
||||||
import ApiService from 'src/services/ApiService';
|
import ApiService from 'src/services/ApiService';
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
|
import { RoomProvider } from 'src/pages/Teacher/ManageRoom/RoomContext';
|
||||||
|
|
||||||
jest.mock('src/services/WebsocketService');
|
jest.mock('src/services/WebsocketService');
|
||||||
jest.mock('src/services/ApiService');
|
jest.mock('src/services/ApiService');
|
||||||
|
|
@ -16,6 +17,7 @@ jest.mock('react-router-dom', () => ({
|
||||||
useNavigate: jest.fn(),
|
useNavigate: jest.fn(),
|
||||||
useParams: jest.fn(),
|
useParams: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('src/pages/Teacher/ManageRoom/RoomContext');
|
||||||
|
|
||||||
const mockSocket = {
|
const mockSocket = {
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
|
|
@ -33,7 +35,7 @@ const mockQuiz: QuizType = {
|
||||||
folderName: 'folder-name',
|
folderName: 'folder-name',
|
||||||
userId: 'user-id',
|
userId: 'user-id',
|
||||||
created_at: new Date(),
|
created_at: new Date(),
|
||||||
updated_at: new Date()
|
updated_at: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockStudents: StudentType[] = [
|
const mockStudents: StudentType[] = [
|
||||||
|
|
@ -51,13 +53,18 @@ const mockAnswerData: AnswerReceptionFromBackendType = {
|
||||||
describe('ManageRoom', () => {
|
describe('ManageRoom', () => {
|
||||||
const navigate = jest.fn();
|
const navigate = jest.fn();
|
||||||
const useParamsMock = useParams as jest.Mock;
|
const useParamsMock = useParams as jest.Mock;
|
||||||
|
const mockSetSelectedRoom = jest.fn();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
(useNavigate as jest.Mock).mockReturnValue(navigate);
|
(useNavigate as jest.Mock).mockReturnValue(navigate);
|
||||||
useParamsMock.mockReturnValue({ id: 'test-quiz-id' });
|
useParamsMock.mockReturnValue({ quizId: 'test-quiz-id', roomName: 'Test Room' });
|
||||||
(ApiService.getQuiz as jest.Mock).mockResolvedValue(mockQuiz);
|
(ApiService.getQuiz as jest.Mock).mockResolvedValue(mockQuiz);
|
||||||
(webSocketService.connect as jest.Mock).mockReturnValue(mockSocket);
|
(webSocketService.connect as jest.Mock).mockReturnValue(mockSocket);
|
||||||
|
(RoomProvider as jest.Mock).mockReturnValue({
|
||||||
|
selectedRoom: { id: '1', title: 'Test Room' },
|
||||||
|
setSelectedRoom: mockSetSelectedRoom,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('prepares to launch quiz and fetches quiz data', async () => {
|
test('prepares to launch quiz and fetches quiz data', async () => {
|
||||||
|
|
@ -71,7 +78,7 @@ describe('ManageRoom', () => {
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
|
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
|
||||||
createSuccessCallback('test-room-name');
|
createSuccessCallback('Test Room');
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|
@ -89,7 +96,10 @@ describe('ManageRoom', () => {
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText('Test Quiz')).toBeInTheDocument();
|
expect(screen.getByText('Test Quiz')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Salle: test-room-name')).toBeInTheDocument();
|
|
||||||
|
const roomHeader = document.querySelector('h1');
|
||||||
|
expect(roomHeader).toHaveTextContent('Salle : TEST ROOM');
|
||||||
|
|
||||||
expect(screen.getByText('0/60')).toBeInTheDocument();
|
expect(screen.getByText('0/60')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Question 1/2')).toBeInTheDocument();
|
expect(screen.getByText('Question 1/2')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
@ -106,11 +116,11 @@ describe('ManageRoom', () => {
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
|
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
|
||||||
createSuccessCallback('test-room-name');
|
createSuccessCallback('Test Room');
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText('Salle: test-room-name')).toBeInTheDocument();
|
expect(screen.getByText(/Salle\s*:\s*Test Room/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -125,7 +135,7 @@ describe('ManageRoom', () => {
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
|
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
|
||||||
createSuccessCallback('test-room-name');
|
createSuccessCallback('Test Room');
|
||||||
});
|
});
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|
@ -153,6 +163,64 @@ describe('ManageRoom', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('handles next question', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<ManageRoom />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
|
||||||
|
createSuccessCallback('Test Room');
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('Lancer'));
|
||||||
|
fireEvent.click(screen.getByText('Rythme du professeur'));
|
||||||
|
fireEvent.click(screen.getAllByText('Lancer')[1]);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
screen.debug();
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextQuestionButton = await screen.findByRole('button', { name: /Prochaine question/i });
|
||||||
|
expect(nextQuestionButton).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(nextQuestionButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Question 2/2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles disconnect', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<ManageRoom />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
|
||||||
|
createSuccessCallback('Test Room');
|
||||||
|
});
|
||||||
|
|
||||||
|
const disconnectButton = screen.getByText('Quitter');
|
||||||
|
fireEvent.click(disconnectButton);
|
||||||
|
|
||||||
|
const confirmButton = screen.getAllByText('Confirmer');
|
||||||
|
fireEvent.click(confirmButton[1]);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(webSocketService.disconnect).toHaveBeenCalled();
|
||||||
|
expect(navigate).toHaveBeenCalledWith('/teacher/dashboard');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('handles submit-answer-room event', async () => {
|
test('handles submit-answer-room event', async () => {
|
||||||
const consoleSpy = jest.spyOn(console, 'log');
|
const consoleSpy = jest.spyOn(console, 'log');
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|
@ -188,13 +256,15 @@ describe('ManageRoom', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(consoleSpy).toHaveBeenCalledWith('Received answer from Student 1 for question 1: Answer1');
|
expect(consoleSpy).toHaveBeenCalledWith(
|
||||||
|
'Received answer from Student 1 for question 1: Answer1'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
consoleSpy.mockRestore();
|
consoleSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handles next question', async () => {
|
test('vide la liste des étudiants après déconnexion', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
|
|
@ -205,38 +275,12 @@ describe('ManageRoom', () => {
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
|
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
|
||||||
createSuccessCallback('test-room-name');
|
createSuccessCallback('Test Room');
|
||||||
});
|
|
||||||
|
|
||||||
const launchButton = screen.getByText('Lancer');
|
|
||||||
fireEvent.click(launchButton);
|
|
||||||
|
|
||||||
const rythmeButton = screen.getByText('Rythme du professeur');
|
|
||||||
fireEvent.click(rythmeButton);
|
|
||||||
|
|
||||||
const secondLaunchButton = screen.getAllByText('Lancer');
|
|
||||||
fireEvent.click(secondLaunchButton[1]);
|
|
||||||
|
|
||||||
const nextQuestionButton = screen.getByText('Prochaine question');
|
|
||||||
fireEvent.click(nextQuestionButton);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('Question 2/2')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('handles disconnect', async () => {
|
|
||||||
await act(async () => {
|
|
||||||
render(
|
|
||||||
<MemoryRouter>
|
|
||||||
<ManageRoom />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
|
const userJoinedCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'user-joined')[1];
|
||||||
createSuccessCallback('test-room-name');
|
userJoinedCallback(mockStudents[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
const disconnectButton = screen.getByText('Quitter');
|
const disconnectButton = screen.getByText('Quitter');
|
||||||
|
|
@ -246,8 +290,7 @@ describe('ManageRoom', () => {
|
||||||
fireEvent.click(confirmButton[1]);
|
fireEvent.click(confirmButton[1]);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(webSocketService.disconnect).toHaveBeenCalled();
|
expect(screen.queryByText('Student 1')).not.toBeInTheDocument();
|
||||||
expect(navigate).toHaveBeenCalledWith('/teacher/dashboard');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -37,9 +37,10 @@ describe('WebSocketService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('createRoom should emit create-room event', () => {
|
test('createRoom should emit create-room event', () => {
|
||||||
|
const roomName = 'Test Room';
|
||||||
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||||
WebsocketService.createRoom();
|
WebsocketService.createRoom(roomName);
|
||||||
expect(mockSocket.emit).toHaveBeenCalledWith('create-room');
|
expect(mockSocket.emit).toHaveBeenCalledWith('create-room', roomName);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('nextQuestion should emit next-question event with correct parameters', () => {
|
test('nextQuestion should emit next-question event with correct parameters', () => {
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ function formatLatex(text: string): string {
|
||||||
.replace(/\\\((.*?)\\\)/g, (_, inner) =>
|
.replace(/\\\((.*?)\\\)/g, (_, inner) =>
|
||||||
katex.renderToString(inner, { displayMode: false })
|
katex.renderToString(inner, { displayMode: false })
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log('Error rendering LaTeX (KaTeX):', error);
|
||||||
renderedText = text;
|
renderedText = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,16 @@ interface Props {
|
||||||
const StudentWaitPage: React.FC<Props> = ({ students, launchQuiz, setQuizMode }) => {
|
const StudentWaitPage: React.FC<Props> = ({ students, launchQuiz, setQuizMode }) => {
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
|
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleLaunchClick = () => {
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="wait">
|
<div className="wait">
|
||||||
<div className='button'>
|
<div className='button'>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={() => setIsDialogOpen(true)}
|
onClick={handleLaunchClick}
|
||||||
startIcon={<PlayArrow />}
|
startIcon={<PlayArrow />}
|
||||||
fullWidth
|
fullWidth
|
||||||
sx={{ fontWeight: 600, fontSize: 20 }}
|
sx={{ fontWeight: 600, fontSize: 20 }}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// constants.tsx
|
// constants.tsx
|
||||||
const ENV_VARIABLES = {
|
const ENV_VARIABLES = {
|
||||||
MODE: 'production',
|
MODE: process.env.MODE || "production",
|
||||||
VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || "",
|
VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || "",
|
||||||
BACKEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.BACKEND_PORT}`:''}` : process.env.VITE_BACKEND_URL || '',
|
BACKEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.BACKEND_PORT}`:''}` : process.env.VITE_BACKEND_URL || '',
|
||||||
FRONTEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.PORT}`:''}` : ''
|
FRONTEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.PORT}`:''}` : ''
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
.form-container{
|
.form-container {
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
|
@ -15,34 +15,35 @@
|
||||||
width: 400px;
|
width: 400px;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
form {
|
form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: #5271ff;
|
background-color: #5271ff;
|
||||||
color: white;
|
color: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
button:hover {
|
/* This hover was affecting the entire App */
|
||||||
|
/* button:hover {
|
||||||
background-color: #5271ff;
|
background-color: #5271ff;
|
||||||
}
|
} */
|
||||||
.home-button-container{
|
.home-button-container {
|
||||||
background: none;
|
background: none;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
.home-button-container:hover{
|
.home-button-container:hover {
|
||||||
background: none;
|
background: none;
|
||||||
color: black;
|
color: black;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ const SimpleLogin: React.FC = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
|
console.log(`SimpleLogin: login: email: ${email}, password: ${password}`);
|
||||||
const result = await ApiService.login(email, password);
|
const result = await ApiService.login(email, password);
|
||||||
if (result !== true) {
|
if (result !== true) {
|
||||||
setConnectionError(result);
|
setConnectionError(result);
|
||||||
|
|
@ -71,9 +72,10 @@ const SimpleLogin: React.FC = () => {
|
||||||
|
|
||||||
<div className="login-links">
|
<div className="login-links">
|
||||||
|
|
||||||
<Link to="/resetPassword">
|
|
||||||
Réinitialiser le mot de passe
|
{/* <Link to="/resetPassword"> */}
|
||||||
</Link>
|
<del>Réinitialiser le mot de passe</del>
|
||||||
|
{/* </Link> */}
|
||||||
|
|
||||||
<Link to="/register">
|
<Link to="/register">
|
||||||
Créer un compte
|
Créer un compte
|
||||||
|
|
|
||||||
|
|
@ -39,17 +39,20 @@ const JoinRoom: React.FC = () => {
|
||||||
console.log(`JoinRoom: handleCreateSocket: ${ENV_VARIABLES.VITE_BACKEND_URL}`);
|
console.log(`JoinRoom: handleCreateSocket: ${ENV_VARIABLES.VITE_BACKEND_URL}`);
|
||||||
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||||
|
|
||||||
socket.on('join-success', () => {
|
socket.on('join-success', (roomJoinedName) => {
|
||||||
setIsWaitingForTeacher(true);
|
setIsWaitingForTeacher(true);
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
console.log('Successfully joined the room.');
|
console.log(`on(join-success): Successfully joined the room ${roomJoinedName}`);
|
||||||
});
|
});
|
||||||
socket.on('next-question', (question: QuestionType) => {
|
socket.on('next-question', (question: QuestionType) => {
|
||||||
|
console.log('on(next-question): Received next-question:', question);
|
||||||
setQuizMode('teacher');
|
setQuizMode('teacher');
|
||||||
setIsWaitingForTeacher(false);
|
setIsWaitingForTeacher(false);
|
||||||
setQuestion(question);
|
setQuestion(question);
|
||||||
});
|
});
|
||||||
socket.on('launch-student-mode', (questions: QuestionType[]) => {
|
socket.on('launch-student-mode', (questions: QuestionType[]) => {
|
||||||
|
console.log('on(launch-student-mode): Received launch-student-mode:', questions);
|
||||||
|
|
||||||
setQuizMode('student');
|
setQuizMode('student');
|
||||||
setIsWaitingForTeacher(false);
|
setIsWaitingForTeacher(false);
|
||||||
setQuestions(questions);
|
setQuestions(questions);
|
||||||
|
|
@ -98,6 +101,8 @@ const JoinRoom: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (username && roomName) {
|
if (username && roomName) {
|
||||||
|
console.log(`Tentative de rejoindre : ${roomName}, utilisateur : ${username}`);
|
||||||
|
|
||||||
webSocketService.joinRoom(roomName, username);
|
webSocketService.joinRoom(roomName, username);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -168,12 +173,12 @@ const JoinRoom: React.FC = () => {
|
||||||
error={connectionError}>
|
error={connectionError}>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
type="number"
|
type="text"
|
||||||
label="Numéro de la salle"
|
label="Nom de la salle"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={roomName}
|
value={roomName}
|
||||||
onChange={(e) => setRoomName(e.target.value)}
|
onChange={(e) => setRoomName(e.target.value.toUpperCase())}
|
||||||
placeholder="Numéro de la salle"
|
placeholder="Nom de la salle"
|
||||||
sx={{ marginBottom: '1rem' }}
|
sx={{ marginBottom: '1rem' }}
|
||||||
fullWidth={true}
|
fullWidth={true}
|
||||||
onKeyDown={handleReturnKey}
|
onKeyDown={handleReturnKey}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,13 @@ import ApiService from '../../../services/ApiService';
|
||||||
import './dashboard.css';
|
import './dashboard.css';
|
||||||
import ImportModal from 'src/components/ImportModal/ImportModal';
|
import ImportModal from 'src/components/ImportModal/ImportModal';
|
||||||
//import axios from 'axios';
|
//import axios from 'axios';
|
||||||
|
import { RoomType } from 'src/Types/RoomType';
|
||||||
|
// import { useRooms } from '../ManageRoom/RoomContext';
|
||||||
import {
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
TextField,
|
TextField,
|
||||||
IconButton,
|
IconButton,
|
||||||
InputAdornment,
|
InputAdornment,
|
||||||
|
|
@ -23,6 +28,7 @@ import {
|
||||||
NativeSelect,
|
NativeSelect,
|
||||||
CardContent,
|
CardContent,
|
||||||
styled,
|
styled,
|
||||||
|
DialogContentText
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
|
|
@ -33,7 +39,7 @@ import {
|
||||||
FolderCopy,
|
FolderCopy,
|
||||||
ContentCopy,
|
ContentCopy,
|
||||||
Edit,
|
Edit,
|
||||||
Share,
|
Share
|
||||||
// DriveFileMove
|
// DriveFileMove
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
|
|
||||||
|
|
@ -43,7 +49,7 @@ const CustomCard = styled(Card)({
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
margin: '40px 0 20px 0', // Add top margin to make space for the tab
|
margin: '40px 0 20px 0', // Add top margin to make space for the tab
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
paddingTop: '20px', // Ensure content inside the card doesn't overlap with the tab
|
paddingTop: '20px' // Ensure content inside the card doesn't overlap with the tab
|
||||||
});
|
});
|
||||||
|
|
||||||
const Dashboard: React.FC = () => {
|
const Dashboard: React.FC = () => {
|
||||||
|
|
@ -53,6 +59,13 @@ const Dashboard: React.FC = () => {
|
||||||
const [showImportModal, setShowImportModal] = useState<boolean>(false);
|
const [showImportModal, setShowImportModal] = useState<boolean>(false);
|
||||||
const [folders, setFolders] = useState<FolderType[]>([]);
|
const [folders, setFolders] = useState<FolderType[]>([]);
|
||||||
const [selectedFolderId, setSelectedFolderId] = useState<string>(''); // Selected folder
|
const [selectedFolderId, setSelectedFolderId] = useState<string>(''); // Selected folder
|
||||||
|
const [rooms, setRooms] = useState<RoomType[]>([]);
|
||||||
|
const [openAddRoomDialog, setOpenAddRoomDialog] = useState(false);
|
||||||
|
const [newRoomTitle, setNewRoomTitle] = useState('');
|
||||||
|
// const { selectedRoom, selectRoom, createRoom } = useRooms();
|
||||||
|
const [selectedRoom, selectRoom] = useState<RoomType>(); // menu
|
||||||
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
|
const [showErrorDialog, setShowErrorDialog] = useState(false);
|
||||||
|
|
||||||
// Filter quizzes based on search term
|
// Filter quizzes based on search term
|
||||||
// const filteredQuizzes = quizzes.filter(quiz =>
|
// const filteredQuizzes = quizzes.filter(quiz =>
|
||||||
|
|
@ -65,7 +78,6 @@ const Dashboard: React.FC = () => {
|
||||||
);
|
);
|
||||||
}, [quizzes, searchTerm]);
|
}, [quizzes, searchTerm]);
|
||||||
|
|
||||||
|
|
||||||
// Group quizzes by folder
|
// Group quizzes by folder
|
||||||
const quizzesByFolder = filteredQuizzes.reduce((acc, quiz) => {
|
const quizzesByFolder = filteredQuizzes.reduce((acc, quiz) => {
|
||||||
if (!acc[quiz.folderName]) {
|
if (!acc[quiz.folderName]) {
|
||||||
|
|
@ -77,28 +89,80 @@ const Dashboard: React.FC = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
if (!ApiService.isLoggedIn()) {
|
const isLoggedIn = await ApiService.isLoggedIn();
|
||||||
navigate("/login");
|
console.log(`Dashboard: isLoggedIn: ${isLoggedIn}`);
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
navigate('/teacher/login');
|
||||||
return;
|
return;
|
||||||
}
|
} else {
|
||||||
else {
|
const userRooms = await ApiService.getUserRooms();
|
||||||
const userFolders = await ApiService.getUserFolders();
|
setRooms(userRooms as RoomType[]);
|
||||||
|
|
||||||
|
const userFolders = await ApiService.getUserFolders();
|
||||||
setFolders(userFolders as FolderType[]);
|
setFolders(userFolders as FolderType[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (rooms.length > 0 && !selectedRoom) {
|
||||||
|
selectRoom(rooms[rooms.length - 1]);
|
||||||
|
localStorage.setItem('selectedRoomId', rooms[rooms.length - 1]._id);
|
||||||
|
}
|
||||||
|
}, [rooms, selectedRoom]);
|
||||||
|
|
||||||
|
const handleSelectRoom = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
if (event.target.value === 'add-room') {
|
||||||
|
setOpenAddRoomDialog(true);
|
||||||
|
} else {
|
||||||
|
selectRoomByName(event.target.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Créer une salle
|
||||||
|
const createRoom = async (title: string) => {
|
||||||
|
// Créer la salle et récupérer l'objet complet
|
||||||
|
const newRoom = await ApiService.createRoom(title);
|
||||||
|
|
||||||
|
// Mettre à jour la liste des salles
|
||||||
|
const updatedRooms = await ApiService.getUserRooms();
|
||||||
|
setRooms(updatedRooms as RoomType[]);
|
||||||
|
|
||||||
|
// Sélectionner la nouvelle salle avec son ID
|
||||||
|
selectRoomByName(newRoom); // Utiliser l'ID de l'objet retourné
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Sélectionner une salle
|
||||||
|
const selectRoomByName = (roomId: string) => {
|
||||||
|
const room = rooms.find(r => r._id === roomId);
|
||||||
|
selectRoom(room);
|
||||||
|
localStorage.setItem('selectedRoomId', roomId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateRoom = async () => {
|
||||||
|
if (newRoomTitle.trim()) {
|
||||||
|
try {
|
||||||
|
await createRoom(newRoomTitle);
|
||||||
|
const userRooms = await ApiService.getUserRooms();
|
||||||
|
setRooms(userRooms as RoomType[]);
|
||||||
|
setOpenAddRoomDialog(false);
|
||||||
|
setNewRoomTitle('');
|
||||||
|
} catch (error) {
|
||||||
|
setErrorMessage(error instanceof Error ? error.message : "Erreur inconnue");
|
||||||
|
setShowErrorDialog(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSelectFolder = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
const handleSelectFolder = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
setSelectedFolderId(event.target.value);
|
setSelectedFolderId(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchQuizzesForFolder = async () => {
|
const fetchQuizzesForFolder = async () => {
|
||||||
|
|
||||||
if (selectedFolderId == '') {
|
if (selectedFolderId == '') {
|
||||||
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
|
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
|
||||||
//console.log("show all quizzes")
|
//console.log("show all quizzes")
|
||||||
|
|
@ -109,33 +173,29 @@ const Dashboard: React.FC = () => {
|
||||||
//console.log("folder: ", folder.title, " quiz: ", folderQuizzes);
|
//console.log("folder: ", folder.title, " quiz: ", folderQuizzes);
|
||||||
// add the folder.title to the QuizType if the folderQuizzes is an array
|
// add the folder.title to the QuizType if the folderQuizzes is an array
|
||||||
addFolderTitleToQuizzes(folderQuizzes, folder.title);
|
addFolderTitleToQuizzes(folderQuizzes, folder.title);
|
||||||
quizzes = quizzes.concat(folderQuizzes as QuizType[])
|
quizzes = quizzes.concat(folderQuizzes as QuizType[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
setQuizzes(quizzes as QuizType[]);
|
setQuizzes(quizzes as QuizType[]);
|
||||||
}
|
} else {
|
||||||
else {
|
console.log('show some quizzes');
|
||||||
console.log("show some quizzes")
|
|
||||||
const folderQuizzes = await ApiService.getFolderContent(selectedFolderId);
|
const folderQuizzes = await ApiService.getFolderContent(selectedFolderId);
|
||||||
console.log("folderQuizzes: ", folderQuizzes);
|
console.log('folderQuizzes: ', folderQuizzes);
|
||||||
// get the folder title from its id
|
// get the folder title from its id
|
||||||
const folderTitle = folders.find((folder) => folder._id === selectedFolderId)?.title || '';
|
const folderTitle =
|
||||||
|
folders.find((folder) => folder._id === selectedFolderId)?.title || '';
|
||||||
addFolderTitleToQuizzes(folderQuizzes, folderTitle);
|
addFolderTitleToQuizzes(folderQuizzes, folderTitle);
|
||||||
setQuizzes(folderQuizzes as QuizType[]);
|
setQuizzes(folderQuizzes as QuizType[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchQuizzesForFolder();
|
fetchQuizzesForFolder();
|
||||||
}, [selectedFolderId]);
|
}, [selectedFolderId]);
|
||||||
|
|
||||||
|
|
||||||
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setSearchTerm(event.target.value);
|
setSearchTerm(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleRemoveQuiz = async (quiz: QuizType) => {
|
const handleRemoveQuiz = async (quiz: QuizType) => {
|
||||||
try {
|
try {
|
||||||
const confirmed = window.confirm('Voulez-vous vraiment supprimer ce quiz?');
|
const confirmed = window.confirm('Voulez-vous vraiment supprimer ce quiz?');
|
||||||
|
|
@ -149,30 +209,27 @@ const Dashboard: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleDuplicateQuiz = async (quiz: QuizType) => {
|
const handleDuplicateQuiz = async (quiz: QuizType) => {
|
||||||
try {
|
try {
|
||||||
await ApiService.duplicateQuiz(quiz._id);
|
await ApiService.duplicateQuiz(quiz._id);
|
||||||
if (selectedFolderId == '') {
|
if (selectedFolderId == '') {
|
||||||
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
|
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
|
||||||
console.log("show all quizzes")
|
console.log('show all quizzes');
|
||||||
let quizzes: QuizType[] = [];
|
let quizzes: QuizType[] = [];
|
||||||
|
|
||||||
for (const folder of folders as FolderType[]) {
|
for (const folder of folders as FolderType[]) {
|
||||||
const folderQuizzes = await ApiService.getFolderContent(folder._id);
|
const folderQuizzes = await ApiService.getFolderContent(folder._id);
|
||||||
console.log("folder: ", folder.title, " quiz: ", folderQuizzes);
|
console.log('folder: ', folder.title, ' quiz: ', folderQuizzes);
|
||||||
addFolderTitleToQuizzes(folderQuizzes, folder.title);
|
addFolderTitleToQuizzes(folderQuizzes, folder.title);
|
||||||
quizzes = quizzes.concat(folderQuizzes as QuizType[]);
|
quizzes = quizzes.concat(folderQuizzes as QuizType[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
setQuizzes(quizzes as QuizType[]);
|
setQuizzes(quizzes as QuizType[]);
|
||||||
}
|
} else {
|
||||||
else {
|
console.log('show some quizzes');
|
||||||
console.log("show some quizzes")
|
|
||||||
const folderQuizzes = await ApiService.getFolderContent(selectedFolderId);
|
const folderQuizzes = await ApiService.getFolderContent(selectedFolderId);
|
||||||
addFolderTitleToQuizzes(folderQuizzes, selectedFolderId);
|
addFolderTitleToQuizzes(folderQuizzes, selectedFolderId);
|
||||||
setQuizzes(folderQuizzes as QuizType[]);
|
setQuizzes(folderQuizzes as QuizType[]);
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error duplicating quiz:', error);
|
console.error('Error duplicating quiz:', error);
|
||||||
|
|
@ -181,7 +238,6 @@ const Dashboard: React.FC = () => {
|
||||||
|
|
||||||
const handleOnImport = () => {
|
const handleOnImport = () => {
|
||||||
setShowImportModal(true);
|
setShowImportModal(true);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateQuiz = (questions: string[]) => {
|
const validateQuiz = (questions: string[]) => {
|
||||||
|
|
@ -193,7 +249,6 @@ const Dashboard: React.FC = () => {
|
||||||
// Otherwise the quiz is invalid
|
// Otherwise the quiz is invalid
|
||||||
for (let i = 0; i < questions.length; i++) {
|
for (let i = 0; i < questions.length; i++) {
|
||||||
try {
|
try {
|
||||||
// questions[i] = QuestionService.ignoreImgTags(questions[i]);
|
|
||||||
const parsedItem = parse(questions[i]);
|
const parsedItem = parse(questions[i]);
|
||||||
Template(parsedItem[0]);
|
Template(parsedItem[0]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -206,9 +261,8 @@ const Dashboard: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadTxtFile = async (quiz: QuizType) => {
|
const downloadTxtFile = async (quiz: QuizType) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const selectedQuiz = await ApiService.getQuiz(quiz._id) as QuizType;
|
const selectedQuiz = (await ApiService.getQuiz(quiz._id)) as QuizType;
|
||||||
//quizzes.find((quiz) => quiz._id === quiz._id);
|
//quizzes.find((quiz) => quiz._id === quiz._id);
|
||||||
|
|
||||||
if (!selectedQuiz) {
|
if (!selectedQuiz) {
|
||||||
|
|
@ -216,7 +270,7 @@ const Dashboard: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
//const { title, content } = selectedQuiz;
|
//const { title, content } = selectedQuiz;
|
||||||
let quizContent = "";
|
let quizContent = '';
|
||||||
const title = selectedQuiz.title;
|
const title = selectedQuiz.title;
|
||||||
console.log(selectedQuiz.content);
|
console.log(selectedQuiz.content);
|
||||||
selectedQuiz.content.forEach((question, qIndex) => {
|
selectedQuiz.content.forEach((question, qIndex) => {
|
||||||
|
|
@ -231,7 +285,9 @@ const Dashboard: React.FC = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!validateQuiz(selectedQuiz.content)) {
|
if (!validateQuiz(selectedQuiz.content)) {
|
||||||
window.alert('Attention! Ce quiz contient des questions invalides selon le format GIFT.');
|
window.alert(
|
||||||
|
'Attention! Ce quiz contient des questions invalides selon le format GIFT.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const blob = new Blob([quizContent], { type: 'text/plain' });
|
const blob = new Blob([quizContent], { type: 'text/plain' });
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
|
|
@ -239,8 +295,6 @@ const Dashboard: React.FC = () => {
|
||||||
a.download = `${filename}.gift`;
|
a.download = `${filename}.gift`;
|
||||||
a.href = window.URL.createObjectURL(blob);
|
a.href = window.URL.createObjectURL(blob);
|
||||||
a.click();
|
a.click();
|
||||||
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error exporting selected quiz:', error);
|
console.error('Error exporting selected quiz:', error);
|
||||||
}
|
}
|
||||||
|
|
@ -255,7 +309,6 @@ const Dashboard: React.FC = () => {
|
||||||
setFolders(userFolders as FolderType[]);
|
setFolders(userFolders as FolderType[]);
|
||||||
const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType;
|
const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType;
|
||||||
setSelectedFolderId(newlyCreatedFolder._id);
|
setSelectedFolderId(newlyCreatedFolder._id);
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating folder:', error);
|
console.error('Error creating folder:', error);
|
||||||
|
|
@ -263,7 +316,6 @@ const Dashboard: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteFolder = async () => {
|
const handleDeleteFolder = async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const confirmed = window.confirm('Voulez-vous vraiment supprimer ce dossier?');
|
const confirmed = window.confirm('Voulez-vous vraiment supprimer ce dossier?');
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
|
|
@ -273,18 +325,17 @@ const Dashboard: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
|
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
|
||||||
console.log("show all quizzes")
|
console.log('show all quizzes');
|
||||||
let quizzes: QuizType[] = [];
|
let quizzes: QuizType[] = [];
|
||||||
|
|
||||||
for (const folder of folders as FolderType[]) {
|
for (const folder of folders as FolderType[]) {
|
||||||
const folderQuizzes = await ApiService.getFolderContent(folder._id);
|
const folderQuizzes = await ApiService.getFolderContent(folder._id);
|
||||||
console.log("folder: ", folder.title, " quiz: ", folderQuizzes);
|
console.log('folder: ', folder.title, ' quiz: ', folderQuizzes);
|
||||||
quizzes = quizzes.concat(folderQuizzes as QuizType[])
|
quizzes = quizzes.concat(folderQuizzes as QuizType[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
setQuizzes(quizzes as QuizType[]);
|
setQuizzes(quizzes as QuizType[]);
|
||||||
setSelectedFolderId('');
|
setSelectedFolderId('');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting folder:', error);
|
console.error('Error deleting folder:', error);
|
||||||
}
|
}
|
||||||
|
|
@ -294,12 +345,15 @@ const Dashboard: React.FC = () => {
|
||||||
try {
|
try {
|
||||||
// folderId: string GET THIS FROM CURRENT FOLDER
|
// folderId: string GET THIS FROM CURRENT FOLDER
|
||||||
// currentTitle: string GET THIS FROM CURRENT FOLDER
|
// currentTitle: string GET THIS FROM CURRENT FOLDER
|
||||||
const newTitle = prompt('Entrée le nouveau nom du fichier', folders.find((folder) => folder._id === selectedFolderId)?.title);
|
const newTitle = prompt(
|
||||||
|
'Entrée le nouveau nom du fichier',
|
||||||
|
folders.find((folder) => folder._id === selectedFolderId)?.title
|
||||||
|
);
|
||||||
if (newTitle) {
|
if (newTitle) {
|
||||||
const renamedFolderId = selectedFolderId;
|
const renamedFolderId = selectedFolderId;
|
||||||
const result = await ApiService.renameFolder(selectedFolderId, newTitle);
|
const result = await ApiService.renameFolder(selectedFolderId, newTitle);
|
||||||
|
|
||||||
if (result !== true ) {
|
if (result !== true) {
|
||||||
window.alert(`Une erreur est survenue: ${result}`);
|
window.alert(`Une erreur est survenue: ${result}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -331,46 +385,94 @@ const Dashboard: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateQuiz = () => {
|
const handleCreateQuiz = () => {
|
||||||
navigate("/teacher/editor-quiz/new");
|
navigate('/teacher/editor-quiz/new');
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleEditQuiz = (quiz: QuizType) => {
|
const handleEditQuiz = (quiz: QuizType) => {
|
||||||
navigate(`/teacher/editor-quiz/${quiz._id}`);
|
navigate(`/teacher/editor-quiz/${quiz._id}`);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleLancerQuiz = (quiz: QuizType) => {
|
const handleLancerQuiz = (quiz: QuizType) => {
|
||||||
navigate(`/teacher/manage-room/${quiz._id}`);
|
if (selectedRoom) {
|
||||||
}
|
navigate(`/teacher/manage-room/${quiz._id}/${selectedRoom.title}`);
|
||||||
|
} else {
|
||||||
|
const randomSixDigit = Math.floor(100000 + Math.random() * 900000);
|
||||||
|
navigate(`/teacher/manage-room/${quiz._id}/${randomSixDigit}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleShareQuiz = async (quiz: QuizType) => {
|
const handleShareQuiz = async (quiz: QuizType) => {
|
||||||
try {
|
try {
|
||||||
const email = prompt(`Veuillez saisir l'email de la personne avec qui vous souhaitez partager ce quiz`, "");
|
const email = prompt(
|
||||||
|
`Veuillez saisir l'email de la personne avec qui vous souhaitez partager ce quiz`,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
if (email) {
|
if (email) {
|
||||||
const result = await ApiService.ShareQuiz(quiz._id, email);
|
const result = await ApiService.ShareQuiz(quiz._id, email);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
|
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.alert(`Quiz partagé avec succès!`)
|
window.alert(`Quiz partagé avec succès!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors du partage du quiz:', error);
|
console.error('Erreur lors du partage du quiz:', error);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<div className="dashboard">
|
<div className="dashboard">
|
||||||
|
|
||||||
<div className="title">Tableau de bord</div>
|
<div className="title">Tableau de bord</div>
|
||||||
|
|
||||||
|
<div className="roomSelection">
|
||||||
|
<label htmlFor="select-room">Sélectionner une salle: </label>
|
||||||
|
<select value={selectedRoom?._id || ''} onChange={(e) => handleSelectRoom(e)}>
|
||||||
|
<option value="" disabled>
|
||||||
|
-- Sélectionner une salle --
|
||||||
|
</option>
|
||||||
|
{rooms.map((room) => (
|
||||||
|
<option key={room._id} value={room._id}>
|
||||||
|
{room.title}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
<option value="add-room">Ajouter salle</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedRoom && (
|
||||||
|
<div className="roomTitle">
|
||||||
|
<h2>Salle sélectionnée: {selectedRoom.title}</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Dialog open={openAddRoomDialog} onClose={() => setOpenAddRoomDialog(false)}>
|
||||||
|
<DialogTitle>Créer une nouvelle salle</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
value={newRoomTitle}
|
||||||
|
onChange={(e) => setNewRoomTitle(e.target.value.toUpperCase())}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setOpenAddRoomDialog(false)}>Annuler</Button>
|
||||||
|
<Button onClick={handleCreateRoom}>Créer</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
<Dialog open={showErrorDialog} onClose={() => setShowErrorDialog(false)}>
|
||||||
|
<DialogTitle>Erreur</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>{errorMessage}</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setShowErrorDialog(false)}>Fermer</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<div className="search-bar">
|
<div className="search-bar">
|
||||||
<TextField
|
<TextField
|
||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
|
|
@ -389,8 +491,8 @@ const Dashboard: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='folder'>
|
<div className="folder">
|
||||||
<div className='select'>
|
<div className="select">
|
||||||
<NativeSelect
|
<NativeSelect
|
||||||
id="select-folder"
|
id="select-folder"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|
@ -400,48 +502,65 @@ const Dashboard: React.FC = () => {
|
||||||
<option value=""> Tous les dossiers... </option>
|
<option value=""> Tous les dossiers... </option>
|
||||||
|
|
||||||
{folders.map((folder: FolderType) => (
|
{folders.map((folder: FolderType) => (
|
||||||
<option value={folder._id} key={folder._id}> {folder.title} </option>
|
<option value={folder._id} key={folder._id}>
|
||||||
|
{' '}
|
||||||
|
{folder.title}{' '}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</NativeSelect>
|
</NativeSelect>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='actions'>
|
<div className="actions">
|
||||||
<Tooltip title="Ajouter dossier" placement="top">
|
<Tooltip title="Ajouter dossier" placement="top">
|
||||||
<IconButton
|
<IconButton color="primary" onClick={handleCreateFolder}>
|
||||||
color="primary"
|
{' '}
|
||||||
onClick={handleCreateFolder}
|
<Add />{' '}
|
||||||
> <Add /> </IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip title="Renommer dossier" placement="top">
|
<Tooltip title="Renommer dossier" placement="top">
|
||||||
|
<div>
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={handleRenameFolder}
|
onClick={handleRenameFolder}
|
||||||
disabled={selectedFolderId == ''} // cannot action on all
|
disabled={selectedFolderId == ''} // cannot action on all
|
||||||
> <Edit /> </IconButton>
|
>
|
||||||
|
{' '}
|
||||||
|
<Edit />{' '}
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip title="Dupliquer dossier" placement="top">
|
<Tooltip title="Dupliquer dossier" placement="top">
|
||||||
|
<div>
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={handleDuplicateFolder}
|
onClick={handleDuplicateFolder}
|
||||||
disabled={selectedFolderId == ''} // cannot action on all
|
disabled={selectedFolderId == ''} // cannot action on all
|
||||||
> <FolderCopy /> </IconButton>
|
>
|
||||||
|
{' '}
|
||||||
|
<FolderCopy />{' '}
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip title="Supprimer dossier" placement="top">
|
<Tooltip title="Supprimer dossier" placement="top">
|
||||||
|
<div>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="delete"
|
aria-label="delete"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={handleDeleteFolder}
|
onClick={handleDeleteFolder}
|
||||||
disabled={selectedFolderId == ''} // cannot action on all
|
disabled={selectedFolderId == ''} // cannot action on all
|
||||||
> <DeleteOutline /> </IconButton>
|
>
|
||||||
|
{' '}
|
||||||
|
<DeleteOutline />{' '}
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='ajouter'>
|
<div className="ajouter">
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|
@ -459,47 +578,59 @@ const Dashboard: React.FC = () => {
|
||||||
>
|
>
|
||||||
Import
|
Import
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className='list'>
|
<div className="list">
|
||||||
{Object.keys(quizzesByFolder).map(folderName => (
|
{Object.keys(quizzesByFolder).map((folderName) => (
|
||||||
<CustomCard key={folderName} className='folder-card'>
|
<CustomCard key={folderName} className="folder-card">
|
||||||
<div className='folder-tab'>{folderName}</div>
|
<div className="folder-tab">{folderName}</div>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{quizzesByFolder[folderName].map((quiz: QuizType) => (
|
{quizzesByFolder[folderName].map((quiz: QuizType) => (
|
||||||
<div className='quiz' key={quiz._id}>
|
<div className="quiz" key={quiz._id}>
|
||||||
<div className='title'>
|
<div className="title">
|
||||||
<Tooltip title="Lancer quiz" placement="top">
|
<Tooltip title="Lancer quiz" placement="top">
|
||||||
<Button
|
<div>
|
||||||
variant="outlined"
|
<Button
|
||||||
onClick={() => handleLancerQuiz(quiz)}
|
variant="outlined"
|
||||||
disabled={!validateQuiz(quiz.content)}
|
onClick={() => handleLancerQuiz(quiz)}
|
||||||
>
|
disabled={!validateQuiz(quiz.content)}
|
||||||
{`${quiz.title} (${quiz.content.length} question${quiz.content.length > 1 ? 's' : ''})`}
|
>
|
||||||
</Button>
|
{`${quiz.title} (${quiz.content.length} question${
|
||||||
|
quiz.content.length > 1 ? 's' : ''
|
||||||
|
})`}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='actions'>
|
<div className="actions">
|
||||||
<Tooltip title="Télécharger quiz" placement="top">
|
<Tooltip title="Télécharger quiz" placement="top">
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => downloadTxtFile(quiz)}
|
onClick={() => downloadTxtFile(quiz)}
|
||||||
> <FileDownload /> </IconButton>
|
>
|
||||||
|
{' '}
|
||||||
|
<FileDownload />{' '}
|
||||||
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip title="Modifier quiz" placement="top">
|
<Tooltip title="Modifier quiz" placement="top">
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => handleEditQuiz(quiz)}
|
onClick={() => handleEditQuiz(quiz)}
|
||||||
> <Edit /> </IconButton>
|
>
|
||||||
|
{' '}
|
||||||
|
<Edit />{' '}
|
||||||
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip title="Dupliquer quiz" placement="top">
|
<Tooltip title="Dupliquer quiz" placement="top">
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => handleDuplicateQuiz(quiz)}
|
onClick={() => handleDuplicateQuiz(quiz)}
|
||||||
> <ContentCopy /> </IconButton>
|
>
|
||||||
|
{' '}
|
||||||
|
<ContentCopy />{' '}
|
||||||
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip title="Supprimer quiz" placement="top">
|
<Tooltip title="Supprimer quiz" placement="top">
|
||||||
|
|
@ -507,14 +638,20 @@ const Dashboard: React.FC = () => {
|
||||||
aria-label="delete"
|
aria-label="delete"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => handleRemoveQuiz(quiz)}
|
onClick={() => handleRemoveQuiz(quiz)}
|
||||||
> <DeleteOutline /> </IconButton>
|
>
|
||||||
|
{' '}
|
||||||
|
<DeleteOutline />{' '}
|
||||||
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip title="Partager quiz" placement="top">
|
<Tooltip title="Partager quiz" placement="top">
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => handleShareQuiz(quiz)}
|
onClick={() => handleShareQuiz(quiz)}
|
||||||
> <Share /> </IconButton>
|
>
|
||||||
|
{' '}
|
||||||
|
<Share />{' '}
|
||||||
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -529,7 +666,6 @@ const Dashboard: React.FC = () => {
|
||||||
handleOnImport={handleOnImport}
|
handleOnImport={handleOnImport}
|
||||||
selectedFolder={selectedFolderId}
|
selectedFolder={selectedFolderId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -542,4 +678,3 @@ function addFolderTitleToQuizzes(folderQuizzes: string | QuizType[], folderName:
|
||||||
console.log(`quiz: ${quiz.title} folder: ${quiz.folderName}`);
|
console.log(`quiz: ${quiz.title} folder: ${quiz.folderName}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,93 @@
|
||||||
// ManageRoom.tsx
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { ParsedGIFTQuestion, BaseQuestion, parse, Question } from 'gift-pegjs';
|
import { ParsedGIFTQuestion, BaseQuestion, parse, Question } from 'gift-pegjs';
|
||||||
import { isSimpleNumericalAnswer, isRangeNumericalAnswer, isHighLowNumericalAnswer } from "gift-pegjs/typeGuards";
|
import {
|
||||||
|
isSimpleNumericalAnswer,
|
||||||
|
isRangeNumericalAnswer,
|
||||||
|
isHighLowNumericalAnswer
|
||||||
|
} from 'gift-pegjs/typeGuards';
|
||||||
import LiveResultsComponent from 'src/components/LiveResults/LiveResults';
|
import LiveResultsComponent from 'src/components/LiveResults/LiveResults';
|
||||||
// import { QuestionService } from '../../../services/QuestionService';
|
import webSocketService, {
|
||||||
import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService';
|
AnswerReceptionFromBackendType
|
||||||
|
} from '../../../services/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 { ENV_VARIABLES } from 'src/constants';
|
import { ENV_VARIABLES } from 'src/constants';
|
||||||
import { StudentType, Answer } from '../../../Types/StudentType';
|
import { StudentType, Answer } from '../../../Types/StudentType';
|
||||||
import { Button } from '@mui/material';
|
|
||||||
import LoadingCircle from 'src/components/LoadingCircle/LoadingCircle';
|
import LoadingCircle from 'src/components/LoadingCircle/LoadingCircle';
|
||||||
import { Refresh, Error } from '@mui/icons-material';
|
import { Refresh, Error } from '@mui/icons-material';
|
||||||
import StudentWaitPage from 'src/components/StudentWaitPage/StudentWaitPage';
|
import StudentWaitPage from 'src/components/StudentWaitPage/StudentWaitPage';
|
||||||
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
|
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
|
||||||
//import QuestionNavigation from 'src/components/QuestionNavigation/QuestionNavigation';
|
|
||||||
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';
|
||||||
|
|
||||||
const ManageRoom: React.FC = () => {
|
const ManageRoom: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [roomName, setRoomName] = useState<string>('');
|
|
||||||
const [socket, setSocket] = useState<Socket | null>(null);
|
const [socket, setSocket] = useState<Socket | null>(null);
|
||||||
const [students, setStudents] = useState<StudentType[]>([]);
|
const [students, setStudents] = useState<StudentType[]>([]);
|
||||||
const quizId = useParams<{ id: string }>();
|
const { quizId = '', roomName = '' } = useParams<{ quizId: string, roomName: string }>();
|
||||||
const [quizQuestions, setQuizQuestions] = useState<QuestionType[] | undefined>();
|
const [quizQuestions, setQuizQuestions] = useState<QuestionType[] | undefined>();
|
||||||
const [quiz, setQuiz] = useState<QuizType | null>(null);
|
const [quiz, setQuiz] = useState<QuizType | null>(null);
|
||||||
const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher');
|
const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher');
|
||||||
const [connectingError, setConnectingError] = useState<string>('');
|
const [connectingError, setConnectingError] = useState<string>('');
|
||||||
const [currentQuestion, setCurrentQuestion] = useState<QuestionType | undefined>(undefined);
|
const [currentQuestion, setCurrentQuestion] = useState<QuestionType | undefined>(undefined);
|
||||||
const [quizStarted, setQuizStarted] = useState(false);
|
const [quizStarted, setQuizStarted] = useState(false);
|
||||||
|
const [formattedRoomName, setFormattedRoomName] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (quizId.id) {
|
const verifyLogin = async () => {
|
||||||
const fetchquiz = async () => {
|
if (!ApiService.isLoggedIn()) {
|
||||||
|
navigate('/teacher/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const quiz = await ApiService.getQuiz(quizId.id as string);
|
verifyLogin();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!roomName || !quizId) {
|
||||||
|
window.alert(
|
||||||
|
`Une erreur est survenue.\n La salle ou le quiz n'a pas été spécifié.\nVeuillez réessayer plus tard.`
|
||||||
|
);
|
||||||
|
console.error(`Room "${roomName}" or Quiz "${quizId}" not found.`);
|
||||||
|
navigate('/teacher/dashboard');
|
||||||
|
}
|
||||||
|
if (roomName && !socket) {
|
||||||
|
createWebSocketRoom();
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
disconnectWebSocket();
|
||||||
|
};
|
||||||
|
}, [roomName, navigate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (quizId) {
|
||||||
|
const fetchQuiz = async () => {
|
||||||
|
const quiz = await ApiService.getQuiz(quizId);
|
||||||
|
|
||||||
if (!quiz) {
|
if (!quiz) {
|
||||||
window.alert(`Une erreur est survenue.\n Le quiz ${quizId.id} n'a pas été trouvé\nVeuillez réessayer plus tard`)
|
window.alert(
|
||||||
console.error('Quiz not found for id:', quizId.id);
|
`Une erreur est survenue.\n Le quiz ${quizId} n'a pas été trouvé\nVeuillez réessayer plus tard`
|
||||||
|
);
|
||||||
|
console.error('Quiz not found for id:', quizId);
|
||||||
navigate('/teacher/dashboard');
|
navigate('/teacher/dashboard');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setQuiz(quiz as QuizType);
|
setQuiz(quiz as QuizType);
|
||||||
|
|
||||||
if (!socket) {
|
|
||||||
console.log(`no socket in ManageRoom, creating one.`);
|
|
||||||
createWebSocketRoom();
|
|
||||||
}
|
|
||||||
|
|
||||||
// return () => {
|
|
||||||
// webSocketService.disconnect();
|
|
||||||
// };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchquiz();
|
fetchQuiz();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
window.alert(`Une erreur est survenue.\n Le quiz ${quizId.id} n'a pas été trouvé\nVeuillez réessayer plus tard`)
|
window.alert(
|
||||||
console.error('Quiz not found for id:', quizId.id);
|
`Une erreur est survenue.\n Le quiz ${quizId} n'a pas été trouvé\nVeuillez réessayer plus tard`
|
||||||
|
);
|
||||||
|
console.error('Quiz not found for id:', quizId);
|
||||||
navigate('/teacher/dashboard');
|
navigate('/teacher/dashboard');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -73,75 +95,69 @@ const ManageRoom: React.FC = () => {
|
||||||
|
|
||||||
const disconnectWebSocket = () => {
|
const disconnectWebSocket = () => {
|
||||||
if (socket) {
|
if (socket) {
|
||||||
webSocketService.endQuiz(roomName);
|
webSocketService.endQuiz(formattedRoomName);
|
||||||
webSocketService.disconnect();
|
webSocketService.disconnect();
|
||||||
setSocket(null);
|
setSocket(null);
|
||||||
setQuizQuestions(undefined);
|
setQuizQuestions(undefined);
|
||||||
setCurrentQuestion(undefined);
|
setCurrentQuestion(undefined);
|
||||||
setStudents(new Array<StudentType>());
|
setStudents(new Array<StudentType>());
|
||||||
setRoomName('');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const createWebSocketRoom = () => {
|
const createWebSocketRoom = () => {
|
||||||
console.log('Creating WebSocket room...');
|
|
||||||
setConnectingError('');
|
|
||||||
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||||
|
const roomNameUpper = roomName.toUpperCase();
|
||||||
|
setFormattedRoomName(roomNameUpper);
|
||||||
|
console.log(`Creating WebSocket room named ${roomNameUpper}`);
|
||||||
socket.on('connect', () => {
|
socket.on('connect', () => {
|
||||||
webSocketService.createRoom();
|
webSocketService.createRoom(roomNameUpper);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('connect_error', (error) => {
|
socket.on('connect_error', (error) => {
|
||||||
setConnectingError('Erreur lors de la connexion... Veuillez réessayer');
|
setConnectingError('Erreur lors de la connexion... Veuillez réessayer');
|
||||||
console.error('ManageRoom: WebSocket connection error:', error);
|
console.error('ManageRoom: WebSocket connection error:', error);
|
||||||
});
|
});
|
||||||
socket.on('create-success', (roomName: string) => {
|
|
||||||
setRoomName(roomName);
|
socket.on('create-success', (createdRoomName: string) => {
|
||||||
});
|
console.log(`Room created: ${createdRoomName}`);
|
||||||
socket.on('create-failure', () => {
|
|
||||||
console.log('Error creating room.');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('user-joined', (student: StudentType) => {
|
socket.on('user-joined', (student: StudentType) => {
|
||||||
console.log(`Student joined: name = ${student.name}, id = ${student.id}`);
|
console.log(`Student joined: name = ${student.name}, id = ${student.id}, quizMode = ${quizMode}, quizStarted = ${quizStarted}`);
|
||||||
|
|
||||||
setStudents((prevStudents) => [...prevStudents, student]);
|
setStudents((prevStudents) => [...prevStudents, student]);
|
||||||
|
|
||||||
|
// only send nextQuestion if the quiz has started
|
||||||
|
if (!quizStarted) return;
|
||||||
|
|
||||||
if (quizMode === 'teacher') {
|
if (quizMode === 'teacher') {
|
||||||
webSocketService.nextQuestion(roomName, currentQuestion);
|
webSocketService.nextQuestion(formattedRoomName, currentQuestion);
|
||||||
} else if (quizMode === 'student') {
|
} else if (quizMode === 'student') {
|
||||||
webSocketService.launchStudentModeQuiz(roomName, quizQuestions);
|
webSocketService.launchStudentModeQuiz(formattedRoomName, quizQuestions);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
socket.on('join-failure', (message) => {
|
socket.on('join-failure', (message) => {
|
||||||
setConnectingError(message);
|
setConnectingError(message);
|
||||||
setSocket(null);
|
setSocket(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('user-disconnected', (userId: string) => {
|
socket.on('user-disconnected', (userId: string) => {
|
||||||
console.log(`Student left: id = ${userId}`);
|
console.log(`Student left: id = ${userId}`);
|
||||||
setStudents((prevUsers) => prevUsers.filter((user) => user.id !== userId));
|
setStudents((prevUsers) => prevUsers.filter((user) => user.id !== userId));
|
||||||
});
|
});
|
||||||
|
|
||||||
setSocket(socket);
|
setSocket(socket);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// This is here to make sure the correct value is sent when user join
|
|
||||||
if (socket) {
|
|
||||||
console.log(`Listening for user-joined in room ${roomName}`);
|
|
||||||
socket.on('user-joined', (_student: StudentType) => {
|
|
||||||
if (quizMode === 'teacher') {
|
|
||||||
webSocketService.nextQuestion(roomName, currentQuestion);
|
|
||||||
} else if (quizMode === 'student') {
|
|
||||||
webSocketService.launchStudentModeQuiz(roomName, quizQuestions);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (socket) {
|
if (socket) {
|
||||||
// handle the case where user submits an answer
|
console.log(`Listening for submit-answer-room in room ${formattedRoomName}`);
|
||||||
console.log(`Listening for submit-answer-room in room ${roomName}`);
|
|
||||||
socket.on('submit-answer-room', (answerData: AnswerReceptionFromBackendType) => {
|
socket.on('submit-answer-room', (answerData: AnswerReceptionFromBackendType) => {
|
||||||
const { answer, idQuestion, idUser, username } = answerData;
|
const { answer, idQuestion, idUser, username } = answerData;
|
||||||
console.log(`Received answer from ${username} for question ${idQuestion}: ${answer}`);
|
console.log(
|
||||||
|
`Received answer from ${username} for question ${idQuestion}: ${answer}`
|
||||||
|
);
|
||||||
if (!quizQuestions) {
|
if (!quizQuestions) {
|
||||||
console.log('Quiz questions not found (cannot update answers without them).');
|
console.log('Quiz questions not found (cannot update answers without them).');
|
||||||
return;
|
return;
|
||||||
|
|
@ -149,7 +165,6 @@ const ManageRoom: React.FC = () => {
|
||||||
|
|
||||||
// Update the students state using the functional form of setStudents
|
// Update the students state using the functional form of setStudents
|
||||||
setStudents((prevStudents) => {
|
setStudents((prevStudents) => {
|
||||||
// print the list of current student names
|
|
||||||
console.log('Current students:');
|
console.log('Current students:');
|
||||||
prevStudents.forEach((student) => {
|
prevStudents.forEach((student) => {
|
||||||
console.log(student.name);
|
console.log(student.name);
|
||||||
|
|
@ -160,17 +175,31 @@ const ManageRoom: React.FC = () => {
|
||||||
console.log(`Comparing ${student.id} to ${idUser}`);
|
console.log(`Comparing ${student.id} to ${idUser}`);
|
||||||
if (student.id === idUser) {
|
if (student.id === idUser) {
|
||||||
foundStudent = true;
|
foundStudent = true;
|
||||||
const existingAnswer = student.answers.find((ans) => ans.idQuestion === idQuestion);
|
const existingAnswer = student.answers.find(
|
||||||
|
(ans) => ans.idQuestion === idQuestion
|
||||||
|
);
|
||||||
let updatedAnswers: Answer[] = [];
|
let updatedAnswers: Answer[] = [];
|
||||||
if (existingAnswer) {
|
if (existingAnswer) {
|
||||||
// Update the existing answer
|
|
||||||
updatedAnswers = student.answers.map((ans) => {
|
updatedAnswers = student.answers.map((ans) => {
|
||||||
console.log(`Comparing ${ans.idQuestion} to ${idQuestion}`);
|
console.log(`Comparing ${ans.idQuestion} to ${idQuestion}`);
|
||||||
return (ans.idQuestion === idQuestion ? { ...ans, answer, isCorrect: checkIfIsCorrect(answer, idQuestion, quizQuestions!) } : ans);
|
return ans.idQuestion === idQuestion
|
||||||
|
? {
|
||||||
|
...ans,
|
||||||
|
answer,
|
||||||
|
isCorrect: checkIfIsCorrect(
|
||||||
|
answer,
|
||||||
|
idQuestion,
|
||||||
|
quizQuestions!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
: ans;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Add a new answer
|
const newAnswer = {
|
||||||
const newAnswer = { idQuestion, answer, isCorrect: checkIfIsCorrect(answer, idQuestion, quizQuestions!) };
|
idQuestion,
|
||||||
|
answer,
|
||||||
|
isCorrect: checkIfIsCorrect(answer, idQuestion, quizQuestions!)
|
||||||
|
};
|
||||||
updatedAnswers = [...student.answers, newAnswer];
|
updatedAnswers = [...student.answers, newAnswer];
|
||||||
}
|
}
|
||||||
return { ...student, answers: updatedAnswers };
|
return { ...student, answers: updatedAnswers };
|
||||||
|
|
@ -185,73 +214,8 @@ const ManageRoom: React.FC = () => {
|
||||||
});
|
});
|
||||||
setSocket(socket);
|
setSocket(socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
}, [socket, currentQuestion, quizQuestions]);
|
}, [socket, currentQuestion, quizQuestions]);
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (socket) {
|
|
||||||
// const submitAnswerHandler = (answerData: answerSubmissionType) => {
|
|
||||||
// const { answer, idQuestion, username } = answerData;
|
|
||||||
// console.log(`Received answer from ${username} for question ${idQuestion}: ${answer}`);
|
|
||||||
|
|
||||||
// // print the list of current student names
|
|
||||||
// console.log('Current students:');
|
|
||||||
// students.forEach((student) => {
|
|
||||||
// console.log(student.name);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // Update the students state using the functional form of setStudents
|
|
||||||
// setStudents((prevStudents) => {
|
|
||||||
// let foundStudent = false;
|
|
||||||
// const updatedStudents = prevStudents.map((student) => {
|
|
||||||
// if (student.id === username) {
|
|
||||||
// foundStudent = true;
|
|
||||||
// const updatedAnswers = student.answers.map((ans) => {
|
|
||||||
// const newAnswer: Answer = { answer, isCorrect: checkIfIsCorrect(answer, idQuestion, quizQuestions!), idQuestion };
|
|
||||||
// console.log(`Updating answer for ${student.name} for question ${idQuestion} to ${answer}`);
|
|
||||||
// return (ans.idQuestion === idQuestion ? { ...ans, newAnswer } : ans);
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
// return { ...student, answers: updatedAnswers };
|
|
||||||
// }
|
|
||||||
// return student;
|
|
||||||
// });
|
|
||||||
// if (!foundStudent) {
|
|
||||||
// console.log(`Student ${username} not found in the list of students in LiveResults`);
|
|
||||||
// }
|
|
||||||
// return updatedStudents;
|
|
||||||
// });
|
|
||||||
|
|
||||||
|
|
||||||
// // make a copy of the students array so we can update it
|
|
||||||
// // const updatedStudents = [...students];
|
|
||||||
|
|
||||||
// // const student = updatedStudents.find((student) => student.id === idUser);
|
|
||||||
// // if (!student) {
|
|
||||||
// // // this is a bad thing if an answer was submitted but the student isn't in the list
|
|
||||||
// // console.log(`Student ${idUser} not found in the list of students in LiveResults`);
|
|
||||||
// // return;
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// // const isCorrect = checkIfIsCorrect(answer, idQuestion);
|
|
||||||
// // const newAnswer: Answer = { answer, isCorrect, idQuestion };
|
|
||||||
// // student.answers.push(newAnswer);
|
|
||||||
// // // print list of answers
|
|
||||||
// // console.log('Answers:');
|
|
||||||
// // student.answers.forEach((answer) => {
|
|
||||||
// // console.log(answer.answer);
|
|
||||||
// // });
|
|
||||||
// // setStudents(updatedStudents); // update the state
|
|
||||||
// };
|
|
||||||
|
|
||||||
// socket.on('submit-answer', submitAnswerHandler);
|
|
||||||
// return () => {
|
|
||||||
// socket.off('submit-answer');
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
// }, [socket]);
|
|
||||||
|
|
||||||
|
|
||||||
const nextQuestion = () => {
|
const nextQuestion = () => {
|
||||||
if (!quizQuestions || !currentQuestion || !quiz?.content) return;
|
if (!quizQuestions || !currentQuestion || !quiz?.content) return;
|
||||||
|
|
||||||
|
|
@ -260,7 +224,7 @@ const ManageRoom: React.FC = () => {
|
||||||
if (nextQuestionIndex === undefined || nextQuestionIndex > quizQuestions.length - 1) return;
|
if (nextQuestionIndex === undefined || nextQuestionIndex > quizQuestions.length - 1) return;
|
||||||
|
|
||||||
setCurrentQuestion(quizQuestions[nextQuestionIndex]);
|
setCurrentQuestion(quizQuestions[nextQuestionIndex]);
|
||||||
webSocketService.nextQuestion(roomName, quizQuestions[nextQuestionIndex]);
|
webSocketService.nextQuestion(formattedRoomName, quizQuestions[nextQuestionIndex]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const previousQuestion = () => {
|
const previousQuestion = () => {
|
||||||
|
|
@ -270,7 +234,7 @@ const ManageRoom: React.FC = () => {
|
||||||
|
|
||||||
if (prevQuestionIndex === undefined || prevQuestionIndex < 0) return;
|
if (prevQuestionIndex === undefined || prevQuestionIndex < 0) return;
|
||||||
setCurrentQuestion(quizQuestions[prevQuestionIndex]);
|
setCurrentQuestion(quizQuestions[prevQuestionIndex]);
|
||||||
webSocketService.nextQuestion(roomName, quizQuestions[prevQuestionIndex]);
|
webSocketService.nextQuestion(formattedRoomName, quizQuestions[prevQuestionIndex]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const initializeQuizQuestion = () => {
|
const initializeQuizQuestion = () => {
|
||||||
|
|
@ -298,7 +262,7 @@ const ManageRoom: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentQuestion(quizQuestions[0]);
|
setCurrentQuestion(quizQuestions[0]);
|
||||||
webSocketService.nextQuestion(roomName, quizQuestions[0]);
|
webSocketService.nextQuestion(formattedRoomName, quizQuestions[0]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const launchStudentMode = () => {
|
const launchStudentMode = () => {
|
||||||
|
|
@ -310,13 +274,15 @@ const ManageRoom: React.FC = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setQuizQuestions(quizQuestions);
|
setQuizQuestions(quizQuestions);
|
||||||
webSocketService.launchStudentModeQuiz(roomName, quizQuestions);
|
webSocketService.launchStudentModeQuiz(formattedRoomName, quizQuestions);
|
||||||
};
|
};
|
||||||
|
|
||||||
const launchQuiz = () => {
|
const launchQuiz = () => {
|
||||||
if (!socket || !roomName || !quiz?.content || quiz?.content.length === 0) {
|
if (!socket || !formattedRoomName || !quiz?.content || quiz?.content.length === 0) {
|
||||||
// TODO: This error happens when token expires! Need to handle it properly
|
// TODO: This error happens when token expires! Need to handle it properly
|
||||||
console.log(`Error launching quiz. socket: ${socket}, roomName: ${roomName}, quiz: ${quiz}`);
|
console.log(
|
||||||
|
`Error launching quiz. socket: ${socket}, roomName: ${formattedRoomName}, quiz: ${quiz}`
|
||||||
|
);
|
||||||
setQuizStarted(true);
|
setQuizStarted(true);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
@ -328,7 +294,6 @@ const ManageRoom: React.FC = () => {
|
||||||
case 'teacher':
|
case 'teacher':
|
||||||
setQuizStarted(true);
|
setQuizStarted(true);
|
||||||
return launchTeacherMode();
|
return launchTeacherMode();
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -337,7 +302,7 @@ const ManageRoom: React.FC = () => {
|
||||||
setCurrentQuestion(quizQuestions[questionIndex]);
|
setCurrentQuestion(quizQuestions[questionIndex]);
|
||||||
|
|
||||||
if (quizMode === 'teacher') {
|
if (quizMode === 'teacher') {
|
||||||
webSocketService.nextQuestion(roomName, quizQuestions[questionIndex]);
|
webSocketService.nextQuestion(formattedRoomName, quizQuestions[questionIndex]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -347,7 +312,11 @@ const ManageRoom: React.FC = () => {
|
||||||
navigate('/teacher/dashboard');
|
navigate('/teacher/dashboard');
|
||||||
};
|
};
|
||||||
|
|
||||||
function checkIfIsCorrect(answer: string | number | boolean, idQuestion: number, questions: QuestionType[]): boolean {
|
function checkIfIsCorrect(
|
||||||
|
answer: string | number | boolean,
|
||||||
|
idQuestion: number,
|
||||||
|
questions: QuestionType[]
|
||||||
|
): boolean {
|
||||||
const questionInfo = questions.find((q) =>
|
const questionInfo = questions.find((q) =>
|
||||||
q.question.id ? q.question.id === idQuestion.toString() : false
|
q.question.id ? q.question.id === idQuestion.toString() : false
|
||||||
) as QuestionType | undefined;
|
) as QuestionType | undefined;
|
||||||
|
|
@ -370,8 +339,7 @@ const ManageRoom: React.FC = () => {
|
||||||
const answerNumber = parseFloat(answerText);
|
const answerNumber = parseFloat(answerText);
|
||||||
if (!isNaN(answerNumber)) {
|
if (!isNaN(answerNumber)) {
|
||||||
return (
|
return (
|
||||||
answerNumber <= choice.numberHigh &&
|
answerNumber <= choice.numberHigh && answerNumber >= choice.numberLow
|
||||||
answerNumber >= choice.numberLow
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -401,8 +369,7 @@ const ManageRoom: React.FC = () => {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!formattedRoomName) {
|
||||||
if (!roomName) {
|
|
||||||
return (
|
return (
|
||||||
<div className="center">
|
<div className="center">
|
||||||
{!connectingError ? (
|
{!connectingError ? (
|
||||||
|
|
@ -425,47 +392,51 @@ const ManageRoom: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='room'>
|
<div className="room">
|
||||||
<div className='roomHeader'>
|
<h1>Salle : {formattedRoomName}</h1>
|
||||||
|
<div className="roomHeader">
|
||||||
<DisconnectButton
|
<DisconnectButton
|
||||||
onReturn={handleReturn}
|
onReturn={handleReturn}
|
||||||
askConfirm
|
askConfirm
|
||||||
message={`Êtes-vous sûr de vouloir quitter?`} />
|
message={`Êtes-vous sûr de vouloir quitter?`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="headerContent"
|
||||||
|
style={{
|
||||||
<div className='headerContent' style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
|
display: 'flex',
|
||||||
<div style={{ flex: 1, display: 'flex', justifyContent: 'center' }}>
|
justifyContent: 'space-between',
|
||||||
<div className='title'>Salle: {roomName}</div>
|
alignItems: 'center',
|
||||||
</div>
|
width: '100%'
|
||||||
{quizStarted && (
|
}}
|
||||||
<div className='userCount subtitle smallText' style={{ display: 'flex', alignItems: 'center' }}>
|
>
|
||||||
|
{(
|
||||||
|
<div
|
||||||
|
className="userCount subtitle smallText"
|
||||||
|
style={{ display: "flex", justifyContent: "flex-end" }}
|
||||||
|
>
|
||||||
<GroupIcon style={{ marginRight: '5px' }} />
|
<GroupIcon style={{ marginRight: '5px' }} />
|
||||||
{students.length}/60
|
{students.length}/60
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='dumb'></div>
|
<div className="dumb"></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* the following breaks the css (if 'room' classes are nested) */}
|
{/* the following breaks the css (if 'room' classes are nested) */}
|
||||||
<div className=''>
|
<div className="">
|
||||||
|
|
||||||
{quizQuestions ? (
|
{quizQuestions ? (
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
<div className="title center-h-align mb-2">{quiz?.title}</div>
|
<div className="title center-h-align mb-2">{quiz?.title}</div>
|
||||||
{!isNaN(Number(currentQuestion?.question.id)) && (
|
{!isNaN(Number(currentQuestion?.question.id)) && (
|
||||||
<strong className='number of questions'>
|
<strong className="number of questions">
|
||||||
Question {Number(currentQuestion?.question.id)}/{quizQuestions?.length}
|
Question {Number(currentQuestion?.question.id)}/
|
||||||
|
{quizQuestions?.length}
|
||||||
</strong>
|
</strong>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{quizMode === 'teacher' && (
|
{quizMode === 'teacher' && (
|
||||||
|
|
||||||
<div className="mb-1">
|
<div className="mb-1">
|
||||||
{/* <QuestionNavigation
|
{/* <QuestionNavigation
|
||||||
currentQuestionId={Number(currentQuestion?.question.id)}
|
currentQuestionId={Number(currentQuestion?.question.id)}
|
||||||
|
|
@ -474,12 +445,10 @@ const ManageRoom: React.FC = () => {
|
||||||
nextQuestion={nextQuestion}
|
nextQuestion={nextQuestion}
|
||||||
/> */}
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mb-2 flex-column-wrapper">
|
<div className="mb-2 flex-column-wrapper">
|
||||||
<div className="preview-and-result-container">
|
<div className="preview-and-result-container">
|
||||||
|
|
||||||
{currentQuestion && (
|
{currentQuestion && (
|
||||||
<QuestionDisplay
|
<QuestionDisplay
|
||||||
showAnswer={false}
|
showAnswer={false}
|
||||||
|
|
@ -496,42 +465,46 @@ const ManageRoom: React.FC = () => {
|
||||||
showSelectedQuestion={showSelectedQuestion}
|
showSelectedQuestion={showSelectedQuestion}
|
||||||
students={students}
|
students={students}
|
||||||
></LiveResultsComponent>
|
></LiveResultsComponent>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{quizMode === 'teacher' && (
|
{quizMode === 'teacher' && (
|
||||||
<div className="questionNavigationButtons" style={{ display: 'flex', justifyContent: 'center' }}>
|
<div
|
||||||
|
className="questionNavigationButtons"
|
||||||
|
style={{ display: 'flex', justifyContent: 'center' }}
|
||||||
|
>
|
||||||
<div className="previousQuestionButton">
|
<div className="previousQuestionButton">
|
||||||
<Button onClick={previousQuestion}
|
<Button
|
||||||
|
onClick={previousQuestion}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={Number(currentQuestion?.question.id) <= 1}>
|
disabled={Number(currentQuestion?.question.id) <= 1}
|
||||||
|
>
|
||||||
Question précédente
|
Question précédente
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="nextQuestionButton">
|
<div className="nextQuestionButton">
|
||||||
<Button onClick={nextQuestion}
|
<Button
|
||||||
|
onClick={nextQuestion}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={Number(currentQuestion?.question.id) >= quizQuestions.length}
|
disabled={
|
||||||
|
Number(currentQuestion?.question.id) >=
|
||||||
|
quizQuestions.length
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Prochaine question
|
Prochaine question
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>)}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
) : (
|
) : (
|
||||||
|
|
||||||
<StudentWaitPage
|
<StudentWaitPage
|
||||||
students={students}
|
students={students}
|
||||||
launchQuiz={launchQuiz}
|
launchQuiz={launchQuiz}
|
||||||
setQuizMode={setQuizMode}
|
setQuizMode={setQuizMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
59
client/src/pages/Teacher/ManageRoom/RoomContext.tsx
Normal file
59
client/src/pages/Teacher/ManageRoom/RoomContext.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import ApiService from '../../../services/ApiService';
|
||||||
|
import { RoomType } from 'src/Types/RoomType';
|
||||||
|
import React from "react";
|
||||||
|
import { RoomContext } from './useRooms';
|
||||||
|
|
||||||
|
export const RoomProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const [rooms, setRooms] = useState<RoomType[]>([]);
|
||||||
|
const [selectedRoom, setSelectedRoom] = useState<RoomType | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadRooms = async () => {
|
||||||
|
const userRooms = await ApiService.getUserRooms();
|
||||||
|
const roomsList = userRooms as RoomType[];
|
||||||
|
setRooms(roomsList);
|
||||||
|
|
||||||
|
const savedRoomId = localStorage.getItem('selectedRoomId');
|
||||||
|
if (savedRoomId) {
|
||||||
|
const savedRoom = roomsList.find(r => r._id === savedRoomId);
|
||||||
|
if (savedRoom) {
|
||||||
|
setSelectedRoom(savedRoom);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roomsList.length > 0) {
|
||||||
|
setSelectedRoom(roomsList[0]);
|
||||||
|
localStorage.setItem('selectedRoomId', roomsList[0]._id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadRooms();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Sélectionner une salle
|
||||||
|
const selectRoom = (roomId: string) => {
|
||||||
|
const room = rooms.find(r => r._id === roomId) || null;
|
||||||
|
setSelectedRoom(room);
|
||||||
|
localStorage.setItem('selectedRoomId', roomId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Créer une salle
|
||||||
|
const createRoom = async (title: string) => {
|
||||||
|
// Créer la salle et récupérer l'objet complet
|
||||||
|
const newRoom = await ApiService.createRoom(title);
|
||||||
|
|
||||||
|
// Mettre à jour la liste des salles
|
||||||
|
const updatedRooms = await ApiService.getUserRooms();
|
||||||
|
setRooms(updatedRooms as RoomType[]);
|
||||||
|
|
||||||
|
// Sélectionner la nouvelle salle avec son ID
|
||||||
|
selectRoom(newRoom); // Utiliser l'ID de l'objet retourné
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<RoomContext.Provider value={{ rooms, selectedRoom, selectRoom, createRoom }}>
|
||||||
|
{children}
|
||||||
|
</RoomContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
20
client/src/pages/Teacher/ManageRoom/useRooms.ts
Normal file
20
client/src/pages/Teacher/ManageRoom/useRooms.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { RoomType } from 'src/Types/RoomType';
|
||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
//import { RoomContext } from './RoomContext';
|
||||||
|
|
||||||
|
type RoomContextType = {
|
||||||
|
rooms: RoomType[];
|
||||||
|
selectedRoom: RoomType | null;
|
||||||
|
selectRoom: (roomId: string) => void;
|
||||||
|
createRoom: (title: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RoomContext = createContext<RoomContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const useRooms = () => {
|
||||||
|
const context = useContext(RoomContext);
|
||||||
|
if (!context) throw new Error('useRooms must be used within a RoomProvider');
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
@ -4,6 +4,7 @@ import { ENV_VARIABLES } from '../constants';
|
||||||
|
|
||||||
import { FolderType } from 'src/Types/FolderType';
|
import { FolderType } from 'src/Types/FolderType';
|
||||||
import { QuizType } from 'src/Types/QuizType';
|
import { QuizType } from 'src/Types/QuizType';
|
||||||
|
import { RoomType } from 'src/Types/RoomType';
|
||||||
|
|
||||||
type ApiResponse = boolean | string;
|
type ApiResponse = boolean | string;
|
||||||
|
|
||||||
|
|
@ -164,6 +165,7 @@ class ApiService {
|
||||||
* @returns A error string if unsuccessful,
|
* @returns A error string if unsuccessful,
|
||||||
*/
|
*/
|
||||||
public async register(name: string, email: string, password: string, roles: string[]): Promise<any> {
|
public async register(name: string, email: string, password: string, roles: string[]): Promise<any> {
|
||||||
|
console.log(`ApiService.register: name: ${name}, email: ${email}, password: ${password}, roles: ${roles}`);
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
|
|
@ -178,7 +180,8 @@ class ApiService {
|
||||||
|
|
||||||
console.log(result);
|
console.log(result);
|
||||||
if (result.status == 200) {
|
if (result.status == 200) {
|
||||||
window.location.href = result.request.responseURL;
|
//window.location.href = result.request.responseURL;
|
||||||
|
window.location.href = '/login';
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new Error(`La connexion a échoué. Status: ${result.status}`);
|
throw new Error(`La connexion a échoué. Status: ${result.status}`);
|
||||||
|
|
@ -199,15 +202,12 @@ class ApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns true if successful
|
|
||||||
* @returns A error string if unsuccessful,
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* @returns true if successful
|
* @returns true if successful
|
||||||
* @returns An error string if unsuccessful
|
* @returns An error string if unsuccessful
|
||||||
*/
|
*/
|
||||||
public async login(email: string, password: string): Promise<any> {
|
public async login(email: string, password: string): Promise<any> {
|
||||||
|
console.log(`login: email: ${email}, password: ${password}`);
|
||||||
try {
|
try {
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
throw new Error("L'email et le mot de passe sont requis.");
|
throw new Error("L'email et le mot de passe sont requis.");
|
||||||
|
|
@ -217,11 +217,16 @@ public async login(email: string, password: string): Promise<any> {
|
||||||
const headers = this.constructRequestHeaders();
|
const headers = this.constructRequestHeaders();
|
||||||
const body = { email, password };
|
const body = { email, password };
|
||||||
|
|
||||||
|
console.log(`login: POST ${url} body: ${JSON.stringify(body)}`);
|
||||||
const result: AxiosResponse = await axios.post(url, body, { headers: headers });
|
const result: AxiosResponse = await axios.post(url, body, { headers: headers });
|
||||||
|
console.log(`login: result: ${result.status}, ${result.data}`);
|
||||||
|
|
||||||
// If login is successful, redirect the user
|
// If login is successful, redirect the user
|
||||||
if (result.status === 200) {
|
if (result.status === 200) {
|
||||||
window.location.href = result.request.responseURL;
|
//window.location.href = result.request.responseURL;
|
||||||
|
this.saveToken(result.data.token);
|
||||||
|
this.saveUsername(result.data.username);
|
||||||
|
window.location.href = '/teacher/dashboard';
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`La connexion a échoué. Statut: ${result.status}`);
|
throw new Error(`La connexion a échoué. Statut: ${result.status}`);
|
||||||
|
|
@ -927,6 +932,195 @@ public async login(email: string, password: string): Promise<any> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//ROOM routes
|
||||||
|
|
||||||
|
public async getUserRooms(): Promise<RoomType[] | string> {
|
||||||
|
try {
|
||||||
|
const url: string = this.constructRequestUrl(`/room/getUserRooms`);
|
||||||
|
const headers = this.constructRequestHeaders();
|
||||||
|
|
||||||
|
const result: AxiosResponse = await axios.get(url, { headers: headers });
|
||||||
|
|
||||||
|
if (result.status !== 200) {
|
||||||
|
throw new Error(`L'obtention des salles utilisateur a échoué. Status: ${result.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data.data.map((room: RoomType) => ({ _id: room._id, title: room.title }));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error details: ", error);
|
||||||
|
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const err = error as AxiosError;
|
||||||
|
const data = err.response?.data as { error: string } | undefined;
|
||||||
|
const url = err.config?.url || 'URL inconnue';
|
||||||
|
return data?.error || `Erreur serveur inconnue lors de la requête (${url}).`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Une erreur inattendue s'est produite.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getRoomContent(roomId: string): Promise<RoomType> {
|
||||||
|
try {
|
||||||
|
const url = this.constructRequestUrl(`/room/${roomId}`);
|
||||||
|
const headers = this.constructRequestHeaders();
|
||||||
|
|
||||||
|
const response = await axios.get<{ data: RoomType }>(url, { headers });
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`Failed to get room: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data.data;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const serverError = error.response?.data?.error;
|
||||||
|
throw new Error(serverError || 'Erreur serveur inconnue');
|
||||||
|
}
|
||||||
|
throw new Error('Erreur réseau');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getRoomTitleByUserId(userId: string): Promise<string[] | string> {
|
||||||
|
try {
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error(`L'ID utilisateur est requis.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url: string = this.constructRequestUrl(`/room/getRoomTitleByUserId/${userId}`);
|
||||||
|
const headers = this.constructRequestHeaders();
|
||||||
|
|
||||||
|
const result: AxiosResponse = await axios.get(url, { headers });
|
||||||
|
|
||||||
|
if (result.status !== 200) {
|
||||||
|
throw new Error(`L'obtention des titres des salles a échoué. Status: ${result.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data.titles;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error details: ", error);
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const err = error as AxiosError;
|
||||||
|
const data = err.response?.data as { error: string } | undefined;
|
||||||
|
return data?.error || 'Erreur serveur inconnue lors de la requête.';
|
||||||
|
}
|
||||||
|
return `Une erreur inattendue s'est produite.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public async getRoomTitle(roomId: string): Promise<string | string> {
|
||||||
|
try {
|
||||||
|
if (!roomId) {
|
||||||
|
throw new Error(`L'ID de la salle est requis.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url: string = this.constructRequestUrl(`/room/getRoomTitle/${roomId}`);
|
||||||
|
const headers = this.constructRequestHeaders();
|
||||||
|
|
||||||
|
const result: AxiosResponse = await axios.get(url, { headers });
|
||||||
|
|
||||||
|
if (result.status !== 200) {
|
||||||
|
throw new Error(`L'obtention du titre de la salle a échoué. Status: ${result.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data.title;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error details: ", error);
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const err = error as AxiosError;
|
||||||
|
const data = err.response?.data as { error: string } | undefined;
|
||||||
|
return data?.error || 'Erreur serveur inconnue lors de la requête.';
|
||||||
|
}
|
||||||
|
return `Une erreur inattendue s'est produite.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public async createRoom(title: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
if (!title) {
|
||||||
|
throw new Error("Le titre de la salle est requis.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const url: string = this.constructRequestUrl(`/room/create`);
|
||||||
|
const headers = this.constructRequestHeaders();
|
||||||
|
const body = { title };
|
||||||
|
|
||||||
|
const result = await axios.post<{ roomId: string }>(url, body, { headers });
|
||||||
|
return `Salle créée avec succès. ID de la salle: ${result.data.roomId}`;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const err = error as AxiosError;
|
||||||
|
|
||||||
|
const serverMessage = (err.response?.data as { message?: string })?.message
|
||||||
|
|| (err.response?.data as { error?: string })?.error
|
||||||
|
|| err.message;
|
||||||
|
|
||||||
|
if (err.response?.status === 409) {
|
||||||
|
throw new Error(serverMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(serverMessage || "Erreur serveur inconnue");
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteRoom(roomId: string): Promise<string | string> {
|
||||||
|
try {
|
||||||
|
if (!roomId) {
|
||||||
|
throw new Error(`L'ID de la salle est requis.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url: string = this.constructRequestUrl(`/room/delete/${roomId}`);
|
||||||
|
const headers = this.constructRequestHeaders();
|
||||||
|
|
||||||
|
const result: AxiosResponse = await axios.delete(url, { headers });
|
||||||
|
|
||||||
|
if (result.status !== 200) {
|
||||||
|
throw new Error(`La suppression de la salle a échoué. Status: ${result.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Salle supprimée avec succès.`;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error details: ", error);
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const err = error as AxiosError;
|
||||||
|
const data = err.response?.data as { error: string } | undefined;
|
||||||
|
return data?.error || 'Erreur serveur inconnue lors de la suppression de la salle.';
|
||||||
|
}
|
||||||
|
return `Une erreur inattendue s'est produite.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async renameRoom(roomId: string, newTitle: string): Promise<string | string> {
|
||||||
|
try {
|
||||||
|
if (!roomId || !newTitle) {
|
||||||
|
throw new Error(`L'ID de la salle et le nouveau titre sont requis.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url: string = this.constructRequestUrl(`/room/rename`);
|
||||||
|
const headers = this.constructRequestHeaders();
|
||||||
|
const body = { roomId, newTitle };
|
||||||
|
|
||||||
|
const result: AxiosResponse = await axios.put(url, body, { headers });
|
||||||
|
|
||||||
|
if (result.status !== 200) {
|
||||||
|
throw new Error(`La mise à jour du titre de la salle a échoué. Status: ${result.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Titre de la salle mis à jour avec succès.`;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error details: ", error);
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const err = error as AxiosError;
|
||||||
|
const data = err.response?.data as { error: string } | undefined;
|
||||||
|
return data?.error || 'Erreur serveur inconnue lors de la mise à jour du titre.';
|
||||||
|
}
|
||||||
|
return `Une erreur inattendue s'est produite.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Images Route
|
// Images Route
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,13 @@ class AuthService {
|
||||||
|
|
||||||
async fetchAuthData(){
|
async fetchAuthData(){
|
||||||
try {
|
try {
|
||||||
|
// console.info(`MODE: ${ENV_VARIABLES.MODE}`);
|
||||||
|
// if (ENV_VARIABLES.MODE === 'development') {
|
||||||
|
// return { authActive: true };
|
||||||
|
// }
|
||||||
const response = await fetch(this.constructRequestUrl('/auth/getActiveAuth'));
|
const response = await fetch(this.constructRequestUrl('/auth/getActiveAuth'));
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
console.log('Data:', JSON.stringify(data));
|
||||||
return data.authActive;
|
return data.authActive;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors de la récupération des données d\'auth:', error);
|
console.error('Erreur lors de la récupération des données d\'auth:', error);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
// WebSocketService.tsx
|
|
||||||
import { io, Socket } from 'socket.io-client';
|
import { io, Socket } from 'socket.io-client';
|
||||||
|
|
||||||
// Must (manually) sync these types to server/socket/socket.js
|
// Must (manually) sync these types to server/socket/socket.js
|
||||||
|
|
@ -46,19 +45,32 @@ class WebSocketService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createRoom() {
|
createRoom(roomName: string) {
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
this.socket.emit('create-room');
|
this.socket.emit('create-room', roomName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deleteRoom(roomName: string) {
|
||||||
|
// console.log('WebsocketService: deleteRoom', roomName);
|
||||||
|
// if (this.socket) {
|
||||||
|
// console.log('WebsocketService: emit: delete-room', roomName);
|
||||||
|
// this.socket.emit('delete-room', roomName);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
nextQuestion(roomName: string, question: unknown) {
|
nextQuestion(roomName: string, question: unknown) {
|
||||||
|
console.log('WebsocketService: nextQuestion', roomName, question);
|
||||||
|
if (!question) {
|
||||||
|
throw new Error('WebsocketService: nextQuestion: question is null');
|
||||||
|
}
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
this.socket.emit('next-question', { roomName, question });
|
this.socket.emit('next-question', { roomName, question });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
launchStudentModeQuiz(roomName: string, questions: unknown) {
|
launchStudentModeQuiz(roomName: string, questions: unknown) {
|
||||||
|
console.log('WebsocketService: launchStudentModeQuiz', roomName, questions, this.socket);
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
this.socket.emit('launch-student-mode', { roomName, questions });
|
this.socket.emit('launch-student-mode', { roomName, questions });
|
||||||
}
|
}
|
||||||
|
|
@ -76,21 +88,9 @@ class WebSocketService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
submitAnswer(answerData: AnswerSubmissionToBackendType
|
submitAnswer(answerData: AnswerSubmissionToBackendType) {
|
||||||
// roomName: string,
|
|
||||||
// answer: string | number | boolean,
|
|
||||||
// username: string,
|
|
||||||
// idQuestion: string
|
|
||||||
) {
|
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
this.socket?.emit('submit-answer',
|
this.socket?.emit('submit-answer', answerData
|
||||||
// {
|
|
||||||
// answer: answer,
|
|
||||||
// roomName: roomName,
|
|
||||||
// username: username,
|
|
||||||
// idQuestion: idQuestion
|
|
||||||
// }
|
|
||||||
answerData
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
class AppError extends Error {
|
class AppError extends Error {
|
||||||
constructor(message, statusCode) {
|
constructor(message, statusCode) {
|
||||||
super(message);
|
super(message);
|
||||||
this.statusCode = statusCode;
|
this.statusCode = statusCode || 500;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Object.setPrototypeOf(this, new.target.prototype);
|
||||||
|
|
||||||
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
}
|
||||||
|
}
|
||||||
module.exports = AppError;
|
module.exports = AppError;
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,7 @@ describe(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
authConfigInstance.loadConfigTest(validModule); // On injecte la configuration mockée
|
authConfigInstance.loadConfigTest(validModule); // On injecte la configuration mockée
|
||||||
|
// TODO new AuthManager(...) essaie d'établir une connexion MongoDB et ça laisse un "open handle" dans Jest
|
||||||
authmanagerInstance = new AuthManager(expressMock,authConfigInstance.config);
|
authmanagerInstance = new AuthManager(expressMock,authConfigInstance.config);
|
||||||
authmanagerInstance.getUserModel();
|
authmanagerInstance.getUserModel();
|
||||||
expect(logSpy).toHaveBeenCalledTimes(0);
|
expect(logSpy).toHaveBeenCalledTimes(0);
|
||||||
|
|
|
||||||
257
server/__tests__/rooms.test.js
Normal file
257
server/__tests__/rooms.test.js
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
jest.mock("../middleware/AppError", () => {
|
||||||
|
const actualAppError = jest.requireActual("../middleware/AppError");
|
||||||
|
|
||||||
|
return jest.fn().mockImplementation((message, statusCode) => {
|
||||||
|
return new actualAppError(message, statusCode);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const Rooms = require("../models/room");
|
||||||
|
const ObjectId = require("mongodb").ObjectId;
|
||||||
|
describe("Rooms", () => {
|
||||||
|
let rooms;
|
||||||
|
let db;
|
||||||
|
let collection;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
collection = {
|
||||||
|
findOne: jest.fn(),
|
||||||
|
insertOne: jest.fn(),
|
||||||
|
find: jest.fn().mockReturnValue({ toArray: jest.fn() }),
|
||||||
|
deleteOne: jest.fn(),
|
||||||
|
deleteMany: jest.fn(),
|
||||||
|
updateOne: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
db = {
|
||||||
|
connect: jest.fn(),
|
||||||
|
getConnection: jest.fn().mockReturnThis(),
|
||||||
|
collection: jest.fn().mockReturnValue(collection),
|
||||||
|
};
|
||||||
|
|
||||||
|
rooms = new Rooms(db);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("create", () => {
|
||||||
|
it("should return insertedId on success", async () => {
|
||||||
|
collection.findOne.mockResolvedValue(null);
|
||||||
|
collection.insertOne.mockResolvedValue({ insertedId: "abc123" });
|
||||||
|
|
||||||
|
const result = await rooms.create("test", "userId");
|
||||||
|
expect(result).toBe("abc123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error when userId is missing", async () => {
|
||||||
|
await expect(rooms.create("test", undefined)).rejects.toThrowError(
|
||||||
|
new Error("Missing required parameter(s)", 400)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw conflict error when room exists", async () => {
|
||||||
|
collection.findOne.mockResolvedValue({
|
||||||
|
_id: "660c72b2f9b1d8b3a4c8e4d3b",
|
||||||
|
userId: "12345",
|
||||||
|
title: "existing room",
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(rooms.create("existing room", "12345")).rejects.toThrowError(
|
||||||
|
new Error("Room already exists", 409)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("getUserRooms", () => {
|
||||||
|
it("should return all rooms for a user", async () => {
|
||||||
|
const userId = "12345";
|
||||||
|
const userRooms = [
|
||||||
|
{ title: "room 1", userId },
|
||||||
|
{ title: "room 2", userId },
|
||||||
|
];
|
||||||
|
|
||||||
|
collection.find().toArray.mockResolvedValue(userRooms);
|
||||||
|
|
||||||
|
const result = await rooms.getUserRooms(userId);
|
||||||
|
|
||||||
|
expect(db.connect).toHaveBeenCalled();
|
||||||
|
expect(db.collection).toHaveBeenCalledWith("rooms");
|
||||||
|
expect(collection.find).toHaveBeenCalledWith({ userId });
|
||||||
|
expect(result).toEqual(userRooms);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getOwner", () => {
|
||||||
|
it("should return the owner of a room", async () => {
|
||||||
|
const roomId = "60c72b2f9b1d8b3a4c8e4d3b";
|
||||||
|
const userId = "12345";
|
||||||
|
|
||||||
|
collection.findOne.mockResolvedValue({ userId });
|
||||||
|
|
||||||
|
const result = await rooms.getOwner(roomId);
|
||||||
|
|
||||||
|
expect(db.connect).toHaveBeenCalled();
|
||||||
|
expect(db.collection).toHaveBeenCalledWith("rooms");
|
||||||
|
expect(collection.findOne).toHaveBeenCalledWith({
|
||||||
|
_id: new ObjectId(roomId),
|
||||||
|
});
|
||||||
|
expect(result).toBe(userId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("delete", () => {
|
||||||
|
it("should delete a room and return true", async () => {
|
||||||
|
const roomId = "60c72b2f9b1d8b3a4c8e4d3b";
|
||||||
|
|
||||||
|
collection.deleteOne.mockResolvedValue({ deletedCount: 1 });
|
||||||
|
|
||||||
|
const result = await rooms.delete(roomId);
|
||||||
|
|
||||||
|
expect(db.connect).toHaveBeenCalled();
|
||||||
|
expect(db.collection).toHaveBeenCalledWith("rooms");
|
||||||
|
expect(collection.deleteOne).toHaveBeenCalledWith({
|
||||||
|
_id: new ObjectId(roomId),
|
||||||
|
});
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if the room does not exist", async () => {
|
||||||
|
const roomId = "60c72b2f9b1d8b3a4c8e4d3b";
|
||||||
|
|
||||||
|
collection.deleteOne.mockResolvedValue({ deletedCount: 0 });
|
||||||
|
|
||||||
|
const result = await rooms.delete(roomId);
|
||||||
|
|
||||||
|
expect(db.connect).toHaveBeenCalled();
|
||||||
|
expect(db.collection).toHaveBeenCalledWith("rooms");
|
||||||
|
expect(collection.deleteOne).toHaveBeenCalledWith({
|
||||||
|
_id: new ObjectId(roomId),
|
||||||
|
});
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("rename", () => {
|
||||||
|
it("should rename a room and return true", async () => {
|
||||||
|
const roomId = "60c72b2f9b1d8b3a4c8e4d3b";
|
||||||
|
const newTitle = "new room name";
|
||||||
|
const userId = "12345";
|
||||||
|
|
||||||
|
collection.updateOne.mockResolvedValue({ modifiedCount: 1 });
|
||||||
|
|
||||||
|
const result = await rooms.rename(roomId, userId, newTitle);
|
||||||
|
|
||||||
|
expect(db.connect).toHaveBeenCalled();
|
||||||
|
expect(db.collection).toHaveBeenCalledWith("rooms");
|
||||||
|
expect(collection.updateOne).toHaveBeenCalledWith(
|
||||||
|
{ _id: new ObjectId(roomId), userId: userId },
|
||||||
|
{ $set: { title: newTitle } }
|
||||||
|
);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if the room does not exist", async () => {
|
||||||
|
const roomId = "60c72b2f9b1d8b3a4c8e4d3b";
|
||||||
|
const newTitle = "new room name";
|
||||||
|
const userId = "12345";
|
||||||
|
|
||||||
|
collection.updateOne.mockResolvedValue({ modifiedCount: 0 });
|
||||||
|
|
||||||
|
const result = await rooms.rename(roomId, userId, newTitle);
|
||||||
|
|
||||||
|
expect(db.connect).toHaveBeenCalled();
|
||||||
|
expect(db.collection).toHaveBeenCalledWith("rooms");
|
||||||
|
expect(collection.updateOne).toHaveBeenCalledWith(
|
||||||
|
{ _id: new ObjectId(roomId), userId: userId },
|
||||||
|
{ $set: { title: newTitle } }
|
||||||
|
);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error if the new title is already in use", async () => {
|
||||||
|
const roomId = "60c72b2f9b1d8b3a4c8e4d3b";
|
||||||
|
const newTitle = "existing room";
|
||||||
|
const userId = "12345";
|
||||||
|
|
||||||
|
collection.findOne.mockResolvedValue({ title: newTitle });
|
||||||
|
collection.updateOne.mockResolvedValue({ modifiedCount: 0 });
|
||||||
|
|
||||||
|
await expect(rooms.rename(roomId, userId, newTitle)).rejects.toThrow(
|
||||||
|
"Room with name 'existing room' already exists."
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(db.connect).toHaveBeenCalled();
|
||||||
|
expect(db.collection).toHaveBeenCalledWith("rooms");
|
||||||
|
expect(collection.findOne).toHaveBeenCalledWith({
|
||||||
|
userId: userId,
|
||||||
|
title: newTitle,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("roomExists", () => {
|
||||||
|
it("should return true if room exists", async () => {
|
||||||
|
const title = "TEST ROOM";
|
||||||
|
const userId = '66fc70bea1b9e87655cf17c9';
|
||||||
|
|
||||||
|
collection.findOne.mockResolvedValue({ title, userId });
|
||||||
|
|
||||||
|
const result = await rooms.roomExists(title, userId);
|
||||||
|
|
||||||
|
expect(db.connect).toHaveBeenCalled();
|
||||||
|
expect(db.collection).toHaveBeenCalledWith("rooms");
|
||||||
|
expect(collection.findOne).toHaveBeenCalledWith({ title: title.toUpperCase(), userId });
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if room does not exist", async () => {
|
||||||
|
const title = "NONEXISTENT ROOM";
|
||||||
|
const userId = '66fc70bea1b9e87655cf17c9';
|
||||||
|
|
||||||
|
collection.findOne.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await rooms.roomExists(title, userId);
|
||||||
|
|
||||||
|
expect(db.connect).toHaveBeenCalled();
|
||||||
|
expect(db.collection).toHaveBeenCalledWith('rooms');
|
||||||
|
expect(collection.findOne).toHaveBeenCalledWith({ title: title.toUpperCase(), userId });
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getRoomById", () => {
|
||||||
|
it("should return a room by ID", async () => {
|
||||||
|
const roomId = "60c72b2f9b1d8b3a4c8e4d3b";
|
||||||
|
const room = {
|
||||||
|
_id: new ObjectId(roomId),
|
||||||
|
title: "test room",
|
||||||
|
};
|
||||||
|
|
||||||
|
collection.findOne.mockResolvedValue(room);
|
||||||
|
|
||||||
|
const result = await rooms.getRoomById(roomId);
|
||||||
|
|
||||||
|
expect(db.connect).toHaveBeenCalled();
|
||||||
|
expect(db.collection).toHaveBeenCalledWith("rooms");
|
||||||
|
expect(collection.findOne).toHaveBeenCalledWith({
|
||||||
|
_id: new ObjectId(roomId),
|
||||||
|
});
|
||||||
|
expect(result).toEqual(room);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error if the room does not exist", async () => {
|
||||||
|
const roomId = "60c72b2f9b1d8b3a4c8e4d3b";
|
||||||
|
|
||||||
|
collection.findOne.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(rooms.getRoomById(roomId)).rejects.toThrowError(
|
||||||
|
new Error(`Room ${roomId} not found`, 404)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(db.connect).toHaveBeenCalled();
|
||||||
|
expect(db.collection).toHaveBeenCalledWith("rooms");
|
||||||
|
expect(collection.findOne).toHaveBeenCalledWith({
|
||||||
|
_id: new ObjectId(roomId),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -60,45 +60,42 @@ describe("websocket server", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should create a room", (done) => {
|
test("should create a room", (done) => {
|
||||||
teacherSocket.emit("create-room", "room1");
|
|
||||||
teacherSocket.on("create-success", (roomName) => {
|
teacherSocket.on("create-success", (roomName) => {
|
||||||
expect(roomName).toBe("ROOM1");
|
expect(roomName).toBe("ROOM1");
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
teacherSocket.emit("create-room", "room1");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should not create a room if it already exists", (done) => {
|
test("should not create a room if it already exists", (done) => {
|
||||||
teacherSocket.emit("create-room", "room1");
|
|
||||||
teacherSocket.on("create-failure", () => {
|
teacherSocket.on("create-failure", () => {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
teacherSocket.emit("create-room", "room1");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should join a room", (done) => {
|
test("should join a room", (done) => {
|
||||||
studentSocket.emit("join-room", {
|
studentSocket.on("join-success", (roomName) => {
|
||||||
enteredRoomName: "ROOM1",
|
expect(roomName).toBe("ROOM1");
|
||||||
username: "student1",
|
|
||||||
});
|
|
||||||
studentSocket.on("join-success", () => {
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
studentSocket.emit("join-room", {
|
||||||
|
enteredRoomName: "room1",
|
||||||
|
username: "student1",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should not join a room if it does not exist", (done) => {
|
test("should not join a room if it does not exist", (done) => {
|
||||||
|
studentSocket.on("join-failure", () => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
studentSocket.emit("join-room", {
|
studentSocket.emit("join-room", {
|
||||||
enteredRoomName: "ROOM2",
|
enteredRoomName: "ROOM2",
|
||||||
username: "student1",
|
username: "student1",
|
||||||
});
|
});
|
||||||
studentSocket.on("join-failure", () => {
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should launch student mode", (done) => {
|
test("should launch student mode", (done) => {
|
||||||
teacherSocket.emit("launch-student-mode", {
|
|
||||||
roomName: "ROOM1",
|
|
||||||
questions: [{ question: "question1" }, { question: "question2" }],
|
|
||||||
});
|
|
||||||
studentSocket.on("launch-student-mode", (questions) => {
|
studentSocket.on("launch-student-mode", (questions) => {
|
||||||
expect(questions).toEqual([
|
expect(questions).toEqual([
|
||||||
{ question: "question1" },
|
{ question: "question1" },
|
||||||
|
|
@ -106,26 +103,24 @@ describe("websocket server", () => {
|
||||||
]);
|
]);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
teacherSocket.emit("launch-student-mode", {
|
||||||
|
roomName: "ROOM1",
|
||||||
|
questions: [{ question: "question1" }, { question: "question2" }],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should send next question", (done) => {
|
test("should send next question", (done) => {
|
||||||
teacherSocket.emit("next-question", {
|
|
||||||
roomName: "ROOM1",
|
|
||||||
question: { question: "question2" },
|
|
||||||
});
|
|
||||||
studentSocket.on("next-question", (question) => {
|
studentSocket.on("next-question", (question) => {
|
||||||
expect(question).toEqual({ question: "question2" });
|
expect(question).toEqual({ question: "question2" });
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
teacherSocket.emit("next-question", {
|
||||||
|
roomName: "ROOM1",
|
||||||
|
question: { question: "question2" },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should send answer", (done) => {
|
test("should send answer", (done) => {
|
||||||
studentSocket.emit("submit-answer", {
|
|
||||||
roomName: "ROOM1",
|
|
||||||
username: "student1",
|
|
||||||
answer: "answer1",
|
|
||||||
idQuestion: 1,
|
|
||||||
});
|
|
||||||
teacherSocket.on("submit-answer-room", (answer) => {
|
teacherSocket.on("submit-answer-room", (answer) => {
|
||||||
expect(answer).toEqual({
|
expect(answer).toEqual({
|
||||||
idUser: studentSocket.id,
|
idUser: studentSocket.id,
|
||||||
|
|
@ -135,32 +130,38 @@ describe("websocket server", () => {
|
||||||
});
|
});
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
studentSocket.emit("submit-answer", {
|
||||||
|
roomName: "ROOM1",
|
||||||
|
username: "student1",
|
||||||
|
answer: "answer1",
|
||||||
|
idQuestion: 1,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should not join a room if no room name is provided", (done) => {
|
test("should not join a room if no room name is provided", (done) => {
|
||||||
|
studentSocket.on("join-failure", () => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
studentSocket.emit("join-room", {
|
studentSocket.emit("join-room", {
|
||||||
enteredRoomName: "",
|
enteredRoomName: "",
|
||||||
username: "student1",
|
username: "student1",
|
||||||
});
|
});
|
||||||
studentSocket.on("join-failure", () => {
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should not join a room if the username is not provided", (done) => {
|
test("should not join a room if the username is not provided", (done) => {
|
||||||
studentSocket.emit("join-room", { enteredRoomName: "ROOM2", username: "" });
|
|
||||||
studentSocket.on("join-failure", () => {
|
studentSocket.on("join-failure", () => {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
studentSocket.emit("join-room", { enteredRoomName: "ROOM2", username: "" });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should end quiz", (done) => {
|
test("should end quiz", (done) => {
|
||||||
teacherSocket.emit("end-quiz", {
|
|
||||||
roomName: "ROOM1",
|
|
||||||
});
|
|
||||||
studentSocket.on("end-quiz", () => {
|
studentSocket.on("end-quiz", () => {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
teacherSocket.emit("end-quiz", {
|
||||||
|
roomName: "ROOM1",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should disconnect", (done) => {
|
test("should disconnect", (done) => {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ const db = require('./config/db.js');
|
||||||
// instantiate the models
|
// instantiate the models
|
||||||
const quiz = require('./models/quiz.js');
|
const quiz = require('./models/quiz.js');
|
||||||
const quizModel = new quiz(db);
|
const quizModel = new quiz(db);
|
||||||
|
const room = require('./models/room.js');
|
||||||
|
const roomModel = new room(db);
|
||||||
const folders = require('./models/folders.js');
|
const folders = require('./models/folders.js');
|
||||||
const foldersModel = new folders(db, quizModel);
|
const foldersModel = new folders(db, quizModel);
|
||||||
const users = require('./models/users.js');
|
const users = require('./models/users.js');
|
||||||
|
|
@ -22,6 +24,8 @@ const imageModel = new images(db);
|
||||||
// instantiate the controllers
|
// instantiate the controllers
|
||||||
const usersController = require('./controllers/users.js');
|
const usersController = require('./controllers/users.js');
|
||||||
const usersControllerInstance = new usersController(userModel);
|
const usersControllerInstance = new usersController(userModel);
|
||||||
|
const roomsController = require('./controllers/room.js');
|
||||||
|
const roomsControllerInstance = new roomsController(roomModel);
|
||||||
const foldersController = require('./controllers/folders.js');
|
const foldersController = require('./controllers/folders.js');
|
||||||
const foldersControllerInstance = new foldersController(foldersModel);
|
const foldersControllerInstance = new foldersController(foldersModel);
|
||||||
const quizController = require('./controllers/quiz.js');
|
const quizController = require('./controllers/quiz.js');
|
||||||
|
|
@ -31,12 +35,14 @@ const imagesControllerInstance = new imagesController(imageModel);
|
||||||
|
|
||||||
// export the controllers
|
// export the controllers
|
||||||
module.exports.users = usersControllerInstance;
|
module.exports.users = usersControllerInstance;
|
||||||
|
module.exports.rooms = roomsControllerInstance;
|
||||||
module.exports.folders = foldersControllerInstance;
|
module.exports.folders = foldersControllerInstance;
|
||||||
module.exports.quizzes = quizControllerInstance;
|
module.exports.quizzes = quizControllerInstance;
|
||||||
module.exports.images = imagesControllerInstance;
|
module.exports.images = imagesControllerInstance;
|
||||||
|
|
||||||
//import routers (instantiate controllers as side effect)
|
//import routers (instantiate controllers as side effect)
|
||||||
const userRouter = require('./routers/users.js');
|
const userRouter = require('./routers/users.js');
|
||||||
|
const roomRouter = require('./routers/room.js');
|
||||||
const folderRouter = require('./routers/folders.js');
|
const folderRouter = require('./routers/folders.js');
|
||||||
const quizRouter = require('./routers/quiz.js');
|
const quizRouter = require('./routers/quiz.js');
|
||||||
const imagesRouter = require('./routers/images.js')
|
const imagesRouter = require('./routers/images.js')
|
||||||
|
|
@ -89,6 +95,7 @@ app.use(bodyParser.json());
|
||||||
|
|
||||||
// Create routes
|
// Create routes
|
||||||
app.use('/api/user', userRouter);
|
app.use('/api/user', userRouter);
|
||||||
|
app.use('/api/room', roomRouter);
|
||||||
app.use('/api/folder', folderRouter);
|
app.use('/api/folder', folderRouter);
|
||||||
app.use('/api/quiz', quizRouter);
|
app.use('/api/quiz', quizRouter);
|
||||||
app.use('/api/image', imagesRouter);
|
app.use('/api/image', imagesRouter);
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ class AuthManager{
|
||||||
this.addModules()
|
this.addModules()
|
||||||
this.simpleregister = userModel;
|
this.simpleregister = userModel;
|
||||||
this.registerAuths()
|
this.registerAuths()
|
||||||
|
console.log(`AuthManager: constructor: this.configs: ${JSON.stringify(this.configs)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserModel(){
|
getUserModel(){
|
||||||
|
|
@ -54,17 +55,22 @@ class AuthManager{
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
async login(userInfo,req,res,next){ //passport and simpleauth use next
|
async login(userInfo,req,res,next){ //passport and simpleauth use next
|
||||||
const tokenToSave = jwt.create(userInfo.email, userInfo._id,userInfo.roles);
|
const tokenToSave = jwt.create(userInfo.email, userInfo._id, userInfo.roles);
|
||||||
res.redirect(`/auth/callback?user=${tokenToSave}&username=${userInfo.name}`);
|
res.redirect(`/auth/callback?user=${tokenToSave}&username=${userInfo.name}`);
|
||||||
console.info(`L'utilisateur '${userInfo.name}' vient de se connecter`)
|
console.info(`L'utilisateur '${userInfo.name}' vient de se connecter`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
async loginSimple(email,pswd,req,res,next){ //passport and simpleauth use next
|
async loginSimple(email,pswd,req,res,next){ //passport and simpleauth use next
|
||||||
|
console.log(`auth-manager: loginSimple: email: ${email}, pswd: ${pswd}`);
|
||||||
const userInfo = await this.simpleregister.login(email, pswd);
|
const userInfo = await this.simpleregister.login(email, pswd);
|
||||||
const tokenToSave = jwt.create(userInfo.email, userInfo._id,userInfo.roles);
|
console.log(`auth-manager: loginSimple: userInfo: ${JSON.stringify(userInfo)}`);
|
||||||
res.redirect(`/auth/callback?user=${tokenToSave}&username=${userInfo.name}`);
|
userInfo.roles = ['teacher']; // hard coded role
|
||||||
console.info(`L'utilisateur '${userInfo.name}' vient de se connecter`)
|
const tokenToSave = jwt.create(userInfo.email, userInfo._id, userInfo.roles);
|
||||||
|
console.log(`auth-manager: loginSimple: tokenToSave: ${tokenToSave}`);
|
||||||
|
//res.redirect(`/auth/callback?user=${tokenToSave}&username=${userInfo.email}`);
|
||||||
|
res.status(200).json({token: tokenToSave});
|
||||||
|
console.info(`L'utilisateur '${userInfo.email}' vient de se connecter`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async register(userInfos){
|
async register(userInfos){
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ class SimpleAuth {
|
||||||
}
|
}
|
||||||
|
|
||||||
async register(self, req, res) {
|
async register(self, req, res) {
|
||||||
|
console.log(`simpleauth.js.register: ${JSON.stringify(req.body)}`);
|
||||||
try {
|
try {
|
||||||
let userInfos = {
|
let userInfos = {
|
||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
|
|
@ -34,7 +35,11 @@ class SimpleAuth {
|
||||||
roles: req.body.roles
|
roles: req.body.roles
|
||||||
};
|
};
|
||||||
let user = await self.authmanager.register(userInfos)
|
let user = await self.authmanager.register(userInfos)
|
||||||
if (user) res.redirect("/login")
|
if (user) {
|
||||||
|
return res.status(200).json({
|
||||||
|
message: 'User created'
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
|
|
@ -44,6 +49,7 @@ class SimpleAuth {
|
||||||
}
|
}
|
||||||
|
|
||||||
async authenticate(self, req, res, next) {
|
async authenticate(self, req, res, next) {
|
||||||
|
console.log(`authenticate: ${JSON.stringify(req.body)}`);
|
||||||
try {
|
try {
|
||||||
const { email, password } = req.body;
|
const { email, password } = req.body;
|
||||||
|
|
||||||
|
|
@ -54,6 +60,7 @@ class SimpleAuth {
|
||||||
}
|
}
|
||||||
|
|
||||||
await self.authmanager.loginSimple(email, password, req, res, next);
|
await self.authmanager.loginSimple(email, password, req, res, next);
|
||||||
|
// return res.status(200).json({ message: 'Logged in' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const statusCode = error.statusCode || 500;
|
const statusCode = error.statusCode || 500;
|
||||||
const message = error.message || "An internal server error occurred";
|
const message = error.message || "An internal server error occurred";
|
||||||
|
|
|
||||||
9
server/auth_config-development.json
Normal file
9
server/auth_config-development.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"auth": {
|
||||||
|
"simpleauth": {
|
||||||
|
"enabled": true,
|
||||||
|
"name": "provider3",
|
||||||
|
"SESSION_SECRET": "your_session_secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const pathAuthConfig = './auth_config.json';
|
// set pathAuthConfig to './auth_config-development.json' if NODE_ENV is set to development
|
||||||
|
const pathAuthConfig = process.env.NODE_ENV === 'development' ? './auth_config-development.json' : './auth_config.json';
|
||||||
|
|
||||||
const configPath = path.join(process.cwd(), pathAuthConfig);
|
const configPath = path.join(process.cwd(), pathAuthConfig);
|
||||||
|
|
||||||
|
|
@ -12,10 +13,11 @@ class AuthConfig {
|
||||||
// Méthode pour lire le fichier de configuration JSON
|
// Méthode pour lire le fichier de configuration JSON
|
||||||
loadConfig() {
|
loadConfig() {
|
||||||
try {
|
try {
|
||||||
|
console.info(`Chargement du fichier de configuration: ${configPath}`);
|
||||||
const configData = fs.readFileSync(configPath, 'utf-8');
|
const configData = fs.readFileSync(configPath, 'utf-8');
|
||||||
this.config = JSON.parse(configData);
|
this.config = JSON.parse(configData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la lecture du fichier de configuration. Ne pas se fier si vous n'avez pas mit de fichier de configuration.");
|
console.error("Erreur lors de la lecture du fichier de configuration. Ne pas se fier si vous n'avez pas mis de fichier de configuration.");
|
||||||
this.config = {};
|
this.config = {};
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -139,6 +141,8 @@ class AuthConfig {
|
||||||
|
|
||||||
// Méthode pour retourner la configuration des fournisseurs PassportJS pour le frontend
|
// Méthode pour retourner la configuration des fournisseurs PassportJS pour le frontend
|
||||||
getActiveAuth() {
|
getActiveAuth() {
|
||||||
|
console.log(`getActiveAuth: this.config: ${JSON.stringify(this.config)}`);
|
||||||
|
console.log(`getActiveAuth: this.config.auth: ${JSON.stringify(this.config.auth)}`);
|
||||||
if (this.config && this.config.auth) {
|
if (this.config && this.config.auth) {
|
||||||
const passportConfig = {};
|
const passportConfig = {};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
exports.UNAUTHORIZED_NO_TOKEN_GIVEN = {
|
exports.UNAUTHORIZED_NO_TOKEN_GIVEN = {
|
||||||
message: 'Accès refusé. Aucun jeton fourni.',
|
message: 'Accès refusé. Aucun jeton fourni.',
|
||||||
code: 401
|
code: 401
|
||||||
}
|
};
|
||||||
exports.UNAUTHORIZED_INVALID_TOKEN = {
|
exports.UNAUTHORIZED_INVALID_TOKEN = {
|
||||||
message: 'Accès refusé. Jeton invalide.',
|
message: 'Accès refusé. Jeton invalide.',
|
||||||
code: 401
|
code: 401
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.MISSING_REQUIRED_PARAMETER = {
|
exports.MISSING_REQUIRED_PARAMETER = {
|
||||||
message: 'Paramètre requis manquant.',
|
message: 'Paramètre requis manquant.',
|
||||||
code: 400
|
code: 400
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.MISSING_OIDC_PARAMETER = (name) => {
|
exports.MISSING_OIDC_PARAMETER = (name) => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -21,116 +21,135 @@ exports.MISSING_OIDC_PARAMETER = (name) => {
|
||||||
|
|
||||||
exports.USER_ALREADY_EXISTS = {
|
exports.USER_ALREADY_EXISTS = {
|
||||||
message: 'L\'utilisateur existe déjà.',
|
message: 'L\'utilisateur existe déjà.',
|
||||||
code: 400
|
code: 409
|
||||||
}
|
};
|
||||||
exports.LOGIN_CREDENTIALS_ERROR = {
|
exports.LOGIN_CREDENTIALS_ERROR = {
|
||||||
message: 'L\'email et le mot de passe ne correspondent pas.',
|
message: 'L\'email et le mot de passe ne correspondent pas.',
|
||||||
code: 401
|
code: 401
|
||||||
}
|
};
|
||||||
exports.GENERATE_PASSWORD_ERROR = {
|
exports.GENERATE_PASSWORD_ERROR = {
|
||||||
message: 'Une erreur s\'est produite lors de la création d\'un nouveau mot de passe.',
|
message: 'Une erreur s\'est produite lors de la création d\'un nouveau mot de passe.',
|
||||||
code: 400
|
code: 500
|
||||||
}
|
};
|
||||||
exports.UPDATE_PASSWORD_ERROR = {
|
exports.UPDATE_PASSWORD_ERROR = {
|
||||||
message: 'Une erreur s\'est produite lors de la mise à jours du mot de passe.',
|
message: 'Une erreur s\'est produite lors de la mise à jours du mot de passe.',
|
||||||
code: 400
|
code: 500
|
||||||
}
|
};
|
||||||
exports.DELETE_USER_ERROR = {
|
exports.DELETE_USER_ERROR = {
|
||||||
message: 'Une erreur s\'est produite lors de suppression de l\'utilisateur.',
|
message: 'Une erreur s\'est produite lors de suppression de l\'utilisateur.',
|
||||||
code: 400
|
code: 500
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.IMAGE_NOT_FOUND = {
|
exports.IMAGE_NOT_FOUND = {
|
||||||
message: 'Nous n\'avons pas trouvé l\'image.',
|
message: 'Nous n\'avons pas trouvé l\'image.',
|
||||||
code: 404
|
code: 404
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.QUIZ_NOT_FOUND = {
|
exports.QUIZ_NOT_FOUND = {
|
||||||
message: 'Aucun quiz portant cet identifiant n\'a été trouvé.',
|
message: 'Aucun quiz portant cet identifiant n\'a été trouvé.',
|
||||||
code: 404
|
code: 404
|
||||||
}
|
};
|
||||||
exports.QUIZ_ALREADY_EXISTS = {
|
exports.QUIZ_ALREADY_EXISTS = {
|
||||||
message: 'Le quiz existe déjà.',
|
message: 'Le quiz existe déjà.',
|
||||||
code: 400
|
code: 409
|
||||||
}
|
};
|
||||||
exports.UPDATE_QUIZ_ERROR = {
|
exports.UPDATE_QUIZ_ERROR = {
|
||||||
message: 'Une erreur s\'est produite lors de la mise à jour du quiz.',
|
message: 'Une erreur s\'est produite lors de la mise à jour du quiz.',
|
||||||
code: 400
|
code: 500
|
||||||
}
|
};
|
||||||
exports.DELETE_QUIZ_ERROR = {
|
exports.DELETE_QUIZ_ERROR = {
|
||||||
message: 'Une erreur s\'est produite lors de la suppression du quiz.',
|
message: 'Une erreur s\'est produite lors de la suppression du quiz.',
|
||||||
code: 400
|
code: 500
|
||||||
}
|
};
|
||||||
exports.GETTING_QUIZ_ERROR = {
|
exports.GETTING_QUIZ_ERROR = {
|
||||||
message: 'Une erreur s\'est produite lors de la récupération du quiz.',
|
message: 'Une erreur s\'est produite lors de la récupération du quiz.',
|
||||||
code: 400
|
code: 500
|
||||||
}
|
};
|
||||||
exports.MOVING_QUIZ_ERROR = {
|
exports.MOVING_QUIZ_ERROR = {
|
||||||
message: 'Une erreur s\'est produite lors du déplacement du quiz.',
|
message: 'Une erreur s\'est produite lors du déplacement du quiz.',
|
||||||
code: 400
|
code: 500
|
||||||
}
|
};
|
||||||
exports.DUPLICATE_QUIZ_ERROR = {
|
exports.DUPLICATE_QUIZ_ERROR = {
|
||||||
message: 'Une erreur s\'est produite lors de la duplication du quiz.',
|
message: 'Une erreur s\'est produite lors de la duplication du quiz.',
|
||||||
code: 400
|
code: 500
|
||||||
}
|
};
|
||||||
exports.COPY_QUIZ_ERROR = {
|
exports.COPY_QUIZ_ERROR = {
|
||||||
message: 'Une erreur s\'est produite lors de la copie du quiz.',
|
message: 'Une erreur s\'est produite lors de la copie du quiz.',
|
||||||
code: 400
|
code: 500
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.FOLDER_NOT_FOUND = {
|
exports.FOLDER_NOT_FOUND = {
|
||||||
message: 'Aucun dossier portant cet identifiant n\'a été trouvé.',
|
message: 'Aucun dossier portant cet identifiant n\'a été trouvé.',
|
||||||
code: 404
|
code: 404
|
||||||
}
|
};
|
||||||
exports.FOLDER_ALREADY_EXISTS = {
|
exports.FOLDER_ALREADY_EXISTS = {
|
||||||
message: 'Le dossier existe déjà.',
|
message: 'Le dossier existe déjà.',
|
||||||
code: 409
|
code: 409
|
||||||
}
|
};
|
||||||
exports.UPDATE_FOLDER_ERROR = {
|
exports.UPDATE_FOLDER_ERROR = {
|
||||||
message: 'Une erreur s\'est produite lors de la mise à jour du dossier.',
|
message: 'Une erreur s\'est produite lors de la mise à jour du dossier.',
|
||||||
code: 400
|
code: 500
|
||||||
}
|
};
|
||||||
exports.DELETE_FOLDER_ERROR = {
|
exports.DELETE_FOLDER_ERROR = {
|
||||||
message: 'Une erreur s\'est produite lors de la suppression du dossier.',
|
message: 'Une erreur s\'est produite lors de la suppression du dossier.',
|
||||||
code: 400
|
code: 500
|
||||||
}
|
};
|
||||||
exports.GETTING_FOLDER_ERROR = {
|
exports.GETTING_FOLDER_ERROR = {
|
||||||
message: 'Une erreur s\'est produite lors de la récupération du dossier.',
|
message: 'Une erreur s\'est produite lors de la récupération du dossier.',
|
||||||
code: 400
|
code: 500
|
||||||
}
|
};
|
||||||
exports.MOVING_FOLDER_ERROR = {
|
exports.MOVING_FOLDER_ERROR = {
|
||||||
message: 'Une erreur s\'est produite lors du déplacement du dossier.',
|
message: 'Une erreur s\'est produite lors du déplacement du dossier.',
|
||||||
code: 400
|
code: 500
|
||||||
}
|
};
|
||||||
exports.DUPLICATE_FOLDER_ERROR = {
|
exports.DUPLICATE_FOLDER_ERROR = {
|
||||||
message: 'Une erreur s\'est produite lors de la duplication du dossier.',
|
message: 'Une erreur s\'est produite lors de la duplication du dossier.',
|
||||||
code: 400
|
code: 500
|
||||||
}
|
};
|
||||||
exports.COPY_FOLDER_ERROR = {
|
exports.COPY_FOLDER_ERROR = {
|
||||||
message: 'Une erreur s\'est produite lors de la copie du dossier.',
|
message: 'Une erreur s\'est produite lors de la copie du dossier.',
|
||||||
code: 400
|
code: 500
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
exports.ROOM_NOT_FOUND = {
|
||||||
|
message: "Aucune salle trouvée avec cet identifiant.",
|
||||||
|
code: 404
|
||||||
|
};
|
||||||
|
exports.ROOM_ALREADY_EXISTS = {
|
||||||
|
message: 'Une salle avec ce nom existe déjà',
|
||||||
|
code: 409
|
||||||
|
};
|
||||||
|
exports.UPDATE_ROOM_ERROR = {
|
||||||
|
message: 'Une erreur s\'est produite lors de la mise à jour de la salle.',
|
||||||
|
code: 500
|
||||||
|
};
|
||||||
|
exports.DELETE_ROOM_ERROR = {
|
||||||
|
message: 'Une erreur s\'est produite lors de la suppression de la salle.',
|
||||||
|
code: 500
|
||||||
|
};
|
||||||
|
exports.GETTING_ROOM_ERROR = {
|
||||||
|
message: 'Une erreur s\'est produite lors de la récupération de la salle.',
|
||||||
|
code: 500
|
||||||
|
};
|
||||||
|
exports.MOVING_ROOM_ERROR = {
|
||||||
|
message: 'Une erreur s\'est produite lors du déplacement de la salle.',
|
||||||
|
code: 500
|
||||||
|
};
|
||||||
|
exports.DUPLICATE_ROOM_ERROR = {
|
||||||
|
message: 'Une erreur s\'est produite lors de la duplication de la salle.',
|
||||||
|
code: 500
|
||||||
|
};
|
||||||
|
exports.COPY_ROOM_ERROR = {
|
||||||
|
message: 'Une erreur s\'est produite lors de la copie de la salle.',
|
||||||
|
code: 500
|
||||||
|
};
|
||||||
|
|
||||||
exports.NOT_IMPLEMENTED = {
|
exports.NOT_IMPLEMENTED = {
|
||||||
message: 'Route not implemented yet!',
|
message: "Route non encore implémentée. Fonctionnalité en cours de développement.",
|
||||||
code: 400
|
code: 501
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// static ok(res, results) {200
|
// static ok(res, results) {200
|
||||||
|
|
|
||||||
219
server/controllers/room.js
Normal file
219
server/controllers/room.js
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
const AppError = require("../middleware/AppError.js");
|
||||||
|
const {
|
||||||
|
MISSING_REQUIRED_PARAMETER,
|
||||||
|
ROOM_NOT_FOUND,
|
||||||
|
ROOM_ALREADY_EXISTS,
|
||||||
|
GETTING_ROOM_ERROR,
|
||||||
|
DELETE_ROOM_ERROR,
|
||||||
|
UPDATE_ROOM_ERROR,
|
||||||
|
} = require("../constants/errorCodes");
|
||||||
|
|
||||||
|
class RoomsController {
|
||||||
|
constructor(roomsModel) {
|
||||||
|
this.rooms = roomsModel;
|
||||||
|
this.getRoomTitle = this.getRoomTitle.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
create = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
if (!req.user || !req.user.userId) {
|
||||||
|
throw new AppError(MISSING_REQUIRED_PARAMETER);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title } = req.body;
|
||||||
|
if (!title) {
|
||||||
|
throw new AppError(MISSING_REQUIRED_PARAMETER);
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedTitle = title.toUpperCase().trim();
|
||||||
|
|
||||||
|
const roomExists = await this.rooms.roomExists(normalizedTitle, req.user.userId);
|
||||||
|
if (roomExists) {
|
||||||
|
throw new AppError(ROOM_ALREADY_EXISTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.rooms.create(normalizedTitle, req.user.userId);
|
||||||
|
|
||||||
|
return res.status(201).json({
|
||||||
|
message: "Room créée avec succès.",
|
||||||
|
roomId: result.insertedId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
getUserRooms = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const rooms = await this.rooms.getUserRooms(req.user.userId);
|
||||||
|
|
||||||
|
if (!rooms) {
|
||||||
|
throw new AppError(ROOM_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
data: rooms,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getRoomContent = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { roomId } = req.params;
|
||||||
|
|
||||||
|
if (!roomId) {
|
||||||
|
throw new AppError(MISSING_REQUIRED_PARAMETER);
|
||||||
|
}
|
||||||
|
const content = await this.rooms.getContent(roomId);
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
throw new AppError(GETTING_ROOM_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
data: content,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
delete = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { roomId } = req.params;
|
||||||
|
|
||||||
|
if (!roomId) {
|
||||||
|
throw new AppError(MISSING_REQUIRED_PARAMETER);
|
||||||
|
}
|
||||||
|
|
||||||
|
const owner = await this.rooms.getOwner(roomId);
|
||||||
|
|
||||||
|
if (owner != req.user.userId) {
|
||||||
|
throw new AppError(ROOM_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.rooms.delete(roomId);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new AppError(DELETE_ROOM_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
message: "Salle supprimé avec succès.",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
rename = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { roomId, newTitle } = req.body;
|
||||||
|
|
||||||
|
if (!roomId || !newTitle) {
|
||||||
|
throw new AppError(MISSING_REQUIRED_PARAMETER);
|
||||||
|
}
|
||||||
|
|
||||||
|
const owner = await this.rooms.getOwner(roomId);
|
||||||
|
|
||||||
|
if (owner != req.user.userId) {
|
||||||
|
throw new AppError(ROOM_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = await this.rooms.roomExists(newTitle, req.user.userId);
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
throw new AppError(ROOM_ALREADY_EXISTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.rooms.rename(roomId, req.user.userId, newTitle);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new AppError(UPDATE_ROOM_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
message: "Salle mis <20> jours avec succ<63>s.",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getRoomById = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { roomId } = req.params;
|
||||||
|
|
||||||
|
if (!roomId) {
|
||||||
|
throw new AppError(MISSING_REQUIRED_PARAMETER);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this room mine
|
||||||
|
const owner = await this.rooms.getOwner(roomId);
|
||||||
|
|
||||||
|
if (owner != req.user.userId) {
|
||||||
|
throw new AppError(ROOM_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await this.rooms.getRoomById(roomId);
|
||||||
|
|
||||||
|
if (!room) {
|
||||||
|
throw new AppError(ROOM_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
data: room,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
getRoomTitle = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { roomId } = req.params;
|
||||||
|
|
||||||
|
if (!roomId) {
|
||||||
|
throw new AppError(MISSING_REQUIRED_PARAMETER);
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await this.rooms.getRoomById(roomId);
|
||||||
|
|
||||||
|
if (room instanceof Error) {
|
||||||
|
throw new AppError(ROOM_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({ title: room.title });
|
||||||
|
} catch (error) {
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getRoomTitleByUserId = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { userId } = req.params;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new AppError(MISSING_REQUIRED_PARAMETER);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rooms = await this.rooms.getUserRooms(userId);
|
||||||
|
|
||||||
|
if (!rooms || rooms.length === 0) {
|
||||||
|
throw new AppError(ROOM_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomTitles = rooms.map((room) => room.title);
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
titles: roomTitles,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RoomsController;
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
class AppError extends Error {
|
class AppError extends Error {
|
||||||
constructor (errorCode) {
|
constructor (errorCode) {
|
||||||
super(errorCode.message)
|
super(errorCode.message);
|
||||||
this.statusCode = errorCode.code;
|
this.statusCode = errorCode.code;
|
||||||
this.isOperational = true; // Optional: to distinguish operational errors from programming errors
|
this.isOperational = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = AppError;
|
||||||
|
|
||||||
module.exports = AppError;
|
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,20 @@ const AppError = require("./AppError");
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
const errorHandler = (error, req, res, _next) => {
|
const errorHandler = (error, req, res, _next) => {
|
||||||
|
res.setHeader('Cache-Control', 'no-store');
|
||||||
|
|
||||||
if (error instanceof AppError) {
|
if (error instanceof AppError) {
|
||||||
logError(error);
|
return res.status(error.statusCode).json({
|
||||||
return res.status(error.statusCode).json({
|
message: error.message,
|
||||||
error: error.message
|
code: error.statusCode
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logError(error.stack);
|
logError(error.stack);
|
||||||
return res.status(505).send("Oups! We screwed up big time. ┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻");
|
return res.status(505).send("Oups! We screwed up big time. ┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻");
|
||||||
}
|
};
|
||||||
|
|
||||||
const logError = (error) => {
|
const logError = (error) => {
|
||||||
const time = new Date();
|
const time = new Date();
|
||||||
var log_file = fs.createWriteStream(__dirname + '/../debug.log', {flags : 'a'});
|
var log_file = fs.createWriteStream(__dirname + '/../debug.log', {flags : 'a'});
|
||||||
log_file.write(time + '\n' + error + '\n\n');
|
log_file.write(time + '\n' + error + '\n\n');
|
||||||
|
|
|
||||||
173
server/models/room.js
Normal file
173
server/models/room.js
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
const ObjectId = require("mongodb").ObjectId;
|
||||||
|
|
||||||
|
class Rooms
|
||||||
|
{
|
||||||
|
constructor(db)
|
||||||
|
{
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(title, userId) {
|
||||||
|
if (!title || !userId) {
|
||||||
|
throw new Error("Missing required parameter(s)");
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = await this.roomExists(title, userId);
|
||||||
|
if (exists) {
|
||||||
|
throw new Error("Room already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.db.connect();
|
||||||
|
const conn = this.db.getConnection();
|
||||||
|
const roomsCollection = conn.collection("rooms");
|
||||||
|
|
||||||
|
const newRoom = {
|
||||||
|
userId: userId,
|
||||||
|
title: title,
|
||||||
|
created_at: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await roomsCollection.insertOne(newRoom);
|
||||||
|
|
||||||
|
return result.insertedId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserRooms(userId)
|
||||||
|
{
|
||||||
|
await this.db.connect();
|
||||||
|
const conn = this.db.getConnection();
|
||||||
|
|
||||||
|
const roomsCollection = conn.collection("rooms");
|
||||||
|
|
||||||
|
const result = await roomsCollection.find({ userId: userId }).toArray();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOwner(roomId)
|
||||||
|
{
|
||||||
|
await this.db.connect();
|
||||||
|
const conn = this.db.getConnection();
|
||||||
|
|
||||||
|
const roomsCollection = conn.collection("rooms");
|
||||||
|
|
||||||
|
const room = await roomsCollection.findOne({
|
||||||
|
_id: ObjectId.createFromHexString(roomId),
|
||||||
|
});
|
||||||
|
|
||||||
|
return room.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getContent(roomId)
|
||||||
|
{
|
||||||
|
await this.db.connect();
|
||||||
|
const conn = this.db.getConnection();
|
||||||
|
const roomsCollection = conn.collection("rooms");
|
||||||
|
if (!ObjectId.isValid(roomId))
|
||||||
|
{
|
||||||
|
return null; // Évite d'envoyer une requête invalide
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await roomsCollection.findOne({ _id: new ObjectId(roomId) });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(roomId)
|
||||||
|
{
|
||||||
|
await this.db.connect();
|
||||||
|
const conn = this.db.getConnection();
|
||||||
|
|
||||||
|
const roomsCollection = conn.collection("rooms");
|
||||||
|
|
||||||
|
const roomResult = await roomsCollection.deleteOne({
|
||||||
|
_id: ObjectId.createFromHexString(roomId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (roomResult.deletedCount != 1) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async rename(roomId, userId, newTitle)
|
||||||
|
{
|
||||||
|
await this.db.connect();
|
||||||
|
const conn = this.db.getConnection();
|
||||||
|
|
||||||
|
const roomsCollection = conn.collection("rooms");
|
||||||
|
|
||||||
|
const existingRoom = await roomsCollection.findOne({
|
||||||
|
title: newTitle,
|
||||||
|
userId: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingRoom)
|
||||||
|
throw new Error(`Room with name '${newTitle}' already exists.`);
|
||||||
|
|
||||||
|
const result = await roomsCollection.updateOne(
|
||||||
|
{ _id: ObjectId.createFromHexString(roomId), userId: userId },
|
||||||
|
{ $set: { title: newTitle } }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.modifiedCount != 1) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async roomExists(title, userId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await this.db.connect();
|
||||||
|
const conn = this.db.getConnection();
|
||||||
|
const existingRoom = await conn.collection("rooms").findOne({
|
||||||
|
title: title.toUpperCase(),
|
||||||
|
userId: userId,
|
||||||
|
});
|
||||||
|
return !!existingRoom;
|
||||||
|
} catch (error)
|
||||||
|
{
|
||||||
|
throw new Error(`Database error (${error})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async getRoomById(roomId)
|
||||||
|
{
|
||||||
|
await this.db.connect();
|
||||||
|
const conn = this.db.getConnection();
|
||||||
|
|
||||||
|
const roomsCollection = conn.collection("rooms");
|
||||||
|
|
||||||
|
const room = await roomsCollection.findOne({
|
||||||
|
_id: ObjectId.createFromHexString(roomId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!room) throw new Error(`Room ${roomId} not found`, 404);
|
||||||
|
|
||||||
|
return room;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRoomWithContent(roomId)
|
||||||
|
{
|
||||||
|
const room = await this.getRoomById(roomId);
|
||||||
|
|
||||||
|
const content = await this.getContent(roomId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...room,
|
||||||
|
content: content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async getRoomTitleByUserId(userId)
|
||||||
|
{
|
||||||
|
await this.db.connect();
|
||||||
|
const conn = this.db.getConnection();
|
||||||
|
|
||||||
|
const roomsCollection = conn.collection("rooms");
|
||||||
|
|
||||||
|
const rooms = await roomsCollection.find({ userId: userId }).toArray();
|
||||||
|
|
||||||
|
return rooms.map((room) => room.title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Rooms;
|
||||||
|
|
@ -54,6 +54,7 @@ class Users {
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(email, password) {
|
async login(email, password) {
|
||||||
|
console.log(`models/users: login: email: ${email}, password: ${password}`);
|
||||||
try {
|
try {
|
||||||
await this.db.connect();
|
await this.db.connect();
|
||||||
const conn = this.db.getConnection();
|
const conn = this.db.getConnection();
|
||||||
|
|
@ -74,7 +75,7 @@ class Users {
|
||||||
error.statusCode = 401;
|
error.statusCode = 401;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
console.log(`models/users: login: FOUND user: ${JSON.stringify(user)}`);
|
||||||
return user;
|
return user;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
|
||||||
18
server/routers/room.js
Normal file
18
server/routers/room.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const jwt = require('../middleware/jwtToken.js');
|
||||||
|
const rooms = require('../app.js').rooms;
|
||||||
|
const asyncHandler = require('./routerUtils.js');
|
||||||
|
|
||||||
|
router.post("/create", jwt.authenticate, asyncHandler(rooms.create));
|
||||||
|
router.post("/roomExists", jwt.authenticate, asyncHandler(rooms.roomExists));
|
||||||
|
router.get("/getUserRooms", jwt.authenticate, asyncHandler(rooms.getUserRooms));
|
||||||
|
router.get('/getRoomTitle/:roomId', jwt.authenticate, asyncHandler(rooms.getRoomTitle));
|
||||||
|
router.get('/getRoomTitleByUserId/:userId', jwt.authenticate, asyncHandler(rooms.getRoomTitleByUserId));
|
||||||
|
router.get("/getRoomContent/:roomId", jwt.authenticate, asyncHandler(rooms.getRoomContent));
|
||||||
|
router.delete("/delete/:roomId", jwt.authenticate, asyncHandler(rooms.delete));
|
||||||
|
router.put("/rename", jwt.authenticate, asyncHandler(rooms.rename));
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
||||||
|
module.exports.rooms = rooms;
|
||||||
|
|
@ -6,7 +6,7 @@ const setupWebsocket = (io) => {
|
||||||
|
|
||||||
io.on("connection", (socket) => {
|
io.on("connection", (socket) => {
|
||||||
if (totalConnections >= MAX_TOTAL_CONNECTIONS) {
|
if (totalConnections >= MAX_TOTAL_CONNECTIONS) {
|
||||||
console.log("Connection limit reached. Disconnecting client.");
|
console.log("socket.js: Connection limit reached. Disconnecting client.");
|
||||||
socket.emit(
|
socket.emit(
|
||||||
"join-failure",
|
"join-failure",
|
||||||
"Le nombre maximum de connexions a été atteint"
|
"Le nombre maximum de connexions a été atteint"
|
||||||
|
|
@ -17,58 +17,67 @@ const setupWebsocket = (io) => {
|
||||||
|
|
||||||
totalConnections++;
|
totalConnections++;
|
||||||
console.log(
|
console.log(
|
||||||
"A user connected:",
|
"socket.js: A user connected:",
|
||||||
socket.id,
|
socket.id,
|
||||||
"| Total connections:",
|
"| Total connections:",
|
||||||
totalConnections
|
totalConnections
|
||||||
);
|
);
|
||||||
|
|
||||||
socket.on("create-room", (sentRoomName) => {
|
socket.on("create-room", (sentRoomName) => {
|
||||||
|
console.log(`socket.js: Demande de création de salle avec le nom : ${sentRoomName}`);
|
||||||
|
|
||||||
if (sentRoomName) {
|
if (sentRoomName) {
|
||||||
const roomName = sentRoomName.toUpperCase();
|
const roomName = sentRoomName.toUpperCase();
|
||||||
if (!io.sockets.adapter.rooms.get(roomName)) {
|
if (!io.sockets.adapter.rooms.get(roomName)) {
|
||||||
socket.join(roomName);
|
socket.join(roomName);
|
||||||
socket.emit("create-success", roomName);
|
socket.emit("create-success", roomName);
|
||||||
|
console.log(`socket.js: Salle créée avec succès : ${roomName}`);
|
||||||
} else {
|
} else {
|
||||||
socket.emit("create-failure");
|
socket.emit("create-failure", `La salle ${roomName} existe déjà.`);
|
||||||
}
|
console.log(`socket.js: Échec de création : ${roomName} existe déjà`);
|
||||||
} else {
|
|
||||||
const roomName = generateRoomName();
|
|
||||||
if (!io.sockets.adapter.rooms.get(roomName)) {
|
|
||||||
socket.join(roomName);
|
|
||||||
socket.emit("create-success", roomName);
|
|
||||||
} else {
|
|
||||||
socket.emit("create-failure");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
reportSalles();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function reportSalles() {
|
||||||
|
console.log("socket.js: Salles existantes :", Array.from(io.sockets.adapter.rooms.keys()));
|
||||||
|
}
|
||||||
|
|
||||||
socket.on("join-room", ({ enteredRoomName, username }) => {
|
socket.on("join-room", ({ enteredRoomName, username }) => {
|
||||||
if (io.sockets.adapter.rooms.has(enteredRoomName)) {
|
const roomToCheck = enteredRoomName.toUpperCase();
|
||||||
const clientsInRoom =
|
console.log(
|
||||||
io.sockets.adapter.rooms.get(enteredRoomName).size;
|
`socket.js: Requête de connexion : salle="${roomToCheck}", utilisateur="${username}"`
|
||||||
|
);
|
||||||
|
reportSalles();
|
||||||
|
|
||||||
|
if (io.sockets.adapter.rooms.has(roomToCheck)) {
|
||||||
|
console.log("socket.js: La salle existe");
|
||||||
|
const clientsInRoom = io.sockets.adapter.rooms.get(roomToCheck).size;
|
||||||
|
|
||||||
if (clientsInRoom <= MAX_USERS_PER_ROOM) {
|
if (clientsInRoom <= MAX_USERS_PER_ROOM) {
|
||||||
|
console.log("socket.js: La salle n'est pas pleine avec ", clientsInRoom, " utilisateurs");
|
||||||
const newStudent = {
|
const newStudent = {
|
||||||
id: socket.id,
|
id: socket.id,
|
||||||
name: username,
|
name: username,
|
||||||
answers: [],
|
answers: [],
|
||||||
};
|
};
|
||||||
socket.join(enteredRoomName);
|
socket.join(roomToCheck);
|
||||||
socket
|
socket.to(roomToCheck).emit("user-joined", newStudent);
|
||||||
.to(enteredRoomName)
|
socket.emit("join-success", roomToCheck);
|
||||||
.emit("user-joined", newStudent);
|
|
||||||
socket.emit("join-success");
|
|
||||||
} else {
|
} else {
|
||||||
|
console.log("socket.js: La salle est pleine avec ", clientsInRoom, " utilisateurs");
|
||||||
socket.emit("join-failure", "La salle est remplie");
|
socket.emit("join-failure", "La salle est remplie");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
console.log("socket.js: La salle n'existe pas");
|
||||||
socket.emit("join-failure", "Le nom de la salle n'existe pas");
|
socket.emit("join-failure", "Le nom de la salle n'existe pas");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("next-question", ({ roomName, question }) => {
|
socket.on("next-question", ({ roomName, question }) => {
|
||||||
// console.log("next-question", roomName, question);
|
console.log("socket.js: next-question", roomName, question);
|
||||||
|
console.log("socket.js: rediffusion de la question", question);
|
||||||
socket.to(roomName).emit("next-question", question);
|
socket.to(roomName).emit("next-question", question);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -77,21 +86,25 @@ const setupWebsocket = (io) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("end-quiz", ({ roomName }) => {
|
socket.on("end-quiz", ({ roomName }) => {
|
||||||
|
console.log("socket.js: end-quiz", roomName);
|
||||||
socket.to(roomName).emit("end-quiz");
|
socket.to(roomName).emit("end-quiz");
|
||||||
|
io.sockets.adapter.rooms.delete(roomName);
|
||||||
|
reportSalles();
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("message", (data) => {
|
socket.on("message", (data) => {
|
||||||
console.log("Received message from", socket.id, ":", data);
|
console.log("socket.js: Received message from", socket.id, ":", data);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
socket.on("disconnect", () => {
|
||||||
totalConnections--;
|
totalConnections--;
|
||||||
console.log(
|
console.log(
|
||||||
"A user disconnected:",
|
"socket.js: A user disconnected:",
|
||||||
socket.id,
|
socket.id,
|
||||||
"| Total connections:",
|
"| Total connections:",
|
||||||
totalConnections
|
totalConnections
|
||||||
);
|
);
|
||||||
|
reportSalles();
|
||||||
|
|
||||||
for (const [room] of io.sockets.adapter.rooms) {
|
for (const [room] of io.sockets.adapter.rooms) {
|
||||||
if (room !== socket.id) {
|
if (room !== socket.id) {
|
||||||
|
|
@ -109,17 +122,6 @@ const setupWebsocket = (io) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const generateRoomName = (length = 6) => {
|
|
||||||
const characters = "0123456789";
|
|
||||||
let result = "";
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
result += characters.charAt(
|
|
||||||
Math.floor(Math.random() * characters.length)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { setupWebsocket };
|
module.exports = { setupWebsocket };
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue