Merge pull request #1 from louis-antoine-etsmtl/creation-image
Create base image
27
Dockerfile
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Frontend build stage
|
||||
FROM node:18 AS frontend_build
|
||||
WORKDIR /usr/src/app/client
|
||||
COPY ./client/package*.json ./
|
||||
RUN npm install
|
||||
COPY ./client .
|
||||
RUN npm run build
|
||||
|
||||
# Backend build stage
|
||||
FROM node:18 AS backend_build
|
||||
WORKDIR /usr/src/app/serveur
|
||||
COPY ./serveur/package*.json ./
|
||||
RUN npm install
|
||||
COPY ./serveur .
|
||||
|
||||
# Nginx build stage
|
||||
FROM nginx AS nginx_build
|
||||
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Final stage
|
||||
FROM node:18
|
||||
WORKDIR /app
|
||||
COPY --from=frontend_build /usr/src/app/client/build ./client
|
||||
COPY --from=backend_build /usr/src/app/serveur ./
|
||||
COPY --from=nginx_build /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
17
client/Dockerfile
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# client
|
||||
|
||||
FROM node:18 AS build
|
||||
|
||||
WORKDIR /usr/src/app/client
|
||||
|
||||
COPY ./package*.json ./
|
||||
|
||||
COPY ./ .
|
||||
|
||||
RUN npm install
|
||||
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 5173
|
||||
|
||||
CMD [ "npm", "run", "preview" ]
|
||||
3
client/babel.config.cjs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript']
|
||||
};
|
||||
19
client/index.html
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/katex@latest/dist/katex.min.css"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<title>Évalue Ton Savoir</title>
|
||||
<base href="/" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
17
client/jest.config.cjs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
|
||||
module.exports = {
|
||||
roots: ['<rootDir>/src'],
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx)$': 'ts-jest',
|
||||
'^.+\\.(js|jsx)$': 'babel-jest'
|
||||
},
|
||||
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
|
||||
testEnvironment: 'jsdom',
|
||||
//moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
setupFiles: ['./jest.setup.cjs'],
|
||||
moduleNameMapper: {
|
||||
'\\.(css|less|scss|sass)$': 'identity-obj-proxy'
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!nanoid/)']
|
||||
};
|
||||
7
client/jest.setup.cjs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
global.import = {
|
||||
meta: {
|
||||
env: {
|
||||
VITE_BACKEND_URL: 'https://ets-glitch-backend.glitch.me/'
|
||||
}
|
||||
}
|
||||
};
|
||||
11129
client/package-lock.json
generated
Normal file
66
client/package.json
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"name": "pfe004-evaluetonsavoir",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@mui/icons-material": "^5.14.18",
|
||||
"@mui/lab": "^5.0.0-alpha.153",
|
||||
"@mui/material": "^5.15.11",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"axios": "^1.6.7",
|
||||
"esbuild": "^0.20.2",
|
||||
"gift-pegjs": "^0.2.1",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"katex": "^0.16.9",
|
||||
"marked": "^9.1.2",
|
||||
"nanoid": "^5.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-latex": "^2.0.0",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"uuid": "^9.0.1",
|
||||
"vite-plugin-checker": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.23.3",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@testing-library/jest-dom": "^6.1.4",
|
||||
"@testing-library/react": "^14.1.0",
|
||||
"@types/jest": "^29.5.8",
|
||||
"@types/node": "^20.8.8",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-latex": "^2.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.5",
|
||||
"vite-plugin-rewrite-all": "^1.0.1"
|
||||
}
|
||||
}
|
||||
6
client/public/Logo.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="244" height="225" fill="none" viewBox="0 0 244 225">
|
||||
<path stroke="#636363" stroke-width="4" d="M79.06 48.494 122 6.787l88.131 85.601L122 177.99l-42.94-41.707c-24.958-24.242-24.958-63.547 0-87.79Z"/>
|
||||
<path stroke="#636363" stroke-width="4" d="M123 32.777c34.318 0 62 26.713 62 59.5 0 32.786-27.682 59.5-62 59.5-34.317 0-62-26.714-62-59.5 0-32.787 27.683-59.5 62-59.5Z"/>
|
||||
<path stroke="#636363" stroke-width="4" d="M74 128.777v-73h96v73H74Zm53 1v-74m1 44h42m-26 30v-29m-15 12h16m-6 1v-13m2 6h4"/>
|
||||
<path fill="#636363" d="M111.412 190.336c0 1.2-.576 2.268-1.728 3.204-1.128.936-2.436 1.62-3.924 2.052-1.464.432-2.88.648-4.248.648-.36 0-.624-.012-.792-.036v-1.224c.048 0 .744-.108 2.088-.324 1.344-.24 2.472-.648 3.384-1.224.936-.576 1.404-1.224 1.404-1.944 0-.864-.648-1.296-1.944-1.296-1.752 0-3.408.6-4.968 1.8-1.56 1.2-2.34 2.46-2.34 3.78 0 1.32.816 2.172 2.448 2.556.36.096.6.192.72.288.12.096.18.264.18.504s-.12.444-.36.612c-.216.144-.732.36-1.548.648-4.416 1.512-6.624 3.864-6.624 7.056 0 .648.12 1.2.36 1.656 1.464-1.872 3.36-3.396 5.688-4.572 2.328-1.176 4.572-1.764 6.732-1.764 1.128 0 2.028.252 2.7.756.672.48 1.008 1.152 1.008 2.016 0 1.344-.624 2.688-1.872 4.032-1.248 1.344-2.868 2.436-4.86 3.276-1.968.864-3.936 1.296-5.904 1.296-1.944 0-3.48-.36-4.608-1.08-.024.024-.132.204-.324.54-.48.96-.888 1.632-1.224 2.016l-1.188-.504c.168-.672.66-1.692 1.476-3.06-1.104-1.248-1.656-2.64-1.656-4.176 0-1.56.624-3.036 1.872-4.428 1.272-1.416 3.036-2.58 5.292-3.492-1.128-.936-1.692-1.908-1.692-2.916 0-1.368.72-2.76 2.16-4.176 1.44-1.44 3.192-2.604 5.256-3.492 2.088-.888 4.044-1.332 5.868-1.332.96 0 1.728.216 2.304.648.576.408.864.96.864 1.656Zm-7.56 14.58c-1.416 0-3.072.54-4.968 1.62-1.872 1.08-3.396 2.364-4.572 3.852.96 1.08 2.316 1.62 4.068 1.62 1.776 0 3.504-.564 5.184-1.692 1.704-1.152 2.556-2.388 2.556-3.708 0-.528-.204-.936-.612-1.224-.408-.312-.96-.468-1.656-.468Zm24.802-13.896-5.148-.108c-5.256 0-7.884 1.152-7.884 3.456 0 .6.216 1.092.648 1.476.456.36 1.128.54 2.016.54.888 0 2.052-.408 3.492-1.224l.54.936a8.62 8.62 0 0 1-2.52 1.872c-.936.456-1.872.684-2.808.684-.936 0-1.74-.312-2.412-.936-.672-.624-1.008-1.404-1.008-2.34 0-1.896 1.02-3.552 3.06-4.968 2.064-1.416 4.848-2.124 8.352-2.124.288 0 1.32.048 3.096.144 1.776.096 3.18.144 4.212.144 1.032 0 1.98-.156 2.844-.468.336-.096.54-.144.612-.144.336 0 .504.216.504.648 0 .624-.552 1.188-1.656 1.692-1.08.504-2.328.756-3.744.756-1.584 1.536-2.976 4.224-4.176 8.064-1.176 3.816-2.592 6.732-4.248 8.748-1.632 1.992-3.576 2.988-5.832 2.988-.888 0-1.728-.288-2.52-.864l.828-1.368c.288.144.612.276.972.396l.504.072c.12.024.3.036.54.036 1.2 0 2.268-.576 3.204-1.728.96-1.152 1.956-3.324 2.988-6.516 1.056-3.216 1.872-5.34 2.448-6.372.6-1.056 1.632-2.22 3.096-3.492Zm23.303 13.464 2.556-.036v1.476a6.202 6.202 0 0 0-1.008-.072c-.384 0-.816.024-1.296.072 0 1.848-.888 3.72-2.664 5.616-1.776 1.896-3.924 3.432-6.444 4.608-2.496 1.2-4.824 1.8-6.984 1.8-1.128 0-2.052-.288-2.772-.864-.696-.576-1.044-1.332-1.044-2.268 0-1.392.924-2.844 2.772-4.356 1.848-1.512 4.008-2.76 6.48-3.744 2.472-.984 4.704-1.56 6.696-1.728-.24-.36-.612-.672-1.116-.936-.504-.264-1.536-.672-3.096-1.224-1.56-.576-2.664-1.2-3.312-1.872-.648-.696-.972-1.668-.972-2.916s.672-2.676 2.016-4.284a18.287 18.287 0 0 1 4.752-4.032c1.848-1.104 3.468-1.656 4.86-1.656.624 0 1.14.228 1.548.684.408.432.612.972.612 1.62 0 1.056-.636 2.388-1.908 3.996-1.272 1.584-2.688 2.796-4.248 3.636l-.864-1.224c.936-.48 1.752-1.224 2.448-2.232.72-1.032 1.08-1.92 1.08-2.664 0-.744-.396-1.116-1.188-1.116-1.128 0-2.424.708-3.888 2.124-1.44 1.392-2.16 2.7-2.16 3.924 0 .84.216 1.5.648 1.98.432.48 1.524 1.08 3.276 1.8 1.752.696 3 1.32 3.744 1.872.744.528 1.236 1.2 1.476 2.016Zm-16.056 9.396c0 1.152.936 1.728 2.808 1.728 1.032 0 2.364-.384 3.996-1.152a13.126 13.126 0 0 0 4.356-3.168c1.272-1.344 1.908-2.724 1.908-4.14 0-.168-.036-.42-.108-.756-3.288.312-6.276 1.236-8.964 2.772-2.664 1.536-3.996 3.108-3.996 4.716Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
BIN
client/public/logo.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
4
client/public/people.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.313 26.102l-6.296-3.488c2.34-1.841 2.976-5.459 2.976-7.488v-4.223c0-2.796-3.715-5.91-7.447-5.91-3.73 0-7.544 3.114-7.544 5.91v4.223c0 1.845 0.78 5.576 3.144 7.472l-6.458 3.503s-1.688 0.752-1.688 1.689v2.534c0 0.933 0.757 1.689 1.688 1.689h21.625c0.931 0 1.688-0.757 1.688-1.689v-2.534c0-0.994-1.689-1.689-1.689-1.689zM23.001 30.015h-21.001v-1.788c0.143-0.105 0.344-0.226 0.502-0.298 0.047-0.021 0.094-0.044 0.139-0.070l6.459-3.503c0.589-0.32 0.979-0.912 1.039-1.579s-0.219-1.32-0.741-1.739c-1.677-1.345-2.396-4.322-2.396-5.911v-4.223c0-1.437 2.708-3.91 5.544-3.91 2.889 0 5.447 2.44 5.447 3.91v4.223c0 1.566-0.486 4.557-2.212 5.915-0.528 0.416-0.813 1.070-0.757 1.739s0.446 1.267 1.035 1.589l6.296 3.488c0.055 0.030 0.126 0.063 0.184 0.089 0.148 0.063 0.329 0.167 0.462 0.259v1.809zM30.312 21.123l-6.39-3.488c2.34-1.841 3.070-5.459 3.070-7.488v-4.223c0-2.796-3.808-5.941-7.54-5.941-2.425 0-4.904 1.319-6.347 3.007 0.823 0.051 1.73 0.052 2.514 0.302 1.054-0.821 2.386-1.308 3.833-1.308 2.889 0 5.54 2.47 5.54 3.941v4.223c0 1.566-0.58 4.557-2.305 5.915-0.529 0.416-0.813 1.070-0.757 1.739 0.056 0.67 0.445 1.267 1.035 1.589l6.39 3.488c0.055 0.030 0.126 0.063 0.184 0.089 0.148 0.063 0.329 0.167 0.462 0.259v1.779h-4.037c0.61 0.46 0.794 1.118 1.031 2h3.319c0.931 0 1.688-0.757 1.688-1.689v-2.503c-0.001-0.995-1.689-1.691-1.689-1.691z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
11
client/public/student.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.3 8.40007L21.82 6.40008C21.7248 6.27314 21.6008 6.17066 21.4583 6.10111C21.3157 6.03156 21.1586 5.99693 21 6.00008H11.2C11.0555 6.00007 10.9128 6.03135 10.7816 6.09177C10.6504 6.15219 10.5339 6.24031 10.44 6.35007L8.71998 8.35008C8.57227 8.53401 8.49435 8.76424 8.49998 9.00008V16.2901C8.50262 18.0317 9.19567 19.7013 10.4272 20.9328C11.6588 22.1644 13.3283 22.8574 15.07 22.8601H16.93C18.6716 22.8574 20.3412 22.1644 21.5728 20.9328C22.8043 19.7013 23.4973 18.0317 23.5 16.2901V9.00008C23.5 8.7837 23.4298 8.57317 23.3 8.40007Z" fill="#FFCC80"/>
|
||||
<path d="M29.78 28.38L25.78 23.38C25.664 23.2321 25.5086 23.1198 25.3318 23.0562C25.1549 22.9925 24.9637 22.98 24.78 23.02L16 25L7.21999 23C7.03632 22.96 6.84507 22.9725 6.6682 23.0362C6.49133 23.0998 6.33598 23.2121 6.21999 23.36L2.21999 28.36C2.10392 28.5064 2.03116 28.6823 2.00995 28.8679C1.98874 29.0534 2.01993 29.2413 2.09999 29.41C2.17815 29.5839 2.3044 29.7319 2.46385 29.8364C2.62331 29.9409 2.80933 29.9977 2.99999 30H29C29.1885 29.9995 29.373 29.9457 29.5322 29.8448C29.6914 29.744 29.8189 29.6002 29.9 29.43C29.98 29.2613 30.0112 29.0734 29.99 28.8879C29.9688 28.7023 29.8961 28.5264 29.78 28.38Z" fill="#01579B"/>
|
||||
<path d="M29.29 6.00003L16.29 2.00003C16.0999 1.95002 15.9001 1.95002 15.71 2.00003L2.71 6.00003C2.49742 6.06422 2.31226 6.19735 2.1837 6.37841C2.05515 6.55947 1.99052 6.77817 2 7.00003C1.9917 7.22447 2.0592 7.44518 2.19163 7.62659C2.32405 7.80799 2.5137 7.93954 2.73 8.00003L15.73 11.6C15.906 11.6534 16.094 11.6534 16.27 11.6L29.27 8.00003C29.4863 7.93954 29.6759 7.80799 29.8084 7.62659C29.9408 7.44518 30.0083 7.22447 30 7.00003C30.0095 6.77817 29.9448 6.55947 29.8163 6.37841C29.6877 6.19735 29.5026 6.06422 29.29 6.00003Z" fill="#01579B"/>
|
||||
<path d="M11.22 6C11.0756 5.99999 10.9328 6.03127 10.8016 6.09169C10.6704 6.15211 10.5539 6.24023 10.46 6.35L8.74 8.35C8.58509 8.53114 8.49998 8.76166 8.5 9V16.29C8.50264 18.0317 9.19569 19.7012 10.4272 20.9328C11.6588 22.1643 13.3283 22.8574 15.07 22.86H16V6H11.22Z" fill="#FFE0B2"/>
|
||||
<path d="M7.21999 23C7.03632 22.96 6.84507 22.9725 6.6682 23.0362C6.49133 23.0998 6.33598 23.2121 6.21999 23.36L2.21999 28.36C2.10392 28.5064 2.03116 28.6823 2.00995 28.8679C1.98874 29.0534 2.01993 29.2413 2.09999 29.41C2.17815 29.5839 2.3044 29.7319 2.46385 29.8364C2.62331 29.9409 2.80933 29.9977 2.99999 30H16V25L7.21999 23Z" fill="#0277BD"/>
|
||||
<path d="M15.71 2.00002L2.71 6.00002C2.49742 6.06422 2.31226 6.19734 2.1837 6.3784C2.05515 6.55947 1.99052 6.77817 2 7.00002C1.9917 7.22447 2.0592 7.44518 2.19163 7.62658C2.32405 7.80799 2.5137 7.93954 2.73 8.00002L15.73 11.6C15.8194 11.6146 15.9106 11.6146 16 11.6V2.00002C15.9039 1.98469 15.8061 1.98469 15.71 2.00002Z" fill="#0277BD"/>
|
||||
<path d="M2.73 8.00003L8.5 9.56003V16.29C8.50264 18.0317 9.19569 19.7013 10.4272 20.9328C11.6588 22.1643 13.3283 22.8574 15.07 22.86H16.93C18.6717 22.8574 20.3412 22.1643 21.5728 20.9328C22.8043 19.7013 23.4974 18.0317 23.5 16.29V9.56003L29.27 8.00003C29.4863 7.93954 29.6759 7.80799 29.8084 7.62659C29.9408 7.44518 30.0083 7.22447 30 7.00003C30.0095 6.77817 29.9448 6.55947 29.8163 6.37841C29.6877 6.19735 29.5026 6.06422 29.29 6.00003L16.29 2.00003C16.0999 1.95002 15.9001 1.95002 15.71 2.00003L2.71 6.00003C2.49742 6.06422 2.31226 6.19735 2.1837 6.37841C2.05515 6.55947 1.99052 6.77817 2 7.00003C1.9917 7.22447 2.0592 7.44518 2.19163 7.62659C2.32405 7.80799 2.5137 7.93954 2.73 8.00003ZM21.5 16.29C21.4974 17.5013 21.015 18.6621 20.1586 19.5186C19.3021 20.3751 18.1412 20.8574 16.93 20.86H15.07C13.8588 20.8574 12.6979 20.3751 11.8414 19.5186C10.985 18.6621 10.5026 17.5013 10.5 16.29V10.11L15.73 11.56C15.906 11.6134 16.094 11.6134 16.27 11.56L21.5 10.11V16.29ZM16 4.05003L25.44 7.00003L16 9.56003L6.56 7.00003L16 4.05003Z" fill="#263238"/>
|
||||
<path d="M25.78 23.38C25.664 23.2321 25.5086 23.1198 25.3318 23.0562C25.1549 22.9925 24.9637 22.98 24.78 23.02L16 25L7.21999 23C7.03632 22.96 6.84507 22.9725 6.6682 23.0362C6.49133 23.0998 6.33598 23.2121 6.21999 23.36L2.21999 28.36C2.10392 28.5064 2.03116 28.6823 2.00995 28.8679C1.98874 29.0534 2.01993 29.2413 2.09999 29.41C2.17815 29.5839 2.3044 29.7319 2.46385 29.8364C2.62331 29.9409 2.80933 29.9977 2.99999 30H29C29.1885 29.9995 29.373 29.9457 29.5322 29.8448C29.6914 29.744 29.8189 29.6002 29.9 29.43C29.98 29.2613 30.0112 29.0734 29.99 28.8879C29.9688 28.7023 29.8961 28.5264 29.78 28.38L25.78 23.38ZM5.07999 28L7.38999 25.11L15.78 27C15.9251 27.0299 16.0748 27.0299 16.22 27L24.61 25.13L26.92 28H5.07999Z" fill="#263238"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
238
client/public/teacher.svg
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--noto" preserveAspectRatio="xMidYMid meet">
|
||||
<linearGradient id="IconifyId17ecdb2904d178eab20895" gradientUnits="userSpaceOnUse" x1="63.999" y1="116.605" x2="63.999" y2="39.511" gradientTransform="matrix(1 0 0 -1 0 128)">
|
||||
<stop offset="0" stop-color="#26a69a">
|
||||
</stop>
|
||||
<stop offset="1" stop-color="#00796b">
|
||||
</stop>
|
||||
</linearGradient>
|
||||
<path fill="url(#IconifyId17ecdb2904d178eab20895)" d="M6.36 10.9h115.29v77.52H6.36z">
|
||||
</path>
|
||||
<linearGradient id="IconifyId17ecdb2904d178eab20896" gradientUnits="userSpaceOnUse" x1="63.999" y1="119.455" x2="63.999" y2="37.224" gradientTransform="matrix(1 0 0 -1 0 128)">
|
||||
<stop offset="0" stop-color="#8d6e63">
|
||||
</stop>
|
||||
<stop offset=".779" stop-color="#795548">
|
||||
</stop>
|
||||
</linearGradient>
|
||||
<path d="M119.29 13.26v72.81H8.71V13.26h110.58M124 8.55H4v82.23h120V8.55z" fill="url(#IconifyId17ecdb2904d178eab20896)">
|
||||
</path>
|
||||
<path d="M98.9 79.85c-1.25-2.27.34-4.58 3.06-7.44c4.31-4.54 9-15.07 4.64-25.76c.03-.06-.86-1.86-.83-1.92l-1.79-.09c-.57-.08-20.26-.12-39.97-.12s-39.4.04-39.97.12c0 0-2.65 1.95-2.63 2.01c-4.35 10.69.33 21.21 4.64 25.76c2.71 2.86 4.3 5.17 3.06 7.44c-1.21 2.21-4.81 2.53-4.81 2.53s.83 2.26 2.83 3.48c1.85 1.13 4.13 1.39 5.7 1.43c0 0 6.15 8.51 22.23 8.51h17.9c16.08 0 22.23-8.51 22.23-8.51c1.57-.04 3.85-.3 5.7-1.43c2-1.22 2.83-3.48 2.83-3.48s-3.61-.32-4.82-2.53z" fill="#232020">
|
||||
</path>
|
||||
<radialGradient id="IconifyId17ecdb2904d178eab20897" cx="99.638" cy="45.85" r="23.419" gradientTransform="matrix(1 0 0 .4912 -21.055 59.629)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".728" stop-color="#444140" stop-opacity="0">
|
||||
</stop>
|
||||
<stop offset="1" stop-color="#444140">
|
||||
</stop>
|
||||
</radialGradient>
|
||||
<path d="M63.99 95.79v-9.44l28.57-2.26l2.6 3.2s-6.15 8.51-22.23 8.51l-8.94-.01z" fill="url(#IconifyId17ecdb2904d178eab20897)">
|
||||
</path>
|
||||
<radialGradient id="IconifyId17ecdb2904d178eab20898" cx="76.573" cy="49.332" r="6.921" gradientTransform="matrix(-.9057 .4238 -.3144 -.6719 186.513 79.36)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".663" stop-color="#444140">
|
||||
</stop>
|
||||
<stop offset="1" stop-color="#444140" stop-opacity="0">
|
||||
</stop>
|
||||
</radialGradient>
|
||||
<path d="M95.1 83.16c-4.28-6.5 5.21-8.93 5.21-8.93l.01.01c-1.65 2.05-2.4 3.84-1.43 5.61c1.21 2.21 4.81 2.53 4.81 2.53s-4.91 4.36-8.6.78z" fill="url(#IconifyId17ecdb2904d178eab20898)">
|
||||
</path>
|
||||
<radialGradient id="IconifyId17ecdb2904d178eab20899" cx="94.509" cy="68.91" r="30.399" gradientTransform="matrix(-.0746 -.9972 .8311 -.0622 33.494 157.622)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".725" stop-color="#444140" stop-opacity="0">
|
||||
</stop>
|
||||
<stop offset="1" stop-color="#444140">
|
||||
</stop>
|
||||
</radialGradient>
|
||||
<path d="M106.62 46.65c4.25 10.35-.22 21.01-4.41 25.51c-.57.62-3.01 3.01-3.57 4.92c0 0-9.54-13.31-12.39-21.13c-.57-1.58-1.1-3.2-1.17-4.88c-.05-1.26.14-2.76.87-3.83c.89-1.31 20.16-1.7 20.16-1.7c0 .01.51 1.11.51 1.11z" fill="url(#IconifyId17ecdb2904d178eab20899)">
|
||||
</path>
|
||||
<radialGradient id="IconifyId17ecdb2904d178eab20900" cx="44.31" cy="68.91" r="30.399" gradientTransform="matrix(.0746 -.9972 -.8311 -.0622 98.274 107.563)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".725" stop-color="#444140" stop-opacity="0">
|
||||
</stop>
|
||||
<stop offset="1" stop-color="#444140">
|
||||
</stop>
|
||||
</radialGradient>
|
||||
<path d="M21.4 46.65c-4.24 10.35.23 21.01 4.41 25.5c.58.62 3.01 3.01 3.57 4.92c0 0 9.54-13.31 12.39-21.13c.58-1.58 1.1-3.2 1.17-4.88c.05-1.26-.14-2.76-.87-3.83c-.89-1.31-1.93-.96-3.44-.96c-2.88 0-15.49-.74-16.47-.74c.01.02-.76 1.12-.76 1.12z" fill="url(#IconifyId17ecdb2904d178eab20900)">
|
||||
</path>
|
||||
<radialGradient id="IconifyId17ecdb2904d178eab20901" cx="49.439" cy="45.85" r="23.419" gradientTransform="matrix(-1 0 0 .4912 98.878 59.629)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".728" stop-color="#444140" stop-opacity="0">
|
||||
</stop>
|
||||
<stop offset="1" stop-color="#444140">
|
||||
</stop>
|
||||
</radialGradient>
|
||||
<path d="M64.03 95.79v-9.44l-28.57-2.26l-2.6 3.2s6.15 8.51 22.23 8.51l8.94-.01z" fill="url(#IconifyId17ecdb2904d178eab20901)">
|
||||
</path>
|
||||
<radialGradient id="IconifyId17ecdb2904d178eab20902" cx="26.374" cy="49.332" r="6.921" gradientTransform="matrix(.9057 .4238 .3144 -.6719 -13.024 100.635)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".663" stop-color="#444140">
|
||||
</stop>
|
||||
<stop offset="1" stop-color="#444140" stop-opacity="0">
|
||||
</stop>
|
||||
</radialGradient>
|
||||
<path d="M32.92 83.16c4.28-6.5-5.21-8.93-5.21-8.93l-.01.01c1.65 2.05 2.4 3.84 1.43 5.61c-1.21 2.21-4.81 2.53-4.81 2.53s4.91 4.36 8.6.78z" fill="url(#IconifyId17ecdb2904d178eab20902)">
|
||||
</path>
|
||||
<linearGradient id="IconifyId17ecdb2904d178eab20903" gradientUnits="userSpaceOnUse" x1="64" y1="25.908" x2="64" y2="10.938" gradientTransform="matrix(1 0 0 -1 0 128)">
|
||||
<stop offset="0" stop-color="#e1f5fe">
|
||||
</stop>
|
||||
<stop offset="1" stop-color="#81d4fa">
|
||||
</stop>
|
||||
</linearGradient>
|
||||
<path d="M114.5 120.75c0-15.47-25.34-23.56-50.36-23.56H64c-25.14.03-50.5 7.32-50.5 23.56V124h101v-3.25z" fill="url(#IconifyId17ecdb2904d178eab20903)">
|
||||
</path>
|
||||
<g>
|
||||
<path fill="#3c2b24" d="M64 92.33h-9.08v9.98l9.06 2.38l9.1-2.38v-9.98z">
|
||||
</path>
|
||||
</g>
|
||||
<g>
|
||||
<linearGradient id="IconifyId17ecdb2904d178eab20904" gradientUnits="userSpaceOnUse" x1="29.113" y1="29.156" x2="29.113" y2="4.97" gradientTransform="matrix(1 0 0 -1 0 128)">
|
||||
<stop offset="0" stop-color="#ffa000">
|
||||
</stop>
|
||||
<stop offset=".341" stop-color="#ff9300">
|
||||
</stop>
|
||||
<stop offset=".972" stop-color="#ff7100">
|
||||
</stop>
|
||||
<stop offset="1" stop-color="#ff6f00">
|
||||
</stop>
|
||||
</linearGradient>
|
||||
<path d="M12 120.75V124h32.89l1.33-27.04C27.52 99.72 12 107.15 12 120.75z" fill="url(#IconifyId17ecdb2904d178eab20904)">
|
||||
</path>
|
||||
<linearGradient id="IconifyId17ecdb2904d178eab20905" gradientUnits="userSpaceOnUse" x1="98.888" y1="29.435" x2="98.888" y2="4.807" gradientTransform="matrix(1 0 0 -1 0 128)">
|
||||
<stop offset="0" stop-color="#ffa000">
|
||||
</stop>
|
||||
<stop offset=".341" stop-color="#ff9300">
|
||||
</stop>
|
||||
<stop offset=".972" stop-color="#ff7100">
|
||||
</stop>
|
||||
<stop offset="1" stop-color="#ff6f00">
|
||||
</stop>
|
||||
</linearGradient>
|
||||
<path d="M81.78 96.96L83.1 124H116v-3.25c0-13.6-15.52-21.03-34.22-23.79z" fill="url(#IconifyId17ecdb2904d178eab20905)">
|
||||
</path>
|
||||
<g>
|
||||
<path fill="#66c0e8" d="M54.03 92.12l9.99 12.82l-16.24 6.64l-2.41-14.54z">
|
||||
</path>
|
||||
<path fill="#66c0e8" d="M73.97 92.12l-9.99 12.82l16.24 6.64l2.41-14.54z">
|
||||
</path>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M48.88 95s-1.14 2.72-1.94 6c-1.59 6.52-1.69 15.8-1.69 15.8s-6.89-2.34-9.04-8.05c-2.54-6.75 1.75-10.46 1.75-10.46s.9-.38 4.68-2.46s6.24-.83 6.24-.83z" fill="#af5214">
|
||||
</path>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M79.12 95s1.14 2.72 1.94 6c1.59 6.52 1.69 15.8 1.69 15.8s6.89-2.34 9.04-8.05c2.54-6.75-1.75-10.46-1.75-10.46s-.9-.38-4.68-2.46s-6.24-.83-6.24-.83z" fill="#af5214">
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M91.12 50.43H36.47c-5.89 0-10.71 5.14-10.71 11.41s4.82 11.41 10.71 11.41H91.12c5.89 0 10.71-5.14 10.71-11.41s-4.82-11.41-10.71-11.41z" fill="#3c2b24">
|
||||
</path>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M63.79 11.07c-17.4 0-33.52 18.61-33.52 45.4c0 26.64 16.61 39.81 33.52 39.81S97.31 83.1 97.31 56.46c0-26.78-16.11-45.39-33.52-45.39z" fill="#70534a">
|
||||
</path>
|
||||
</g>
|
||||
<g fill="#1a1717">
|
||||
<ellipse cx="47.98" cy="58.81" rx="4.93" ry="5.1">
|
||||
</ellipse>
|
||||
<ellipse cx="79.13" cy="58.81" rx="4.93" ry="5.1">
|
||||
</ellipse>
|
||||
</g>
|
||||
<g fill="#1a1717">
|
||||
<path d="M55.37 49.82c-.93-1.23-3.07-3.01-7.23-3.01s-6.31 1.79-7.23 3.01c-.41.54-.31 1.17-.02 1.55c.26.35 1.04.68 1.9.39s2.54-1.16 5.35-1.18c2.81.02 4.49.89 5.35 1.18c.86.29 1.64-.03 1.9-.39c.28-.38.39-1.01-.02-1.55z">
|
||||
</path>
|
||||
<path d="M86.36 49.82c-.93-1.23-3.07-3.01-7.23-3.01s-6.31 1.79-7.23 3.01c-.41.54-.31 1.17-.02 1.55c.26.35 1.04.68 1.9.39s2.54-1.16 5.35-1.18c2.81.02 4.49.89 5.35 1.18c.86.29 1.64-.03 1.9-.39c.29-.38.39-1.01-.02-1.55z">
|
||||
</path>
|
||||
</g>
|
||||
<path d="M67.65 68.06c-.11-.04-.21-.07-.32-.08h-7.08c-.11.01-.22.04-.32.08c-.64.26-.99.92-.69 1.63c.3.71 1.71 2.69 4.55 2.69s4.25-1.99 4.55-2.69c.31-.71-.05-1.37-.69-1.63z" fill="#33251f">
|
||||
</path>
|
||||
<path d="M72.32 76.14c-3.18 1.89-13.63 1.89-16.81 0c-1.83-1.09-3.7.58-2.94 2.24c.75 1.63 6.44 5.42 11.37 5.42s10.55-3.79 11.3-5.42c.76-1.66-1.09-3.33-2.92-2.24z" fill="#1a1717">
|
||||
</path>
|
||||
<path d="M93.83 52.93c-.07-1.19-.12-1.31-1.69-1.81c-1.23-.39-7.95-.94-13.01-.66c-.36.02-.71.04-1.04.07c-4.59.39-10.1 2.24-14.24 2.34c-1.76.04-9.01-1.86-14.14-2.26c-.33-.02-.66-.05-1-.06c-5.07-.26-11.82.33-13.05.73c-1.57.51-1.62.63-1.68 1.82c-.07 1.19.13 2.2 1.06 2.51c1.27.42 1.28 2 2.13 6.54c.77 4.14 2.62 7.41 10.57 7.98c.34.02.66.04.98.04c7.03.1 9.45-4.53 10.25-6.07c1.49-2.86 1.02-6.8 4.96-6.81c3.93-.01 3.56 3.86 5.07 6.71c.81 1.53 3.17 6.18 10.14 6.08c.34 0 .69-.02 1.05-.05c7.94-.62 9.78-3.9 10.52-8.04c.82-4.55.83-6.14 2.09-6.56c.91-.3 1.11-1.31 1.03-2.5zM53.28 68.17c-1.22.57-2.85.86-4.57.86c-3.59-.01-7.57-1.27-9.01-3.81c-2.04-3.62-2.57-10.94.03-12.47c1.14-.67 4.99-1.13 8.97-.96c4.13.18 8.4 1.04 9.94 3.06c2.55 3.33-1.5 11.5-5.36 13.32zm34.9-3.1c-1.43 2.56-5.44 3.85-9.05 3.86c-1.7.01-3.31-.27-4.51-.83c-3.87-1.8-7.97-9.94-5.45-13.29c1.53-2.04 5.82-2.92 9.96-3.12c3.97-.19 7.81.25 8.94.91c2.61 1.52 2.13 8.84.11 12.47z" fill="#212121" stroke="#212121" stroke-width=".55" stroke-miterlimit="10">
|
||||
</path>
|
||||
<g>
|
||||
<linearGradient id="IconifyId17ecdb2904d178eab20906" gradientUnits="userSpaceOnUse" x1="79.569" y1="22.713" x2="76.946" y2="11.668" gradientTransform="matrix(1 0 0 -1 0 128)">
|
||||
<stop offset=".002" stop-color="#212121" stop-opacity=".2">
|
||||
</stop>
|
||||
<stop offset="1" stop-color="#212121" stop-opacity=".6">
|
||||
</stop>
|
||||
</linearGradient>
|
||||
<path d="M101.67 121.61l.57-2.2l.01-.05l1.93-7.6l-6.9-1.98l-34.92-10.03c-.05-.01-.09-.01-.13-.03a6.177 6.177 0 0 0-7.51 4.27L48.97 124h52.02l.68-2.39z" opacity=".67" fill="url(#IconifyId17ecdb2904d178eab20906)">
|
||||
</path>
|
||||
<path d="M105.75 111.88c.29-1.01-.29-2.06-1.3-2.34l-38.69-11.1a6.19 6.19 0 0 0-7.65 4.24L52 124h50.28l3.47-12.12z" fill="#424242">
|
||||
</path>
|
||||
<linearGradient id="IconifyId17ecdb2904d178eab20907" gradientUnits="userSpaceOnUse" x1="81.84" y1="17.098" x2="79.869" y2="10.486" gradientTransform="matrix(1 0 0 -1 0 128)">
|
||||
<stop offset="0" stop-color="#ef5350">
|
||||
</stop>
|
||||
<stop offset="1" stop-color="#e53935">
|
||||
</stop>
|
||||
</linearGradient>
|
||||
<path d="M105.08 120.31c.35-1.22-.38-2.5-1.62-2.85l-41.52-11.9c-4.53-1.3-5.32 2.35-6.59 6.78L52 124h52.02l1.06-3.69z" fill="url(#IconifyId17ecdb2904d178eab20907)">
|
||||
</path>
|
||||
<linearGradient id="IconifyId17ecdb2904d178eab20908" gradientUnits="userSpaceOnUse" x1="58.405" y1="19.113" x2="60.268" y2="24.969" gradientTransform="matrix(1 0 0 -1 0 128)">
|
||||
<stop offset="0" stop-color="#212121">
|
||||
</stop>
|
||||
<stop offset="1" stop-color="#424242">
|
||||
</stop>
|
||||
</linearGradient>
|
||||
<path d="M63.26 98.24a6.172 6.172 0 0 0-5.14 4.42L52 124h3.87l7.39-25.76z" fill="url(#IconifyId17ecdb2904d178eab20908)">
|
||||
</path>
|
||||
<path d="M64.33 101.57c.18 0 .38.02.59.07l37.25 10.7l-.31 1.08c-11.79-3.29-34.29-9.62-38.94-11.16c.24-.33.71-.69 1.41-.69m0-3.33c-4.52 0-6.78 5.57-3.12 6.94c4.03 1.5 42.93 12.32 42.93 12.32l1.58-5.52c.31-1.06-.19-2.14-1.11-2.4L65.77 98.42c-.5-.12-.98-.18-1.44-.18z" fill="#424242" opacity=".2">
|
||||
</path>
|
||||
<linearGradient id="IconifyId17ecdb2904d178eab20909" gradientUnits="userSpaceOnUse" x1="-117.44" y1="-972.312" x2="-73.995" y2="-972.312" gradientTransform="matrix(.9612 .2758 -.3192 1.1123 -136.555 1216.41)">
|
||||
<stop offset=".01" stop-color="#bdbdbd">
|
||||
</stop>
|
||||
<stop offset=".987" stop-color="#f8f8f7">
|
||||
</stop>
|
||||
</linearGradient>
|
||||
<path d="M103.37 112.12l-39.8-11.42c-1.08-.31-2.26.46-2.62 1.71l-.06.22c-.36 1.25.23 2.53 1.31 2.84l39.8 11.42s-.34-.83.07-2.3c.41-1.48 1.3-2.47 1.3-2.47z" fill="url(#IconifyId17ecdb2904d178eab20909)">
|
||||
</path>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M104.07 25.11c-2.44-3.69-7.91-8.64-12.82-8.97c-.79-4.72-5.84-8.72-10.73-10.27c-13.23-4.19-21.84.51-26.46 3.03c-.96.52-7.17 3.97-11.51 1.5c-2.72-1.55-2.67-5.74-2.67-5.74s-8.52 3.25-5.61 12.3c-2.93.12-6.77 1.36-8.8 5.47c-2.42 4.9-1.56 8.99-.86 10.95c-2.52 2.14-5.69 6.69-3.52 12.6c1.64 4.45 8.17 6.5 8.17 6.5c-.46 8.01 1.03 12.94 1.82 14.93c.14.35.63.32.72-.04c.99-3.97 4.37-17.8 4.03-20.21c0 0 11.35-2.25 22.17-10.22c2.2-1.62 4.59-3 7.13-4.01c13.59-5.41 16.43 3.82 16.43 3.82s9.42-1.81 12.26 11.27c1.07 4.9 1.79 12.75 2.4 18.24c.04.39.57.47.72.11c.95-2.18 2.85-6.5 3.3-10.91c.16-1.55 4.34-3.6 6.14-10.26c2.41-8.88-.54-17.42-2.31-20.09z" fill="#232020">
|
||||
</path>
|
||||
<radialGradient id="IconifyId17ecdb2904d178eab20910" cx="82.019" cy="84.946" r="35.633" gradientTransform="matrix(.3076 .9515 .706 -.2282 -3.184 -15.605)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".699" stop-color="#444140" stop-opacity="0">
|
||||
</stop>
|
||||
<stop offset="1" stop-color="#444140">
|
||||
</stop>
|
||||
</radialGradient>
|
||||
<path d="M100.22 55.5c.16-1.55 4.34-3.6 6.14-10.26c.19-.71.35-1.43.5-2.15c1.46-8.09-1.16-15.52-2.79-17.98c-2.26-3.41-7.1-7.89-11.69-8.81c-.4-.05-.79-.1-1.16-.12c0 0 .33 2.15-.54 3.86c-1.12 2.22-3.41 2.75-3.41 2.75c11.97 11.98 11.12 22 12.95 32.71z" fill="url(#IconifyId17ecdb2904d178eab20910)">
|
||||
</path>
|
||||
<radialGradient id="IconifyId17ecdb2904d178eab20911" cx="47.28" cy="123.8" r="9.343" gradientTransform="matrix(.8813 .4726 .5603 -1.045 -63.752 111.228)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".58" stop-color="#444140">
|
||||
</stop>
|
||||
<stop offset="1" stop-color="#444140" stop-opacity="0">
|
||||
</stop>
|
||||
</radialGradient>
|
||||
<path d="M56.95 7.39c-1.1.53-2.06 1.06-2.9 1.51c-.96.52-7.17 3.97-11.51 1.5c-2.67-1.52-2.67-5.58-2.67-5.72c-1.23 1.57-4.95 12.78 5.93 13.53c4.69.32 7.58-3.77 9.3-7.23c.62-1.26 1.59-3.1 1.85-3.59z" fill="url(#IconifyId17ecdb2904d178eab20911)">
|
||||
</path>
|
||||
<radialGradient id="IconifyId17ecdb2904d178eab20912" cx="159.055" cy="62.862" r="28.721" gradientTransform="matrix(-.9378 -.3944 -.2182 .5285 231.04 50.678)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".699" stop-color="#444140" stop-opacity="0">
|
||||
</stop>
|
||||
<stop offset="1" stop-color="#444140">
|
||||
</stop>
|
||||
</radialGradient>
|
||||
<path d="M79.16 5.47c7.32 1.98 10.89 5.71 12.08 10.68c.35 1.46.77 15.08-25.23-.4c-9.67-5.76-7.03-9.36-5.9-9.77c4.42-1.6 10.85-2.73 19.05-.51z" fill="url(#IconifyId17ecdb2904d178eab20912)">
|
||||
</path>
|
||||
<radialGradient id="IconifyId17ecdb2904d178eab20913" cx="43.529" cy="115.276" r="8.575" gradientTransform="matrix(1 0 0 -1.2233 0 153.742)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".702" stop-color="#444140" stop-opacity="0">
|
||||
</stop>
|
||||
<stop offset="1" stop-color="#444140">
|
||||
</stop>
|
||||
</radialGradient>
|
||||
<path d="M39.84 4.68c-.01.01-.03.01-.06.03h-.01c-.93.39-8.24 3.78-5.51 12.25l7.78 1.25c-6.89-6.98-2.17-13.55-2.17-13.55s-.02.01-.03.02z" fill="url(#IconifyId17ecdb2904d178eab20913)">
|
||||
</path>
|
||||
<radialGradient id="IconifyId17ecdb2904d178eab20914" cx="42.349" cy="100.139" r="16.083" gradientTransform="matrix(-.9657 -.2598 -.2432 .9037 107.598 -51.632)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".66" stop-color="#444140" stop-opacity="0">
|
||||
</stop>
|
||||
<stop offset="1" stop-color="#444140">
|
||||
</stop>
|
||||
</radialGradient>
|
||||
<path d="M39.07 17.73l-4.81-.77c-.19 0-.83.06-1.18.11c-2.71.38-5.9 1.78-7.63 5.36c-1.86 3.86-1.81 7.17-1.3 9.38c.15.74.45 1.58.45 1.58s2.38-2.26 8.05-2.41l6.42-13.25z" fill="url(#IconifyId17ecdb2904d178eab20914)">
|
||||
</path>
|
||||
<radialGradient id="IconifyId17ecdb2904d178eab20915" cx="38.533" cy="84.609" r="16.886" gradientTransform="matrix(.9907 .1363 .1915 -1.3921 -15.841 155.923)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".598" stop-color="#444140" stop-opacity="0">
|
||||
</stop>
|
||||
<stop offset="1" stop-color="#444140">
|
||||
</stop>
|
||||
</radialGradient>
|
||||
<path d="M24.37 33.58c-2.37 2.1-5.56 6.79-3.21 12.61c1.77 4.39 8.09 6.29 8.09 6.29c0 .02 1.26.4 1.91.4l1.48-21.9c-3.03 0-5.94.91-7.82 2.22c.03.03-.46.35-.45.38z" fill="url(#IconifyId17ecdb2904d178eab20915)">
|
||||
</path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 16 KiB |
1
client/public/vite.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
65
client/rapport/H24-iteration1.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# Plan d'itération 1
|
||||
|
||||
## Étapes jalons
|
||||
|
||||
| Étape jalon | Date |
|
||||
| :------------------------------------------------ | :--------- |
|
||||
| Début de l'itération | 2024/01/08 |
|
||||
| Première rencontre avec les promoteurs | 2024/01/10 |
|
||||
| Démo de l'application et révision des user cases | 2024/02/06 |
|
||||
| Fin de l'itération | 2024/02/08 |
|
||||
|
||||
## Objectifs clés
|
||||
|
||||
Les objectifs clés de cette itération sont les suivants:
|
||||
|
||||
- Trouver des options alternatives d'hébergement de l'application
|
||||
- Mettre à jour les users cases
|
||||
- Configurer nos environnements de travail et se familiariser avec le projet
|
||||
- Débuter l'ajout ou la modification de quelques fonctionnalitées
|
||||
|
||||
|
||||
## Affectations d'éléments de travail
|
||||
|
||||
| Nom / Description | Priorité | [Taille estimée (points)](#commentEstimer 'Comment estimer?') | Assigné à (nom) | Documents de référence |
|
||||
| ------------------------------ | -------: | ------------------------------------------------------------: | --------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| Révision des user-stories | 1 | 4 | tous ||
|
||||
| Solution d'hébergement de l'application | 1| 4 | tous ||
|
||||
| Initialisation de la solution de base de données | 1| 4| Mathieu | |
|
||||
| Exportation pdf | 2| 2| Mélanie | |
|
||||
| Afficher le code de salle de manière permanente | 2| 1| Samy | |
|
||||
|
||||
## Problèmes principaux rencontrés
|
||||
|
||||
| Problème | Notes |
|
||||
| -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|Quelques difficultés avec l'installation de modules lors de l'installation initiale du projet sur nos postes de travail | |
|
||||
| La communication avec l'équipe de la STI pour obtenir un serveur à pris beaucoup de temps | |
|
||||
|
||||
## Critères d'évaluation
|
||||
|
||||
> Une brève description de la façon d'évaluer si les objectifs (définis plus haut) de haut niveau ont été atteints.
|
||||
> Vos critères d'évaluation doivent être objectifs (aucun membre de l'équipe ne peut avoir une opinion divergente) et quantifiables (sauf pour ceux évalués par l'auxiliaire d'enseignement). En voici des exemples:
|
||||
|
||||
- Mise à jour du document de Recueil de User Stories
|
||||
|
||||
## Évaluation
|
||||
|
||||
| Résumé | |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Cible d'évaluation | Itération |
|
||||
| Date d'évaluation | 2024/02/06 |
|
||||
| Participants | **Équipe** : Louis-Antoine Caron, Samy Waddah, Mathieu Roy, Mélanie St-Hilaire<br> **professeur** : Christopher Fuhrman |
|
||||
| État du projet | 🟢 |
|
||||
|
||||
### Éléments de travail: prévus vs réalisés
|
||||
|
||||
Une nouvelle solution d'hébergement à été choisie, une machine virtuelle sera fourni par la STI. Les users case ont été révisé avec les clients et mis à jours selon l'état actuel du projet. La configuration de la base de données à bien commencé.
|
||||
|
||||
### Évaluation par rapport aux résultats selon les critères d'évaluation
|
||||
|
||||
Une bonne partie des critères ont été atteint.
|
||||
|
||||
## Autres préoccupations et écarts
|
||||
|
||||
La nouvelle solution d'hébergement n'est pas encore accessible et ne peux donc pas être déployée pour le moment.
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
# Document de Recueil de User Stories
|
||||
|
||||
Version: 2
|
||||
|
||||
Dernière mise à jours: 6 février 2024
|
||||
|
||||
Auteurs:
|
||||
- Louis-Antoine Caron
|
||||
- Samy Waddah
|
||||
- Mathieu Roy
|
||||
- Mélanie St-Hilaire
|
||||
|
||||
## User stories
|
||||
|
||||
### User story 1 : Connexion d'un étudiant à un quiz en cours
|
||||
- **En tant que :** étudiant
|
||||
- **Je veux :** pouvoir accéder à un quiz après qu'il soit commencé
|
||||
- **Pour que :** je puisse répondre aux questions du quiz
|
||||
|
||||
### User story 2 : Imprimer un quiz et son solutionnaire
|
||||
- **En tant que :** professeur
|
||||
- **Je veux :** pouvoir exporter un quiz ainsi que son solutionnaire
|
||||
- **Pour que :** je puisse l'imprimer
|
||||
|
||||
### User story 3 : Upload images directement dans un quiz
|
||||
- **En tant que :** professeur
|
||||
- **Je veux :** pouvoir upload les images directement dans le quiz
|
||||
- **Pour que :** je ne veut pas perde les images si elles disparaissent d'un service externe (google drive, onedrive...)
|
||||
|
||||
### User story 4 : Performance
|
||||
- **En tant que :** professeur
|
||||
- **Je veux :** pouvoir faire des quiz avec des grandes classes
|
||||
- **Pour que :** je n'aie pas de bug lorsqu'on est beaucoup à utiliser le service
|
||||
|
||||
### User story 5 : Sauvegarde de quiz
|
||||
- **En tant que :** professeur
|
||||
- **Je veux :** que les quiz soient sauvegardé dans la plateforme
|
||||
- **Pour que :** je n'ai pas à transporter mes quiz d'une classe à l'autre
|
||||
|
||||
### User story 6 : Numéro de salle permanent par professeur
|
||||
- **En tant que :** professeur
|
||||
- **Je veux :** pouvoir réutiliser le même code d'accès pour toutes mes classes
|
||||
- **Pour que :** il soit facile pour mes étudiants de s'en rappeler
|
||||
|
||||
### User story 7 : Bug: perte de connexion
|
||||
- **En tant que :** étudiant
|
||||
- **Je veux :** que mon téléphone puisse tomber en veille sans être déconnecté de la plateforme
|
||||
- **Pour que :** je puisse continuer de participer au cours et garder le même compte
|
||||
|
||||
### User story 8 : Amélioration de l'interface mobile
|
||||
- **En tant que :** étudiant
|
||||
- **Je veux :** pouvoir utiliser la plateforme depuis mon téléphone
|
||||
- **Pour que :** ce soit plus facile de répondre
|
||||
|
||||
### User story 9 : Partager et dupliquer un quiz
|
||||
- **En tant que :** professeur
|
||||
- **Je veux :** pouvoir envoyer un quiz à un autre professeur
|
||||
- **Pour que :** il puisse en créer une copie sur son compte
|
||||
|
||||
### User story 10 : Bug: Syntaxe commentaire
|
||||
- **En tant que :** professeur
|
||||
- **Je veux :** pouvoir mettre des commentaires dans mon quiz
|
||||
- **Pour que :** il soit plus facile de comprendre mon quiz
|
||||
|
||||
### User story 11 : Utiliser la palette de couleurs de l'ETS
|
||||
- **En tant que :** promoteurs
|
||||
- **Je veux :** que la plateforme soit plus ""ETS""
|
||||
- **Pour que :** il soit plus facile à adopter dans le cadre de l'ETS
|
||||
|
||||
### User story 12 : Copier/Coller à partir de template de question
|
||||
- **En tant que :** professeur
|
||||
- **Je veux :** pouvoir copier des template de question
|
||||
- **Pour que :** il soit plus facilàe et efficient de créer mes quiz
|
||||
|
||||
### User story 13 : Création de dossiers de quiz
|
||||
- **En tant que :** professeur
|
||||
- **Je veux :** pouvoir créer des dossiers pour classer mes quiz
|
||||
- **Pour que :** il soit plus facile de trouver les quiz par cours
|
||||
|
||||
### User story 14 : Monitorer l'utilisation
|
||||
- **En tant que :** professeur
|
||||
- **Je veux :** pouvoir consulter un tableau avec les noms des d'enseignants-utilisateurs, nombre de quiz lancés par semaine ou par mois, le nombre de connexions totales
|
||||
- **Pour que :** je puisse justifier le maintien des serveurs et le support informatique au besoin
|
||||
|
||||
## Priorisation des user stories
|
||||
|
||||
| User story | Priorité | Notes |
|
||||
| ------------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------- |
|
||||
| User story *: Création d'un serveur | 🔴 | |
|
||||
| User story 1: Connexion d'un étudiant a un quiz en cours | 🔴 | |
|
||||
| User story 2: Imprimer un quiz et son solutionnaire | 🟡 | Geneviève: Important avec des images + j'imprime pour la révision des étudiants |
|
||||
| User story 3: Upload images directement dans un quiz | 🔴 | |
|
||||
| User story 4: Performance | 🔴 | Servir de 5 à 10 profs en même temps, ayant des classes de 60 personnes |
|
||||
| User story 5: Sauvegarde de quiz | 🔴 | Tout est dans le cloud donc pas besoin de trainer les quiz avec nous |
|
||||
| User story 6: Numéro de salle permanent par professeur | 🟢 | Plus facile pour les étudiants lorsque c'est toujours ex: Pitagore |
|
||||
| User story 7: Bug: perte de connection | 🔴 | |
|
||||
| User story 8: Amélioration de l'interface mobile | 🟡 | |
|
||||
| User story 9: partager et dupliquer un quiz | 🟠 | Avec un lien directement à partir de la plateforme ( pas besoin de partager des fichiers txt) |
|
||||
| User story 10: Bug: Syntaxe commentaire | 🟠 | |
|
||||
| User story 11: Utiliser la palette de couleurs de l'ETS | 🟢 | https://www.etsmtl.ca/ets/gouvernance/logos-et-identite-visuelle |
|
||||
| User story 12: Copier/Coller à partir de template de question | 🟠 | |
|
||||
| User story 13: Création de dossiers de quiz | 🟡 | |
|
||||
| User story 14: Monitorer l'utilisation | 🟡 | |
|
||||
|
||||
> Légende :
|
||||
>
|
||||
> 🔴 Essentielles (Critical) :
|
||||
> > Ces user stories sont absolument nécessaires pour le fonctionnement de base du produit. Elles représentent les fonctionnalités cruciales et indispensables.
|
||||
>
|
||||
> 🟠 Importantes (High) :
|
||||
> > Ces user stories sont importantes pour la valeur globale du produit, mais le produit pourrait encore fonctionner sans elles. Elles ajoutent des fonctionnalités significatives et améliorent l'expérience utilisateur.
|
||||
>
|
||||
> 🟡 Souhaitables (Medium) :
|
||||
> > Ces user stories apportent des améliorations supplémentaires ou des fonctionnalités agréables à avoir, mais elles ne sont pas essentielles. Elles contribuent à l'enrichissement de l'expérience utilisateur sans être critiques.
|
||||
>
|
||||
> 🟢 Accessoires (Low) :
|
||||
> > Ces user stories représentent des fonctionnalités ou des améliorations de moindre importance. Elles peuvent être reportées ou omises sans compromettre l'intégrité du produit. Elles sont souvent liées à des fonctionnalités cosmétiques ou des ajustements mineurs.
|
||||
|
||||
BIN
client/rapport/documentation/SRS-PFE004.pdf
Normal file
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 26 KiB |
72
client/rapport/rapport_iteration_1.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# Plan d'itération 1
|
||||
|
||||
## Étapes jalons
|
||||
|
||||
| Étape jalon | Date |
|
||||
| :------------------------------------- | :--------- |
|
||||
| Début de l'itération | 2023/09/05 |
|
||||
| Première rencontre avec les promoteurs | 2023/09/19 |
|
||||
| Démo des fichiers SRS et liste des CU | 2023/10/02 |
|
||||
| Fin de l'itération | 2023/10/04 |
|
||||
|
||||
## Objectifs clés
|
||||
|
||||
Les objectifs clés de cette itération sont les suivants:
|
||||
|
||||
- Créer un recueil des user stories
|
||||
- Prioriser les user stories
|
||||
- Créer un document SRS
|
||||
- Créer une liste des cas d'utilisation
|
||||
|
||||
## Affectations d'éléments de travail
|
||||
|
||||
| Nom / Description | Priorité | [Taille estimée (points)](#commentEstimer 'Comment estimer?') | Assigné à (nom) | Documents de référence |
|
||||
| ------------------------------ | -------: | ------------------------------------------------------------: | --------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| premiere liste de user-stories | 1 | 4 | tous | [Document de Recueil de User Stories ](./documentation/Document-de-Recueil-de-User-Stories.pdf) |
|
||||
| Priorisation des user-stories | 2 | 2 | tous | [Document de Recueil de User Stories ](./documentation/Document-de-Recueil-de-User-Stories.pdf) |
|
||||
| Document SRS | 3 | 4 | tous | [Document SRS](./documentation/SRS-PFE004.pdf) |
|
||||
|
||||
## Problèmes principaux rencontrés
|
||||
|
||||
| Problème | Notes |
|
||||
| -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| L'équipe n'est pas sure de la politique de confidentialité de socrative et de la possibilité de s'inspirer de son iterface utilisateur | Nous nous somme finalement éloigné de ce design pour des raison de temps |
|
||||
| Les disponibilités des membres de l'équipe sont difficile à coordonner avec celles des promoteurs. | Nous avons décidé de faire des rencontres avec les promoteurs en ligne, et d'alterner les rencontre entre les lundi et les mardi |
|
||||
| Mauvaise compréhension sur la sécurité de l'application. Est-ce que les professeurs ont besoins de comptes sécuritaires ou non? | Nous avons finalement écarter ce problème en nous focalisant sur l'objectif d'apporter de la valeurs aux client. L'application actuelle n'a pas besoin de sécurité puisque tout le monde peut prendre le role de professeur. (pas de comptes) |
|
||||
|
||||
## Critères d'évaluation
|
||||
|
||||
> Une brève description de la façon d'évaluer si les objectifs (définis plus haut) de haut niveau ont été atteints.
|
||||
> Vos critères d'évaluation doivent être objectifs (aucun membre de l'équipe ne peut avoir une opinion divergente) et quantifiables (sauf pour ceux évalués par l'auxiliaire d'enseignement). En voici des exemples:
|
||||
|
||||
- Le document des user stories doit être approuvé et repriorisé par les promoteurs.
|
||||
- Le document SRS doit être approuvé par les promoteurs.
|
||||
- Le document SRS doit comporter au moins 10 cas d'utilisation.
|
||||
- Le document SRS doit comporter au moins 10 exigences fonctionnelles et 5 exigences non-fonctionnelles.
|
||||
- Le document SRS doit comporter une liste des hypothèses du projet.
|
||||
|
||||
## Évaluation
|
||||
|
||||
| Résumé | |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Cible d'évaluation | Itération |
|
||||
| Date d'évaluation | 2023/10/04 |
|
||||
| Participants | **Équipe** : Paul Berguin, Mihai Floca, Francois Richard, Bavithra Jeyarasa, Emerick Paul<br> **professeur** : Christopher Fuhrman |
|
||||
| État du projet | 🟢 |
|
||||
|
||||
### Éléments de travail: prévus vs réalisés
|
||||
|
||||
Tous les documents prévus ont été réalisés. Les documents ont été approuvés par les promoteurs.
|
||||
|
||||
**Note :** Nous n'avons cependant pas pris le temps de corriger les fautes d'orthographes et de grammaire des documents. Il est possible que des fautes soient présentes dans les documents, les prochains documents seront plus soignés.
|
||||
|
||||
### Évaluation par rapport aux résultats selon les critères d'évaluation
|
||||
|
||||
Tout les critères d'évaluation ont été atteints.
|
||||
|
||||
## Autres préoccupations et écarts
|
||||
|
||||
Nous n'avons pas encore de solution de déploiement pour l'application. Nous envisageons d'utiliser la technologie websocket qui est assez lourde et restreint les options de déploiement.
|
||||
|
||||
<a name="commentEstimer">Comment estimer la taille :</a>
|
||||
<https://docs.google.com/a/etsmtl.net/document/d/1bDy0chpWQbK9bZ82zdsBweuAgNYni3T2k79xihr6CuU/edit?usp=sharing>
|
||||
119
client/rapport/rapport_iteration_2.md
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# Plan d'itération 2
|
||||
|
||||
## Étapes jalons
|
||||
|
||||
| Étape jalon | Date |
|
||||
| :------------------- | :--------- |
|
||||
| Début de l'itération | 2023/10/05 |
|
||||
| Démo | 2023/10/31 |
|
||||
| Fin de l'itération | 2023/11/02 |
|
||||
|
||||
## Objectifs clés
|
||||
|
||||
Les objectifs clés de cette itération sont les suivants:
|
||||
|
||||
- Céation d'une application web fonctionnelle
|
||||
- Complétion des cas d'utilisation en relation avec les user-stories prioritaires
|
||||
- Recherche préliminaire sur les technologies de déploiement
|
||||
|
||||
## Affectations d'éléments de travail
|
||||
|
||||
| Nom / Description | Priorité | [Taille estimée (points)](#commentEstimer 'Comment estimer?') | Assigné à (nom) | Documents de référence | État |
|
||||
| ------------------------------------------------------------------------------ | -------: | ------------------------------------------------------------: | --------------- | ----------------------------------------------------------------------------------------------- | ---- |
|
||||
| Création du squelette de l'application | 1 | 1 | Francois | | 🟢 |
|
||||
| CU 03 – Création d’un questionnaire | 1 | 4 | Francois | [Document SRS](./documentation/SRS-PFE004.pdf) | 🟢 |
|
||||
| CU 04 – Suppression d'un questionnaire | 3 | 1 | Francois | [Document SRS](./documentation/SRS-PFE004.pdf) | 🟢 |
|
||||
| CU 05 – Ajout d’une question à un questionnaire | 1 | 2 | Francois | [Document SRS](./documentation/SRS-PFE004.pdf) | 🟢 |
|
||||
| CU 06 – Suppression d’une question d’un questionnaire | 1 | 1 | Francois | [Document SRS](./documentation/SRS-PFE004.pdf) | 🟢 |
|
||||
| CU 07 – Modification d’une question d’un questionnaire | 1 | 1 | Francois | [Document SRS](./documentation/SRS-PFE004.pdf) | 🟢 |
|
||||
| CU 08 – Visualisation des questionnaires | 2 | 1 | Paul | [Document SRS](./documentation/SRS-PFE004.pdf) | 🟢 |
|
||||
| CU 09 – Lancement d’un quiz | 2 | 4 | Paul | [Document SRS](./documentation/SRS-PFE004.pdf) | 🟢 |
|
||||
| CU 10 – Connection à un quiz | 2 | 4 | Paul | [Document SRS](./documentation/SRS-PFE004.pdf) | 🟢 |
|
||||
| CU 11 – Répondre à une question | 1 | 3 | Mihai | [Document SRS](./documentation/SRS-PFE004.pdf) | 🟢 |
|
||||
| CU 12 – Passer à la question suivante | 2 | 1 | Paul | [Document SRS](./documentation/SRS-PFE004.pdf) | 🟢 |
|
||||
| US 15 – Dupliquer un questionnaire | 2 | 1 | Mihai | [Document de Recueil de User Stories ](./documentation/Document-de-Recueil-de-User-Stories.pdf) | 🟢 |
|
||||
| US 08 – Création commentaires (ou rétroaction) sur les réponses d'une question | 1 | 1 | Francois | [Document de Recueil de User Stories ](./documentation/Document-de-Recueil-de-User-Stories.pdf) | 🟢 |
|
||||
| Support du LateX dans les quiz | 3 | 1 | Francois | | 🟢 |
|
||||
|
||||
## Problèmes principaux rencontrés
|
||||
|
||||
| Problème | Notes |
|
||||
| --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| Choix du framework à utiliser pour le frontend de l'application | Nous avons finalement choisi d'utiliser React.js pour le frontend de l'application. |
|
||||
| Choix de la technologie pour la communication des questions des quiz entre étudiants et professeurs | Nous avons opté pour l'utilisation de websocket, dans un soucis de simplicité |
|
||||
| Support de GIFT | Nous avons choisi d'utiliser le template créé par le professeur |
|
||||
|
||||
## Critères d'évaluation
|
||||
|
||||
> Une brève description de la façon d'évaluer si les objectifs (définis plus haut) de haut niveau ont été atteints.
|
||||
> Vos critères d'évaluation doivent être objectifs (aucun membre de l'équipe ne peut avoir une opinion divergente) et quantifiables (sauf pour ceux évalués par l'auxiliaire d'enseignement). En voici des exemples:
|
||||
|
||||
- L'application doit être fonctionnelle
|
||||
- L'utilisateur doit pouvoir créer un questionnaire
|
||||
- L'utilisateur doit pouvoir supprimer un questionnaire
|
||||
- L'utilisateur doit pouvoir ajouter une question à un questionnaire
|
||||
- L'utilisateur doit pouvoir supprimer une question d'un questionnaire
|
||||
- L'utilisateur doit pouvoir modifier une question d'un questionnaire
|
||||
- L'utilisateur doit pouvoir visualiser les questionnaires
|
||||
- L'utilisateur doit pouvoir lancer un quiz
|
||||
- L'utilisateur doit pouvoir se connecter à un quiz
|
||||
- L'utilisateur doit pouvoir répondre à une question
|
||||
- L'utilisateur doit pouvoir passer à la question suivante
|
||||
- Création d'une couverture suite de test de 80% de l'application
|
||||
|
||||
## Évaluation
|
||||
|
||||
| Résumé | |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Cible d'évaluation | Itération |
|
||||
| Date d'évaluation | 2023/11/02 |
|
||||
| Participants | **Équipe** : Paul Berguin, Mihai Floca, Francois Richard, Bavithra Jeyarasa, Emerick Paul<br> **professeur** : Christopher Fuhrman |
|
||||
| État du projet | 🟢 |
|
||||
|
||||
### Éléments de travail: prévus vs réalisés
|
||||
|
||||
Nous avons réalisé tous les éléments de travail prévus, bien que certains éléments présentent encore quelques bogues. De plus, peu d'effort ont étés assignés a la qualité de l'interface utilisateur.
|
||||
|
||||
### Évaluation par rapport aux résultats selon les critères d'évaluation
|
||||
|
||||
La pluspart des objectifs ont été atteints. L'application est fonctionnelle, mais présente encore quelques bogues. La couverture de test est de 95% pour le backend et de 0% pour le frontend.
|
||||
D'un autre coté, nous avons ajoutés des fonctionnalités qui n'étaient pas prévues, comme la visualisation en temps réel des réponses dans un tableau coté professeur.
|
||||
|
||||
## Autres préoccupations et écarts
|
||||
|
||||
- Nous n'avons pas encore de solution de déploiement pour l'application. Héroku demande un compte de crédit pour déployer une application. Les solutions GCP, AWS et Azure sont très complexes et demandent beaucoup de temps pour être mises en place.
|
||||
- La couverture de test ne nous satisfait pas. Nous avons eu de la difficulté à tester les composants React. Cet objectif est donc reporté à l'itération 3.
|
||||
- Pour le moment, les quizs sont sauvegardés dans les cookies du navigateur.
|
||||
|
||||
## Retour des promoteurs suite à la démo
|
||||
|
||||
- Les promoteurs ont appréciés la démo
|
||||
- Ils ont demandé que nos prochains efforts soient concentrés sur le support d'image dans les questions des quiz
|
||||
- Ils ont aussi demandés la gestion de la langue dans les questions des quiz (Francais en priorité)
|
||||
- Ils ont aussi soulevés la question de sauvegarde des quiz et des questions. Le professeur a suggéré de sauvegarder les quizs dans des fichier textes que l'on pourrait ensuite importer dans l'application. Cela permettrai de ne pas avoir à gérer une base de données.
|
||||
- Ils ont aussi demandés que l'on puisse cacher les réponses dans le tableau récapitulatif (coté professeur).
|
||||
- Ils ont aussi demandés que l'on se concentre sur l'option de création d'un quiz au rythme de l'étudiant
|
||||
- Ils ont aussi soulevés le fait qu'ils ne connaissent pas le format GIFT. Il faudrait donc ajouter un lien vers la documentation de GIFT dans l'application.
|
||||
- Ils ont aussi soulevés la question du déploiement de l'application. (solutions proposées : github pages)
|
||||
- Ils ont aussi demandés que l'on ajoute le % de réussite de chaque question dans le tableau récapitulatif (coté professeur).
|
||||
|
||||
# Principaux diagrammes
|
||||
|
||||
## diagramme de conception actuel de l'application :
|
||||
|
||||

|
||||
|
||||
## diagramme de déploiement actuel de l'application :
|
||||
|
||||

|
||||
|
||||
## diagramme de séquence actuel de JoinRoom :
|
||||
|
||||

|
||||
|
||||
## diagramme de séquence actuel de ManageRoom :
|
||||
|
||||

|
||||
|
||||
<a name="commentEstimer">Comment estimer la taille :</a>
|
||||
<https://docs.google.com/a/etsmtl.net/document/d/1bDy0chpWQbK9bZ82zdsBweuAgNYni3T2k79xihr6CuU/edit?usp=sharing>
|
||||
110
client/rapport/rapport_iteration_3.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# Plan d'itération 3
|
||||
|
||||
## Étapes jalons
|
||||
|
||||
| Étape jalon | Date |
|
||||
| :------------------- | :--------- |
|
||||
| Début de l'itération | 2023/11/03 |
|
||||
| Démo | 2023/10/13 |
|
||||
| Fin de l'itération | 2023/11/16 |
|
||||
|
||||
## Objectifs clés
|
||||
|
||||
Les objectifs clés de cette itération sont les suivants:
|
||||
|
||||
- Création d'une option de création de quiz au rythme des étudiants
|
||||
- Support des images dans les questions
|
||||
- Support des formules mathématiques dans les questions
|
||||
- Gestion de l'import/export des questionnaires
|
||||
- Déploiement d'un prototype de l'application
|
||||
|
||||
## Affectations d'éléments de travail
|
||||
|
||||
| Nom / Description | Priorité | [Taille estimée (points)](#commentEstimer 'Comment estimer?') | Assigné à (nom) | Documents de référence | État |
|
||||
| --------------------------------------------------------------------------------------------------- | -------: | ------------------------------------------------------------: | --------------- | ----------------------------------------------------------------------------------------------- | ---- |
|
||||
| US 08 – Questionnaire au rythme de l'étudiant | 1 | 2 | Paul | [Document de Recueil de User Stories ](./documentation/Document-de-Recueil-de-User-Stories.pdf) | 🟢 |
|
||||
| US 17 – Rétroaction a une question répondu par l'étudiant | 1 | 2 | Mihai | [Document de Recueil de User Stories ](./documentation/Document-de-Recueil-de-User-Stories.pdf) | 🟢 |
|
||||
| US 04 – Support des images | 1 | 4 | Bavithra | [Document de Recueil de User Stories ](./documentation/Document-de-Recueil-de-User-Stories.pdf) | 🔴 |
|
||||
| Suite a la démo précédente : Import/Export des questionnaires | 2 | 4 | Emerick | | 🔴 |
|
||||
| Suite a la démo précédente : Création d'une documentation GIFT | 2 | 1 | Francois/Paul | | 🟢 |
|
||||
| Suite a la démo précédente : Déploiement d'un prototype Frontend de l'application | 3 | 3 | Mihai | | 🟢 |
|
||||
| Suite a la démo précédente : Déploiement d'un prototype Backend de l'application | 3 | 3 | Paul | | 🟢 |
|
||||
| Suite a la démo précédente : Ajout du % de réussite des questions dans le tableau | 2 | 1 | Paul | | 🟢 |
|
||||
| Suite a la démo précédente : traduction de l'application de l'anglais vers le Francais | 1 | 1 | tous | | 🟠 |
|
||||
| Suite a la démo précédente : Ajout de l'option de cacher les réponses dans le tableau en temps réel | 2 | 1 | Paul | | 🟢 |
|
||||
|
||||
## Problèmes principaux rencontrés
|
||||
|
||||
| Problème | Notes |
|
||||
| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Support de l'image dans l'éditeur de quiz | Afin d'accélerer l'avancée du projet, nous avons opté pour un éditeur, choix qui complique le support de l'image pour chaque question |
|
||||
| Support du import/export de fichiers textes pour quizs | Nous avons pris plus de temps que prévu pour réaliser cette fonctionnalité. |
|
||||
| déploiement de l'application backend | Nous avons essayer plusieurs options. Nous avons essayer de déployer le backend sous forme de docker sur Azure. Nous avons aussi déployer notre backend avec glitch.com. Le docker a couté 4$ sans appel un weekend. Gitch.com est gratuit mais turn off l'application au bout de 5 minutes d'inactivité |
|
||||
|
||||
## Critères d'évaluation
|
||||
|
||||
> Une brève description de la façon d'évaluer si les objectifs (définis plus haut) de haut niveau ont été atteints.
|
||||
> Vos critères d'évaluation doivent être objectifs (aucun membre de l'équipe ne peut avoir une opinion divergente) et quantifiables (sauf pour ceux évalués par l'auxiliaire d'enseignement). En voici des exemples:
|
||||
|
||||
- Tous les tests unitaires passent
|
||||
- Les fonctionnalités discutés durant la derniere démo sont implémentées
|
||||
- L'application est déployée
|
||||
|
||||
## Évaluation
|
||||
|
||||
| Résumé | |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Cible d'évaluation | Itération |
|
||||
| Date d'évaluation | 2023/11/16 |
|
||||
| Participants | **Équipe** : Paul Berguin, Mihai Floca, Francois Richard, Bavithra Jeyarasa, Emerick Paul<br> **professeur** : Christopher Fuhrman |
|
||||
| État du projet | 🟠 |
|
||||
|
||||
### Éléments de travail: prévus vs réalisés
|
||||
|
||||
Certains des éléments de travail prévus n'ont pas été réalisés.
|
||||
|
||||
- L'import/export de quizs n'a pas été réalisé.
|
||||
- En effet, nous avons eu de la difficulté à trouver une solution pour importer/exporter des quizs. Nous avons essayé plusieurs solutions, mais aucune n'a fonctionné. Nous avons donc décidé de reporter cette fonctionnalité à l'itération 4. Nous prioriserons cette fonctionnalité pour l'itération 4.
|
||||
- Le support des images dans les questions n'a pas été réalisé.
|
||||
- Le choix d'utiliser un éditeur de texte pour les questions a compliqué le support des images. Nous avons donc décidé de reporter cette fonctionnalité à l'itération 4. Nous prioriserons cette fonctionnalité pour l'itération 4.
|
||||
- Nous pensons qu'il serait intéressant, pour les prochaines itérations, de gerer la création des questions avec un composant personnalisé qui permettrait de créer des questions en n'utilisant que des boutons et avec des champs spécifiques pour le contenu des questions/réponses. Cela permettrait de simplifier le support des images et des formules mathématiques.
|
||||
- La traduction de l'application est partiellement réalisée.
|
||||
- Nous avons travailler sur l'interface graphique de l'application.
|
||||
|
||||
La plupart des éléments de travail prévus ont été réalisés.
|
||||
|
||||
### Évaluation par rapport aux résultats selon les critères d'évaluation
|
||||
|
||||
- Tous les tests unitaires passent
|
||||
- Nous avons ajouté des tests unitaires pour les composants React (il faut encore en ajouter). Nous avons aussi ajouté des tests unitaires pour le backend.
|
||||
- Les fonctionnalités discutés durant la derniere démo sont partiellement implémentées. (7.5/10)
|
||||
- L'application est déployée
|
||||
- Nous avons déployé le backend sur glitch.com. L'application est disponible à l'adresse suivante : https://ets-glitch-backend.glitch.me/
|
||||
- Nous avons déployé le frontend sur Vercel. L'application est disponible à l'adresse suivante : https://evaluetonsavoir.vercel.app/
|
||||
|
||||
## Autres préoccupations et écarts
|
||||
|
||||
- La solution de déploiement du backend n'est pas idéale. Nous avons essayé de déployer le backend sur Azure, mais cela nous a couté 4$ sans appel un weekend. Nous avons donc décidé de déployer le backend sur glitch.com. Cependant, glitch.com turn off l'application au bout de 5 minutes d'inactivité. Glitch.com n'est pas une solution idéale pour le déploiement d'une application. Cependant pour un prototype, cette solution nous satisfait. Nous envisageons de discuter avec les promoteurs pour explorer d'autres solutions de déploiement.
|
||||
- Le support du LateX coté étudiant n'est toujours pas implémenté. Nous avons décidé de prioriser cette fonctionnalité à l'itération 4.
|
||||
|
||||
## Retour des promoteurs suite à la démo
|
||||
|
||||
- Les promoteurs ont appréciés la démo
|
||||
- Ils ont repéré quelques bogues
|
||||
- pas de bouton de déconnexion
|
||||
- reload des pages ne fonctionne pas sur la solution déployée
|
||||
- probleme de connexion a un nom de salle inexistant
|
||||
- nom de la salle en chiffres uniquement
|
||||
- support du LateX coté étudiant
|
||||
- Ils ont demandés la possibilité de retourner sur une question (dans le tableau des résultats en temps réel) pour donner de meilleur rétroaction aux étudiants
|
||||
- Ils ont demandés de prioriser :
|
||||
- le support du LateX coté étudiant
|
||||
- le support des images dans les questions
|
||||
- l'import/export des questionnaires
|
||||
|
||||
# Principaux diagrammes
|
||||
|
||||
pas de changements majeurs par rapport à l'itération 2
|
||||
|
||||
<a name="commentEstimer">Comment estimer la taille :</a>
|
||||
<https://docs.google.com/a/etsmtl.net/document/d/1bDy0chpWQbK9bZ82zdsBweuAgNYni3T2k79xihr6CuU/edit?usp=sharing>
|
||||
138
client/rapport/rapport_iteration_4.md
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
# Plan d'itération 3
|
||||
|
||||
## Étapes jalons
|
||||
|
||||
| Étape jalon | Date |
|
||||
| :------------------- | :--------- |
|
||||
| Début de l'itération | 2023/11/17 |
|
||||
| Démo | 2023/12/05 |
|
||||
| remise du projet | 2023/12/13 |
|
||||
| Présentation finale | 2023/12/13 |
|
||||
| Fin de l'itération | 2023/12/13 |
|
||||
|
||||
## Objectifs clés
|
||||
|
||||
Les objectifs clés de cette itération sont les suivants:
|
||||
|
||||
- Création d'une option de création de quiz au rythme des étudiants
|
||||
- Support des images dans les questions
|
||||
- Support des formules mathématiques dans les questions
|
||||
- Gestion de l'import/export des questionnaires
|
||||
- Déploiement d'un prototype de l'application
|
||||
|
||||
## Affectations d'éléments de travail
|
||||
|
||||
| Nom / Description | Priorité | [Taille estimée (points)](#commentEstimer 'Comment estimer?') | Assigné à (nom) | Documents de référence | État |
|
||||
| ------------------------------------------------------------------------------------------------- | -------: | ------------------------------------------------------------: | ---------------- | ---------------------- | ---- |
|
||||
| Suite a la démo précédente : traduction de l'application de l'anglais vers le Francais | 2 | 1 | tous | | 🟢 |
|
||||
| Suite a la démo précédente : import/export des questionnaires | 1 | 4 | Mihai | | 🟢 |
|
||||
| Suite a la démo précédente : support des images | 1 | 4 | Paul | | 🟢 |
|
||||
| Suite a la démo précédente : nom de salle en chiffres | 3 | 1 | Paul | | 🟢 |
|
||||
| Suite a la démo précédente : reload fonctionnel sur vercel | 1 | 1 | Mihai | | 🟢 |
|
||||
| Suite a la démo précédente : bouton de déconnexion lorsqu'un quiz est lancé | 3 | 1 | Paul | | 🟢 |
|
||||
| Suite a la démo précédente : support du LateX coté étudiant | 1 | 4 | Francois | | 🟢 |
|
||||
| Suite a la démo précédente : retour sur une question dans le tableau des résultats en temps reels | 3 | 1 | Paul | | 🟢 |
|
||||
| déploiement continue des applications | 3 | 1 | tous | | 🟢 |
|
||||
| Finaliser les suites de tests et les ajouter aux déploiement continue des application | 3 | 4 | Bavithra/Emerick | | 🟢 |
|
||||
| Amélioration de l'interface utilisateur | 4 | 4 | Mihai | | 🟢 |
|
||||
| Investigation d'une autre solution de déploiement backend | 4 | 4 | Paul | | 🟢 |
|
||||
| Effectuer un premier stress test de l'application | 4 | 4 | Paul | | 🟢 |
|
||||
|
||||
## Problèmes principaux rencontrés
|
||||
|
||||
| Problème | Notes |
|
||||
| ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Support de l'image dans l'éditeur de quiz | Le support de l'image fonctionne mais nous pensons qu'il peut être amélioré, surtout si on se détache de l'éditeur de questionnaire sous forme de texte |
|
||||
| choix de librairie pour les composants visuel | Nous avons longtemps hésiter d'utiliser une librairie pour le coté visuel (bootstrap ou autre). Nous avons finalement décidé d'utiliser Material UI. Cela nous permet d'avoir un visuel beaucoup plus professionnel. |
|
||||
| choix du composant pour l'affichage des questions et choix de réponses coté étudiant | Nous hésitons entre utiliser le composant GIFTTemplate, ou créer un composant spécifique. La seconde solution serait plus intéressante car plus personnalisable mais ajoute de la complexité au developpement. |
|
||||
| Les utilisateurs sur téléphones sont déconnecté s'ils sont trop longtemps inactifs et que le téléphone se met en veille. | L'équipe hésite sur le fait que cette fonctionnalité soit un problème ou non. Nous avons décidé de laisser le comportement tel quel pour le moment. Cela permet de fermet au maximum le nombre de connexions ouvertes |
|
||||
| Certaines des adresses d'images ne fonctionnent pas | Nous n'avons pas eu le temps d'investiguer ce problème |
|
||||
| Support de l'exportation des documents sous format pdf | Nous n'avons pas eu le temps de supporter cette fonctionnalité |
|
||||
|
||||
## Critères d'évaluation
|
||||
|
||||
> Une brève description de la façon d'évaluer si les objectifs (définis plus haut) de haut niveau ont été atteints.
|
||||
> Vos critères d'évaluation doivent être objectifs (aucun membre de l'équipe ne peut avoir une opinion divergente) et quantifiables (sauf pour ceux évalués par l'auxiliaire d'enseignement). En voici des exemples:
|
||||
|
||||
- Tous les tests unitaires passent (couverture de 80%)
|
||||
- Les fonctionnalités discutés durant la derniere démo sont implémentées
|
||||
|
||||
## Évaluation
|
||||
|
||||
| Résumé | |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Cible d'évaluation | Itération |
|
||||
| Date d'évaluation | 2023/11/16 |
|
||||
| Participants | **Équipe** : Paul Berguin, Mihai Floca, Francois Richard, Bavithra Jeyarasa, Emerick Paul<br> **professeur** : Christopher Fuhrman |
|
||||
| État du projet | 🟢 |
|
||||
|
||||
### Éléments de travail: prévus vs réalisés
|
||||
|
||||
Tous les éléments de travail prévus ont été réalisés. Nous avons aussi complêtement retravailler l'interface utilisateur pour la rendre plus professionnelle.
|
||||
De plus nous avons une couverture de test très satisfaisante.
|
||||
Enfin, nous avons intégré un système de déploiement continue pour le backend et le frontend.
|
||||
|
||||
### Évaluation par rapport aux résultats selon les critères d'évaluation
|
||||
|
||||
| Critère d'évaluation | Évaluation |
|
||||
| ---------------------------------------------------------------------- | ---------- |
|
||||
| Tous les tests unitaires passent (couverture de 80%) | |
|
||||
| Les fonctionnalités discutés durant la derniere démo sont implémentées | |
|
||||
|
||||
## Autres préoccupations et écarts
|
||||
|
||||
Nous avons effectué une refonte complête de l'interface utilisateur. Ce changement peut être considéré comme un écart par rapport au plan initial. Cependant, nous pensons que ce changement est bénéfique pour le projet. En effet, l'interface utilisateur est beaucoup plus professionnelle et plus facile à utiliser. Toutes les fonctionnalités initialement présentes on été conservées. Aucun bris n'est a reporter suite a ce changement.
|
||||
|
||||
## Retour des promoteurs suite à la démo
|
||||
|
||||
## Propositions d'amélioration
|
||||
|
||||
Lors de cette session, notre équipe s'est concentrée sur l'implémentation des fonctionnalités éssentielles de l'application. Notre objectif principal était de livrer un produit fonctionnel, bien qu'imparfait. Cela permettra aux promoteurs de tester l'application avec des utilisateurs réels et donner un retour sur les améliorations à apporter et les bug rencontrés.
|
||||
|
||||
Le produit que nous livrons est donc un prototype fonctionnel. Nous avons donc laissé de coté certaines fonctionnalités qui pourraient être intéressantes pour la prochaine équipe. Voici une liste de ces fonctionnalités et nos commentaires sur leur importance:
|
||||
|
||||
| amélioration | Commentaire |
|
||||
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| support de tous les types de question | Pour le moment on ne supporte que 4 types de question (composants \components\Questions ). Il serait intéressant de supporter tous les types de GIFT ce qui permettrait une meilleur portabilité des quiz. En effet les promoteurs pourrait avoir envie d'importer un quiz depuis moodle vers notre solution. (recherche global "TODO" dans le code) |
|
||||
| Refactoring de de la page EditorQuiz | Actuellement l'application propose a l'utilisateur de créer les quiz en écrivant directement sous format GIFT dans un éditeur de text. Il serait intéressant de se débarrasser de cet éditeur et de permettre a l'utilisateur de créer des quiz, d'ajouter des question, choisir leurs types et d'ajouter des réponses seulement en cliquant sur des boutons. De sorte qu'ils n'aient pas a se soucier du format. |
|
||||
| Refactoring l'interface graphique | Pour le moment l'interface graphique est correct, mais il serait intéressant de la poffiner afin de présenter un produit final plus poussé. Nous avons choisit de ne pas utiliser bootstrap dans un soucis de clarification du code et pour des question de futur performance. Nous n'avons pas non plus utilisés tailwind css dans l'optique de garder notre code HTML aussi clair que possible. Si un changement est envisagé au niveau de la technologie du UI. Nous conseillons Tailwind css plutot que Bootstrap. |
|
||||
| Support de l'exportation des documents sous format pdf | Nous n'avons pas eu le temps de supporter cette fonctionnalité. Cependant, nous pensons que c'est une fonctionnalité importante. En effet, cela permettrait aux professeurs de pouvoir imprimer les quiz et de les distribuer aux étudiants. |
|
||||
| Retours d'informations manquantes | Le premier stress test de l'application (hébergé sur Glitch) à permis de mettre en avant certaines erreurs, bug ou manque de retours d'information. Par exemple, quand on essaye de se connecter à une salle pleine (60 étudiants), l'utilisateur devrait voir une page lui indiquant le problème. De plus certaines personnes ont étés déconnecté du questionnaire pendant qu'il réalisait celui-ci. Les erreurs trouvées lors de ce tests ont été listé dans le Notion du projet. |
|
||||
| Améliorations discutées lors du 1er stress test | Les participants au premier stress test nous ont proposer 2 principales améliorations qu'ils trouveraient intéréssantes. La premiere serait de classer le live résult en temps réel en fonction du % de réussite de chaque étudiant, pour que les meilleurs soit en tête de classement. La seconde serait de donner un feedback global du test une fois celui-ci complété (coté étudiant). Ces améliorations seront aussi documentés dans le Notion du projet. |
|
||||
|
||||
## Discussion sur le déploiement Backend (glitch ou Azure)
|
||||
|
||||
Cette décision a été compliqué pour notre équipe. Tout d'abord le fait que nous utilisons les websocket restreint les options de déploiement.
|
||||
|
||||
- Notre premiere option a été Glitch, qui fournis une plateforme gratuite. Le déploiement continu a été implémenté pour cette plateforme. Le principal problème de cette plateforme est la mise en veille de notre backend après 5 minutes d'inactivité. L'avantage de Glitch par contre est que la plateforme permet d'héberger notre backend gratuitement. La plateforme présente de gros problème de performance pour une salle pleine.
|
||||
- D'un autre coté, Azure est aussi une plateforme intéréssante en raison de l'entente entre l'école et la plateforme. Aucun d'entre nous n'avais d'experience avec la plateforme, mais nous avons réussit a déployer grace a l'extension Azure sur VScode. L'option gratuite ne permettait que la connexion de 5 personnes en meme temps. Nous avons donc opté pour le plan (payant) Basic B1. L'avantage principal de Azure est que la plateforme ne met pas en veille notre backend après 5 minutes d'inactivité. Cependant, le plan Basic B1 est payant et nous ne l'avons pas laisser assez longtemps en ligne pour savoir combien il nous couterait. De plus, Azure offre beaucoup plus de fonctionnalités de monitoring et de gestion de l'application.
|
||||
|
||||
## Discussion sur le déploiement Frontend
|
||||
|
||||
Pour le frontend de notre application, nous avons hésité entre Héroku, GithubPages et Vercel. Au final nous avons choisi Vercel pour sa simplicité d'utilisation et son intégration avec Github. Nous avons aussi implémenté le déploiement continu pour le frontend.
|
||||
Cependant après quelques temps d'utilisations nous avons remarqués quelques problèmes en raison de la gratuité de la plateforme. En effet, ce plan ne permet pas la création d'équipe pour gérer le projet et nous avons donc du désigner un membre de l'équipe pour gérer le projet.
|
||||
|
||||
# Principaux diagrammes
|
||||
|
||||
## Diagramme de classe
|
||||
|
||||

|
||||
|
||||
## Diagramme de déploiement
|
||||
|
||||

|
||||
|
||||
## Diagramme de séquence - Création d'une salle
|
||||
|
||||

|
||||
|
||||
## Diagramme de séquence - Rejoindre une salle
|
||||
|
||||

|
||||
|
||||
## Diagramme de séquence - déroulement d'un quiz
|
||||
|
||||

|
||||
|
||||
<a name="commentEstimer">Comment estimer la taille :</a>
|
||||
<https://docs.google.com/a/etsmtl.net/document/d/1bDy0chpWQbK9bZ82zdsBweuAgNYni3T2k79xihr6CuU/edit?usp=sharing>
|
||||
66
client/src/App.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// App.tsx
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
|
||||
// Page main
|
||||
import Home from './pages/Home/Home';
|
||||
|
||||
// Pages espace enseignant
|
||||
import Dashboard from './pages/Teacher/Dashboard/Dashboard';
|
||||
import Share from './pages/Teacher/Share/Share';
|
||||
import Login from './pages/Teacher/Login/Login';
|
||||
import Register from './pages/Teacher/Register/Register';
|
||||
import ResetPassword from './pages/Teacher/ResetPassword/ResetPassword';
|
||||
import ManageRoom from './pages/Teacher/ManageRoom/ManageRoom';
|
||||
import QuizForm from './pages/Teacher/EditorQuiz/EditorQuiz';
|
||||
|
||||
// Pages espace étudiant
|
||||
import JoinRoom from './pages/Student/JoinRoom/JoinRoom';
|
||||
|
||||
// Header/Footer import
|
||||
import Header from './components/Header/Header';
|
||||
import Footer from './components/Footer/Footer';
|
||||
|
||||
import ApiService from './services/ApiService';
|
||||
|
||||
const handleLogout = () => {
|
||||
ApiService.logout();
|
||||
}
|
||||
|
||||
const isLoggedIn = () => {
|
||||
return ApiService.isLogedIn();
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="content">
|
||||
|
||||
<Header
|
||||
isLoggedIn={isLoggedIn}
|
||||
handleLogout={handleLogout}/>
|
||||
|
||||
<div className="app">
|
||||
<main>
|
||||
<Routes>
|
||||
{/* Page main */}
|
||||
<Route path="/" element={<Home />} />
|
||||
|
||||
{/* Pages espace enseignant */}
|
||||
<Route path="/teacher/login" element={<Login />} />
|
||||
<Route path="/teacher/register" element={<Register />} />
|
||||
<Route path="/teacher/resetPassword" element={<ResetPassword />} />
|
||||
<Route path="/teacher/dashboard" element={<Dashboard />} />
|
||||
<Route path="/teacher/share/:id" element={<Share />} />
|
||||
<Route path="/teacher/editor-quiz/:id" element={<QuizForm />} />
|
||||
<Route path="/teacher/manage-room/:id" element={<ManageRoom />} />
|
||||
|
||||
{/* Pages espace étudiant */}
|
||||
<Route path="/student/join-room" element={<JoinRoom />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
<Footer/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
6
client/src/Types/FolderType.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export interface FolderType {
|
||||
_id: string;
|
||||
userId: string;
|
||||
title: string;
|
||||
created_at: string;
|
||||
}
|
||||
6
client/src/Types/QuestionType.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { GIFTQuestion } from 'gift-pegjs';
|
||||
|
||||
export interface QuestionType {
|
||||
question: GIFTQuestion;
|
||||
image: string;
|
||||
}
|
||||
10
client/src/Types/QuizType.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// QuizType.tsx
|
||||
export interface QuizType {
|
||||
_id: string;
|
||||
folderId: string;
|
||||
userId: string;
|
||||
title: string;
|
||||
content: string[];
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
4
client/src/Types/UserType.tsx
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export interface UserType {
|
||||
name: string;
|
||||
id: string;
|
||||
}
|
||||
43
client/src/__tests__/Types/QuestionType.test.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
//QuestionType.test.tsx
|
||||
import { GIFTQuestion } from 'gift-pegjs';
|
||||
import { QuestionType } from '../../Types/QuestionType';
|
||||
|
||||
const mockQuestion: GIFTQuestion = {
|
||||
id: '1',
|
||||
type: 'MC',
|
||||
stem: { format: 'plain', text: 'Sample Question' },
|
||||
title: 'Sample Question',
|
||||
hasEmbeddedAnswers: false,
|
||||
globalFeedback: null,
|
||||
choices: [
|
||||
{ text: { format: 'plain', text: 'Option A' }, isCorrect: true, weight: 1, feedback: null },
|
||||
{ text: { format: 'plain', text: 'Option B' }, isCorrect: false, weight: 0, feedback: null },
|
||||
],
|
||||
};
|
||||
|
||||
const mockQuestionType: QuestionType = {
|
||||
question: mockQuestion,
|
||||
image: 'sample-image-url',
|
||||
};
|
||||
|
||||
describe('QuestionType', () => {
|
||||
test('has the expected structure', () => {
|
||||
expect(mockQuestionType).toEqual(expect.objectContaining({
|
||||
question: expect.any(Object),
|
||||
image: expect.any(String),
|
||||
}));
|
||||
|
||||
expect(mockQuestionType.question).toEqual(expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
type: expect.any(String),
|
||||
stem: expect.objectContaining({
|
||||
format: expect.any(String),
|
||||
text: expect.any(String),
|
||||
}),
|
||||
title: expect.any(String),
|
||||
hasEmbeddedAnswers: expect.any(Boolean),
|
||||
globalFeedback: expect.any(Object),
|
||||
choices: expect.any(Array),
|
||||
}));
|
||||
});
|
||||
});
|
||||
40
client/src/__tests__/Types/QuizType.test.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/*//QuizType.test.tsx
|
||||
import { QuizType } from "../../Types/QuizType";
|
||||
export function isQuizValid(quiz: QuizType): boolean {
|
||||
return quiz.title.length > 0 && quiz.content.length > 0;
|
||||
}
|
||||
|
||||
describe('isQuizValid function', () => {
|
||||
it('returns true for a valid quiz', () => {
|
||||
const validQuiz: QuizType = {
|
||||
_id: '1',
|
||||
title: 'Sample Quiz',
|
||||
content: ['Question 1', 'Question 2'],
|
||||
};
|
||||
|
||||
const result = isQuizValid(validQuiz);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for an invalid quiz with an empty title', () => {
|
||||
const invalidQuiz: QuizType = {
|
||||
_id: '2',
|
||||
title: '',
|
||||
content: ['Question 1', 'Question 2'],
|
||||
};
|
||||
|
||||
const result = isQuizValid(invalidQuiz);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for an invalid quiz with no questions', () => {
|
||||
const invalidQuiz: QuizType = {
|
||||
_id: '3',
|
||||
title: 'Sample Quiz',
|
||||
content: [],
|
||||
};
|
||||
|
||||
const result = isQuizValid(invalidQuiz);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});*/
|
||||
15
client/src/__tests__/Types/UserType.test.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
//UserTyper.test.tsx
|
||||
import { UserType } from "../../Types/UserType";
|
||||
|
||||
const user : UserType = {
|
||||
name: 'Student',
|
||||
id: '123'
|
||||
}
|
||||
|
||||
describe('UserType', () => {
|
||||
test('creates a user with name and id', () => {
|
||||
|
||||
expect(user.name).toBe('Student');
|
||||
expect(user.id).toBe('123');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// Modal.test.tsx
|
||||
import { render, fireEvent, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import ConfirmDialog from '../../../components/ConfirmDialog/ConfirmDialog';
|
||||
|
||||
describe('ConfirmDialog Component', () => {
|
||||
const mockOnConfirm = jest.fn();
|
||||
const mockOnCancel = jest.fn();
|
||||
const mockOnOptionalInputChange = jest.fn();
|
||||
|
||||
const sampleProps = {
|
||||
open: true,
|
||||
title: 'Sample Modal Title',
|
||||
message: 'Sample Modal Message',
|
||||
onConfirm: mockOnConfirm,
|
||||
onCancel: mockOnCancel,
|
||||
hasOptionalInput: true,
|
||||
optionalInputValue: 'Optional Input Value',
|
||||
onOptionalInputChange: mockOnOptionalInputChange
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
render(<ConfirmDialog {...sampleProps} />);
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
expect(screen.getByText('Sample Modal Title')).toBeInTheDocument();
|
||||
expect(screen.getByText('Sample Modal Message')).toBeInTheDocument();
|
||||
|
||||
const optionalInput = screen.getByTestId('optional-input') as HTMLInputElement;
|
||||
expect(optionalInput).toBeInTheDocument();
|
||||
expect(optionalInput.value).toBe('Optional Input Value');
|
||||
|
||||
expect(screen.getByText('Confirmer')).toBeInTheDocument();
|
||||
expect(screen.getByText('Annuler')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onConfirm callback when "Confirmer" button is clicked', () => {
|
||||
const confirmButton = screen.getByText('Confirmer');
|
||||
fireEvent.click(confirmButton);
|
||||
expect(mockOnConfirm).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onCancel callback when "Annuler" button is clicked', () => {
|
||||
const cancelButton = screen.getByText('Annuler');
|
||||
fireEvent.click(cancelButton);
|
||||
expect(mockOnCancel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onOptionalInputChange callback when optional input changes', () => {
|
||||
const optionalInput = screen.getByTestId('optional-input') as HTMLInputElement;
|
||||
fireEvent.change(optionalInput, { target: { value: 'Updated Value' } });
|
||||
expect(mockOnOptionalInputChange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
75
client/src/__tests__/components/Editor/Editor.test.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// Editor.test.tsx
|
||||
import { render, fireEvent, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import Editor from '../../../components/Editor/Editor';
|
||||
|
||||
describe('Editor Component', () => {
|
||||
const mockOnEditorChange = jest.fn();
|
||||
|
||||
const sampleProps = {
|
||||
initialValue: 'Sample Initial Value',
|
||||
onEditorChange: mockOnEditorChange
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
render(<Editor {...sampleProps} />);
|
||||
});
|
||||
|
||||
it('renders correctly with initial value', () => {
|
||||
const editorTextarea = screen.getByRole('textbox') as HTMLTextAreaElement;
|
||||
expect(editorTextarea).toBeInTheDocument();
|
||||
expect(editorTextarea.value).toBe('Sample Initial Value');
|
||||
});
|
||||
|
||||
it('calls onEditorChange callback when editor value changes', () => {
|
||||
const editorTextarea = screen.getByRole('textbox') as HTMLTextAreaElement;
|
||||
fireEvent.change(editorTextarea, { target: { value: 'Updated Value' } });
|
||||
expect(mockOnEditorChange).toHaveBeenCalledWith('Updated Value');
|
||||
});
|
||||
|
||||
it('updates editor value when initialValue prop changes', () => {
|
||||
const updatedProps = {
|
||||
initialValue: 'Updated Initial Value',
|
||||
onEditorChange: mockOnEditorChange
|
||||
};
|
||||
|
||||
render(<Editor {...updatedProps} />);
|
||||
|
||||
const editorTextareas = screen.getAllByRole('textbox') as HTMLTextAreaElement[];
|
||||
const editorTextarea = editorTextareas[1];
|
||||
|
||||
expect(editorTextarea.value).toBe('Updated Initial Value');
|
||||
});
|
||||
|
||||
test('should call change text with the correct value on textarea change', () => {
|
||||
const updatedProps = {
|
||||
initialValue: 'Updated Initial Value',
|
||||
onEditorChange: mockOnEditorChange
|
||||
};
|
||||
|
||||
render(<Editor {...updatedProps} />);
|
||||
|
||||
const editorTextareas = screen.getAllByRole('textbox') as HTMLTextAreaElement[];
|
||||
const editorTextarea = editorTextareas[1];
|
||||
fireEvent.change(editorTextarea, { target: { value: 'New value' } });
|
||||
|
||||
expect(editorTextarea.value).toBe('New value');
|
||||
});
|
||||
|
||||
test('should call onEditorChange with an empty string if textarea value is falsy', () => {
|
||||
const updatedProps = {
|
||||
initialValue: 'Updated Initial Value',
|
||||
onEditorChange: mockOnEditorChange
|
||||
};
|
||||
|
||||
render(<Editor {...updatedProps} />);
|
||||
|
||||
const editorTextareas = screen.getAllByRole('textbox') as HTMLTextAreaElement[];
|
||||
const editorTextarea = editorTextareas[1];
|
||||
fireEvent.change(editorTextarea, { target: { value: '' } });
|
||||
|
||||
expect(editorTextarea.value).toBe('');
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import GIFTTemplatePreview from '../../../components/GiftTemplate/GIFTTemplatePreview';
|
||||
|
||||
describe('GIFTTemplatePreview Component', () => {
|
||||
test('renders error message when questions contain invalid syntax', () => {
|
||||
render(<GIFTTemplatePreview questions={['Invalid GIFT syntax']} />);
|
||||
const errorMessage = screen.findByText(/Erreur inconnue/i, {}, { timeout: 5000 });
|
||||
expect(errorMessage).resolves.toBeInTheDocument();
|
||||
});
|
||||
test('renders preview when valid questions are provided', () => {
|
||||
const questions = [
|
||||
'Question 1 { A | B | C }',
|
||||
'Question 2 { D | E | F }',
|
||||
];
|
||||
render(<GIFTTemplatePreview questions={questions} />);
|
||||
const previewContainer = screen.getByTestId('preview-container');
|
||||
expect(previewContainer).toBeInTheDocument();
|
||||
});
|
||||
test('hides answers when hideAnswers prop is true', () => {
|
||||
const questions = [
|
||||
'Question 1 { A | B | C }',
|
||||
'Question 2 { D | E | F }',
|
||||
];
|
||||
render(<GIFTTemplatePreview questions={questions} hideAnswers />);
|
||||
const previewContainer = screen.getByTestId('preview-container');
|
||||
expect(previewContainer).toBeInTheDocument();
|
||||
});
|
||||
it('renders images correctly', () => {
|
||||
const questions = [
|
||||
'Question 1',
|
||||
'<img src="image1.jpg" alt="Image 1">',
|
||||
'Question 2',
|
||||
'<img src="image2.jpg" alt="Image 2">',
|
||||
];
|
||||
const { getByAltText } = render(<GIFTTemplatePreview questions={questions} />);
|
||||
const image1 = getByAltText('Image 1');
|
||||
const image2 = getByAltText('Image 2');
|
||||
expect(image1).toBeInTheDocument();
|
||||
expect(image2).toBeInTheDocument();
|
||||
});
|
||||
it('renders non-images correctly', () => {
|
||||
const questions = ['Question 1', 'Question 2'];
|
||||
const { queryByAltText } = render(<GIFTTemplatePreview questions={questions} />);
|
||||
const image1 = queryByAltText('Image 1');
|
||||
const image2 = queryByAltText('Image 2');
|
||||
expect(image1).toBeNull();
|
||||
expect(image2).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
//color.test.tsx
|
||||
import { colors } from "../../../../components/GiftTemplate/constants";
|
||||
|
||||
describe('Colors object', () => {
|
||||
test('All colors are defined', () => {
|
||||
expect(colors.red100).toBeDefined();
|
||||
expect(colors.red300).toBeDefined();
|
||||
expect(colors.red700).toBeDefined();
|
||||
expect(colors.redGray800).toBeDefined();
|
||||
expect(colors.beige100).toBeDefined();
|
||||
expect(colors.beige300).toBeDefined();
|
||||
expect(colors.beige400).toBeDefined();
|
||||
expect(colors.beige500).toBeDefined();
|
||||
expect(colors.beige600).toBeDefined();
|
||||
expect(colors.beige900).toBeDefined();
|
||||
expect(colors.beigeGray800).toBeDefined();
|
||||
expect(colors.green100).toBeDefined();
|
||||
expect(colors.green300).toBeDefined();
|
||||
expect(colors.green400).toBeDefined();
|
||||
expect(colors.green500).toBeDefined();
|
||||
expect(colors.green600).toBeDefined();
|
||||
expect(colors.green700).toBeDefined();
|
||||
expect(colors.greenGray500).toBeDefined();
|
||||
expect(colors.greenGray600).toBeDefined();
|
||||
expect(colors.greenGray700).toBeDefined();
|
||||
expect(colors.teal400).toBeDefined();
|
||||
expect(colors.teal500).toBeDefined();
|
||||
expect(colors.teal600).toBeDefined();
|
||||
expect(colors.teal700).toBeDefined();
|
||||
expect(colors.blue).toBe('#5271FF');
|
||||
expect(colors.success).toBe('hsl(120, 39%, 54%)');
|
||||
expect(colors.danger).toBe('hsl(2, 64%, 58%)');
|
||||
expect(colors.white).toBe('hsl(0, 0%, 100%)');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
//styles.test.tsx
|
||||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { ParagraphStyle } from '../../../../components/GiftTemplate/constants';
|
||||
|
||||
describe('ParagraphStyle', () => {
|
||||
test('applies styles correctly', () => {
|
||||
const theme = 'light';
|
||||
const paragraphText = 'Test paragraph';
|
||||
|
||||
const styles = ParagraphStyle(theme);
|
||||
|
||||
const { container } = render(
|
||||
<p style={convertStylesToObject(styles)}>{paragraphText}</p>
|
||||
);
|
||||
|
||||
const paragraphElement = container.firstChild;
|
||||
|
||||
expect(paragraphElement).toHaveStyle(`color: rgb(0, 0, 0);`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function convertStylesToObject(styles: string): React.CSSProperties {
|
||||
const styleObject: React.CSSProperties = {};
|
||||
styles.split(';').forEach((style) => {
|
||||
const [property, value] = style.split(':');
|
||||
if (property && value) {
|
||||
(styleObject as any)[property.trim()] = value.trim();
|
||||
}
|
||||
});
|
||||
return styleObject;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import '@testing-library/jest-dom';
|
||||
import { theme } from '../../../../components/GiftTemplate/constants/theme';
|
||||
import { colors } from '../../../../components/GiftTemplate/constants/colors';
|
||||
|
||||
describe('Theme', () => {
|
||||
test('returns correct light color', () => {
|
||||
const lightColor = theme('light', 'gray500', 'black500');
|
||||
expect(lightColor).toBe(colors.gray500);
|
||||
});
|
||||
|
||||
test('returns correct dark color', () => {
|
||||
const darkColor = theme('dark', 'gray500', 'black500');
|
||||
expect(darkColor).toBe(colors.black500);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import AnswerIcon from '../../../../components/GiftTemplate/templates/AnswerIcon';
|
||||
|
||||
describe('AnswerIcon', () => {
|
||||
test('renders correct icon when correct is true', () => {
|
||||
const { container } = render(<div dangerouslySetInnerHTML={{ __html: AnswerIcon({ correct: true }) }} />);
|
||||
const svgElement = container.querySelector('svg');
|
||||
|
||||
expect(svgElement).toBeInTheDocument();
|
||||
expect(svgElement).toHaveStyle(`
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
margin-right: 0.2rem;
|
||||
width: 1em;
|
||||
color: rgb(92, 92, 92);
|
||||
`);
|
||||
});
|
||||
|
||||
test('renders incorrect icon when correct is false', () => {
|
||||
const { container } = render(<div dangerouslySetInnerHTML={{ __html: AnswerIcon({ correct: false }) }} />);
|
||||
const svgElement = container.querySelector('svg');
|
||||
|
||||
expect(svgElement).toBeInTheDocument();
|
||||
expect(svgElement).toHaveStyle(`
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
margin-right: 0.2rem;
|
||||
width: 0.75em;
|
||||
color: rgb(79, 216, 79);
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import DragAndDrop from '../../../components/ImportModal/ImportModal';
|
||||
|
||||
describe('DragAndDrop Component', () => {
|
||||
|
||||
it('renders without errors', () => {
|
||||
const handleOnClose = jest.fn();
|
||||
const handleOnImport = jest.fn();
|
||||
const open = true;
|
||||
render(
|
||||
<DragAndDrop
|
||||
handleOnClose={handleOnClose}
|
||||
handleOnImport={handleOnImport}
|
||||
open={open}
|
||||
selectedFolder="selectedFolder"/>
|
||||
);
|
||||
expect(screen.getByText('Importation de quiz')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
|
||||
test('handles drag and drop', () => {
|
||||
const handleOnCloseMock = jest.fn();
|
||||
const handleOnImportMock = jest.fn();
|
||||
render(
|
||||
<DragAndDrop
|
||||
handleOnClose={handleOnCloseMock}
|
||||
handleOnImport={handleOnImportMock}
|
||||
open={true}
|
||||
selectedFolder="selectedFolder"
|
||||
/>
|
||||
);
|
||||
const dropZone = screen.getByText(/Déposer des fichiers ici/i);
|
||||
fireEvent.dragEnter(dropZone);
|
||||
fireEvent.dragOver(dropZone);
|
||||
fireEvent.drop(dropZone, { dataTransfer: { files: [new File([''], 'sample.txt')] } });
|
||||
expect(screen.getByText('📄')).toBeInTheDocument();
|
||||
expect(screen.getByText('sample.txt')).toBeInTheDocument();
|
||||
});
|
||||
it('handles cancel button correctly', () => {
|
||||
const handleOnClose = jest.fn();
|
||||
const handleOnImport = jest.fn();
|
||||
const open = true;
|
||||
const { container } = render(
|
||||
<DragAndDrop handleOnClose={handleOnClose} handleOnImport={handleOnImport} open={open}
|
||||
selectedFolder="selectedFolder" />
|
||||
);
|
||||
const file = new File(['file content'], 'example.txt', { type: 'text/plain' });
|
||||
fireEvent.change(screen.getByText( /cliquez pour ouvrir l'explorateur des fichiers/i), {
|
||||
target: { files: [file] },
|
||||
});
|
||||
fireEvent.click(screen.getByText('Annuler'));
|
||||
expect(container.querySelector('.file-container')).not.toBeInTheDocument();
|
||||
});
|
||||
it('handles import correctly', async () => {
|
||||
const handleOnCloseMock = jest.fn();
|
||||
const handleOnImportMock = jest.fn();
|
||||
render(
|
||||
<DragAndDrop
|
||||
handleOnClose={handleOnCloseMock}
|
||||
handleOnImport={handleOnImportMock}
|
||||
open={true}
|
||||
selectedFolder="selectedFolder"
|
||||
/>
|
||||
);
|
||||
const file = new File(['file content'], 'example.txt', { type: 'text/plain' });
|
||||
fireEvent.change(screen.getByText( /Importer/i), {
|
||||
target: { files: [file] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import LaunchQuizDialog from '../../../components/LaunchQuizDialog/LaunchQuizDialog';
|
||||
|
||||
// Mock the functions passed as props
|
||||
const mockHandleOnClose = jest.fn();
|
||||
const mockLaunchQuiz = jest.fn();
|
||||
const mockSetQuizMode = jest.fn();
|
||||
|
||||
const renderComponent = (open: boolean) => {
|
||||
render(
|
||||
<LaunchQuizDialog
|
||||
open={open}
|
||||
handleOnClose={mockHandleOnClose}
|
||||
launchQuiz={mockLaunchQuiz}
|
||||
setQuizMode={mockSetQuizMode}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
describe('LaunchQuizDialog', () => {
|
||||
it('renders with correct title', () => {
|
||||
renderComponent(true);
|
||||
expect(screen.getByText('Options de lancement du quiz')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders radio buttons for teacher and student modes', () => {
|
||||
renderComponent(true);
|
||||
expect(screen.getByLabelText('Rythme du professeur')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Rythme de l\'étudiant')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls handleOnClose when "Annuler" button is clicked', () => {
|
||||
renderComponent(true);
|
||||
|
||||
fireEvent.click(screen.getByText('Annuler'));
|
||||
|
||||
expect(mockHandleOnClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls launchQuiz when "Lancer" button is clicked', () => {
|
||||
renderComponent(true);
|
||||
|
||||
fireEvent.click(screen.getByText('Lancer'));
|
||||
|
||||
expect(mockLaunchQuiz).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not render when open is false', () => {
|
||||
renderComponent(false);
|
||||
expect(screen.queryByText('Options de lancement du quiz')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle';
|
||||
|
||||
describe('LoadingCircle', () => {
|
||||
it('displays the provided text correctly', () => {
|
||||
const text = 'Veuillez attendre la connexion au serveur...';
|
||||
render(<LoadingCircle text={text} />);
|
||||
|
||||
expect(screen.getByText(text)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import MultipleChoiceQuestion from '../../../../components/Questions/MultipleChoiceQuestion/MultipleChoiceQuestion';
|
||||
|
||||
describe('MultipleChoiceQuestion', () => {
|
||||
const mockHandleOnSubmitAnswer = jest.fn();
|
||||
const choices = [
|
||||
{ feedback: null, isCorrect: true, text: { format: 'plain', text: 'Choice 1' } },
|
||||
{ feedback: null, isCorrect: false, text: { format: 'plain', text: 'Choice 2' } }
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<MultipleChoiceQuestion
|
||||
globalFeedback="feedback"
|
||||
questionTitle="Test Question"
|
||||
choices={choices}
|
||||
handleOnSubmitAnswer={mockHandleOnSubmitAnswer}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
test('renders the question and choices', () => {
|
||||
expect(screen.getByText('Test Question')).toBeInTheDocument();
|
||||
choices.forEach((choice) => {
|
||||
expect(screen.getByText(choice.text.text)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('does not submit when no answer is selected', () => {
|
||||
const submitButton = screen.getByText('Répondre');
|
||||
fireEvent.click(submitButton);
|
||||
expect(mockHandleOnSubmitAnswer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('submits the selected answer', () => {
|
||||
const choiceButton = screen.getByText('Choice 1').closest('button');
|
||||
if (!choiceButton) throw new Error('Choice button not found');
|
||||
fireEvent.click(choiceButton);
|
||||
const submitButton = screen.getByText('Répondre');
|
||||
fireEvent.click(submitButton);
|
||||
expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith('Choice 1');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
// NumericalQuestion.test.tsx
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import NumericalQuestion from '../../../../components/Questions/NumericalQuestion/NumericalQuestion';
|
||||
|
||||
describe('NumericalQuestion Component', () => {
|
||||
const mockHandleSubmitAnswer = jest.fn();
|
||||
|
||||
const sampleProps = {
|
||||
questionTitle: 'Sample Question',
|
||||
correctAnswers: {
|
||||
numberHigh: 10,
|
||||
numberLow: 5,
|
||||
type: 'high-low'
|
||||
},
|
||||
handleOnSubmitAnswer: mockHandleSubmitAnswer,
|
||||
showAnswer: false
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
render(<NumericalQuestion {...sampleProps} />);
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
expect(screen.getByText('Sample Question')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('number-input')).toBeInTheDocument();
|
||||
expect(screen.getByText('Répondre')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles input change correctly', () => {
|
||||
const inputElement = screen.getByTestId('number-input') as HTMLInputElement;
|
||||
|
||||
fireEvent.change(inputElement, { target: { value: '7' } });
|
||||
|
||||
expect(inputElement.value).toBe('7');
|
||||
});
|
||||
|
||||
it('Submit button should be disable if nothing is entered', () => {
|
||||
const submitButton = screen.getByText('Répondre');
|
||||
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('not submited answer if nothing is entered', () => {
|
||||
const submitButton = screen.getByText('Répondre');
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleSubmitAnswer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('submits answer correctly', () => {
|
||||
const inputElement = screen.getByTestId('number-input');
|
||||
const submitButton = screen.getByText('Répondre');
|
||||
|
||||
fireEvent.change(inputElement, { target: { value: '7' } });
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(7);
|
||||
});
|
||||
});
|
||||
140
client/src/__tests__/components/Questions/Question.test.tsx
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
// Question.test.tsx
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import Questions from '../../../components/Questions/Question';
|
||||
import { GIFTQuestion } from 'gift-pegjs';
|
||||
|
||||
//
|
||||
describe('Questions Component', () => {
|
||||
const mockHandleSubmitAnswer = jest.fn();
|
||||
|
||||
const sampleTrueFalseQuestion: GIFTQuestion = {
|
||||
type: 'TF',
|
||||
stem: { format: 'plain', text: 'Sample True/False Question' },
|
||||
isTrue: true,
|
||||
incorrectFeedback: null,
|
||||
correctFeedback: null,
|
||||
title: 'True/False Question',
|
||||
hasEmbeddedAnswers: false,
|
||||
globalFeedback: null,
|
||||
};
|
||||
|
||||
const sampleMultipleChoiceQuestion: GIFTQuestion = {
|
||||
type: 'MC',
|
||||
stem: { format: 'plain', text: 'Sample Multiple Choice Question' },
|
||||
title: 'Multiple Choice Question',
|
||||
hasEmbeddedAnswers: false,
|
||||
globalFeedback: null,
|
||||
choices: [
|
||||
{ feedback: null, isCorrect: true, text: { format: 'plain', text: 'Choice 1' }, weight: 1 },
|
||||
{ feedback: null, isCorrect: false, text: { format: 'plain', text: 'Choice 2' }, weight: 0 },
|
||||
],
|
||||
};
|
||||
|
||||
const sampleNumericalQuestion: GIFTQuestion = {
|
||||
type: 'Numerical',
|
||||
stem: { format: 'plain', text: 'Sample Numerical Question' },
|
||||
title: 'Numerical Question',
|
||||
hasEmbeddedAnswers: false,
|
||||
globalFeedback: null,
|
||||
choices: { numberHigh: 10, numberLow: 5, type: 'high-low' },
|
||||
};
|
||||
|
||||
const sampleShortAnswerQuestion: GIFTQuestion = {
|
||||
type: 'Short',
|
||||
stem: { format: 'plain', text: 'Sample short answer question' },
|
||||
title: 'Short Answer Question Title',
|
||||
hasEmbeddedAnswers: false,
|
||||
globalFeedback: null,
|
||||
choices: [
|
||||
{
|
||||
feedback: { format: 'html', text: 'Correct answer feedback' },
|
||||
isCorrect: true,
|
||||
text: { format: 'html', text: 'Correct Answer' },
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
feedback: { format: 'html', text: 'Incorrect answer feedback' },
|
||||
isCorrect: false,
|
||||
text: { format: 'html', text: 'Incorrect Answer' },
|
||||
weight: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const renderComponent = (question: GIFTQuestion) => {
|
||||
render(<Questions question={question} handleOnSubmitAnswer={mockHandleSubmitAnswer} />);
|
||||
};
|
||||
|
||||
it('renders correctly for True/False question', () => {
|
||||
renderComponent(sampleTrueFalseQuestion);
|
||||
|
||||
expect(screen.getByText('Sample True/False Question')).toBeInTheDocument();
|
||||
expect(screen.getByText('Vrai')).toBeInTheDocument();
|
||||
expect(screen.getByText('Faux')).toBeInTheDocument();
|
||||
expect(screen.getByText('Répondre')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders correctly for Multiple Choice question', () => {
|
||||
renderComponent(sampleMultipleChoiceQuestion);
|
||||
|
||||
expect(screen.getByText('Sample Multiple Choice Question')).toBeInTheDocument();
|
||||
expect(screen.getByText('Choice 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Choice 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Répondre')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles selection and submission for Multiple Choice question', () => {
|
||||
renderComponent(sampleMultipleChoiceQuestion);
|
||||
|
||||
const choiceButton = screen.getByText('Choice 1').closest('button')!;
|
||||
fireEvent.click(choiceButton);
|
||||
|
||||
const submitButton = screen.getByText('Répondre');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('Choice 1');
|
||||
});
|
||||
|
||||
it('renders correctly for Numerical question', () => {
|
||||
renderComponent(sampleNumericalQuestion);
|
||||
|
||||
expect(screen.getByText('Sample Numerical Question')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('number-input')).toBeInTheDocument();
|
||||
expect(screen.getByText('Répondre')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles input and submission for Numerical question', () => {
|
||||
renderComponent(sampleNumericalQuestion);
|
||||
|
||||
const inputElement = screen.getByTestId('number-input') as HTMLInputElement;
|
||||
fireEvent.change(inputElement, { target: { value: '7' } });
|
||||
|
||||
const submitButton = screen.getByText('Répondre');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(7);
|
||||
});
|
||||
|
||||
it('renders correctly for Short Answer question', () => {
|
||||
renderComponent(sampleShortAnswerQuestion);
|
||||
|
||||
expect(screen.getByText('Sample short answer question')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('text-input')).toBeInTheDocument();
|
||||
expect(screen.getByText('Répondre')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles input and submission for Short Answer question', () => {
|
||||
renderComponent(sampleShortAnswerQuestion);
|
||||
|
||||
const inputElement = screen.getByTestId('text-input') as HTMLInputElement;
|
||||
fireEvent.change(inputElement, { target: { value: 'User Input' } });
|
||||
|
||||
const submitButton = screen.getByText('Répondre');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('User Input');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
// ShortAnswerQuestion.test.tsx
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import ShortAnswerQuestion from '../../../../components/Questions/ShortAnswerQuestion/ShortAnswerQuestion';
|
||||
|
||||
describe('ShortAnswerQuestion Component', () => {
|
||||
const mockHandleSubmitAnswer = jest.fn();
|
||||
|
||||
const sampleProps = {
|
||||
questionTitle: 'Sample Question',
|
||||
choices: [
|
||||
{
|
||||
feedback: {
|
||||
format: 'text',
|
||||
text: 'Correct answer feedback'
|
||||
},
|
||||
isCorrect: true,
|
||||
text: {
|
||||
format: 'text',
|
||||
text: 'Correct Answer'
|
||||
}
|
||||
},
|
||||
{
|
||||
feedback: null,
|
||||
isCorrect: false,
|
||||
text: {
|
||||
format: 'text',
|
||||
text: 'Incorrect Answer'
|
||||
}
|
||||
}
|
||||
],
|
||||
handleOnSubmitAnswer: mockHandleSubmitAnswer,
|
||||
showAnswer: false
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
render(<ShortAnswerQuestion {...sampleProps} />);
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
expect(screen.getByText('Sample Question')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId('text-input')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Répondre')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles input change correctly', () => {
|
||||
const inputElement = screen.getByTestId('text-input') as HTMLInputElement;
|
||||
|
||||
fireEvent.change(inputElement, { target: { value: 'User Input' } });
|
||||
|
||||
expect(inputElement.value).toBe('User Input');
|
||||
});
|
||||
|
||||
it('Submit button should be disable if nothing is entered', () => {
|
||||
const submitButton = screen.getByText('Répondre');
|
||||
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('not submited answer if nothing is entered', () => {
|
||||
const submitButton = screen.getByText('Répondre');
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleSubmitAnswer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('submits answer correctly', () => {
|
||||
const inputElement = screen.getByTestId('text-input') as HTMLInputElement;
|
||||
const submitButton = screen.getByText('Répondre');
|
||||
|
||||
fireEvent.change(inputElement, { target: { value: 'User Input' } });
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('User Input');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
// TrueFalseQuestion.test.tsx
|
||||
import { render, fireEvent, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import TrueFalseQuestion from '../../../../components/Questions/TrueFalseQuestion/TrueFalseQuestion';
|
||||
|
||||
describe('TrueFalseQuestion Component', () => {
|
||||
const mockHandleSubmitAnswer = jest.fn();
|
||||
|
||||
const sampleProps = {
|
||||
questionTitle: 'Sample True/False Question',
|
||||
correctAnswer: true,
|
||||
handleOnSubmitAnswer: mockHandleSubmitAnswer,
|
||||
showAnswer: false
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
render(<TrueFalseQuestion {...sampleProps} />);
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
expect(screen.getByText('Sample True/False Question')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Vrai')).toBeInTheDocument();
|
||||
expect(screen.getByText('Faux')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Répondre')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Submit button should be disabled if no option is selected', () => {
|
||||
const submitButton = screen.getByText('Répondre');
|
||||
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('not submit answer if no option is selected', () => {
|
||||
const submitButton = screen.getByText('Répondre');
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleSubmitAnswer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('submits answer correctly for True', () => {
|
||||
const trueButton = screen.getByText('Vrai');
|
||||
const submitButton = screen.getByText('Répondre');
|
||||
|
||||
fireEvent.click(trueButton);
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('submits answer correctly for False', () => {
|
||||
const falseButton = screen.getByText('Faux');
|
||||
const submitButton = screen.getByText('Répondre');
|
||||
|
||||
fireEvent.click(falseButton);
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import ReturnButton from '../../../components/ReturnButton/ReturnButton';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: jest.fn()
|
||||
}));
|
||||
|
||||
describe('ReturnButton', () => {
|
||||
test('navigates back when askConfirm is false', () => {
|
||||
const navigateMock = jest.fn();
|
||||
(useNavigate as jest.Mock).mockReturnValue(navigateMock);
|
||||
render(<ReturnButton askConfirm={false} />);
|
||||
fireEvent.click(screen.getByText('Retour'));
|
||||
expect(navigateMock).toHaveBeenCalledWith(-1);
|
||||
});
|
||||
|
||||
test('shows confirmation modal when askConfirm is true', () => {
|
||||
render(<ReturnButton askConfirm={true} />);
|
||||
fireEvent.click(screen.getByText('Retour'));
|
||||
const confirmButton = screen.getByTestId('modal-confirm-button');
|
||||
expect(confirmButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
/*test('navigates back after confirming in the modal', () => {
|
||||
const navigateMock = jest.fn();
|
||||
(useNavigate as jest.Mock).mockReturnValue(navigateMock);
|
||||
render(<ReturnButton askConfirm={true} />);
|
||||
fireEvent.click(screen.getByText('Retour'));
|
||||
const confirmButton = screen.getByTestId('modal-confirm-button');
|
||||
fireEvent.click(confirmButton);
|
||||
expect(navigateMock).toHaveBeenCalledWith(-1);
|
||||
});*/
|
||||
|
||||
test('cancels navigation when canceling in the modal', () => {
|
||||
const navigateMock = jest.fn();
|
||||
(useNavigate as jest.Mock).mockReturnValue(navigateMock);
|
||||
render(<ReturnButton askConfirm={true} />);
|
||||
fireEvent.click(screen.getByText('Retour'));
|
||||
fireEvent.click(screen.getByText('Annuler'));
|
||||
expect(navigateMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
// Importez le type UserType s'il n'est pas déjà importé
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import UserWaitPage from '../../../components/UserWaitPage/UserWaitPage';
|
||||
|
||||
describe('UserWaitPage Component', () => {
|
||||
const mockUsers = [
|
||||
{ id: '1', name: 'User1' },
|
||||
{ id: '2', name: 'User2' },
|
||||
{ id: '3', name: 'User3' },
|
||||
];
|
||||
|
||||
const mockProps = {
|
||||
users: mockUsers,
|
||||
launchQuiz: jest.fn(),
|
||||
roomName: 'Test Room',
|
||||
setQuizMode: jest.fn(),
|
||||
};
|
||||
|
||||
test('renders UserWaitPage with correct content', () => {
|
||||
render(<UserWaitPage {...mockProps} />);
|
||||
|
||||
expect(screen.getByText(/Salle: Test Room/)).toBeInTheDocument();
|
||||
|
||||
const launchButton = screen.getByRole('button', { name: /Lancer/i });
|
||||
expect(launchButton).toBeInTheDocument();
|
||||
|
||||
mockUsers.forEach((user) => {
|
||||
expect(screen.getByText(user.name)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('clicking on "Lancer" button opens LaunchQuizDialog', () => {
|
||||
render(<UserWaitPage {...mockProps} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Lancer/i }));
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
})
|
||||
35
client/src/__tests__/pages/Home/Home.test.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { render, fireEvent, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import Home from '../../../pages/Home/Home';
|
||||
|
||||
describe('Home', () => {
|
||||
it('renders Home component with page title and buttons', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Home />
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Espace\s*étudiant/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Espace\s*enseignant/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('navigates to the correct routes when student and teacher buttons are clicked', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Home />
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
const studentButton = screen.getByText(/Espace\s*étudiant/);
|
||||
expect(studentButton).toBeInTheDocument();
|
||||
fireEvent.click(studentButton);
|
||||
expect(window.location.pathname).toBe('/student/join-room');
|
||||
|
||||
const teacherButton = screen.getByText(/Espace\s*enseignant/);
|
||||
expect(teacherButton).toBeInTheDocument();
|
||||
fireEvent.click(teacherButton);
|
||||
expect(window.location.pathname).toBe('/teacher/dashboard');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { QuestionType } from '../../../../Types/QuestionType';
|
||||
import StudentModeQuiz from '../../../../components/StudentModeQuiz/StudentModeQuiz';
|
||||
|
||||
describe('StudentModeQuiz', () => {
|
||||
const mockQuestions: QuestionType[] = [
|
||||
{
|
||||
question: {
|
||||
id: '1',
|
||||
type: 'MC',
|
||||
stem: { format: 'plain', text: 'Sample Question 1' },
|
||||
title: 'Sample Question 1',
|
||||
hasEmbeddedAnswers: false,
|
||||
globalFeedback: null,
|
||||
choices: [
|
||||
{ text: { format: 'plain', text: 'Option A' }, isCorrect: true, weight: 1, feedback: null },
|
||||
{ text: { format: 'plain', text: 'Option B' }, isCorrect: false, weight: 0, feedback: null },
|
||||
],
|
||||
},
|
||||
image: '<img src="sample-image-url" alt="Sample Image" />',
|
||||
},
|
||||
{
|
||||
question: {
|
||||
id: '2',
|
||||
type: 'TF',
|
||||
stem: { format: 'plain', text: 'Sample Question 2' },
|
||||
isTrue: true,
|
||||
incorrectFeedback: null,
|
||||
correctFeedback: null,
|
||||
title: 'Question 2',
|
||||
hasEmbeddedAnswers: false,
|
||||
globalFeedback: null,
|
||||
},
|
||||
image: 'sample-image-url-2',
|
||||
},
|
||||
];
|
||||
|
||||
const mockSubmitAnswer = jest.fn();
|
||||
const mockDisconnectWebSocket = jest.fn();
|
||||
|
||||
|
||||
test('renders the initial question', async () => {
|
||||
render(
|
||||
<StudentModeQuiz
|
||||
questions={mockQuestions}
|
||||
submitAnswer={mockSubmitAnswer}
|
||||
disconnectWebSocket={mockDisconnectWebSocket}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Sample Question 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option A')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option B')).toBeInTheDocument();
|
||||
expect(screen.getByText('Déconnexion')).toBeInTheDocument();
|
||||
|
||||
});
|
||||
|
||||
test('handles answer submission text', () => {
|
||||
|
||||
render(
|
||||
<StudentModeQuiz
|
||||
questions={mockQuestions}
|
||||
submitAnswer={mockSubmitAnswer}
|
||||
disconnectWebSocket={mockDisconnectWebSocket}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Option A'));
|
||||
fireEvent.click(screen.getByText('Répondre'));
|
||||
|
||||
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', '1');
|
||||
});
|
||||
|
||||
test('handles disconnect button click', () => {
|
||||
render(
|
||||
<StudentModeQuiz
|
||||
questions={mockQuestions}
|
||||
submitAnswer={mockSubmitAnswer}
|
||||
disconnectWebSocket={mockDisconnectWebSocket}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(screen.getByText('Déconnexion'));
|
||||
|
||||
expect(mockDisconnectWebSocket).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('navigates to the next question', () => {
|
||||
render(
|
||||
<StudentModeQuiz
|
||||
questions={mockQuestions}
|
||||
submitAnswer={mockSubmitAnswer}
|
||||
disconnectWebSocket={mockDisconnectWebSocket}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Option A'));
|
||||
fireEvent.click(screen.getByText('Répondre'));
|
||||
fireEvent.click(screen.getByText('Question suivante'));
|
||||
|
||||
|
||||
expect(screen.getByText('Sample Question 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('T')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('navigates to the previous question', () => {
|
||||
|
||||
render(
|
||||
<StudentModeQuiz
|
||||
questions={mockQuestions}
|
||||
submitAnswer={mockSubmitAnswer}
|
||||
disconnectWebSocket={mockDisconnectWebSocket}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Option A'));
|
||||
fireEvent.click(screen.getByText('Répondre'));
|
||||
|
||||
fireEvent.click(screen.getByText('Question précédente'));
|
||||
|
||||
|
||||
expect(screen.getByText('Sample Question 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option B')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
//TeacherModeQuiz.test.tsx
|
||||
import { render, screen, fireEvent} from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { GIFTQuestion } from 'gift-pegjs';
|
||||
|
||||
import TeacherModeQuiz from '../../../../components/TeacherModeQuiz/TeacherModeQuiz';
|
||||
|
||||
describe('TeacherModeQuiz', () => {
|
||||
const mockQuestion: GIFTQuestion = {
|
||||
id: '1',
|
||||
type: 'MC',
|
||||
stem: { format: 'plain', text: 'Sample Question' },
|
||||
title: 'Sample Question',
|
||||
hasEmbeddedAnswers: false,
|
||||
globalFeedback: null,
|
||||
choices: [
|
||||
{ text: { format: 'plain', text: 'Option A' }, isCorrect: true, weight: 1, feedback: null },
|
||||
{ text: { format: 'plain', text: 'Option B' }, isCorrect: false, weight: 0, feedback: null },
|
||||
],
|
||||
};
|
||||
|
||||
const mockSubmitAnswer = jest.fn();
|
||||
const mockDisconnectWebSocket = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<TeacherModeQuiz
|
||||
questionInfos={{ question: mockQuestion, image: 'sample-image-url' }}
|
||||
submitAnswer={mockSubmitAnswer}
|
||||
disconnectWebSocket={mockDisconnectWebSocket}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
test('renders the initial question', () => {
|
||||
expect(screen.getByText('Question 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Sample Question')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option A')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option B')).toBeInTheDocument();
|
||||
expect(screen.getByText('Déconnexion')).toBeInTheDocument();
|
||||
expect(screen.getByText('Répondre')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('handles answer submission and displays wait text', () => {
|
||||
fireEvent.click(screen.getByText('Option A'));
|
||||
fireEvent.click(screen.getByText('Répondre'));
|
||||
|
||||
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', '1');
|
||||
expect(screen.getByText('En attente pour la prochaine question...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('handles disconnect button click', () => {
|
||||
fireEvent.click(screen.getByText('Déconnexion'));
|
||||
|
||||
expect(mockDisconnectWebSocket).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import Dashboard from '../../../../pages/Teacher/Dashboard/Dashboard';
|
||||
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {};
|
||||
return {
|
||||
getItem: (key: string) => store[key] || null,
|
||||
setItem: (key: string, value: string) => (store[key] = value.toString()),
|
||||
clear: () => (store = {}),
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: jest.fn(),
|
||||
}));
|
||||
|
||||
|
||||
describe('Dashboard Component', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.setItem('quizzes', JSON.stringify([]));
|
||||
});
|
||||
|
||||
test('renders Dashboard with default state', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Dashboard />
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(screen.getByText(/Tableau de bord/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('adds a quiz and checks if it is displayed', () => {
|
||||
const mockQuizzes = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Sample Quiz',
|
||||
questions: ['Question 1?', 'Question 2?'],
|
||||
},
|
||||
];
|
||||
localStorage.setItem('quizzes', JSON.stringify(mockQuizzes));
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Dashboard />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Sample Quiz/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('opens ImportModal when "Importer" button is clicked', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Dashboard />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText(/Importer/i));
|
||||
|
||||
expect(screen.getByText(/Importation de quiz/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import QuizForm from '../../../../pages/Teacher/EditorQuiz/EditorQuiz';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {};
|
||||
return {
|
||||
getItem: (key: string) => store[key] || null,
|
||||
setItem: (key: string, value: string) => (store[key] = value.toString()),
|
||||
clear: () => (store = {}),
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('QuizForm Component', () => {
|
||||
test('renders QuizForm with default state for a new quiz', () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/teacher/editor-quiz/new']}>
|
||||
<QuizForm />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Éditeur de quiz')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Éditeur')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Prévisualisation')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders QuizForm for a new quiz', async () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/teacher/editor-quiz']}>
|
||||
<QuizForm />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Éditeur de quiz/i)).toBeInTheDocument();
|
||||
|
||||
const editorTextArea = screen.getByRole('textbox');
|
||||
fireEvent.change(editorTextArea, { target: { value: 'Sample question?' } });
|
||||
|
||||
await waitFor(() => {
|
||||
const sampleQuestionElements = screen.queryAllByText(/Sample question\?/i);
|
||||
expect(sampleQuestionElements.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const saveButton = screen.getByText(/Enregistrer/i);
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Sauvegarder le questionnaire/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
39
client/src/__tests__/services/QuestionService.test.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { QuestionService } from "../../services/QuestionService";
|
||||
|
||||
describe('QuestionService', () => {
|
||||
describe('getImage', () => {
|
||||
it('should return empty string for text without image tag', () => {
|
||||
const text = 'This is a sample text without an image tag.';
|
||||
const imageUrl = QuestionService.getImage(text);
|
||||
expect(imageUrl).toBe('');
|
||||
});
|
||||
|
||||
it('should return the image tag from the text', () => {
|
||||
const text = 'This is a sample text with an <img src="image.jpg" alt="Sample Image" /> tag.';
|
||||
const imageUrl = QuestionService.getImage(text);
|
||||
expect(imageUrl).toBe('<img src="image.jpg" alt="Sample Image" />');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getImageSource', () => {
|
||||
it('should return the image source from the image tag in the text', () => {
|
||||
const text = '<img src="image.jpg" alt="Sample Image" />';
|
||||
const imageUrl = QuestionService.getImageSource(text);
|
||||
expect(imageUrl).toBe('src="image.jpg" alt="Sample Image" /');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ignoreImgTags', () => {
|
||||
it('should return the same text if it does not contain an image tag', () => {
|
||||
const text = 'This is a sample text without an image tag.';
|
||||
const result = QuestionService.ignoreImgTags(text);
|
||||
expect(result).toBe(text);
|
||||
});
|
||||
|
||||
it('should remove the image tag from the text', () => {
|
||||
const text = 'This is a sample text with an <img src="image.jpg" alt="Sample Image" /> tag.';
|
||||
const result = QuestionService.ignoreImgTags(text);
|
||||
expect(result).toBe('This is a sample text with an tag.');
|
||||
});
|
||||
});
|
||||
});
|
||||
65
client/src/__tests__/services/QuizService.test.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/*import { QuizService } from '../../services/QuizService';
|
||||
import { QuizType } from '../../Types/QuizType';
|
||||
|
||||
// we need to mock localStorage for this test
|
||||
if (typeof window === 'undefined') {
|
||||
global.window = {} as Window & typeof globalThis;
|
||||
}
|
||||
|
||||
const localStorageMock = (() => {
|
||||
let store: { [key: string]: string } = {};
|
||||
return {
|
||||
getItem(key: string) {
|
||||
return store[key] || null;
|
||||
},
|
||||
setItem(key: string, value: string) {
|
||||
store[key] = value.toString();
|
||||
},
|
||||
removeItem(key: string) {
|
||||
delete store[key];
|
||||
},
|
||||
clear() {
|
||||
store = {};
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock
|
||||
});
|
||||
|
||||
/*describe('QuizService', () => {
|
||||
const mockQuizzes: QuizType[] = [
|
||||
{ _id: 'quiz1', title: 'Quiz One', content: ['Q1', 'Q2'] },
|
||||
{ _id: 'quiz2', title: 'Quiz Two', content: ['Q3', 'Q4'] }
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
localStorageMock.setItem('quizzes', JSON.stringify(mockQuizzes));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorageMock.removeItem('quizzes');
|
||||
});
|
||||
|
||||
test('should return quiz for valid id', () => {
|
||||
const quiz = QuizService.getQuizById('quiz1', localStorageMock);
|
||||
expect(quiz).toEqual(mockQuizzes[0]);
|
||||
});
|
||||
|
||||
test('should return undefined for invalid id', () => {
|
||||
const quiz = QuizService.getQuizById('nonexistent', localStorageMock);
|
||||
expect(quiz).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should return undefined for undefined id', () => {
|
||||
const quiz = QuizService.getQuizById(undefined, localStorageMock);
|
||||
expect(quiz).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should handle empty localStorage', () => {
|
||||
localStorageMock.removeItem('quizzes');
|
||||
const quiz = QuizService.getQuizById('quiz1', localStorageMock);
|
||||
expect(quiz).toBeUndefined();
|
||||
});
|
||||
});*/
|
||||
88
client/src/__tests__/services/WebsocketService.test.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
//WebsocketService.test.tsx
|
||||
import WebsocketService from '../../services/WebsocketService';
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import { ENV_VARIABLES } from '../../constants';
|
||||
|
||||
jest.mock('socket.io-client');
|
||||
|
||||
jest.mock('../../constants', () => ({
|
||||
ENV_VARIABLES: {
|
||||
VITE_BACKEND_URL: 'https://ets-glitch-backend.glitch.me/'
|
||||
}
|
||||
}));
|
||||
|
||||
describe('WebSocketService', () => {
|
||||
let mockSocket: Partial<Socket>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockSocket = {
|
||||
emit: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
connect: jest.fn()
|
||||
};
|
||||
|
||||
(io as jest.Mock).mockReturnValue(mockSocket);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('connect should initialize socket connection', () => {
|
||||
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||
expect(io).toHaveBeenCalled();
|
||||
expect(WebsocketService['socket']).toBe(mockSocket);
|
||||
});
|
||||
|
||||
test('disconnect should terminate socket connection', () => {
|
||||
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||
expect(WebsocketService['socket']).toBeTruthy();
|
||||
WebsocketService.disconnect();
|
||||
expect(mockSocket.disconnect).toHaveBeenCalled();
|
||||
expect(WebsocketService['socket']).toBeNull();
|
||||
});
|
||||
|
||||
test('createRoom should emit create-room event', () => {
|
||||
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||
WebsocketService.createRoom();
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith('create-room');
|
||||
});
|
||||
|
||||
test('nextQuestion should emit next-question event with correct parameters', () => {
|
||||
const roomName = 'testRoom';
|
||||
const question = { id: 1, text: 'Sample Question' };
|
||||
|
||||
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||
WebsocketService.nextQuestion(roomName, question);
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith('next-question', { roomName, question });
|
||||
});
|
||||
|
||||
test('launchStudentModeQuiz should emit launch-student-mode event with correct parameters', () => {
|
||||
const roomName = 'testRoom';
|
||||
const questions = [{ id: 1, text: 'Sample Question' }];
|
||||
|
||||
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||
WebsocketService.launchStudentModeQuiz(roomName, questions);
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith('launch-student-mode', {
|
||||
roomName,
|
||||
questions
|
||||
});
|
||||
});
|
||||
|
||||
test('endQuiz should emit end-quiz event with correct parameters', () => {
|
||||
const roomName = 'testRoom';
|
||||
|
||||
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||
WebsocketService.endQuiz(roomName);
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith('end-quiz', { roomName });
|
||||
});
|
||||
|
||||
test('joinRoom should emit join-room event with correct parameters', () => {
|
||||
const enteredRoomName = 'testRoom';
|
||||
const username = 'testUser';
|
||||
|
||||
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||
WebsocketService.joinRoom(enteredRoomName, username);
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith('join-room', { enteredRoomName, username });
|
||||
});
|
||||
});
|
||||
77
client/src/components/ConfirmDialog/ConfirmDialog.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// Modal.tsx
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
TextField
|
||||
} from '@mui/material';
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
hasOptionalInput?: boolean;
|
||||
optionalInputValue?: string;
|
||||
onOptionalInputChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
buttonOrderType?: 'normal' | 'warning';
|
||||
};
|
||||
|
||||
const ConfirmDialog: React.FC<Props> = ({
|
||||
open,
|
||||
title,
|
||||
message,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
hasOptionalInput,
|
||||
optionalInputValue,
|
||||
onOptionalInputChange,
|
||||
buttonOrderType = 'normal'
|
||||
}) => {
|
||||
return (
|
||||
<Dialog open={open} onClose={onCancel}>
|
||||
<DialogTitle sx={{ fontWeight: 'bold', fontSize: 24 }}>{title}</DialogTitle>
|
||||
<DialogContentText sx={{ padding: '0 1.5rem 0.5rem 1.5rem' }}>
|
||||
{message}
|
||||
</DialogContentText>
|
||||
{hasOptionalInput && (
|
||||
<DialogContent>
|
||||
<TextField
|
||||
id="optional-input"
|
||||
inputProps={{ 'data-testid': 'optional-input' }}
|
||||
focused
|
||||
fullWidth
|
||||
value={optionalInputValue || ''}
|
||||
onChange={onOptionalInputChange}
|
||||
/>
|
||||
</DialogContent>
|
||||
)}
|
||||
<DialogActions>
|
||||
{buttonOrderType === 'normal' && (
|
||||
<Button variant="outlined" onClick={onCancel}>
|
||||
Annuler
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant={buttonOrderType === 'normal' ? 'contained' : 'outlined'}
|
||||
onClick={onConfirm}
|
||||
data-testid="modal-confirm-button"
|
||||
>
|
||||
Confirmer
|
||||
</Button>
|
||||
{buttonOrderType === 'warning' && (
|
||||
<Button variant="contained" onClick={onCancel}>
|
||||
Annuler
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmDialog;
|
||||
66
client/src/components/DisconnectButton/DisconnectButton.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// GoBackButton.tsx
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ConfirmDialog from '../ConfirmDialog/ConfirmDialog';
|
||||
import { Button } from '@mui/material';
|
||||
import { ChevronLeft } from '@mui/icons-material';
|
||||
|
||||
interface Props {
|
||||
onReturn?: () => void;
|
||||
askConfirm?: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
const DisconnectButton: React.FC<Props> = ({
|
||||
askConfirm = false,
|
||||
message = 'Êtes-vous sûr de vouloir quitter la page ?',
|
||||
onReturn
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
|
||||
const handleOnReturnButtonClick = () => {
|
||||
if (askConfirm) {
|
||||
setShowDialog(true);
|
||||
} else {
|
||||
handleOnReturn();
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
setShowDialog(false);
|
||||
handleOnReturn();
|
||||
};
|
||||
|
||||
const handleOnReturn = () => {
|
||||
if (!!onReturn) {
|
||||
onReturn();
|
||||
} else {
|
||||
navigate(-1);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='returnButton'>
|
||||
<Button
|
||||
variant="text"
|
||||
startIcon={<ChevronLeft />}
|
||||
onClick={handleOnReturnButtonClick}
|
||||
color="primary"
|
||||
sx={{ marginLeft: '-0.5rem', fontSize: 16 }}
|
||||
>
|
||||
Quitter
|
||||
</Button>
|
||||
<ConfirmDialog
|
||||
open={showDialog}
|
||||
title="Confirmer"
|
||||
message={message}
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={() => setShowDialog(false)}
|
||||
buttonOrderType="warning"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisconnectButton;
|
||||
32
client/src/components/Editor/Editor.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Editor.tsx
|
||||
import React, { useState, useRef } from 'react';
|
||||
import './editor.css';
|
||||
|
||||
interface EditorProps {
|
||||
initialValue: string;
|
||||
onEditorChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const Editor: React.FC<EditorProps> = ({ initialValue, onEditorChange }) => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const editorRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
function handleEditorChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||
const text = event.target.value;
|
||||
setValue(text);
|
||||
onEditorChange(text || '');
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<textarea
|
||||
ref={editorRef}
|
||||
onChange={handleEditorChange}
|
||||
value={value}
|
||||
className="editor"
|
||||
></textarea>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Editor;
|
||||
9
client/src/components/Editor/editor.css
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
.editor {
|
||||
width: 100%;
|
||||
height: 50vh;
|
||||
background-color: #f8f9ff;
|
||||
padding-left: 10px;
|
||||
padding-top: 10px;
|
||||
font-size: medium;
|
||||
resize: none;
|
||||
}
|
||||
25
client/src/components/Footer/Footer.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import * as React from 'react';
|
||||
import './footer.css';
|
||||
|
||||
interface FooterProps {
|
||||
|
||||
}
|
||||
|
||||
const Footer: React.FC<FooterProps> = ({ }) => {
|
||||
return (
|
||||
<div className="footer">
|
||||
<div className="footer-content">
|
||||
Réalisé avec ❤ à Montréal par des finissant•e•s de l'ETS
|
||||
</div>
|
||||
<div className="footer-links">
|
||||
<a href="https://github.com/louis-antoine-etsmtl/ETS-PFE042-EvalueTonSavoir-Frontend/tree/main">Frontend GitHub</a>
|
||||
<span className="divider">|</span>
|
||||
<a href="https://github.com/louis-antoine-etsmtl/ETS-PFE042-EvalueTonSavoir-Backend">Backend GitHub</a>
|
||||
<span className="divider">|</span>
|
||||
<a href="https://github.com/louis-antoine-etsmtl/EvalueTonSavoir/wiki">Wiki GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
23
client/src/components/Footer/footer.css
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
.footer {
|
||||
flex-shrink: 0;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.footer-links a {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer-links a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 0 10px;
|
||||
color: #666;
|
||||
}
|
||||
153
client/src/components/GIFTCheatSheet/GiftCheatSheet.tsx
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
// GiftCheatSheet.tsx
|
||||
import React from 'react';
|
||||
import './giftCheatSheet.css';
|
||||
|
||||
const GiftCheatSheet: React.FC = () => {
|
||||
return (
|
||||
<div className="gift-cheat-sheet">
|
||||
<h2 className="subtitle">Informations pratiques sur l'éditeur</h2>
|
||||
<span>
|
||||
L'éditeur utilise le format GIFT (General Import Format Template) créé pour la
|
||||
plateforme Moodle afin de générer les quizs. Ci-dessous vous pouvez retrouver la
|
||||
syntaxe pour chaque type de question ainsi que les champs optionnels :
|
||||
</span>
|
||||
<div className="question-type">
|
||||
<h4>1. Questions Vrai/Faux</h4>
|
||||
<pre>
|
||||
<code className="selectable-text">
|
||||
{'2+2 \\= 4 ? {T\n}// Vous pouvez utiliser les valeurs {T}, {F}, {TRUE} et {FALSE}'}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="question-type">
|
||||
<h4>2. Questions à choix multiple</h4>
|
||||
<pre>
|
||||
<code className="question-code-block selectable-text">
|
||||
{
|
||||
'Quelle ville est la capitale du Canada? {\n~ Toronto\n~ Montréal\n= Ottawa #Bonne réponse!\n}// La bonne réponse est Ottawa'
|
||||
}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
<div className="question-type">
|
||||
<h4>3. Questions à choix multiple avec plusieurs réponses</h4>
|
||||
<pre>
|
||||
<code className="question-code-block selectable-text">
|
||||
{
|
||||
'Quelles villes trouve-t-on au Canada? { \n~ %33.3% Montréal \n~ %33.3% Ottawa \n~ %33.3% Vancouver \n~ %-100% New York \n~ %-100% Paris \n#### La bonne réponse est Montréal, Ottawa et Vancouver \n} //On utilise le signe ~ pour toutes les réponses. On doit indiquer le pourcentage de chaque réponse'
|
||||
}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="question-type">
|
||||
<h4>4. Questions à reponse courte</h4>
|
||||
<pre>
|
||||
<code className="question-code-block selectable-text">
|
||||
{'Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n}// Permet de fournir plusieurs bonnes réponses. Note: Les majuscules ne sont pas prises en compte.'}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="question-type">
|
||||
<h4> 5. Questions numériques </h4>
|
||||
<pre>
|
||||
<code className="question-code-block selectable-text">
|
||||
{
|
||||
'Question {#=Nombre\n} //OU \nQuestion {#=Nombre:Tolérance\n} //OU \nQuestion {#=PetitNombre..GrandNombre\n} // La tolérance est un pourcentage. La réponse doit être comprise entre PetitNombre et GrandNombre'
|
||||
}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="question-type">
|
||||
<h4> 6. Paramètres optionnels </h4>
|
||||
<pre>
|
||||
<code className="question-code-block selectable-text">
|
||||
{'::Titre:: '}
|
||||
<span className="code-comment selectable-text">
|
||||
{' // Ajoute un titre à une question'}
|
||||
</span>
|
||||
<br />
|
||||
{'# Feedback '}
|
||||
<span className="code-comment selectable-text">
|
||||
{' // Feedback pour UNE réponse'}
|
||||
</span>
|
||||
<br />
|
||||
{'// Commentaire '}
|
||||
<span className="code-comment selectable-text">
|
||||
{' // Commentaire non apparent'}
|
||||
</span>
|
||||
<br />
|
||||
{'#### Feedback général '}
|
||||
<span className="code-comment selectable-text">
|
||||
{' // Feedback général pour une question'}
|
||||
</span>
|
||||
<br />
|
||||
{'%50% '}
|
||||
<span className="code-comment selectable-text">
|
||||
{" // Poids d'une réponse (peut être négatif)"}
|
||||
</span>
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="question-type">
|
||||
<h4> 7. Paramètres optionnels </h4>
|
||||
<p>
|
||||
Si vous souhaitez utiliser certains caractères spéciaux dans vos énoncés,
|
||||
réponses ou feedback, vous devez 'échapper' ces derniers en ajoutant un \
|
||||
devant:
|
||||
</p>
|
||||
<pre>
|
||||
<code className="question-code-block selectable-text">
|
||||
{'\\~ \n\\= \n\\# \n\\{ \n\\} \n\\:'}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="question-type">
|
||||
<h4> 8. LaTeX </h4>
|
||||
<p>
|
||||
Le format LaTeX est supporté dans cette application. Vous devez cependant penser
|
||||
à 'échapper' les caractères spéciaux mentionnés plus haut.
|
||||
</p>
|
||||
<p>Exemple d'équation:</p>
|
||||
<pre>
|
||||
<code className="question-code-block">{'$$x\\= \\frac\\{y^2\\}\\{4\\}$$'}</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="question-type">
|
||||
<h4> 9. inserer une image </h4>
|
||||
<p>Pour insérer une image, vous devez utiliser la syntaxe suivante:</p>
|
||||
<pre>
|
||||
<code className="question-code-block">
|
||||
{'<img '}
|
||||
<span className="code-comment">{`un_URL_d_image`}</span>
|
||||
{' >'}
|
||||
</code>
|
||||
</pre>
|
||||
<p style={{ color: 'red' }}>
|
||||
Attention nous ne supportons pas encore les images en tant que réponses à une
|
||||
question
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="question-type">
|
||||
<h4> 10. Informations supplémentaires </h4>
|
||||
<p>
|
||||
GIFT supporte d'autres formats de questions que nous ne gérons pas sur cette
|
||||
application.
|
||||
</p>
|
||||
<p>Vous pouvez retrouver la Documentation de GIFT (en anglais):</p>
|
||||
<a href="https://ethan-ou.github.io/vscode-gift-docs/docs/questions">
|
||||
Documentation de GIFT
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GiftCheatSheet;
|
||||
37
client/src/components/GIFTCheatSheet/giftCheatSheet.css
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
.gift-cheat-sheet {
|
||||
width: 30vw;
|
||||
height: 100%;
|
||||
}
|
||||
.subtitle {
|
||||
color: #3a3a3a;
|
||||
margin-bottom: 2vh;
|
||||
}
|
||||
|
||||
.question-type {
|
||||
margin-bottom: 20;
|
||||
}
|
||||
.question-code-block,
|
||||
.code-comment {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #ffffffbd;
|
||||
padding: 10px;
|
||||
border: 1px solid #000;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.code-comment {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.question-type h4 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
86
client/src/components/GiftTemplate/GIFTTemplatePreview.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// GIFTTemplatePreview.tsx
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Template, { ErrorTemplate } from './templates';
|
||||
import { parse } from 'gift-pegjs';
|
||||
import './styles.css';
|
||||
|
||||
interface GIFTTemplatePreviewProps {
|
||||
questions: string[];
|
||||
hideAnswers?: boolean;
|
||||
}
|
||||
|
||||
const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
|
||||
questions,
|
||||
hideAnswers = false
|
||||
}) => {
|
||||
const [error, setError] = useState('');
|
||||
const [isPreviewReady, setIsPreviewReady] = useState(false);
|
||||
const [items, setItems] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
let previewHTML = '';
|
||||
questions.forEach((item) => {
|
||||
const isImage = item.includes('<img');
|
||||
if (isImage) {
|
||||
const imageUrlMatch = item.match(/<img[^>]+>/i);
|
||||
if (imageUrlMatch) {
|
||||
let imageUrl = imageUrlMatch[0];
|
||||
imageUrl = imageUrl.replace('img', 'img style="width:10vw;" src=');
|
||||
item = item.replace(imageUrlMatch[0], '');
|
||||
previewHTML += `${imageUrl}`;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedItem = parse(item);
|
||||
previewHTML += Template(parsedItem[0], {
|
||||
preview: true,
|
||||
theme: 'light'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
previewHTML += ErrorTemplate(item + '\n' + error.message);
|
||||
} else {
|
||||
previewHTML += ErrorTemplate(item + '\n' + 'Erreur inconnue');
|
||||
}
|
||||
}
|
||||
previewHTML += '';
|
||||
});
|
||||
|
||||
if (hideAnswers) {
|
||||
const svgRegex = /<svg[^>]*>([\s\S]*?)<\/svg>/gi;
|
||||
previewHTML = previewHTML.replace(svgRegex, '');
|
||||
const placeholderRegex = /(placeholder=")[^"]*(")/gi;
|
||||
previewHTML = previewHTML.replace(placeholderRegex, '$1$2');
|
||||
}
|
||||
|
||||
setItems(previewHTML);
|
||||
setIsPreviewReady(true);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
setError(error.message);
|
||||
} else {
|
||||
setError('Une erreur est survenue durant le chargement de la prévisualisation.');
|
||||
}
|
||||
}
|
||||
}, [questions]);
|
||||
|
||||
const PreviewComponent = () => (
|
||||
<React.Fragment>
|
||||
{error ? (
|
||||
<div className="error">{error}</div>
|
||||
) : isPreviewReady ? (
|
||||
<div data-testid="preview-container">
|
||||
<div dangerouslySetInnerHTML={{ __html: items }}></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="loading">Chargement de la prévisualisation...</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
return <PreviewComponent />;
|
||||
};
|
||||
|
||||
export default GIFTTemplatePreview;
|
||||
52
client/src/components/GiftTemplate/constants/colors.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
export const colors = {
|
||||
red100: 'hsl(0, 100%, 97%)',
|
||||
red300: 'hsl(0, 53%, 90%)',
|
||||
red700: 'hsl(0, 45%, 25%)',
|
||||
redGray800: 'hsl(0, 23%, 18%)',
|
||||
beige100: 'hsl(43, 100%, 94%)',
|
||||
beige300: 'hsl(36, 84%, 93%)',
|
||||
beige400: 'hsl(36, 39%, 75%)',
|
||||
beige500: 'hsl(36, 50%, 50%)',
|
||||
beige600: 'hsl(35, 51%, 33%)',
|
||||
beige900: 'hsl(43, 95%, 9%)',
|
||||
beigeGray800: 'hsl(43, 23%, 33%)',
|
||||
green100: 'hsl(134, 68%, 95%)',
|
||||
green300: 'hsl(134, 31%, 82%)',
|
||||
green400: 'hsl(134, 31%, 75%)',
|
||||
green500: 'hsl(134, 31%, 66%)',
|
||||
green600: 'hsl(134, 31%, 44%)',
|
||||
green700: 'hsl(134, 31%, 32%)',
|
||||
greenGray500: 'hsl(134, 18%, 50%)',
|
||||
greenGray600: 'hsl(134, 21%, 44%)',
|
||||
greenGray700: 'hsl(134, 23%, 33%)',
|
||||
teal400: 'hsl(180, 35%, 89%)',
|
||||
teal500: 'hsl(180, 35%, 84%)',
|
||||
teal600: 'hsl(180, 24%, 60%)',
|
||||
teal700: 'hsl(180, 15%, 41%)',
|
||||
cyan100: 'hsl(194, 55%, 98%)',
|
||||
cyan200: 'hsl(194, 60%, 96%)',
|
||||
cyan300: 'hsl(194, 65%, 92%)',
|
||||
navy600: 'hsl(218, 17%, 35%)',
|
||||
gray100: 'hsl(0, 0%, 95%)',
|
||||
gray200: 'hsl(0, 0%, 88%)',
|
||||
gray300: 'hsl(0, 0%, 81%)',
|
||||
gray400: 'hsl(0, 0%, 74%)',
|
||||
gray500: 'hsl(0, 0%, 67%)',
|
||||
gray600: 'hsl(0, 0%, 60%)',
|
||||
gray700: 'hsl(0, 0%, 53%)',
|
||||
gray800: 'hsl(0, 0%, 46%)',
|
||||
gray900: 'hsl(0, 0%, 39%)',
|
||||
black100: 'hsl(0, 0%, 32%)',
|
||||
black200: 'hsl(0, 0%, 28%)',
|
||||
black300: 'hsl(0, 0%, 24%)',
|
||||
black400: 'hsl(0, 0%, 20%)',
|
||||
black500: 'hsl(0, 0%, 16%)',
|
||||
black600: 'hsl(0, 0%, 12%)',
|
||||
black700: 'hsl(0, 0%, 8%)',
|
||||
black800: 'hsl(0, 0%, 4%)',
|
||||
black900: 'hsl(0, 0%, 0%)',
|
||||
blue: '#5271FF',
|
||||
success: 'hsl(120, 39%, 54%)',
|
||||
danger: 'hsl(2, 64%, 58%)',
|
||||
white: 'hsl(0, 0%, 100%)'
|
||||
};
|
||||
3
client/src/components/GiftTemplate/constants/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { colors } from './colors';
|
||||
export { theme } from './theme';
|
||||
export * from './styles';
|
||||
58
client/src/components/GiftTemplate/constants/styles.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { ThemeType } from '../templates/types';
|
||||
import { theme } from './theme';
|
||||
|
||||
export const ParagraphStyle = (t: ThemeType) => `
|
||||
color: ${theme(t, 'black900', 'gray200')};
|
||||
`;
|
||||
|
||||
export const TextAreaStyle = (t: ThemeType) => `
|
||||
width: 100%;
|
||||
height: 7rem;
|
||||
line-height: 1.5;
|
||||
color: ${theme(t, 'black500', 'gray200')};
|
||||
background-color: ${theme(t, 'white', 'black300')};
|
||||
border: ${t === 'light' ? 1 : 1.5}px solid ${theme(t, 'gray300', 'gray900')};
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
||||
padding: 0.5rem;
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
`;
|
||||
|
||||
export const SelectStyle = (t: ThemeType) => `
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
white-space: pre;
|
||||
cursor: default;
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
text-transform: none;
|
||||
min-width: 8rem;
|
||||
line-height: 1.5;
|
||||
color: ${theme(t, 'black500', 'gray200')};
|
||||
background-color: ${theme(t, 'white', 'black300')};
|
||||
border: ${t === 'light' ? 1 : 1.5}px solid ${theme(t, 'gray300', 'gray900')};
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
||||
`;
|
||||
|
||||
export const InputStyle = (t: ThemeType) => `
|
||||
display: inline-block;
|
||||
padding: 0.375rem 0.75rem;
|
||||
line-height: 1.5;
|
||||
color: ${theme(t, 'black500', 'gray200')};
|
||||
background-color: ${theme(t, 'white', 'black300')};
|
||||
border: ${t === 'light' ? 1 : 2}px solid ${theme(t, 'gray300', 'gray900')};
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
width: 90%;
|
||||
`;
|
||||
11
client/src/components/GiftTemplate/constants/theme.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { colors } from './colors';
|
||||
|
||||
type Color = keyof typeof colors;
|
||||
|
||||
export const theme = (theme: 'light' | 'dark', light: Color, dark: Color) => {
|
||||
if (theme === 'light') {
|
||||
return colors[light];
|
||||
} else {
|
||||
return colors[dark];
|
||||
}
|
||||
};
|
||||
263
client/src/components/GiftTemplate/index.ts
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
import Template, { ErrorTemplate } from './templates';
|
||||
import { GIFTQuestion } from './templates/types';
|
||||
import './styles.css';
|
||||
|
||||
const multiple: GIFTQuestion[] = [
|
||||
{
|
||||
type: 'MC',
|
||||
title: null,
|
||||
stem: { format: 'markdown', text: "Who's buried in Grant's \r\n tomb?" },
|
||||
hasEmbeddedAnswers: false,
|
||||
globalFeedback: {
|
||||
format: 'moodle',
|
||||
text: 'Not sure? There are many answers for this question so do not fret. Not sure? There are many answers for this question so do not fret.'
|
||||
},
|
||||
choices: [
|
||||
{
|
||||
isCorrect: false,
|
||||
weight: -50,
|
||||
text: { format: 'moodle', text: 'Grant' },
|
||||
feedback: null
|
||||
},
|
||||
{
|
||||
isCorrect: true,
|
||||
weight: 50,
|
||||
text: { format: 'moodle', text: 'Jefferson' },
|
||||
feedback: null
|
||||
},
|
||||
{
|
||||
isCorrect: true,
|
||||
weight: 50,
|
||||
text: { format: 'moodle', text: 'no one' },
|
||||
feedback: null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'MC',
|
||||
title: null,
|
||||
stem: { format: 'moodle', text: "Grant is _____ in Grant's tomb." },
|
||||
hasEmbeddedAnswers: true,
|
||||
globalFeedback: null,
|
||||
choices: [
|
||||
{
|
||||
isCorrect: true,
|
||||
weight: null,
|
||||
text: { format: 'moodle', text: 'buried' },
|
||||
feedback: null
|
||||
},
|
||||
{
|
||||
isCorrect: true,
|
||||
weight: null,
|
||||
text: { format: 'moodle', text: 'entombed' },
|
||||
feedback: null
|
||||
},
|
||||
{
|
||||
isCorrect: false,
|
||||
weight: null,
|
||||
text: { format: 'moodle', text: 'living' },
|
||||
feedback: null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'TF',
|
||||
title: null,
|
||||
stem: { format: 'moodle', text: "Grant is buried in Grant's tomb." },
|
||||
hasEmbeddedAnswers: false,
|
||||
globalFeedback: null,
|
||||
isTrue: false,
|
||||
incorrectFeedback: null,
|
||||
correctFeedback: null
|
||||
},
|
||||
{
|
||||
type: 'Short',
|
||||
title: null,
|
||||
stem: { format: 'moodle', text: "Who's buried in Grant's tomb?" },
|
||||
hasEmbeddedAnswers: false,
|
||||
globalFeedback: null,
|
||||
choices: [
|
||||
{
|
||||
isCorrect: true,
|
||||
weight: null,
|
||||
text: { format: 'moodle', text: 'no one " has got me' },
|
||||
feedback: null
|
||||
},
|
||||
{
|
||||
isCorrect: true,
|
||||
weight: null,
|
||||
text: { format: 'moodle', text: 'nobody' },
|
||||
feedback: null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'Numerical',
|
||||
title: null,
|
||||
stem: { format: 'moodle', text: 'When was Ulysses S. Grant born?' },
|
||||
hasEmbeddedAnswers: false,
|
||||
globalFeedback: null,
|
||||
choices: {
|
||||
type: 'range',
|
||||
number: 1822,
|
||||
range: 5
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'Matching',
|
||||
title: null,
|
||||
stem: {
|
||||
format: 'moodle',
|
||||
text: 'Match the following countries with their corresponding capitals.'
|
||||
},
|
||||
hasEmbeddedAnswers: false,
|
||||
globalFeedback: null,
|
||||
matchPairs: [
|
||||
{
|
||||
subquestion: { format: 'moodle', text: 'Canada' },
|
||||
subanswer: 'Ottawa'
|
||||
},
|
||||
{
|
||||
subquestion: { format: 'moodle', text: 'Italy' },
|
||||
subanswer: 'Rome'
|
||||
},
|
||||
{
|
||||
subquestion: { format: 'moodle', text: 'Japan' },
|
||||
subanswer: 'Tokyo'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'MC',
|
||||
title: "Grant's Tomb",
|
||||
stem: { format: 'moodle', text: "Grant is _____ in Grant's tomb." },
|
||||
hasEmbeddedAnswers: true,
|
||||
globalFeedback: null,
|
||||
choices: [
|
||||
{
|
||||
isCorrect: false,
|
||||
weight: null,
|
||||
text: { format: 'moodle', text: 'buried' },
|
||||
feedback: { format: 'moodle', text: 'No one is buried there.' }
|
||||
},
|
||||
{
|
||||
isCorrect: true,
|
||||
weight: null,
|
||||
text: { format: 'moodle', text: 'entombed' },
|
||||
feedback: { format: 'moodle', text: 'Right answer!' }
|
||||
},
|
||||
{
|
||||
isCorrect: false,
|
||||
weight: null,
|
||||
text: { format: 'moodle', text: 'living' },
|
||||
feedback: { format: 'moodle', text: 'We hope not!' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'MC',
|
||||
title: null,
|
||||
stem: { format: 'moodle', text: 'Difficult multiple choice question.' },
|
||||
hasEmbeddedAnswers: false,
|
||||
globalFeedback: null,
|
||||
choices: [
|
||||
{
|
||||
isCorrect: false,
|
||||
weight: null,
|
||||
text: { format: 'moodle', text: 'wrong answer' },
|
||||
feedback: { format: 'moodle', text: 'comment on wrong answer' }
|
||||
},
|
||||
{
|
||||
isCorrect: false,
|
||||
weight: 50,
|
||||
text: { format: 'moodle', text: 'half credit answer' },
|
||||
feedback: { format: 'moodle', text: 'comment on answer' }
|
||||
},
|
||||
{
|
||||
isCorrect: true,
|
||||
weight: null,
|
||||
text: { format: 'moodle', text: 'full credit answer' },
|
||||
feedback: { format: 'moodle', text: 'well done!' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'Short',
|
||||
title: "Jesus' hometown (Short answer ex.)",
|
||||
stem: { format: 'moodle', text: 'Jesus Christ was from _____ .' },
|
||||
hasEmbeddedAnswers: true,
|
||||
globalFeedback: null,
|
||||
choices: [
|
||||
{
|
||||
isCorrect: true,
|
||||
weight: null,
|
||||
text: { format: 'moodle', text: 'Nazareth' },
|
||||
feedback: { format: 'moodle', text: "Yes! That's right!" }
|
||||
},
|
||||
{
|
||||
isCorrect: true,
|
||||
weight: 75,
|
||||
text: { format: 'moodle', text: 'Nazereth' },
|
||||
feedback: { format: 'moodle', text: 'Right, but misspelled.' }
|
||||
},
|
||||
{
|
||||
isCorrect: true,
|
||||
weight: 25,
|
||||
text: { format: 'moodle', text: 'Bethlehem' },
|
||||
feedback: {
|
||||
format: 'moodle',
|
||||
text: 'He was born here, but not raised here.'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'Numerical',
|
||||
title: 'Numerical example',
|
||||
stem: { format: 'moodle', text: 'When was Ulysses S. Grant born?' },
|
||||
hasEmbeddedAnswers: false,
|
||||
globalFeedback: null,
|
||||
choices: [
|
||||
{
|
||||
isCorrect: true,
|
||||
weight: null,
|
||||
text: {
|
||||
type: 'range',
|
||||
number: 1822,
|
||||
range: 0
|
||||
},
|
||||
feedback: { format: 'moodle', text: 'Correct! 100% credit' }
|
||||
},
|
||||
{
|
||||
isCorrect: true,
|
||||
weight: 50,
|
||||
text: {
|
||||
type: 'range',
|
||||
number: 1822,
|
||||
range: 2
|
||||
},
|
||||
feedback: {
|
||||
format: 'moodle',
|
||||
text: 'He was born in 1822. You get 50% credit for being close.'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'Essay',
|
||||
title: 'Essay Example',
|
||||
stem: { format: 'moodle', text: 'This is an essay.' },
|
||||
hasEmbeddedAnswers: false,
|
||||
globalFeedback: null
|
||||
}
|
||||
];
|
||||
|
||||
const items = multiple.map((item) => Template(item, { theme: 'dark' })).join('');
|
||||
const errorItemDark = ErrorTemplate('Hello');
|
||||
|
||||
const lightItems = multiple.map((item) => Template(item, { theme: 'light' })).join('');
|
||||
|
||||
const errorItem = ErrorTemplate('Hello');
|
||||
|
||||
const app = document.getElementById('app');
|
||||
if (app) app.innerHTML = items + errorItemDark + lightItems + errorItem;
|
||||
86
client/src/components/GiftTemplate/styles.css
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/* :root {
|
||||
--correct: hsla(134, 31%, 32%, 1);
|
||||
--correct-light: hsla(134, 68%, 95%, 1);
|
||||
|
||||
--light-gray: rgb(202, 202, 202);
|
||||
--moodle-blue-lighter: hsla(194, 55%, 98%, 1);
|
||||
--moodle-blue-light: hsla(194, 60%, 96%, 1);
|
||||
--moodle-blue: #def2f8;
|
||||
--moodle-blue-dark: #c7e4e4;
|
||||
--moodle-blue-darker: #81b1b1;
|
||||
--moodle-blue-darkest: rgb(87, 119, 119);
|
||||
--moodle-alt-light: #fff8e2;
|
||||
--moodle-alt: #fcefdc;
|
||||
--moodle-alt-dark: #b8945e;
|
||||
--moodle-alt-darker: #7d5a29;
|
||||
--moodle-alt-darkest: #2b2101;
|
||||
--moodle-error-light: #fff0f0;
|
||||
--moodle-error: #f3d8d8;
|
||||
--default-box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
||||
--form-input-color: rgb(74, 85, 104);
|
||||
} */
|
||||
|
||||
/* body {
|
||||
background-color: #121212;
|
||||
} */
|
||||
|
||||
/* :root {
|
||||
--correct: hsla(134, 31%, 32%, 1);
|
||||
--correct-light: hsla(134, 68%, 95%, 1);
|
||||
|
||||
--light-gray: rgb(202, 202, 202);
|
||||
--moodle-blue-lighter: hsla(194, 55%, 98%, 1);
|
||||
--moodle-blue-light: hsla(194, 60%, 96%, 1);
|
||||
--moodle-blue: #def2f8;
|
||||
--moodle-blue-dark: #c7e4e4;
|
||||
--moodle-blue-darker: #81b1b1;
|
||||
--moodle-blue-darkest: rgb(87, 119, 119);
|
||||
--moodle-alt-light: #fff8e2;
|
||||
--moodle-alt: #fcefdc;
|
||||
--moodle-alt-dark: #b8945e;
|
||||
--moodle-alt-darker: #7d5a29;
|
||||
--moodle-alt-darkest: #2b2101;
|
||||
--moodle-error-light: #fff0f0;
|
||||
--moodle-error: #f3d8d8;
|
||||
--default-box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
||||
--form-input-color: rgb(74, 85, 104);
|
||||
} */
|
||||
|
||||
/* * {
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
|
||||
Helvetica Neue, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji,
|
||||
Segoe UI Symbol;
|
||||
line-height: 1.5rem;
|
||||
} */
|
||||
/*
|
||||
.gift-textarea::-webkit-input-placeholder {
|
||||
color: #999;
|
||||
}
|
||||
.gift-textarea::-moz-placeholder {
|
||||
color: #999;
|
||||
}
|
||||
.gift-textarea:-ms-input-placeholder {
|
||||
color: #999;
|
||||
}
|
||||
.gift-textarea:-moz-placeholder {
|
||||
color: #999;
|
||||
} */
|
||||
.present-question-title {
|
||||
margin-top: 8vh;
|
||||
margin-bottom: 2vh;
|
||||
font-size: 2rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.preview-container {
|
||||
margin-bottom: 2vh;
|
||||
width: 60vw;
|
||||
height: 100%;
|
||||
}
|
||||
.multiple-choice-answers-container {
|
||||
margin: 1% 0% 1% 0%;
|
||||
}
|
||||
.multiple-choice-answers-container > * {
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
}
|
||||
36
client/src/components/GiftTemplate/templates/AnswerIcon.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { TemplateOptions } from './types';
|
||||
import { theme } from '../constants';
|
||||
import { state } from '.';
|
||||
|
||||
interface AnswerIconOptions extends TemplateOptions {
|
||||
correct: boolean;
|
||||
}
|
||||
|
||||
export default function AnswerIcon({ correct }: AnswerIconOptions): string {
|
||||
const Icon = `
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
margin-right: 0.2rem;
|
||||
`;
|
||||
|
||||
const Correct = `
|
||||
width: 1em;
|
||||
color: ${theme(state.theme, 'success', 'success')};
|
||||
`;
|
||||
|
||||
const Incorrect = `
|
||||
width: 0.75em;
|
||||
color: ${theme(state.theme, 'danger', 'danger')};
|
||||
`;
|
||||
|
||||
const CorrectIcon = (): string => {
|
||||
return `<svg style="${Icon} ${Correct}" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"></path></svg>`;
|
||||
};
|
||||
|
||||
const IncorrectIcon = (): string => {
|
||||
return `<svg style="${Icon} ${Incorrect}" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg>`;
|
||||
};
|
||||
|
||||
return correct ? CorrectIcon() : IncorrectIcon();
|
||||
}
|
||||
14
client/src/components/GiftTemplate/templates/Category.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { TemplateOptions, Category as CategoryType } from './types';
|
||||
import QuestionContainer from './QuestionContainer';
|
||||
import Title from './Title';
|
||||
|
||||
type CategoryOptions = TemplateOptions & CategoryType;
|
||||
|
||||
export default function Category({ title }: CategoryOptions): string {
|
||||
return `${QuestionContainer({
|
||||
children: Title({
|
||||
type: 'Catégorie',
|
||||
title: title
|
||||
})
|
||||
})}`;
|
||||
}
|
||||
22
client/src/components/GiftTemplate/templates/Description.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { TemplateOptions, Description as DescriptionType } from './types';
|
||||
import QuestionContainer from './QuestionContainer';
|
||||
import Title from './Title';
|
||||
import TextType from './TextType';
|
||||
import { ParagraphStyle } from '../constants';
|
||||
import { state } from '.';
|
||||
|
||||
type DescriptionOptions = TemplateOptions & DescriptionType;
|
||||
|
||||
export default function Description({ title, stem }: DescriptionOptions): string {
|
||||
return `${QuestionContainer({
|
||||
children: [
|
||||
Title({
|
||||
type: 'Description',
|
||||
title: title
|
||||
}),
|
||||
`<p style="${ParagraphStyle(state.theme)}">${TextType({
|
||||
text: stem
|
||||
})}</p>`
|
||||
]
|
||||
})}`;
|
||||
}
|
||||
59
client/src/components/GiftTemplate/templates/Error.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { theme, ParagraphStyle } from '../constants';
|
||||
import { state } from '.';
|
||||
|
||||
export default function (text: string): string {
|
||||
const Container = `
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
padding: 1rem 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background-color: ${theme(state.theme, 'red100', 'redGray800')};
|
||||
border: solid ${theme(state.theme, 'red300', 'red700')} 2px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0px 1px 3px ${theme(state.theme, 'gray400', 'black900')};
|
||||
`;
|
||||
|
||||
const document = removeBackslash(lineRegex(documentRegex(text))).split(/\r?\n/);
|
||||
return document[0] !== ``
|
||||
? `<section style="${Container}">${document
|
||||
.map((i) => `<p style="${ParagraphStyle(state.theme)}">${i}</p>`)
|
||||
.join('')}</section>`
|
||||
: ``;
|
||||
}
|
||||
|
||||
function documentRegex(text: string): string {
|
||||
const newText = text
|
||||
.split(/\r?\n/)
|
||||
.map((comment) => comment.replace(/(^[ \\t]+)?(^)((\/\/))(.*)/gm, ''))
|
||||
.join('');
|
||||
|
||||
const newLineAnswer = /([^\\]|[^\S\r\n][^=])(=|~)/g;
|
||||
const correctAnswer = /([^\\]|^{)(([^\\]|^|\\s*)=(.*)(?=[=~}]|\\n))/g;
|
||||
const incorrectAnswer = /([^\\]|^{)(([^\\]|^|\\s*)~(.*)(?=[=~}]|\\n))/g;
|
||||
|
||||
return newText
|
||||
.replace(newLineAnswer, `\n$2`)
|
||||
.replace(correctAnswer, `$1<li>$4</li>`)
|
||||
.replace(incorrectAnswer, `$1<li>$4</li>`);
|
||||
}
|
||||
|
||||
function lineRegex(text: string): string {
|
||||
return text
|
||||
.split(/\r?\n/)
|
||||
.map((category) =>
|
||||
category.replace(/(^[ \\t]+)?(((^|\n)\s*[$]CATEGORY:))(.+)/g, `<br><b>$5</b><br>`)
|
||||
)
|
||||
.map((title) => title.replace(/\s*(::)\s*(.*?)(::)/g, `<br><b>$2</b><br>`))
|
||||
.map((openBracket) => openBracket.replace(/([^\\]|^){([#])?/g, `$1<br>`))
|
||||
.map((closeBracket) => closeBracket.replace(/([^\\]|^)}/g, `$1<br>`))
|
||||
.join('');
|
||||
}
|
||||
|
||||
function removeBackslash(text: string): string {
|
||||
return text
|
||||
.split(/\r?\n/)
|
||||
.map((colon) => colon.replace(/[\\]:/g, ':'))
|
||||
.map((openBracket) => openBracket.replace(/[\\]{/g, '{'))
|
||||
.map((closeBracket) => closeBracket.replace(/[\\]}/g, '}'))
|
||||
.join('');
|
||||
}
|
||||
27
client/src/components/GiftTemplate/templates/Essay.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { TemplateOptions, Essay as EssayType } from './types';
|
||||
import QuestionContainer from './QuestionContainer';
|
||||
import Title from './Title';
|
||||
import TextType from './TextType';
|
||||
import GlobalFeedback from './GlobalFeedback';
|
||||
import { ParagraphStyle, TextAreaStyle } from '../constants';
|
||||
import { state } from '.';
|
||||
|
||||
type EssayOptions = TemplateOptions & EssayType;
|
||||
|
||||
export default function Essay({ title, stem, globalFeedback }: EssayOptions): string {
|
||||
return `${QuestionContainer({
|
||||
children: [
|
||||
Title({
|
||||
type: 'Développement',
|
||||
title: title
|
||||
}),
|
||||
`<p style="${ParagraphStyle(state.theme)}">${TextType({
|
||||
text: stem
|
||||
})}</p>`,
|
||||
`<textarea class="gift-textarea" style="${TextAreaStyle(
|
||||
state.theme
|
||||
)}" placeholder="Entrez votre réponse ici..."></textarea>`,
|
||||
GlobalFeedback({ feedback: globalFeedback })
|
||||
]
|
||||
})}`;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { TemplateOptions, Question } from './types';
|
||||
import TextType from './TextType';
|
||||
import { state } from '.';
|
||||
import { theme } from '../constants';
|
||||
|
||||
interface GlobalFeedbackOptions extends TemplateOptions {
|
||||
feedback: Question['globalFeedback'];
|
||||
}
|
||||
|
||||
export default function GlobalFeedback({ feedback }: GlobalFeedbackOptions): string {
|
||||
const Container = `
|
||||
position: relative;
|
||||
margin-top: 1rem;
|
||||
padding: 0 1rem;
|
||||
background-color: ${theme(state.theme, 'beige100', 'black400')};
|
||||
color: ${theme(state.theme, 'beige900', 'gray200')};
|
||||
border: ${theme(state.theme, 'beige300', 'black300')} ${state.theme === 'light' ? 1 : 2}px solid;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0px 2px 5px ${theme(state.theme, 'gray400', 'black800')};
|
||||
`;
|
||||
|
||||
return feedback !== null
|
||||
? `<div style="${Container}">
|
||||
<p>${TextType({ text: feedback })}</p>
|
||||
</div>`
|
||||
: ``;
|
||||
}
|
||||
87
client/src/components/GiftTemplate/templates/Matching.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { TemplateOptions, Matching as MatchingType } from './types';
|
||||
import QuestionContainer from './QuestionContainer';
|
||||
import Title from './Title';
|
||||
import TextType from './TextType';
|
||||
import GlobalFeedback from './GlobalFeedback';
|
||||
import { ParagraphStyle, SelectStyle } from '../constants';
|
||||
import { state } from '.';
|
||||
|
||||
type MatchingOptions = TemplateOptions & MatchingType;
|
||||
|
||||
interface MatchAnswerOptions extends TemplateOptions {
|
||||
choices: MatchingType['matchPairs'];
|
||||
}
|
||||
|
||||
export default function Matching({
|
||||
title,
|
||||
stem,
|
||||
matchPairs,
|
||||
globalFeedback
|
||||
}: MatchingOptions): string {
|
||||
return `${QuestionContainer({
|
||||
children: [
|
||||
Title({
|
||||
type: 'Appariement',
|
||||
title: title
|
||||
}),
|
||||
`<p style="${ParagraphStyle(state.theme)}">${TextType({
|
||||
text: stem
|
||||
})}</p>`,
|
||||
MatchAnswers({ choices: matchPairs }),
|
||||
GlobalFeedback({ feedback: globalFeedback })
|
||||
]
|
||||
})}`;
|
||||
}
|
||||
|
||||
function MatchAnswers({ choices }: MatchAnswerOptions): string {
|
||||
const Layout = `
|
||||
display: grid;
|
||||
grid-template-columns: fit-content(50%) fit-content(50%);
|
||||
grid-gap: 0.25rem;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Dropdown = `
|
||||
padding: 0.375rem 1.75rem 0.375rem 0.75rem;
|
||||
background-image: url(data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%3E%3Ctitle%3Edown-arrow%3C%2Ftitle%3E%3Cg%20fill%3D%22%23000000%22%3E%3Cpath%20d%3D%22M10.293%2C3.293%2C6%2C7.586%2C1.707%2C3.293A1%2C1%2C0%2C0%2C0%2C.293%2C4.707l5%2C5a1%2C1%2C0%2C0%2C0%2C1.414%2C0l5-5a1%2C1%2C0%2C1%2C0-1.414-1.414Z%22%20fill%3D%22%23${
|
||||
state.theme === 'light' ? '000' : 'ccc'
|
||||
}%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E);
|
||||
background-size: 0.6em;
|
||||
background-position: calc(100% - 0.5em) center;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 0.25rem;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
width: auto;
|
||||
vertical-align: baseline;
|
||||
`;
|
||||
|
||||
const OptionTable = `
|
||||
padding-right: 1rem;
|
||||
`;
|
||||
|
||||
const uniqueMatchOptions = Array.from(new Set(choices.map(({ subanswer }) => subanswer)));
|
||||
|
||||
const result = choices
|
||||
.map(({ subquestion }) => {
|
||||
return `
|
||||
<div style="${OptionTable} ${ParagraphStyle(state.theme)}">
|
||||
${TextType({ text: subquestion })}
|
||||
</div>
|
||||
<div>
|
||||
<select class="gift-select" style="${SelectStyle(state.theme)} ${Dropdown}">
|
||||
<option value="" disabled selected hidden>Choisir...</option>
|
||||
${uniqueMatchOptions.map((subanswer) => `<option>${subanswer}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
return `
|
||||
<div style="${Layout}">
|
||||
${result}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { TemplateOptions, MultipleChoice as MultipleChoiceType } from './types';
|
||||
import QuestionContainer from './QuestionContainer';
|
||||
import GlobalFeedback from './GlobalFeedback';
|
||||
import Title from './Title';
|
||||
import TextType from './TextType';
|
||||
import MultipleChoiceAnswers from './MultipleChoiceAnswers';
|
||||
import { ParagraphStyle } from '../constants';
|
||||
import { state } from '.';
|
||||
|
||||
type MultipleChoiceOptions = TemplateOptions & MultipleChoiceType;
|
||||
|
||||
export default function MultipleChoice({
|
||||
title,
|
||||
stem,
|
||||
choices,
|
||||
globalFeedback
|
||||
}: MultipleChoiceOptions): string {
|
||||
return `${QuestionContainer({
|
||||
children: [
|
||||
Title({
|
||||
type: 'Choix multiple',
|
||||
title: title
|
||||
}),
|
||||
`<p style="${ParagraphStyle(state.theme)}">${TextType({
|
||||
text: stem
|
||||
})}</p>`,
|
||||
MultipleChoiceAnswers({ choices: choices }),
|
||||
GlobalFeedback({ feedback: globalFeedback })
|
||||
]
|
||||
})}`;
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
import { TemplateOptions, TextFormat, Choice, MultipleChoice as MultipleChoiceType } from './types';
|
||||
import TextType from './TextType';
|
||||
import AnswerIcon from './AnswerIcon';
|
||||
import { state } from '.';
|
||||
import { ParagraphStyle, theme } from '../constants';
|
||||
|
||||
type MultipleChoiceAnswerOptions = TemplateOptions & Pick<MultipleChoiceType, 'choices'>;
|
||||
|
||||
type AnswerFeedbackOptions = TemplateOptions & Pick<Choice, 'feedback'>;
|
||||
|
||||
interface AnswerWeightOptions extends TemplateOptions {
|
||||
weight: Choice['weight'];
|
||||
correct: Choice['isCorrect'];
|
||||
}
|
||||
|
||||
export default function MultipleChoiceAnswers({ choices }: MultipleChoiceAnswerOptions) {
|
||||
const id = `id${nanoid(8)}`;
|
||||
|
||||
const isMultipleAnswer = choices.filter(({ isCorrect }) => isCorrect === true).length === 0;
|
||||
|
||||
const prompt = `<span style="${ParagraphStyle(state.theme)}">Choisir une réponse${
|
||||
isMultipleAnswer ? ` ou plusieurs` : ``
|
||||
}:</span>`;
|
||||
const result = choices
|
||||
.map(({ weight, isCorrect, text, feedback }) => {
|
||||
const CustomLabel = `
|
||||
display: inline-block;
|
||||
padding: 0.2em 0 0.2em 0;
|
||||
`;
|
||||
|
||||
const inputId = `id${nanoid(6)}`;
|
||||
|
||||
const isPositiveWeight = weight !== null && weight > 0;
|
||||
const isCorrectOption = isMultipleAnswer ? isPositiveWeight : isCorrect;
|
||||
|
||||
return `
|
||||
<div class='multiple-choice-answers-container'>
|
||||
<input class="gift-input" type="${
|
||||
isMultipleAnswer ? 'checkbox' : 'radio'
|
||||
}" id="${inputId}" name="${id}">
|
||||
${AnswerWeight({ correct: isCorrectOption, weight: weight })}
|
||||
<label style="${CustomLabel} ${ParagraphStyle(state.theme)}" for="${inputId}">
|
||||
${TextType({ text: text as TextFormat })}
|
||||
</label>
|
||||
${AnswerIcon({ correct: isCorrectOption })}
|
||||
${AnswerFeedback({ feedback: feedback })}
|
||||
</input>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
return `${prompt}${result}`;
|
||||
}
|
||||
|
||||
function AnswerWeight({ weight, correct }: AnswerWeightOptions): string {
|
||||
const Container = `
|
||||
box-shadow: 0px 1px 1px ${theme(state.theme, 'gray400', 'black900')};
|
||||
border-radius: 3px;
|
||||
padding-left: 0.2rem;
|
||||
padding-right: 0.2rem;
|
||||
padding-top: 0.05rem;
|
||||
padding-bottom: 0.05rem;
|
||||
`;
|
||||
|
||||
const CorrectWeight = `
|
||||
color: ${theme(state.theme, 'green700', 'green100')};
|
||||
background-color: ${theme(state.theme, 'green100', 'greenGray700')};
|
||||
`;
|
||||
const IncorrectWeight = `
|
||||
color: ${theme(state.theme, 'beige600', 'beige100')};
|
||||
background-color: ${theme(state.theme, 'beige300', 'beigeGray800')};
|
||||
`;
|
||||
|
||||
return weight
|
||||
? `<span style="${Container} ${
|
||||
correct ? `${CorrectWeight}` : `${IncorrectWeight}`
|
||||
}">${weight}%</span>`
|
||||
: ``;
|
||||
}
|
||||
|
||||
function AnswerFeedback({ feedback }: AnswerFeedbackOptions): string {
|
||||
const Container = `
|
||||
color: ${theme(state.theme, 'teal700', 'gray700')};
|
||||
`;
|
||||
|
||||
return feedback ? `<span style="${Container}">${TextType({ text: feedback })}</span>` : ``;
|
||||
}
|
||||
60
client/src/components/GiftTemplate/templates/Numerical.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { TemplateOptions, Numerical as NumericalType, NumericalFormat } from './types';
|
||||
import QuestionContainer from './QuestionContainer';
|
||||
import Title from './Title';
|
||||
import TextType from './TextType';
|
||||
import GlobalFeedback from './GlobalFeedback';
|
||||
import { ParagraphStyle, InputStyle } from '../constants';
|
||||
import { state } from '.';
|
||||
|
||||
type NumericalOptions = TemplateOptions & NumericalType;
|
||||
type NumericalAnswerOptions = TemplateOptions & Pick<NumericalType, 'choices'>;
|
||||
|
||||
export default function Numerical({
|
||||
title,
|
||||
stem,
|
||||
choices,
|
||||
globalFeedback
|
||||
}: NumericalOptions): string {
|
||||
return `${QuestionContainer({
|
||||
children: [
|
||||
Title({
|
||||
type: 'Numérique',
|
||||
title: title
|
||||
}),
|
||||
`<p style="${ParagraphStyle(state.theme)}">${TextType({
|
||||
text: stem
|
||||
})}</p>`,
|
||||
NumericalAnswers({ choices: choices }),
|
||||
GlobalFeedback({ feedback: globalFeedback })
|
||||
]
|
||||
})}`;
|
||||
}
|
||||
|
||||
function NumericalAnswers({ choices }: NumericalAnswerOptions): string {
|
||||
const placeholder = Array.isArray(choices)
|
||||
? choices.map(({ text }) => Answer(text)).join(', ')
|
||||
: Answer(choices);
|
||||
|
||||
return `
|
||||
<div>
|
||||
<span style="${ParagraphStyle(
|
||||
state.theme
|
||||
)}">Réponse: </span><input class="gift-input" type="text" style="${InputStyle(
|
||||
state.theme
|
||||
)}" placeholder="${placeholder}">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function Answer({ type, number, range, numberLow, numberHigh }: NumericalFormat): string {
|
||||
switch (type) {
|
||||
case 'simple':
|
||||
return `${number}`;
|
||||
case 'range':
|
||||
return `${number} ± ${range}`;
|
||||
case 'high-low':
|
||||
return `${numberLow} - ${numberHigh}`;
|
||||
default:
|
||||
return ``;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { TemplateOptions } from './types';
|
||||
import { state } from './index';
|
||||
import { theme } from '../constants';
|
||||
|
||||
export default function QuestionContainer({ children }: TemplateOptions): string {
|
||||
const Container = `
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
padding: 1rem 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background-color: ${theme(state.theme, 'white', 'black600')};
|
||||
border: solid ${theme(state.theme, 'white', 'black500')} 2px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0px 1px 3px ${theme(state.theme, 'gray400', 'black900')};
|
||||
`;
|
||||
|
||||
return `<section style="${Container}">${
|
||||
Array.isArray(children) ? children.join('') : children
|
||||
}</section>`;
|
||||
}
|
||||
46
client/src/components/GiftTemplate/templates/ShortAnswer.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { TemplateOptions, ShortAnswer as ShortAnswerType, TextFormat } from './types';
|
||||
import QuestionContainer from './QuestionContainer';
|
||||
import Title from './Title';
|
||||
import TextType from './TextType';
|
||||
import GlobalFeedback from './GlobalFeedback';
|
||||
import { ParagraphStyle, InputStyle } from '../constants';
|
||||
import { state } from './index';
|
||||
|
||||
type ShortAnswerOptions = TemplateOptions & ShortAnswerType;
|
||||
type AnswerOptions = TemplateOptions & Pick<ShortAnswerType, 'choices'>;
|
||||
|
||||
export default function ShortAnswer({
|
||||
title,
|
||||
stem,
|
||||
choices,
|
||||
globalFeedback
|
||||
}: ShortAnswerOptions): string {
|
||||
return `${QuestionContainer({
|
||||
children: [
|
||||
Title({
|
||||
type: 'Réponse courte',
|
||||
title: title
|
||||
}),
|
||||
`<p style="${ParagraphStyle(state.theme)}">${TextType({
|
||||
text: stem
|
||||
})}</p>`,
|
||||
Answers({ choices: choices }),
|
||||
GlobalFeedback({ feedback: globalFeedback })
|
||||
]
|
||||
})}`;
|
||||
}
|
||||
|
||||
function Answers({ choices }: AnswerOptions): string {
|
||||
const placeholder = choices
|
||||
.map(({ text }) => TextType({ text: text as TextFormat }))
|
||||
.join(', ');
|
||||
return `
|
||||
<div>
|
||||
<span style="${ParagraphStyle(
|
||||
state.theme
|
||||
)}">Réponse: </span><input class="gift-input" type="text" style="${InputStyle(
|
||||
state.theme
|
||||
)}" placeholder="${placeholder}">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
46
client/src/components/GiftTemplate/templates/TextType.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { marked } from 'marked';
|
||||
import katex from 'katex';
|
||||
import { TemplateOptions, TextFormat } from './types';
|
||||
|
||||
interface TextTypeOptions extends TemplateOptions {
|
||||
text: TextFormat;
|
||||
}
|
||||
|
||||
function formatLatex(text: string): string {
|
||||
return text
|
||||
.replace(/\$\$(.*?)\$\$/g, (_, inner) => katex.renderToString(inner, { displayMode: true }))
|
||||
.replace(/\\\[(.*?)\\\]/g, (_, inner) => katex.renderToString(inner, { displayMode: true }))
|
||||
.replace(/\\\((.*?)\\\)/g, (_, inner) =>
|
||||
katex.renderToString(inner, { displayMode: false })
|
||||
);
|
||||
}
|
||||
|
||||
function escapeHTML(text: string) {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
export default function TextType({ text }: TextTypeOptions): string {
|
||||
const formatText = formatLatex(escapeHTML(text.text.trim()));
|
||||
|
||||
switch (text.format) {
|
||||
case 'moodle':
|
||||
case 'plain':
|
||||
return formatText.replace(/(?:\r\n|\r|\n)/g, '<br>');
|
||||
case 'html':
|
||||
return formatText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2');
|
||||
case 'markdown':
|
||||
return (
|
||||
marked
|
||||
.parse(formatText, { breaks: true }) // call marked.parse instead of marked
|
||||
// Strip outer paragraph tags
|
||||
.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2')
|
||||
);
|
||||
default:
|
||||
return ``;
|
||||
}
|
||||
}
|
||||
56
client/src/components/GiftTemplate/templates/Title.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { TemplateOptions, Question } from './types';
|
||||
import { state } from '.';
|
||||
import { theme } from '../constants';
|
||||
|
||||
// Type is string to allow for custom question type text (e,g, "Multiple Choice")
|
||||
interface TitleOptions extends TemplateOptions {
|
||||
type: string;
|
||||
title: Question['title'];
|
||||
}
|
||||
|
||||
export default function Title({ type, title }: TitleOptions): string {
|
||||
const Container = `
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
const QuestionTitle = `
|
||||
color: ${theme(state.theme, 'blue', 'gray200')};
|
||||
`;
|
||||
|
||||
const OptionalTitle = `
|
||||
color: ${theme(state.theme, 'blue', 'gray900')};
|
||||
`;
|
||||
|
||||
const QuestionTypeContainer = `
|
||||
margin-left: auto;
|
||||
padding-left: 0.75rem;
|
||||
flex: none;
|
||||
`;
|
||||
|
||||
const QuestionType = `
|
||||
box-shadow: 0px 1px 3px ${theme(state.theme, 'gray400', 'black700')};
|
||||
padding-left: 0.7rem;
|
||||
padding-right: 0.7rem;
|
||||
padding-top: 0.4rem;
|
||||
padding-bottom: 0.4rem;
|
||||
border-radius: 4px;
|
||||
background-color: ${theme(state.theme, 'white', 'black400')};
|
||||
color: ${theme(state.theme, 'teal700', 'gray300')};
|
||||
`;
|
||||
|
||||
return `
|
||||
<div style="${Container}">
|
||||
<span>
|
||||
${
|
||||
title !== null
|
||||
? `<span style="${QuestionTitle}">${title}</span>`
|
||||
: `<span style="${OptionalTitle}">Titre optionnel...</span>`
|
||||
}
|
||||
</span>
|
||||
<span style="${QuestionTypeContainer} margin-bottom: 10px;">
|
||||
<span style="${QuestionType}">${type}</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
54
client/src/components/GiftTemplate/templates/TrueFalse.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { TemplateOptions, TextChoice, TrueFalse as TrueFalseType } from './types';
|
||||
import QuestionContainer from './QuestionContainer';
|
||||
import TextType from './TextType';
|
||||
import GlobalFeedback from './GlobalFeedback';
|
||||
import MultipleChoiceAnswers from './MultipleChoiceAnswers';
|
||||
import Title from './Title';
|
||||
import { ParagraphStyle } from '../constants';
|
||||
import { state } from '.';
|
||||
|
||||
type TrueFalseOptions = TemplateOptions & TrueFalseType;
|
||||
|
||||
export default function TrueFalse({
|
||||
title,
|
||||
isTrue,
|
||||
stem,
|
||||
correctFeedback,
|
||||
incorrectFeedback,
|
||||
globalFeedback
|
||||
}: TrueFalseOptions): string {
|
||||
const choices: TextChoice[] = [
|
||||
{
|
||||
text: {
|
||||
format: 'moodle',
|
||||
text: 'Vrai'
|
||||
},
|
||||
isCorrect: isTrue,
|
||||
weight: null,
|
||||
feedback: isTrue ? correctFeedback : incorrectFeedback
|
||||
},
|
||||
{
|
||||
text: {
|
||||
format: 'moodle',
|
||||
text: 'Faux'
|
||||
},
|
||||
isCorrect: !isTrue,
|
||||
weight: null,
|
||||
feedback: !isTrue ? correctFeedback : incorrectFeedback
|
||||
}
|
||||
];
|
||||
|
||||
return `${QuestionContainer({
|
||||
children: [
|
||||
Title({
|
||||
type: 'Vrai/Faux',
|
||||
title: title
|
||||
}),
|
||||
`<p style="${ParagraphStyle(state.theme)}">${TextType({
|
||||
text: stem
|
||||
})}</p>`,
|
||||
MultipleChoiceAnswers({ choices: choices }),
|
||||
GlobalFeedback({ feedback: globalFeedback })
|
||||
]
|
||||
})}`;
|
||||
}
|
||||
75
client/src/components/GiftTemplate/templates/index.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import Category from './Category';
|
||||
import Description from './Description';
|
||||
import Essay from './Essay';
|
||||
import Matching from './Matching';
|
||||
import MultipleChoice from './MultipleChoice';
|
||||
import Numerical from './Numerical';
|
||||
import ShortAnswer from './ShortAnswer';
|
||||
import TrueFalse from './TrueFalse';
|
||||
import Error from './Error';
|
||||
import {
|
||||
GIFTQuestion,
|
||||
Category as CategoryType,
|
||||
Description as DescriptionType,
|
||||
MultipleChoice as MultipleChoiceType,
|
||||
Numerical as NumericalType,
|
||||
ShortAnswer as ShortAnswerType,
|
||||
Essay as EssayType,
|
||||
TrueFalse as TrueFalseType,
|
||||
Matching as MatchingType,
|
||||
DisplayOptions
|
||||
} from './types';
|
||||
|
||||
export const state: DisplayOptions = { preview: true, theme: 'light' };
|
||||
|
||||
export default function Template(
|
||||
{ type, ...keys }: GIFTQuestion,
|
||||
options?: Partial<DisplayOptions>
|
||||
): string {
|
||||
Object.assign(state, options);
|
||||
|
||||
switch (type) {
|
||||
case 'Category':
|
||||
return Category({ ...(keys as CategoryType) });
|
||||
case 'Description':
|
||||
return Description({
|
||||
...(keys as DescriptionType)
|
||||
});
|
||||
case 'MC':
|
||||
return MultipleChoice({
|
||||
...(keys as MultipleChoiceType)
|
||||
});
|
||||
case 'Numerical':
|
||||
return Numerical({ ...(keys as NumericalType) });
|
||||
case 'Short':
|
||||
return ShortAnswer({
|
||||
...(keys as ShortAnswerType)
|
||||
});
|
||||
case 'Essay':
|
||||
return Essay({ ...(keys as EssayType) });
|
||||
case 'TF':
|
||||
return TrueFalse({ ...(keys as TrueFalseType) });
|
||||
case 'Matching':
|
||||
return Matching({ ...(keys as MatchingType) });
|
||||
default:
|
||||
return ``;
|
||||
}
|
||||
}
|
||||
|
||||
export function ErrorTemplate(text: string, options?: Partial<DisplayOptions>): string {
|
||||
Object.assign(state, options);
|
||||
|
||||
return Error(text);
|
||||
}
|
||||
|
||||
export {
|
||||
Category,
|
||||
Description,
|
||||
Essay,
|
||||
Matching,
|
||||
MultipleChoice,
|
||||
Numerical,
|
||||
ShortAnswer,
|
||||
TrueFalse,
|
||||
Error
|
||||
};
|
||||
120
client/src/components/GiftTemplate/templates/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
export type Template = (options: TemplateOptions) => string;
|
||||
|
||||
export interface TemplateOptions {
|
||||
children?: Template | string | Array<Template | string>;
|
||||
options?: DisplayOptions;
|
||||
}
|
||||
|
||||
export type ThemeType = 'light' | 'dark';
|
||||
|
||||
export interface DisplayOptions {
|
||||
theme: ThemeType;
|
||||
preview: boolean;
|
||||
}
|
||||
|
||||
export type QuestionType =
|
||||
| 'Description'
|
||||
| 'Category'
|
||||
| 'MC'
|
||||
| 'Numerical'
|
||||
| 'Short'
|
||||
| 'Essay'
|
||||
| 'TF'
|
||||
| 'Matching';
|
||||
|
||||
export type FormatType = 'moodle' | 'html' | 'markdown' | 'plain';
|
||||
export type NumericalType = 'simple' | 'range' | 'high-low';
|
||||
|
||||
export interface TextFormat {
|
||||
format: FormatType;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface NumericalFormat {
|
||||
type: NumericalType;
|
||||
number?: number;
|
||||
range?: number;
|
||||
numberHigh?: number;
|
||||
numberLow?: number;
|
||||
}
|
||||
|
||||
export interface Choice {
|
||||
isCorrect: boolean;
|
||||
weight: number | null;
|
||||
text: TextFormat | NumericalFormat;
|
||||
feedback: TextFormat | null;
|
||||
}
|
||||
|
||||
export interface TextChoice extends Choice {
|
||||
text: TextFormat;
|
||||
}
|
||||
|
||||
export interface NumericalChoice extends Choice {
|
||||
text: NumericalFormat;
|
||||
}
|
||||
|
||||
export interface Question {
|
||||
type: QuestionType;
|
||||
title: string | null;
|
||||
stem: TextFormat;
|
||||
hasEmbeddedAnswers: boolean;
|
||||
globalFeedback: TextFormat | null;
|
||||
}
|
||||
|
||||
export interface Description {
|
||||
type: Extract<QuestionType, 'Description'>;
|
||||
title: string | null;
|
||||
stem: TextFormat;
|
||||
hasEmbeddedAnswers: boolean;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
type: Extract<QuestionType, 'Category'>;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface MultipleChoice extends Question {
|
||||
type: Extract<QuestionType, 'MC'>;
|
||||
choices: TextChoice[];
|
||||
}
|
||||
|
||||
export interface ShortAnswer extends Question {
|
||||
type: Extract<QuestionType, 'Short'>;
|
||||
choices: TextChoice[];
|
||||
}
|
||||
|
||||
export interface Numerical extends Question {
|
||||
type: Extract<QuestionType, 'Numerical'>;
|
||||
choices: NumericalChoice[] | NumericalFormat;
|
||||
}
|
||||
|
||||
export interface Essay extends Question {
|
||||
type: Extract<QuestionType, 'Essay'>;
|
||||
}
|
||||
|
||||
export interface TrueFalse extends Question {
|
||||
type: Extract<QuestionType, 'TF'>;
|
||||
isTrue: boolean;
|
||||
incorrectFeedback: TextFormat | null;
|
||||
correctFeedback: TextFormat | null;
|
||||
}
|
||||
|
||||
export interface Matching extends Question {
|
||||
type: Extract<QuestionType, 'Matching'>;
|
||||
matchPairs: Match[];
|
||||
}
|
||||
|
||||
export interface Match {
|
||||
subquestion: TextFormat;
|
||||
subanswer: string;
|
||||
}
|
||||
|
||||
export type GIFTQuestion =
|
||||
| Description
|
||||
| Category
|
||||
| MultipleChoice
|
||||
| ShortAnswer
|
||||
| Numerical
|
||||
| Essay
|
||||
| TrueFalse
|
||||
| Matching;
|
||||
39
client/src/components/Header/Header.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
import * as React from 'react';
|
||||
import './header.css';
|
||||
import { Button } from '@mui/material';
|
||||
|
||||
interface HeaderProps {
|
||||
isLoggedIn: () => boolean;
|
||||
handleLogout: () => void;
|
||||
}
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({ isLoggedIn, handleLogout }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="header">
|
||||
<img
|
||||
className="logo"
|
||||
src="/logo.png"
|
||||
alt="Logo"
|
||||
onClick={() => navigate('/')}
|
||||
/>
|
||||
|
||||
{isLoggedIn() && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
handleLogout();
|
||||
navigate('/');
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
14
client/src/components/Header/header.css
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
.header {
|
||||
flex-shrink: 0;
|
||||
padding: 15px;
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header img {
|
||||
cursor: pointer;
|
||||
}
|
||||
210
client/src/components/ImportModal/ImportModal.tsx
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
import React, { useState, DragEvent, useRef, useEffect } from 'react';
|
||||
import './importModal.css';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
IconButton
|
||||
} from '@mui/material';
|
||||
import { Clear, Download } from '@mui/icons-material';
|
||||
import ApiService from '../../services/ApiService';
|
||||
|
||||
|
||||
type DroppedFile = {
|
||||
id: number;
|
||||
name: string;
|
||||
icon: string;
|
||||
file: File;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
handleOnClose: () => void;
|
||||
handleOnImport: () => void;
|
||||
open: boolean;
|
||||
selectedFolder: string;
|
||||
}
|
||||
|
||||
const DragAndDrop: React.FC<Props> = ({ handleOnClose, handleOnImport, open, selectedFolder }) => {
|
||||
const [droppedFiles, setDroppedFiles] = useState<DroppedFile[]>([]);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setDroppedFiles([]);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleDragEnter = (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const files = e.dataTransfer.files;
|
||||
handleFiles(files);
|
||||
};
|
||||
|
||||
const handleFiles = (files: FileList) => {
|
||||
const newDroppedFiles = Array.from(files)
|
||||
.filter((file) => file.name.endsWith('.txt'))
|
||||
.map((file, index) => ({
|
||||
id: index,
|
||||
name: file.name,
|
||||
icon: '📄',
|
||||
file
|
||||
}));
|
||||
|
||||
setDroppedFiles((prevFiles) => [...prevFiles, ...newDroppedFiles]);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleOnSave = async () => {
|
||||
const storedQuizzes = JSON.parse(localStorage.getItem('quizzes') || '[]');
|
||||
const quizzesToImportPromises = droppedFiles.map((droppedFile) => {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = async (event) => {
|
||||
if (event.target && event.target.result) {
|
||||
const fileContent = event.target.result as string;
|
||||
//console.log(fileContent);
|
||||
if (fileContent.trim() === '') {
|
||||
resolve(null);
|
||||
}
|
||||
const questions = fileContent.split(/}/)
|
||||
.map(question => {
|
||||
// Remove trailing and leading spaces
|
||||
|
||||
return question.trim()+"}";
|
||||
})
|
||||
.filter(question => question.trim() !== '').slice(0, -1); // Filter out lines with only whitespace characters
|
||||
|
||||
try {
|
||||
// const folders = await ApiService.getUserFolders();
|
||||
|
||||
// Assuming you want to use the first folder
|
||||
// const selectedFolder = folders.length > 0 ? folders[0]._id : null;
|
||||
await ApiService.createQuiz(droppedFile.name.slice(0, -4) || 'Untitled quiz', questions, selectedFolder);
|
||||
resolve('success');
|
||||
} catch (error) {
|
||||
console.error('Error saving quiz:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
reader.readAsText(droppedFile.file);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
Promise.all(quizzesToImportPromises).then((quizzesToImport) => {
|
||||
const verifiedQuizzesToImport = quizzesToImport.filter((quiz) => {
|
||||
return quiz !== null;
|
||||
});
|
||||
|
||||
const updatedQuizzes = [...storedQuizzes, ...verifiedQuizzesToImport];
|
||||
localStorage.setItem('quizzes', JSON.stringify(updatedQuizzes));
|
||||
|
||||
setDroppedFiles([]);
|
||||
handleOnImport();
|
||||
handleOnClose();
|
||||
|
||||
window.location.reload();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const handleRemoveFile = (id: number) => {
|
||||
setDroppedFiles((prevFiles) => prevFiles.filter((file) => file.id !== id));
|
||||
};
|
||||
|
||||
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (files) {
|
||||
handleFiles(files);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBrowseButtonClick = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnCancel = () => {
|
||||
setDroppedFiles([]);
|
||||
handleOnClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onClose={handleOnCancel} fullWidth>
|
||||
<DialogTitle sx={{ fontWeight: 'bold', fontSize: 24 }}>
|
||||
{'Importation de quiz'}
|
||||
</DialogTitle>
|
||||
<DialogContent
|
||||
className="import-container"
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
onClick={handleBrowseButtonClick}
|
||||
>
|
||||
<div className="mb-1">
|
||||
<DialogContentText sx={{ textAlign: 'center' }}>
|
||||
Déposer des fichiers ici ou
|
||||
<br />
|
||||
cliquez pour ouvrir l'explorateur des fichiers
|
||||
</DialogContentText>
|
||||
</div>
|
||||
<Download color="primary" />
|
||||
</DialogContent>
|
||||
<DialogContent>
|
||||
{droppedFiles.map((file) => (
|
||||
<div key={file.id + file.name} className="file-container">
|
||||
<span>{file.icon}</span>
|
||||
<span>{file.name}</span>
|
||||
<IconButton
|
||||
sx={{ padding: 0 }}
|
||||
onClick={() => handleRemoveFile(file.id)}
|
||||
>
|
||||
<Clear />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="outlined" onClick={handleOnCancel}>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button variant="contained" onClick={handleOnSave}>
|
||||
Importer
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileInputChange}
|
||||
multiple
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DragAndDrop;
|
||||
20
client/src/components/ImportModal/importModal.css
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
.import-container {
|
||||
border-style: dashed;
|
||||
border-width: thin;
|
||||
border-color: rgba(128, 128, 128, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 20vh;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
margin: 0 20px 0 20px;
|
||||
}
|
||||
|
||||
.file-container {
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
}
|
||||