mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
Merge ff6c130e78 into ee7a7a0544
This commit is contained in:
commit
3da334967e
40 changed files with 1034 additions and 1738 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -132,3 +132,4 @@ launch.json
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
db-backup/
|
db-backup/
|
||||||
|
/.vs
|
||||||
|
|
|
||||||
44
client/package-lock.json
generated
44
client/package-lock.json
generated
|
|
@ -17,8 +17,11 @@
|
||||||
"@mui/icons-material": "^6.4.6",
|
"@mui/icons-material": "^6.4.6",
|
||||||
"@mui/lab": "^5.0.0-alpha.153",
|
"@mui/lab": "^5.0.0-alpha.153",
|
||||||
"@mui/material": "^6.4.6",
|
"@mui/material": "^6.4.6",
|
||||||
|
"@types/bootstrap": "^5.2.10",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
"axios": "^1.8.1",
|
"axios": "^1.8.1",
|
||||||
|
"bootstrap": "^5.3.3",
|
||||||
|
"bootstrap-icons": "^1.11.3",
|
||||||
"dompurify": "^3.2.3",
|
"dompurify": "^3.2.3",
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"gift-pegjs": "^2.0.0-beta.1",
|
"gift-pegjs": "^2.0.0-beta.1",
|
||||||
|
|
@ -4521,6 +4524,14 @@
|
||||||
"@babel/types": "^7.20.7"
|
"@babel/types": "^7.20.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/bootstrap": {
|
||||||
|
"version": "5.2.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.10.tgz",
|
||||||
|
"integrity": "sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@popperjs/core": "^2.9.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/debug": {
|
"node_modules/@types/debug": {
|
||||||
"version": "4.1.12",
|
"version": "4.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||||
|
|
@ -5536,6 +5547,39 @@
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/bootstrap": {
|
||||||
|
"version": "5.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
|
||||||
|
"integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/twbs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/bootstrap"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"peerDependencies": {
|
||||||
|
"@popperjs/core": "^2.11.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bootstrap-icons": {
|
||||||
|
"version": "1.11.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz",
|
||||||
|
"integrity": "sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/twbs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/bootstrap"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,11 @@
|
||||||
"@mui/icons-material": "^6.4.6",
|
"@mui/icons-material": "^6.4.6",
|
||||||
"@mui/lab": "^5.0.0-alpha.153",
|
"@mui/lab": "^5.0.0-alpha.153",
|
||||||
"@mui/material": "^6.4.6",
|
"@mui/material": "^6.4.6",
|
||||||
|
"@types/bootstrap": "^5.2.10",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
"axios": "^1.8.1",
|
"axios": "^1.8.1",
|
||||||
|
"bootstrap": "^5.3.3",
|
||||||
|
"bootstrap-icons": "^1.11.3",
|
||||||
"dompurify": "^3.2.3",
|
"dompurify": "^3.2.3",
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"gift-pegjs": "^2.0.0-beta.1",
|
"gift-pegjs": "^2.0.0-beta.1",
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,32 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import './footer.css';
|
import 'bootstrap/dist/css/bootstrap.min.css'; // Add Bootstrap CSS import
|
||||||
|
|
||||||
type FooterProps = object; //empty object
|
type FooterProps = object;
|
||||||
|
|
||||||
const Footer: React.FC<FooterProps> = () => {
|
const Footer: React.FC<FooterProps> = () => {
|
||||||
return (
|
return (
|
||||||
<div className="footer">
|
<footer className="py-4 mt-auto">
|
||||||
<div className="footer-content">
|
<div className="container text-center">
|
||||||
Réalisé avec ❤ à Montréal par des finissant•e•s de l'ETS
|
<div className="mb-2">
|
||||||
|
Réalisé avec ❤ à Montréal par des finissant•e•s de l'ÉTS
|
||||||
</div>
|
</div>
|
||||||
<div className="footer-links">
|
<div className="d-flex justify-content-center align-items-center">
|
||||||
<a href="https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/">GitHub</a>
|
<a
|
||||||
<span className="divider">|</span>
|
href="https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/"
|
||||||
<a href="https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/wiki">Wiki GitHub</a>
|
className="text-dark text-decoration-none mx-2 hover-underline"
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
<span className="text-muted mx-2">|</span>
|
||||||
|
<a
|
||||||
|
href="https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/wiki"
|
||||||
|
className="text-dark text-decoration-none mx-2 hover-underline"
|
||||||
|
>
|
||||||
|
Wiki GitHub
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</footer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// GiftCheatSheet.tsx
|
// GiftCheatSheet.tsx
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import './giftCheatSheet.css';
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
import FileCopyIcon from '@mui/icons-material/FileCopy';
|
||||||
|
import { Button } from '@mui/material';
|
||||||
|
|
||||||
const GiftCheatSheet: React.FC = () => {
|
const GiftCheatSheet: React.FC = () => {
|
||||||
const [copySuccess, setCopySuccess] = useState(false);
|
const [copySuccess, setCopySuccess] = useState(false);
|
||||||
|
|
@ -8,195 +10,240 @@ const GiftCheatSheet: React.FC = () => {
|
||||||
const copyToClipboard = (text: string) => {
|
const copyToClipboard = (text: string) => {
|
||||||
navigator.clipboard.writeText(text)
|
navigator.clipboard.writeText(text)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setCopySuccess(true); // Afficher le message de succès
|
setCopySuccess(true);
|
||||||
console.log(copySuccess);
|
|
||||||
// Masquer le message de succès après quelques secondes
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setCopySuccess(false);
|
setCopySuccess(false);
|
||||||
}, 3000); // 3 secondes
|
}, 3000);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Erreur lors de la copie dans le presse-papiers : ', error);
|
console.error('Erreur lors de la copie dans le presse-papiers : ', error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const QuestionVraiFaux = "::Exemple de question vrai/faux:: \n 2+2 \\= 4 ? {T} //Utilisez les valeurs {T}, {F}, {TRUE} et {FALSE}.";
|
const QuestionVraiFaux = "::Exemple de question vrai/faux:: \n 2+2 \\= 4 ? {T} //Utilisez les valeurs {T}, {F}, {TRUE} et {FALSE}.";
|
||||||
const QuestionChoixMul = "::Ville capitale du Canada:: \nQuelle ville est la capitale du Canada? {\n~ Toronto\n~ Montréal\n= Ottawa #Rétroaction spécifique.\n} // Commentaire non visible (au besoin)";
|
const QuestionChoixMul = "::Ville capitale du Canada:: \nQuelle ville est la capitale du Canada? {\n~ Toronto\n~ Montréal\n= Ottawa #Rétroaction spécifique.\n} // Commentaire non visible (au besoin)";
|
||||||
const QuestionChoixMulMany = "::Villes canadiennes:: \n 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#### Rétroaction globale de la question. \n} // Utilisez tilde (signe de vague) pour toutes les réponses. // On doit indiquer le pourcentage de chaque réponse.";
|
const QuestionChoixMulMany = "::Villes canadiennes:: \n 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#### Rétroaction globale de la question. \n} // Utilisez tilde (signe de vague) pour toutes les réponses. // On doit indiquer le pourcentage de chaque réponse.";
|
||||||
const QuestionCourte ="::Clé et porte:: \n Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n} // Permet de fournir plusieurs bonnes réponses. // Note: La casse n'est pas prise en compte.";
|
const QuestionCourte = "::Clé et porte:: \n Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n} // Permet de fournir plusieurs bonnes réponses. // Note: La casse n'est pas prise en compte.";
|
||||||
const QuestionNum ="::Question numérique avec marge:: \nQuel est un nombre de 1 à 5 ? {\n#3:2\n}\n \n// Plage mathématique spécifiée avec des points de fin d'intervalle. \n ::Question numérique avec plage:: \n Quel est un nombre de 1 à 5 ? {\n#1..5\n} \n\n// Réponses numériques multiples avec crédit partiel et commentaires.\n::Question numérique avec plusieurs réponses::\nQuand est né Ulysses S. Grant ? {\n# =1822:0 # Correct ! Crédit complet. \n=%50%1822:2 # Il est né en 1822. Demi-crédit pour être proche.\n}";
|
const QuestionNum = "::Question numérique avec marge:: \nQuel est un nombre de 1 à 5 ? {\n#3:2\n}\n \n// Plage mathématique spécifiée avec des points de fin d'intervalle. \n ::Question numérique avec plage:: \n Quel est un nombre de 1 à 5 ? {\n#1..5\n} \n\n// Réponses numériques multiples avec crédit partiel et commentaires.\n::Question numérique avec plusieurs réponses::\nQuand est né Ulysses S. Grant ? {\n# =1822:0 # Correct ! Crédit complet. \n=%50%1822:2 # Il est né en 1822. Demi-crédit pour être proche.\n}";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="gift-cheat-sheet">
|
<div className="container-fluid p-4 h-100">
|
||||||
<h2 className="subtitle">Informations pratiques sur l'éditeur</h2>
|
|
||||||
<span>
|
{/* Add feedback alert at the top */}
|
||||||
|
{copySuccess && (
|
||||||
|
<div className="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
Texte copié dans le presse-papiers!
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-close"
|
||||||
|
onClick={() => setCopySuccess(false)}
|
||||||
|
aria-label="Close"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h2 className="text-dark mb-4">Informations pratiques sur l'éditeur</h2>
|
||||||
|
<p className="mb-4">
|
||||||
L'éditeur utilise le format GIFT (General Import Format Template) créé pour la
|
L'éditeur utilise le format GIFT (General Import Format Template) créé pour la
|
||||||
plateforme Moodle afin de générer les mini-tests. Ci-dessous vous pouvez retrouver la
|
plateforme Moodle afin de générer les mini-tests. Ci-dessous vous pouvez retrouver la
|
||||||
syntaxe pour chaque type de question :
|
syntaxe pour chaque type de question :
|
||||||
</span>
|
</p>
|
||||||
<div className="question-type">
|
|
||||||
<h4>1. Questions Vrai/Faux</h4>
|
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((section) => (
|
||||||
<pre>
|
<div key={section} className="mb-4">
|
||||||
<code className="selectable-text">
|
{section === 1 && (
|
||||||
|
<>
|
||||||
|
<h4 className="mt-3">1. Questions Vrai/Faux</h4>
|
||||||
|
<pre className="bg-white p-3 border rounded">
|
||||||
|
<code className="font-monospace">
|
||||||
{QuestionVraiFaux}
|
{QuestionVraiFaux}
|
||||||
</code>
|
</code>
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
<button onClick={() => copyToClipboard(QuestionVraiFaux)}>Copier</button>
|
<Button variant="contained" onClick={() => copyToClipboard(QuestionVraiFaux)} className="mb-4" startIcon={<FileCopyIcon />} >
|
||||||
</div>
|
Copier
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="question-type">
|
{section === 2 && (
|
||||||
<h4>2. Questions à choix multiple</h4>
|
<>
|
||||||
<pre>
|
<h4 className="mt-3">2. Questions à choix multiple</h4>
|
||||||
<code className="question-code-block selectable-text">
|
<pre className="bg-white p-3 border rounded">
|
||||||
{
|
<code className="font-monospace">
|
||||||
QuestionChoixMul
|
{QuestionChoixMul}
|
||||||
}
|
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
<button onClick={() => copyToClipboard(QuestionChoixMul)}>Copier</button>
|
<Button variant="contained" onClick={() => copyToClipboard(QuestionChoixMul)} className="mb-4" startIcon={<FileCopyIcon />} >
|
||||||
</div>
|
Copier
|
||||||
<div className="question-type">
|
</Button>
|
||||||
<h4>3. Questions à choix multiple avec plusieurs réponses</h4>
|
</>
|
||||||
<pre>
|
)}
|
||||||
<code className="question-code-block selectable-text">
|
|
||||||
{
|
{section === 3 && (
|
||||||
QuestionChoixMulMany
|
<>
|
||||||
}
|
<h4 className="mt-3">3. Questions à choix multiple avec plusieurs réponses</h4>
|
||||||
|
<pre className="bg-white p-3 border rounded">
|
||||||
|
<code className="font-monospace">
|
||||||
|
{QuestionChoixMulMany}
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
<button onClick={() => copyToClipboard(QuestionChoixMulMany)}>Copier</button>
|
<Button variant="contained" onClick={() => copyToClipboard(QuestionChoixMulMany)} className="mb-4" startIcon={<FileCopyIcon />} >
|
||||||
</div>
|
Copier
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="question-type">
|
{section === 4 && (
|
||||||
<h4>4. Questions à réponse courte</h4>
|
<>
|
||||||
<pre>
|
<h4 className="mt-3">4. Questions à réponse courte</h4>
|
||||||
<code className="question-code-block selectable-text">
|
<pre className="bg-white p-3 border rounded">
|
||||||
|
<code className="font-monospace">
|
||||||
{QuestionCourte}
|
{QuestionCourte}
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
<button onClick={() => copyToClipboard(QuestionCourte)}>Copier</button>
|
<Button variant="contained" onClick={() => copyToClipboard(QuestionCourte)} className="mb-4" startIcon={<FileCopyIcon />} >
|
||||||
</div>
|
Copier
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="question-type">
|
{section === 5 && (
|
||||||
<h4> 5. Questions numériques </h4>
|
<>
|
||||||
<pre>
|
<h4 className="mt-3">5. Questions numériques</h4>
|
||||||
<code className="question-code-block selectable-text">
|
<pre className="bg-white p-3 border rounded">
|
||||||
{
|
<code className="font-monospace">
|
||||||
QuestionNum
|
{QuestionNum}
|
||||||
}
|
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
<button onClick={() => copyToClipboard(QuestionNum)}>Copier</button>
|
<Button variant="contained" onClick={() => copyToClipboard(QuestionNum)} className="mb-4" startIcon={<FileCopyIcon />} >
|
||||||
</div>
|
Copier
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="question-type">
|
{section === 6 && (
|
||||||
<h4> 6. Paramètres optionnels </h4>
|
<>
|
||||||
<pre>
|
<h4 className="mt-3">6. Paramètres optionnels</h4>
|
||||||
<code className="question-code-block selectable-text">
|
<pre className="bg-white p-3 border rounded">
|
||||||
|
<code className="font-monospace">
|
||||||
{'::Titre:: '}
|
{'::Titre:: '}
|
||||||
<span className="code-comment selectable-text">
|
<span className="text-success">
|
||||||
{' // Ajoute un titre à une question'}
|
{' // Ajoute un titre à une question'}
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
{'# Feedback '}
|
{'# Feedback '}
|
||||||
<span className="code-comment selectable-text">
|
<span className="text-success">
|
||||||
{' // Feedback pour UNE réponse'}
|
{' // Feedback pour UNE réponse'}
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
{'// Commentaire '}
|
{'// Commentaire '}
|
||||||
<span className="code-comment selectable-text">
|
<span className="text-success">
|
||||||
{' // Commentaire non apparent'}
|
{' // Commentaire non apparent'}
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
{'#### Feedback général '}
|
{'#### Feedback général '}
|
||||||
<span className="code-comment selectable-text">
|
<span className="text-success">
|
||||||
{' // Feedback général pour une question'}
|
{' // Feedback général pour une question'}
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
{'%50% '}
|
{'%50% '}
|
||||||
<span className="code-comment selectable-text">
|
<span className="text-success">
|
||||||
{" // Poids d'une réponse (peut être négatif)"}
|
{" // Poids d'une réponse (peut être négatif)"}
|
||||||
</span>
|
</span>
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="question-type">
|
{section === 7 && (
|
||||||
<h4> 7. Caractères spéciaux </h4>
|
<>
|
||||||
|
<h4 className="mt-3">7. Caractères spéciaux</h4>
|
||||||
<p>
|
<p>
|
||||||
Si vous souhaitez utiliser certains caractères spéciaux dans vos énoncés,
|
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 \
|
réponses ou feedback, vous devez «échapper» ces derniers en ajoutant un \
|
||||||
devant:
|
devant:
|
||||||
</p>
|
</p>
|
||||||
<pre>
|
<pre className="bg-white p-3 border rounded">
|
||||||
<code className="question-code-block selectable-text">
|
<code className="font-monospace">
|
||||||
{'\\~ \n\\= \n\\# \n\\{ \n\\} \n\\:'}
|
{'\\~ \n\\= \n\\# \n\\{ \n\\} \n\\:'}
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="question-type">
|
{section === 8 && (
|
||||||
<h4> 8. LaTeX et Markdown</h4>
|
<>
|
||||||
|
<h4 className="mt-3">8. LaTeX et Markdown</h4>
|
||||||
<p>
|
<p>
|
||||||
Les formats LaTeX et Markdown sont supportés dans cette application. Vous devez cependant penser
|
Les formats LaTeX et Markdown sont supportés dans cette application. Vous devez cependant penser
|
||||||
à «échapper» les caractères spéciaux mentionnés plus haut.
|
à «échapper» les caractères spéciaux mentionnés plus haut.
|
||||||
</p>
|
</p>
|
||||||
<p>Exemple d'équation:</p>
|
<p>Exemple d'équation:</p>
|
||||||
<pre>
|
<pre className="bg-white p-3 border rounded">
|
||||||
<code className="question-code-block selectable-text">{'$$x\\= \\frac\\{y^2\\}\\{4\\}$$'}</code>
|
<code className="font-monospace">{'$$x\\= \\frac\\{y^2\\}\\{4\\}$$'}</code>
|
||||||
<code className="question-code-block selectable-text">{'\n$x\\= \\frac\\{y^2\\}\\{4\\}$'}</code>
|
<code className="font-monospace">{'\n$x\\= \\frac\\{y^2\\}\\{4\\}$'}</code>
|
||||||
</pre>
|
</pre>
|
||||||
<p>Exemple de texte Markdown:</p>
|
<p>Exemple de texte Markdown:</p>
|
||||||
<pre>
|
<pre className="bg-white p-3 border rounded">
|
||||||
<code className="question-code-block selectable-text">{'[markdown]Grâce à la balise markdown, Il est possible d\'insérer du texte *italique*, **gras**, du `code` et bien plus.'}</code>
|
<code className="font-monospace">{'[markdown]Grâce à la balise markdown, Il est possible d\'insérer du texte *italique*, **gras**, du `code` et bien plus.'}</code>
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="question-type" id="images-section">
|
{section === 9 && (
|
||||||
<h4> 9. Images </h4>
|
<>
|
||||||
|
<h4 className="mt-3">9. Images</h4>
|
||||||
<p>Il est possible d'insérer une image dans une question, une réponse (choix multiple) et dans une rétroaction. D'abord, <strong>le format de l'élément doit être [markdown]</strong>. Ensuite utilisez la syntaxe suivante :</p>
|
<p>Il est possible d'insérer une image dans une question, une réponse (choix multiple) et dans une rétroaction. D'abord, <strong>le format de l'élément doit être [markdown]</strong>. Ensuite utilisez la syntaxe suivante :</p>
|
||||||
<pre>
|
<pre className="bg-white p-3 border rounded">
|
||||||
<code className="question-code-block">
|
<code className="font-monospace">
|
||||||
{''}
|
{'")'}
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
<p>Exemple d'une question Vrai/Faux avec l'image d'un chat:</p>
|
<p>Exemple d'une question Vrai/Faux avec l'image d'un chat:</p>
|
||||||
<pre>
|
<pre className="bg-white p-3 border rounded">
|
||||||
<code className="question-code-block">
|
<code className="font-monospace">
|
||||||
{'[markdown]Ceci est un chat: \n\n{T}'}
|
{'[markdown]Ceci est un chat: \n\n{T}'}
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
<p>Exemple d'une question à choix multiple avec l'image d'un chat dans une rétroaction :</p>
|
<p>Exemple d'une question à choix multiple avec l'image d'un chat dans une rétroaction :</p>
|
||||||
<pre>
|
<pre className="bg-white p-3 border rounded">
|
||||||
<code className="question-code-block">
|
<code className="font-monospace">
|
||||||
{`[markdown]Qui a initié le développement d'ÉvalueTonSavoir {=ÉTS#OUI! 
|
{`[markdown]Qui a initié le développement d'ÉvalueTonSavoir {=ÉTS#OUI! 
|
||||||
~EPFL#Non...}`}
|
~EPFL#Non...}`}
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
<p>Note : les images étant spécifiées avec la syntaxe Markdown dans GIFT, on doit échapper les caractères spéciales (:) dans l'URL de l'image.</p>
|
<p>Note : les images étant spécifiées avec la syntaxe Markdown dans GIFT, on doit échapper les caractères spéciales (:) dans l'URL de l'image.</p>
|
||||||
<p style={{ color: 'red' }}>
|
<p className="text-danger">
|
||||||
Attention: l'ancienne fonctionnalité avec les balises <code>{'<img>'}</code> n'est plus
|
Attention: l'ancienne fonctionnalité avec les balises <code>{'<img>'}</code> n'est plus
|
||||||
supportée.
|
supportée.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</>
|
||||||
<div className="question-type">
|
)}
|
||||||
<h4> 10. Informations supplémentaires </h4>
|
|
||||||
|
{section === 10 && (
|
||||||
|
<>
|
||||||
|
<h4 className="mt-3">10. Informations supplémentaires</h4>
|
||||||
<p>
|
<p>
|
||||||
GIFT supporte d'autres formats de questions que nous ne gérons pas sur cette
|
GIFT supporte d'autres formats de questions que nous ne gérons pas sur cette
|
||||||
application.
|
application.
|
||||||
</p>
|
</p>
|
||||||
<p>Vous pouvez retrouver la Documentation de GIFT (en anglais):</p>
|
<p>Vous pouvez retrouver la Documentation de GIFT (en anglais):</p>
|
||||||
<a href="https://ethan-ou.github.io/vscode-gift-docs/docs/questions">
|
<a
|
||||||
|
href="https://ethan-ou.github.io/vscode-gift-docs/docs/questions"
|
||||||
|
className="btn btn-link"
|
||||||
|
>
|
||||||
Documentation de GIFT
|
Documentation de GIFT
|
||||||
</a>
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import './header.css';
|
|
||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
import LogoutIcon from '@mui/icons-material/Logout';
|
||||||
|
import LoginIcon from '@mui/icons-material/Login';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
|
|
@ -12,35 +14,36 @@ const Header: React.FC<HeaderProps> = ({ isLoggedIn, handleLogout }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="header">
|
<header className="d-flex justify-content-between align-items-center p-3">
|
||||||
<img
|
<img
|
||||||
className="logo"
|
|
||||||
src="/logo.png"
|
src="/logo.png"
|
||||||
alt="Logo"
|
alt="Logo"
|
||||||
|
className="cursor-pointer"
|
||||||
onClick={() => navigate('/')}
|
onClick={() => navigate('/')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isLoggedIn && (
|
<div>
|
||||||
|
{isLoggedIn ? (
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleLogout();
|
handleLogout();
|
||||||
navigate('/');
|
navigate('/');
|
||||||
}}
|
}}
|
||||||
|
className="mb-4"
|
||||||
|
startIcon={<LogoutIcon />}
|
||||||
>
|
>
|
||||||
Logout
|
Déconnexion
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Link to="/login" className="text-decoration-none">
|
||||||
|
<Button variant="contained" className="mb-4" startIcon={<LoginIcon />} >
|
||||||
|
Connexion
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoggedIn && (
|
|
||||||
<div className="auth-selection-btn">
|
|
||||||
<Link to="/login">
|
|
||||||
<button className="auth-btn">Connexion</button>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
|
|
||||||
.header {
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding: 15px;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header img {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import React, { useState, DragEvent, useRef, useEffect } from 'react';
|
import React, { useState, DragEvent, useRef, useEffect } from 'react';
|
||||||
import './importModal.css';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
@ -12,7 +10,7 @@ import {
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Clear, Download } from '@mui/icons-material';
|
import { Clear, Download } from '@mui/icons-material';
|
||||||
import ApiService from '../../services/ApiService';
|
import ApiService from '../../services/ApiService';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
|
||||||
type DroppedFile = {
|
type DroppedFile = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -48,7 +46,6 @@ const DragAndDrop: React.FC<Props> = ({ handleOnClose, handleOnImport, open, sel
|
||||||
|
|
||||||
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const files = e.dataTransfer.files;
|
const files = e.dataTransfer.files;
|
||||||
handleFiles(files);
|
handleFiles(files);
|
||||||
};
|
};
|
||||||
|
|
@ -66,8 +63,6 @@ const DragAndDrop: React.FC<Props> = ({ handleOnClose, handleOnImport, open, sel
|
||||||
setDroppedFiles((prevFiles) => [...prevFiles, ...newDroppedFiles]);
|
setDroppedFiles((prevFiles) => [...prevFiles, ...newDroppedFiles]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleOnSave = async () => {
|
const handleOnSave = async () => {
|
||||||
const storedQuizzes = JSON.parse(localStorage.getItem('quizzes') || '[]');
|
const storedQuizzes = JSON.parse(localStorage.getItem('quizzes') || '[]');
|
||||||
const quizzesToImportPromises = droppedFiles.map((droppedFile) => {
|
const quizzesToImportPromises = droppedFiles.map((droppedFile) => {
|
||||||
|
|
@ -77,23 +72,16 @@ const DragAndDrop: React.FC<Props> = ({ handleOnClose, handleOnImport, open, sel
|
||||||
reader.onload = async (event) => {
|
reader.onload = async (event) => {
|
||||||
if (event.target && event.target.result) {
|
if (event.target && event.target.result) {
|
||||||
const fileContent = event.target.result as string;
|
const fileContent = event.target.result as string;
|
||||||
//console.log(fileContent);
|
|
||||||
if (fileContent.trim() === '') {
|
if (fileContent.trim() === '') {
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}
|
}
|
||||||
const questions = fileContent.split(/}/)
|
const questions = fileContent.split(/}/)
|
||||||
.map(question => {
|
.map(question => {
|
||||||
// Remove trailing and leading spaces
|
return question.trim() + "}";
|
||||||
|
|
||||||
return question.trim()+"}";
|
|
||||||
})
|
})
|
||||||
.filter(question => question.trim() !== '').slice(0, -1); // Filter out lines with only whitespace characters
|
.filter(question => question.trim() !== '').slice(0, -1);
|
||||||
|
|
||||||
try {
|
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);
|
await ApiService.createQuiz(droppedFile.name.slice(0, -4) || 'Untitled quiz', questions, selectedFolder);
|
||||||
resolve('success');
|
resolve('success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -105,8 +93,6 @@ const DragAndDrop: React.FC<Props> = ({ handleOnClose, handleOnImport, open, sel
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Promise.all(quizzesToImportPromises).then((quizzesToImport) => {
|
Promise.all(quizzesToImportPromises).then((quizzesToImport) => {
|
||||||
const verifiedQuizzesToImport = quizzesToImport.filter((quiz) => {
|
const verifiedQuizzesToImport = quizzesToImport.filter((quiz) => {
|
||||||
return quiz !== null;
|
return quiz !== null;
|
||||||
|
|
@ -118,17 +104,10 @@ const DragAndDrop: React.FC<Props> = ({ handleOnClose, handleOnImport, open, sel
|
||||||
setDroppedFiles([]);
|
setDroppedFiles([]);
|
||||||
handleOnImport();
|
handleOnImport();
|
||||||
handleOnClose();
|
handleOnClose();
|
||||||
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleRemoveFile = (id: number) => {
|
const handleRemoveFile = (id: number) => {
|
||||||
setDroppedFiles((prevFiles) => prevFiles.filter((file) => file.id !== id));
|
setDroppedFiles((prevFiles) => prevFiles.filter((file) => file.id !== id));
|
||||||
};
|
};
|
||||||
|
|
@ -158,14 +137,15 @@ const DragAndDrop: React.FC<Props> = ({ handleOnClose, handleOnImport, open, sel
|
||||||
{'Importation de quiz'}
|
{'Importation de quiz'}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="import-container"
|
className="border border-2 border-dashed border-secondary d-flex flex-column justify-content-center align-items-center p-4 mx-3 my-2"
|
||||||
|
style={{ height: '20vh', cursor: 'pointer' }}
|
||||||
onDragEnter={handleDragEnter}
|
onDragEnter={handleDragEnter}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onClick={handleBrowseButtonClick}
|
onClick={handleBrowseButtonClick}
|
||||||
>
|
>
|
||||||
<div className="mb-1">
|
<div className="mb-2">
|
||||||
<DialogContentText sx={{ textAlign: 'center' }}>
|
<DialogContentText className="text-center">
|
||||||
Déposer des fichiers ici ou
|
Déposer des fichiers ici ou
|
||||||
<br />
|
<br />
|
||||||
cliquez pour ouvrir l'explorateur des fichiers
|
cliquez pour ouvrir l'explorateur des fichiers
|
||||||
|
|
@ -175,7 +155,7 @@ const DragAndDrop: React.FC<Props> = ({ handleOnClose, handleOnImport, open, sel
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
{droppedFiles.map((file) => (
|
{droppedFiles.map((file) => (
|
||||||
<div key={file.id + file.name} className="file-container">
|
<div key={file.id + file.name} className="d-flex align-items-center gap-2 p-1">
|
||||||
<span>{file.icon}</span>
|
<span>{file.icon}</span>
|
||||||
<span>{file.name}</span>
|
<span>{file.name}</span>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { CircularProgress } from '@mui/material';
|
import { CircularProgress } from '@mui/material';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import './loadingCircle.css';
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
text: string;
|
text: string;
|
||||||
|
|
@ -8,8 +8,8 @@ interface Props {
|
||||||
|
|
||||||
const LoadingCircle: React.FC<Props> = ({ text }) => {
|
const LoadingCircle: React.FC<Props> = ({ text }) => {
|
||||||
return (
|
return (
|
||||||
<div className="loading-circle">
|
<div className="d-flex flex-column align-items-center gap-2">
|
||||||
<div className="text-base">{text}</div>
|
<div className="fs-6">{text}</div>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
.loading-circle {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
// StudentModeQuiz.tsx
|
// StudentModeQuiz.tsx
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import QuestionComponent from '../QuestionsDisplay/QuestionDisplay';
|
import QuestionComponent from '../QuestionsDisplay/QuestionDisplay';
|
||||||
import '../../pages/Student/JoinRoom/joinRoom.css';
|
|
||||||
import { QuestionType } from '../../Types/QuestionType';
|
import { QuestionType } from '../../Types/QuestionType';
|
||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
//import QuestionNavigation from '../QuestionNavigation/QuestionNavigation';
|
//import QuestionNavigation from '../QuestionNavigation/QuestionNavigation';
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Box, Button, Chip } from '@mui/material';
|
import { Box, Button, Chip } from '@mui/material';
|
||||||
import { StudentType } from '../../Types/StudentType';
|
import { StudentType } from '../../Types/StudentType';
|
||||||
import { PlayArrow } from '@mui/icons-material';
|
import { PlayArrow } from '@mui/icons-material';
|
||||||
import LaunchQuizDialog from '../LaunchQuizDialog/LaunchQuizDialog';
|
import LaunchQuizDialog from '../LaunchQuizDialog/LaunchQuizDialog';
|
||||||
import { useState } from 'react';
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
import './studentWaitPage.css';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
students: StudentType[];
|
students: StudentType[];
|
||||||
|
|
@ -20,31 +19,31 @@ const StudentWaitPage: React.FC<Props> = ({ students, launchQuiz, setQuizMode })
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="wait">
|
<div className="d-flex flex-column w-100">
|
||||||
<div className='button'>
|
<div className="p-3 d-flex justify-content-center align-items-center">
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={handleLaunchClick}
|
onClick={handleLaunchClick}
|
||||||
startIcon={<PlayArrow />}
|
startIcon={<PlayArrow />}
|
||||||
fullWidth
|
fullWidth
|
||||||
sx={{ fontWeight: 600, fontSize: 20 }}
|
sx={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: 20,
|
||||||
|
maxWidth: '500px' // Optional: limit button width
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Lancer
|
Lancer
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="students">
|
<div className="p-3 w-100 overflow-auto">
|
||||||
|
|
||||||
<Box display="flex" flexWrap="wrap" gap={3}>
|
<Box display="flex" flexWrap="wrap" gap={3}>
|
||||||
|
|
||||||
{students.map((student, index) => (
|
{students.map((student, index) => (
|
||||||
<Box key={student.name + index} >
|
<Box key={student.name + index}>
|
||||||
<Chip label={student.name} sx={{ width: '100%' }} />
|
<Chip label={student.name} sx={{ width: '100%' }} />
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LaunchQuizDialog
|
<LaunchQuizDialog
|
||||||
|
|
@ -53,7 +52,6 @@ const StudentWaitPage: React.FC<Props> = ({ students, launchQuiz, setQuizMode })
|
||||||
launchQuiz={launchQuiz}
|
launchQuiz={launchQuiz}
|
||||||
setQuizMode={setQuizMode}
|
setQuizMode={setQuizMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
.wait {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wait .button {
|
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wait .students {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
padding: 10px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
// TeacherModeQuiz.tsx
|
// TeacherModeQuiz.tsx
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import QuestionComponent from '../QuestionsDisplay/QuestionDisplay';
|
import QuestionComponent from '../QuestionsDisplay/QuestionDisplay';
|
||||||
import '../../pages/Student/JoinRoom/joinRoom.css';
|
|
||||||
import { QuestionType } from '../../Types/QuestionType';
|
import { QuestionType } from '../../Types/QuestionType';
|
||||||
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
|
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
|
||||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material';
|
import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material';
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { BrowserRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import { ThemeProvider, createTheme } from '@mui/material';
|
import { ThemeProvider, createTheme } from '@mui/material';
|
||||||
import '@fortawesome/fontawesome-free/css/all.min.css';
|
import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
|
||||||
import './cssReset.css';
|
import './cssReset.css';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,40 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import './authDrawer.css';
|
|
||||||
import SimpleLogin from './providers/SimpleLogin/Login';
|
import SimpleLogin from './providers/SimpleLogin/Login';
|
||||||
import authService from '../../services/AuthService';
|
import authService from '../../services/AuthService';
|
||||||
import { ENV_VARIABLES } from '../../constants';
|
import { ENV_VARIABLES } from '../../constants';
|
||||||
import ButtonAuth from './providers/OAuth-Oidc/ButtonAuth';
|
import ButtonAuth from './providers/OAuth-Oidc/ButtonAuth';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
|
||||||
const AuthSelection: React.FC = () => {
|
const AuthSelection: React.FC = () => {
|
||||||
const [authData, setAuthData] = useState<any>(null); // Stocke les données d'auth
|
const [authData, setAuthData] = useState<any>(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
ENV_VARIABLES.VITE_BACKEND_URL;
|
ENV_VARIABLES.VITE_BACKEND_URL;
|
||||||
// Récupérer les données d'authentification depuis l'API
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
const data = await authService.fetchAuthData();
|
const data = await authService.fetchAuthData();
|
||||||
setAuthData(data);
|
setAuthData(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="auth-selection-page">
|
<div className="d-flex flex-column align-items-center p-4 w-100">
|
||||||
<h1>Connexion</h1>
|
<h1 className="mb-4">Connexion</h1>
|
||||||
|
|
||||||
{/* Formulaire de connexion Simple Login */}
|
{/* Simple Login Form - Responsive width */}
|
||||||
{authData && authData['simpleauth'] && (
|
{authData && authData['simpleauth'] && (
|
||||||
<div className="form-container">
|
<div className="border rounded-3 p-4 my-2 shadow-sm w-100" style={{ maxWidth: '400px' }}>
|
||||||
<SimpleLogin />
|
<SimpleLogin />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Conteneur OAuth/OIDC */}
|
{/* OAuth/OIDC Providers - Responsive width */}
|
||||||
{authData && Object.keys(authData).some(key => authData[key].type === 'oidc' || authData[key].type === 'oauth') && (
|
{authData && Object.keys(authData).some(key =>
|
||||||
<div className="auth-button-container">
|
authData[key].type === 'oidc' || authData[key].type === 'oauth') && (
|
||||||
|
<div className="d-flex flex-column my-3 w-100" style={{ maxWidth: '400px' }}>
|
||||||
{Object.keys(authData).map((providerKey) => {
|
{Object.keys(authData).map((providerKey) => {
|
||||||
const providerType = authData[providerKey].type;
|
const providerType = authData[providerKey].type;
|
||||||
if (providerType === 'oidc' || providerType === 'oauth') {
|
if (providerType === 'oidc' || providerType === 'oauth') {
|
||||||
|
|
@ -51,9 +51,12 @@ const AuthSelection: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<button
|
||||||
<button className="home-button-container" onClick={() => navigate('/')}>Retour à l'accueil</button>
|
className="btn btn-link text-dark p-0 mt-3"
|
||||||
</div>
|
onClick={() => navigate('/')}
|
||||||
|
>
|
||||||
|
Retour à l'accueil
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
.auth-selection-page {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.form-container {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 15px;
|
|
||||||
margin: 10px 0;
|
|
||||||
width: 400px;
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
input {
|
|
||||||
margin: 5px 0;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
padding: 10px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #5271ff;
|
|
||||||
color: white;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
/* This hover was affecting the entire App */
|
|
||||||
/* button:hover {
|
|
||||||
background-color: #5271ff;
|
|
||||||
} */
|
|
||||||
.home-button-container {
|
|
||||||
background: none;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
.home-button-container:hover {
|
|
||||||
background: none;
|
|
||||||
color: black;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ENV_VARIABLES } from '../../../../constants';
|
import { ENV_VARIABLES } from '../../../../constants';
|
||||||
import '../css/buttonAuth.css';
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
|
||||||
interface ButtonAuthContainerProps {
|
interface ButtonAuthContainerProps {
|
||||||
providerName: string;
|
providerName: string;
|
||||||
|
|
@ -13,14 +13,16 @@ const handleAuthLogin = (provider: string) => {
|
||||||
|
|
||||||
const ButtonAuth: React.FC<ButtonAuthContainerProps> = ({ providerName, providerType }) => {
|
const ButtonAuth: React.FC<ButtonAuthContainerProps> = ({ providerName, providerType }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={`border rounded-3 p-3 my-3 mx-auto shadow-sm ${providerName}-${providerType}-container`} style={{ maxWidth: '400px' }}>
|
||||||
<div className={`${providerName}-${providerType}-container button-container`}>
|
<h2 className="h5 mb-3">Se connecter avec {providerType.toUpperCase()}</h2>
|
||||||
<h2>Se connecter avec {providerType.toUpperCase()}</h2>
|
<button
|
||||||
<button key={providerName} className={`provider-btn ${providerType}-btn`} onClick={() => handleAuthLogin(providerName)}>
|
key={providerName}
|
||||||
|
className={`btn btn-outline-secondary w-100 ${providerType}-btn`}
|
||||||
|
onClick={() => handleAuthLogin(providerName)}
|
||||||
|
>
|
||||||
Continuer avec {providerName}
|
Continuer avec {providerName}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,88 +1,81 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { TextField, Button, CircularProgress } from '@mui/material';
|
||||||
// JoinRoom.tsx
|
import LoginContainer from '../../../../components/LoginContainer/LoginContainer';
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import '../css/simpleLogin.css';
|
|
||||||
import { TextField } from '@mui/material';
|
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
|
||||||
|
|
||||||
import LoginContainer from '../../../../components/LoginContainer/LoginContainer'
|
|
||||||
import ApiService from '../../../../services/ApiService';
|
import ApiService from '../../../../services/ApiService';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
import LoginIcon from '@mui/icons-material/Login';
|
||||||
|
|
||||||
const SimpleLogin: React.FC = () => {
|
const SimpleLogin: React.FC = () => {
|
||||||
|
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
|
|
||||||
const [connectionError, setConnectionError] = useState<string>('');
|
const [connectionError, setConnectionError] = useState<string>('');
|
||||||
const [isConnecting] = useState<boolean>(false);
|
const [isConnecting, setIsConnecting] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
// Cleanup if needed
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
console.log(`SimpleLogin: login: email: ${email}, password: ${password}`);
|
setIsConnecting(true);
|
||||||
const result = await ApiService.login(email, password);
|
const result = await ApiService.login(email, password);
|
||||||
|
setIsConnecting(false);
|
||||||
|
|
||||||
if (result !== true) {
|
if (result !== true) {
|
||||||
setConnectionError(result);
|
setConnectionError(result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginContainer
|
<LoginContainer title='' error={connectionError}>
|
||||||
title=''
|
{/* Email Input */}
|
||||||
error={connectionError}>
|
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Email"
|
label="Courriel"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
className="mb-3 w-100" // Bootstrap classes for spacing and width
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder="Nom d'utilisateur"
|
placeholder="Nom d'utilisateur"
|
||||||
sx={{ marginBottom: '1rem' }}
|
fullWidth // Material-UI fullWidth
|
||||||
fullWidth
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Password Input */}
|
||||||
<TextField
|
<TextField
|
||||||
label="Mot de passe"
|
label="Mot de passe"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
type="password"
|
type="password"
|
||||||
|
className="mb-3 w-100" // Bootstrap classes
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="Nom de la salle"
|
placeholder="Mot de passe"
|
||||||
sx={{ marginBottom: '1rem' }}
|
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LoadingButton
|
{/* Login Button */}
|
||||||
loading={isConnecting}
|
<Button
|
||||||
onClick={login}
|
|
||||||
variant="contained"
|
variant="contained"
|
||||||
sx={{ marginBottom: `${connectionError && '2rem'}` }}
|
className={`w-100 mb-${connectionError ? '4' : '3'}`} // Dynamic margin-bottom
|
||||||
disabled={!email || !password}
|
onClick={login}
|
||||||
|
disabled={!email || !password || isConnecting}
|
||||||
|
startIcon={isConnecting ? <CircularProgress size={20} /> : <LoginIcon />}
|
||||||
|
size="large"
|
||||||
>
|
>
|
||||||
Login
|
Se connecter
|
||||||
</LoadingButton>
|
</Button>
|
||||||
|
|
||||||
<div className="login-links">
|
{/* Links Section */}
|
||||||
|
<div className="d-flex flex-column align-items-center pt-3">
|
||||||
|
<del className="py-1 text-muted">Réinitialiser le mot de passe</del>
|
||||||
{/* <Link to="/resetPassword"> */}
|
<Link
|
||||||
<del>Réinitialiser le mot de passe</del>
|
to="/register"
|
||||||
{/* </Link> */}
|
className="py-1 text-decoration-none text-primary"
|
||||||
|
>
|
||||||
<Link to="/register">
|
|
||||||
Créer un compte
|
Créer un compte
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</LoginContainer>
|
</LoginContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,35 @@
|
||||||
// JoinRoom.tsx
|
import React, { useState, useEffect } from 'react';
|
||||||
import React, { useEffect, useState } from 'react';
|
import {
|
||||||
|
TextField,
|
||||||
import { TextField, FormLabel, RadioGroup, FormControlLabel, Radio, Box } from '@mui/material';
|
FormLabel,
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
RadioGroup,
|
||||||
|
FormControlLabel,
|
||||||
|
Radio,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
CircularProgress
|
||||||
|
} from '@mui/material';
|
||||||
import LoginContainer from '../../../../components/LoginContainer/LoginContainer';
|
import LoginContainer from '../../../../components/LoginContainer/LoginContainer';
|
||||||
import ApiService from '../../../../services/ApiService';
|
import ApiService from '../../../../services/ApiService';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
|
||||||
const Register: React.FC = () => {
|
const Register: React.FC = () => {
|
||||||
|
const [name, setName] = useState('');
|
||||||
const [name, setName] = useState(''); // State for name
|
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [roles, setRoles] = useState<string[]>(['teacher']); // Set 'student' as the default role
|
const [roles, setRoles] = useState<string[]>(['teacher']);
|
||||||
|
|
||||||
const [connectionError, setConnectionError] = useState<string>('');
|
const [connectionError, setConnectionError] = useState<string>('');
|
||||||
const [isConnecting] = useState<boolean>(false);
|
const [isConnecting, setIsConnecting] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => { };
|
return () => { };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleRoleChange = (role: string) => {
|
const handleRoleChange = (role: string) => {
|
||||||
setRoles([role]); // Update the roles array to contain the selected role
|
setRoles([role]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isValidEmail = (email: string) => {
|
const isValidEmail = (email: string) => {
|
||||||
// Basic email format validation
|
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
return emailRegex.test(email);
|
return emailRegex.test(email);
|
||||||
};
|
};
|
||||||
|
|
@ -37,7 +40,9 @@ const Register: React.FC = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsConnecting(true);
|
||||||
const result = await ApiService.register(name, email, password, roles);
|
const result = await ApiService.register(name, email, password, roles);
|
||||||
|
setIsConnecting(false);
|
||||||
|
|
||||||
if (result !== true) {
|
if (result !== true) {
|
||||||
setConnectionError(result);
|
setConnectionError(result);
|
||||||
|
|
@ -46,46 +51,49 @@ const Register: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginContainer
|
<LoginContainer title="Créer un compte" error={connectionError}>
|
||||||
title="Créer un compte"
|
{/* Name Field */}
|
||||||
error={connectionError}
|
|
||||||
>
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Nom"
|
label="Nom"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
className="mb-3 w-100"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
placeholder="Votre nom"
|
placeholder="Votre nom"
|
||||||
sx={{ marginBottom: '1rem' }}
|
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Email Field */}
|
||||||
<TextField
|
<TextField
|
||||||
label="Email"
|
label="Email"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
className="mb-3 w-100"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder="Adresse courriel"
|
placeholder="Adresse courriel"
|
||||||
sx={{ marginBottom: '1rem' }}
|
|
||||||
fullWidth
|
fullWidth
|
||||||
type="email"
|
type="email"
|
||||||
error={!!connectionError && !isValidEmail(email)}
|
error={!!connectionError && !isValidEmail(email)}
|
||||||
helperText={connectionError && !isValidEmail(email) ? "Adresse email invalide." : ""}
|
helperText={connectionError && !isValidEmail(email) ? "Adresse email invalide." : ""}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Password Field */}
|
||||||
<TextField
|
<TextField
|
||||||
label="Mot de passe"
|
label="Mot de passe"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
className="mb-3 w-100"
|
||||||
value={password}
|
value={password}
|
||||||
type="password"
|
type="password"
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="Mot de passe"
|
placeholder="Mot de passe"
|
||||||
sx={{ marginBottom: '1rem' }}
|
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', marginBottom: '1rem' }}>
|
{/* Role Selection */}
|
||||||
<FormLabel component="legend" sx={{ marginRight: '1rem' }}>Choisir votre rôle</FormLabel>
|
<Box className="d-flex align-items-center mb-3">
|
||||||
|
<FormLabel component="legend" className="me-3">
|
||||||
|
Choisir votre rôle
|
||||||
|
</FormLabel>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
row
|
row
|
||||||
aria-label="role"
|
aria-label="role"
|
||||||
|
|
@ -98,15 +106,17 @@ const Register: React.FC = () => {
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<LoadingButton
|
{/* Register Button */}
|
||||||
loading={isConnecting}
|
<Button
|
||||||
onClick={register}
|
|
||||||
variant="contained"
|
variant="contained"
|
||||||
sx={{ marginBottom: `${connectionError && '2rem'}` }}
|
className={`w-100 mb-${connectionError ? '4' : '3'}`}
|
||||||
disabled={!name || !email || !password}
|
onClick={register}
|
||||||
|
disabled={!name || !email || !password || isConnecting}
|
||||||
|
startIcon={isConnecting ? <CircularProgress size={20} /> : null}
|
||||||
|
size="large"
|
||||||
>
|
>
|
||||||
S'inscrire
|
S'inscrire
|
||||||
</LoadingButton>
|
</Button>
|
||||||
</LoginContainer>
|
</LoginContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,25 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { TextField, Button, CircularProgress } from '@mui/material';
|
||||||
// JoinRoom.tsx
|
import LoginContainer from '../../../../components/LoginContainer/LoginContainer';
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { TextField } from '@mui/material';
|
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
|
||||||
|
|
||||||
import LoginContainer from '../../../../components/LoginContainer/LoginContainer'
|
|
||||||
import ApiService from '../../../../services/ApiService';
|
import ApiService from '../../../../services/ApiService';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
|
||||||
const ResetPassword: React.FC = () => {
|
const ResetPassword: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
|
|
||||||
const [connectionError, setConnectionError] = useState<string>('');
|
const [connectionError, setConnectionError] = useState<string>('');
|
||||||
const [isConnecting] = useState<boolean>(false);
|
const [isConnecting, setIsConnecting] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
// Cleanup if needed
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const reset = async () => {
|
const reset = async () => {
|
||||||
|
setIsConnecting(true);
|
||||||
|
try {
|
||||||
const result = await ApiService.resetPassword(email);
|
const result = await ApiService.resetPassword(email);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
|
@ -32,35 +27,44 @@ const ResetPassword: React.FC = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate("/login")
|
navigate("/login");
|
||||||
|
} finally {
|
||||||
|
setIsConnecting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isValidEmail = (email: string) => {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
return emailRegex.test(email);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginContainer
|
<LoginContainer title='Récupération du compte' error={connectionError}>
|
||||||
title='Récupération du compte'
|
{/* Email Field */}
|
||||||
error={connectionError}>
|
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Email"
|
label="Email"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
className="mb-3 w-100"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder="Adresse courriel"
|
placeholder="Adresse courriel"
|
||||||
sx={{ marginBottom: '1rem' }}
|
|
||||||
fullWidth
|
fullWidth
|
||||||
|
type="email"
|
||||||
|
error={!!connectionError && !isValidEmail(email)}
|
||||||
|
helperText={connectionError && !isValidEmail(email) ? "Adresse email invalide." : ""}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LoadingButton
|
{/* Reset Button */}
|
||||||
loading={isConnecting}
|
<Button
|
||||||
onClick={reset}
|
|
||||||
variant="contained"
|
variant="contained"
|
||||||
sx={{ marginBottom: `${connectionError && '2rem'}` }}
|
className={`w-100 mb-${connectionError ? '4' : '3'}`}
|
||||||
disabled={!email}
|
onClick={reset}
|
||||||
|
disabled={!email || isConnecting}
|
||||||
|
startIcon={isConnecting ? <CircularProgress size={20} /> : null}
|
||||||
|
size="large"
|
||||||
>
|
>
|
||||||
Réinitialiser le mot de passe
|
Réinitialiser le mot de passe
|
||||||
</LoadingButton>
|
</Button>
|
||||||
|
|
||||||
</LoginContainer>
|
</LoginContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
.provider-btn {
|
|
||||||
background-color: #ffffff;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
color: black;
|
|
||||||
margin: 4px 0 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-btn:hover {
|
|
||||||
background-color: #dbdbdb;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
color: black;
|
|
||||||
margin: 4px 0 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-container {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 15px;
|
|
||||||
margin: 10px 0;
|
|
||||||
width: 400px;
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
.login-links {
|
|
||||||
padding-top: 10px;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-links a {
|
|
||||||
padding: 4px;
|
|
||||||
color: #333;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-links a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +1,15 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { ENV_VARIABLES } from 'src/constants';
|
import { ENV_VARIABLES } from 'src/constants';
|
||||||
|
|
||||||
import StudentModeQuiz from 'src/components/StudentModeQuiz/StudentModeQuiz';
|
import StudentModeQuiz from 'src/components/StudentModeQuiz/StudentModeQuiz';
|
||||||
import TeacherModeQuiz from 'src/components/TeacherModeQuiz/TeacherModeQuiz';
|
import TeacherModeQuiz from 'src/components/TeacherModeQuiz/TeacherModeQuiz';
|
||||||
import webSocketService, { AnswerSubmissionToBackendType } from '../../../services/WebsocketService';
|
import webSocketService, { AnswerSubmissionToBackendType } from '../../../services/WebsocketService';
|
||||||
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
|
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
|
||||||
|
|
||||||
import './joinRoom.css';
|
|
||||||
import { QuestionType } from '../../../Types/QuestionType';
|
import { QuestionType } from '../../../Types/QuestionType';
|
||||||
import { TextField } from '@mui/material';
|
import { TextField, Button, CircularProgress } from '@mui/material';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoginContainer from 'src/components/LoginContainer/LoginContainer';
|
||||||
|
import ApiService from '../../../services/ApiService';
|
||||||
import LoginContainer from 'src/components/LoginContainer/LoginContainer'
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
|
||||||
import ApiService from '../../../services/ApiService'
|
|
||||||
|
|
||||||
export type AnswerType = Array<string | number | boolean>;
|
export type AnswerType = Array<string | number | boolean>;
|
||||||
|
|
||||||
|
|
@ -39,69 +33,63 @@ const JoinRoom: React.FC = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(`JoinRoom: useEffect: questions: ${JSON.stringify(questions)}`);
|
|
||||||
setAnswers(questions ? Array(questions.length).fill({} as AnswerSubmissionToBackendType) : []);
|
setAnswers(questions ? Array(questions.length).fill({} as AnswerSubmissionToBackendType) : []);
|
||||||
}, [questions]);
|
}, [questions]);
|
||||||
|
|
||||||
|
|
||||||
const handleCreateSocket = () => {
|
const handleCreateSocket = () => {
|
||||||
console.log(`JoinRoom: handleCreateSocket: ${ENV_VARIABLES.VITE_BACKEND_URL}`);
|
|
||||||
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||||
|
|
||||||
socket.on('join-success', (roomJoinedName) => {
|
socket.on('join-success', () => {
|
||||||
setIsWaitingForTeacher(true);
|
setIsWaitingForTeacher(true);
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
console.log(`on(join-success): Successfully joined the room ${roomJoinedName}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('next-question', (question: QuestionType) => {
|
socket.on('next-question', (question: QuestionType) => {
|
||||||
console.log('JoinRoom: on(next-question): Received next-question:', question);
|
|
||||||
setQuizMode('teacher');
|
setQuizMode('teacher');
|
||||||
setIsWaitingForTeacher(false);
|
setIsWaitingForTeacher(false);
|
||||||
setQuestion(question);
|
setQuestion(question);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('launch-teacher-mode', (questions: QuestionType[]) => {
|
socket.on('launch-teacher-mode', (questions: QuestionType[]) => {
|
||||||
console.log('on(launch-teacher-mode): Received launch-teacher-mode:', questions);
|
|
||||||
setQuizMode('teacher');
|
setQuizMode('teacher');
|
||||||
setIsWaitingForTeacher(true);
|
setIsWaitingForTeacher(true);
|
||||||
setQuestions([]); // clear out from last time (in case quiz is repeated)
|
setQuestions([]);
|
||||||
setQuestions(questions);
|
setQuestions(questions);
|
||||||
// wait for next-question
|
|
||||||
});
|
});
|
||||||
socket.on('launch-student-mode', (questions: QuestionType[]) => {
|
|
||||||
console.log('on(launch-student-mode): Received launch-student-mode:', questions);
|
|
||||||
|
|
||||||
|
socket.on('launch-student-mode', (questions: QuestionType[]) => {
|
||||||
setQuizMode('student');
|
setQuizMode('student');
|
||||||
setIsWaitingForTeacher(false);
|
setIsWaitingForTeacher(false);
|
||||||
setQuestions([]); // clear out from last time (in case quiz is repeated)
|
setQuestions([]);
|
||||||
setQuestions(questions);
|
setQuestions(questions);
|
||||||
setQuestion(questions[0]);
|
setQuestion(questions[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('end-quiz', () => {
|
socket.on('end-quiz', () => {
|
||||||
disconnect();
|
disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('join-failure', (message) => {
|
socket.on('join-failure', (message) => {
|
||||||
console.log('Failed to join the room.');
|
|
||||||
setConnectionError(`Erreur de connexion : ${message}`);
|
setConnectionError(`Erreur de connexion : ${message}`);
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('connect_error', (error) => {
|
socket.on('connect_error', (error) => {
|
||||||
switch (error.message) {
|
switch (error.message) {
|
||||||
case 'timeout':
|
case 'timeout':
|
||||||
setConnectionError("JoinRoom: timeout: Le serveur n'est pas disponible");
|
setConnectionError("Le serveur n'est pas disponible");
|
||||||
break;
|
break;
|
||||||
case 'websocket error':
|
case 'websocket error':
|
||||||
setConnectionError("JoinRoom: websocket error: Le serveur n'est pas disponible");
|
setConnectionError("Le serveur n'est pas disponible");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
console.log('Connection Error:', error.message);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setSocket(socket);
|
setSocket(socket);
|
||||||
};
|
};
|
||||||
|
|
||||||
const disconnect = () => {
|
const disconnect = () => {
|
||||||
// localStorage.clear();
|
|
||||||
webSocketService.disconnect();
|
webSocketService.disconnect();
|
||||||
setSocket(null);
|
setSocket(null);
|
||||||
setQuestion(undefined);
|
setQuestion(undefined);
|
||||||
|
|
@ -120,28 +108,22 @@ const JoinRoom: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (username && roomName) {
|
if (username && roomName) {
|
||||||
console.log(`Tentative de rejoindre : ${roomName}, utilisateur : ${username}`);
|
|
||||||
|
|
||||||
webSocketService.joinRoom(roomName, username);
|
webSocketService.joinRoom(roomName, username);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnSubmitAnswer = (answer: AnswerType, idQuestion: number) => {
|
const handleOnSubmitAnswer = (answer: AnswerType, idQuestion: number) => {
|
||||||
console.info(`JoinRoom: handleOnSubmitAnswer: answer: ${answer}, idQuestion: ${idQuestion}`);
|
|
||||||
const answerData: AnswerSubmissionToBackendType = {
|
const answerData: AnswerSubmissionToBackendType = {
|
||||||
roomName: roomName,
|
roomName: roomName,
|
||||||
answer: answer,
|
answer: answer,
|
||||||
username: username,
|
username: username,
|
||||||
idQuestion: idQuestion
|
idQuestion: idQuestion
|
||||||
};
|
};
|
||||||
// localStorage.setItem(`Answer${idQuestion}`, JSON.stringify(answer));
|
|
||||||
setAnswers((prevAnswers) => {
|
setAnswers((prevAnswers) => {
|
||||||
console.log(`JoinRoom: handleOnSubmitAnswer: prevAnswers: ${JSON.stringify(prevAnswers)}`);
|
const newAnswers = [...prevAnswers];
|
||||||
const newAnswers = [...prevAnswers]; // Create a copy of the previous answers array
|
newAnswers[idQuestion - 1] = answerData;
|
||||||
newAnswers[idQuestion - 1] = answerData; // Update the specific answer
|
return newAnswers;
|
||||||
return newAnswers; // Return the new array
|
|
||||||
});
|
});
|
||||||
console.log(`JoinRoom: handleOnSubmitAnswer: answers: ${JSON.stringify(answers)}`);
|
|
||||||
webSocketService.submitAnswer(answerData);
|
webSocketService.submitAnswer(answerData);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -153,22 +135,20 @@ const JoinRoom: React.FC = () => {
|
||||||
|
|
||||||
if (isWaitingForTeacher) {
|
if (isWaitingForTeacher) {
|
||||||
return (
|
return (
|
||||||
<div className='room'>
|
<div className="d-flex flex-column vh-100">
|
||||||
<div className='roomHeader'>
|
<div className="d-flex justify-content-between align-items-center p-3 border-bottom">
|
||||||
|
|
||||||
<DisconnectButton
|
<DisconnectButton
|
||||||
onReturn={disconnect}
|
onReturn={disconnect}
|
||||||
message={`Êtes-vous sûr de vouloir quitter?`} />
|
message={`Êtes-vous sûr de vouloir quitter?`} />
|
||||||
|
|
||||||
<div className='centerTitle'>
|
<div className="text-center">
|
||||||
<div className='title'>Salle: {roomName}</div>
|
<h2 className="mb-1">Salle: {roomName}</h2>
|
||||||
<div className='userCount subtitle'>
|
<p className="text-muted mb-0">
|
||||||
En attente que le professeur lance le questionnaire...
|
En attente que le professeur lance le questionnaire...
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='dumb'></div>
|
<div style={{ width: '48px' }}></div> {/* Spacer for balance */}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -197,41 +177,39 @@ const JoinRoom: React.FC = () => {
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<LoginContainer
|
<LoginContainer title='Rejoindre une salle' error={connectionError}>
|
||||||
title='Rejoindre une salle'
|
|
||||||
error={connectionError}>
|
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
type="text"
|
type="text"
|
||||||
label="Nom de la salle"
|
label="Nom de la salle"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
className="mb-3 w-100"
|
||||||
value={roomName}
|
value={roomName}
|
||||||
onChange={(e) => setRoomName(e.target.value.toUpperCase())}
|
onChange={(e) => setRoomName(e.target.value.toUpperCase())}
|
||||||
placeholder="Nom de la salle"
|
placeholder="Nom de la salle"
|
||||||
sx={{ marginBottom: '1rem' }}
|
fullWidth
|
||||||
fullWidth={true}
|
|
||||||
onKeyDown={handleReturnKey}
|
onKeyDown={handleReturnKey}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Nom d'utilisateur"
|
label="Nom d'utilisateur"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
className="mb-3 w-100"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
placeholder="Nom d'utilisateur"
|
placeholder="Nom d'utilisateur"
|
||||||
sx={{ marginBottom: '1rem' }}
|
fullWidth
|
||||||
fullWidth={true}
|
|
||||||
onKeyDown={handleReturnKey}
|
onKeyDown={handleReturnKey}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LoadingButton
|
<Button
|
||||||
loading={isConnecting}
|
|
||||||
onClick={handleSocket}
|
|
||||||
variant="contained"
|
variant="contained"
|
||||||
sx={{ marginBottom: `${connectionError && '2rem'}` }}
|
className="w-100"
|
||||||
disabled={!username || !roomName}
|
onClick={handleSocket}
|
||||||
>Rejoindre</LoadingButton>
|
disabled={!username || !roomName || isConnecting}
|
||||||
|
startIcon={isConnecting ? <CircularProgress size={20} /> : null}
|
||||||
|
>
|
||||||
|
Rejoindre
|
||||||
|
</Button>
|
||||||
</LoginContainer>
|
</LoginContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
/* .join-room-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 85%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waiting-text {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 85%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
margin: 2rem 4rem 2rem 4rem;
|
|
||||||
width: 25vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-avatar {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
.question-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-component-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-device-width: 768px) {
|
|
||||||
.login-container {
|
|
||||||
width: inherit;
|
|
||||||
}
|
|
||||||
} */
|
|
||||||
|
|
@ -1,19 +1,12 @@
|
||||||
// Dashboard.tsx
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { parse } from 'gift-pegjs';
|
import { parse } from 'gift-pegjs';
|
||||||
|
|
||||||
import Template from 'src/components/GiftTemplate/templates';
|
import Template from 'src/components/GiftTemplate/templates';
|
||||||
import { QuizType } from '../../../Types/QuizType';
|
import { QuizType } from '../../../Types/QuizType';
|
||||||
import { FolderType } from '../../../Types/FolderType';
|
import { FolderType } from '../../../Types/FolderType';
|
||||||
// import { QuestionService } from '../../../services/QuestionService';
|
|
||||||
import ApiService from '../../../services/ApiService';
|
import ApiService from '../../../services/ApiService';
|
||||||
|
|
||||||
import './dashboard.css';
|
|
||||||
import ImportModal from 'src/components/ImportModal/ImportModal';
|
import ImportModal from 'src/components/ImportModal/ImportModal';
|
||||||
//import axios from 'axios';
|
|
||||||
import { RoomType } from 'src/Types/RoomType';
|
import { RoomType } from 'src/Types/RoomType';
|
||||||
// import { useRooms } from '../ManageRoom/RoomContext';
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
|
|
@ -40,17 +33,31 @@ import {
|
||||||
ContentCopy,
|
ContentCopy,
|
||||||
Edit,
|
Edit,
|
||||||
Share
|
Share
|
||||||
// DriveFileMove
|
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
|
||||||
// Create a custom-styled Card component
|
const CustomCard = styled(Card)(({ theme }) => ({
|
||||||
const CustomCard = styled(Card)({
|
overflow: 'visible',
|
||||||
overflow: 'visible', // Override the overflow property
|
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
margin: '40px 0 20px 0', // Add top margin to make space for the tab
|
margin: '40px 0 20px 0',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
paddingTop: '20px' // Ensure content inside the card doesn't overlap with the tab
|
paddingTop: '20px',
|
||||||
});
|
border: `2px solid ${theme.palette.divider}`,
|
||||||
|
'& .folder-tab': {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-33px',
|
||||||
|
left: '9px',
|
||||||
|
padding: '5px 10px',
|
||||||
|
borderRadius: '8px 8px 0 0',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
display: 'inline-block',
|
||||||
|
border: `2px solid ${theme.palette.divider}`,
|
||||||
|
borderBottom: 'none',
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
color: theme.palette.primary.main
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
const Dashboard: React.FC = () => {
|
const Dashboard: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -58,19 +65,14 @@ const Dashboard: React.FC = () => {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [showImportModal, setShowImportModal] = useState<boolean>(false);
|
const [showImportModal, setShowImportModal] = useState<boolean>(false);
|
||||||
const [folders, setFolders] = useState<FolderType[]>([]);
|
const [folders, setFolders] = useState<FolderType[]>([]);
|
||||||
const [selectedFolderId, setSelectedFolderId] = useState<string>(''); // Selected folder
|
const [selectedFolderId, setSelectedFolderId] = useState<string>('');
|
||||||
const [rooms, setRooms] = useState<RoomType[]>([]);
|
const [rooms, setRooms] = useState<RoomType[]>([]);
|
||||||
const [openAddRoomDialog, setOpenAddRoomDialog] = useState(false);
|
const [openAddRoomDialog, setOpenAddRoomDialog] = useState(false);
|
||||||
const [newRoomTitle, setNewRoomTitle] = useState('');
|
const [newRoomTitle, setNewRoomTitle] = useState('');
|
||||||
// const { selectedRoom, selectRoom, createRoom } = useRooms();
|
const [selectedRoom, selectRoom] = useState<RoomType>();
|
||||||
const [selectedRoom, selectRoom] = useState<RoomType>(); // menu
|
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
const [showErrorDialog, setShowErrorDialog] = useState(false);
|
const [showErrorDialog, setShowErrorDialog] = useState(false);
|
||||||
|
|
||||||
// Filter quizzes based on search term
|
|
||||||
// const filteredQuizzes = quizzes.filter(quiz =>
|
|
||||||
// quiz.title.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
// );
|
|
||||||
const filteredQuizzes = useMemo(() => {
|
const filteredQuizzes = useMemo(() => {
|
||||||
return quizzes.filter(
|
return quizzes.filter(
|
||||||
(quiz) =>
|
(quiz) =>
|
||||||
|
|
@ -78,7 +80,6 @@ const Dashboard: React.FC = () => {
|
||||||
);
|
);
|
||||||
}, [quizzes, searchTerm]);
|
}, [quizzes, searchTerm]);
|
||||||
|
|
||||||
// Group quizzes by folder
|
|
||||||
const quizzesByFolder = filteredQuizzes.reduce((acc, quiz) => {
|
const quizzesByFolder = filteredQuizzes.reduce((acc, quiz) => {
|
||||||
if (!acc[quiz.folderName]) {
|
if (!acc[quiz.folderName]) {
|
||||||
acc[quiz.folderName] = [];
|
acc[quiz.folderName] = [];
|
||||||
|
|
@ -90,28 +91,18 @@ const Dashboard: React.FC = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
const isLoggedIn = await ApiService.isLoggedIn();
|
const isLoggedIn = await ApiService.isLoggedIn();
|
||||||
console.log(`Dashboard: isLoggedIn: ${isLoggedIn}`);
|
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
navigate('/teacher/login');
|
navigate('/teacher/login');
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
const userRooms = await ApiService.getUserRooms();
|
const userRooms = await ApiService.getUserRooms();
|
||||||
setRooms(userRooms as RoomType[]);
|
setRooms(userRooms as RoomType[]);
|
||||||
|
|
||||||
const userFolders = await ApiService.getUserFolders();
|
const userFolders = await ApiService.getUserFolders();
|
||||||
setFolders(userFolders as FolderType[]);
|
setFolders(userFolders as FolderType[]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, [navigate]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (rooms.length > 0 && !selectedRoom) {
|
|
||||||
selectRoom(rooms[rooms.length - 1]);
|
|
||||||
localStorage.setItem('selectedRoomId', rooms[rooms.length - 1]._id);
|
|
||||||
}
|
|
||||||
}, [rooms, selectedRoom]);
|
|
||||||
|
|
||||||
const handleSelectRoom = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
const handleSelectRoom = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
if (event.target.value === 'add-room') {
|
if (event.target.value === 'add-room') {
|
||||||
|
|
@ -424,15 +415,20 @@ const Dashboard: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dashboard">
|
<div className="container-fluid py-4">
|
||||||
<div className="title">Tableau de bord</div>
|
<h1 className="mb-4">Tableau de bord</h1>
|
||||||
|
|
||||||
<div className="roomSelection">
|
{/* Room Selection */}
|
||||||
<label htmlFor="select-room">Sélectionner une salle: </label>
|
<div className="row mb-4">
|
||||||
<select value={selectedRoom?._id || ''} onChange={(e) => handleSelectRoom(e)}>
|
<div className="col-md-6">
|
||||||
<option value="" disabled>
|
<div className="d-flex align-items-center">
|
||||||
-- Sélectionner une salle --
|
<label htmlFor="select-room" className="me-2 fw-medium">Sélectionner une salle:</label>
|
||||||
</option>
|
<select
|
||||||
|
className="form-select flex-grow-1"
|
||||||
|
value={selectedRoom?._id || ''}
|
||||||
|
onChange={(e) => handleSelectRoom(e)}
|
||||||
|
>
|
||||||
|
<option value="" disabled>-- Sélectionner une salle --</option>
|
||||||
{rooms.map((room) => (
|
{rooms.map((room) => (
|
||||||
<option key={room._id} value={room._id}>
|
<option key={room._id} value={room._id}>
|
||||||
{room.title}
|
{room.title}
|
||||||
|
|
@ -440,15 +436,17 @@ const Dashboard: React.FC = () => {
|
||||||
))}
|
))}
|
||||||
<option value="add-room">Ajouter salle</option>
|
<option value="add-room">Ajouter salle</option>
|
||||||
</select>
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedRoom && (
|
{selectedRoom && (
|
||||||
<div className="roomTitle">
|
<div className="mb-4">
|
||||||
<h2>Salle sélectionnée: {selectedRoom.title}</h2>
|
<h2>Salle sélectionnée: {selectedRoom.title}</h2>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Dialogs */}
|
||||||
<Dialog open={openAddRoomDialog} onClose={() => setOpenAddRoomDialog(false)}>
|
<Dialog open={openAddRoomDialog} onClose={() => setOpenAddRoomDialog(false)}>
|
||||||
<DialogTitle>Créer une nouvelle salle</DialogTitle>
|
<DialogTitle>Créer une nouvelle salle</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
|
@ -463,6 +461,7 @@ const Dashboard: React.FC = () => {
|
||||||
<Button onClick={handleCreateRoom}>Créer</Button>
|
<Button onClick={handleCreateRoom}>Créer</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog open={showErrorDialog} onClose={() => setShowErrorDialog(false)}>
|
<Dialog open={showErrorDialog} onClose={() => setShowErrorDialog(false)}>
|
||||||
<DialogTitle>Erreur</DialogTitle>
|
<DialogTitle>Erreur</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
|
@ -473,12 +472,15 @@ const Dashboard: React.FC = () => {
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<div className="search-bar">
|
{/* Search Bar */}
|
||||||
|
<div className="row mb-4">
|
||||||
|
<div className="col-12">
|
||||||
<TextField
|
<TextField
|
||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
placeholder="Rechercher un quiz par son titre"
|
placeholder="Rechercher un quiz par son titre"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
className="w-100"
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
|
|
@ -490,167 +492,158 @@ const Dashboard: React.FC = () => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="folder">
|
{/* Folder Selection and Actions */}
|
||||||
<div className="select">
|
<div className="row mb-4 align-items-center">
|
||||||
|
<div className="col-md-8 col-lg-9 mb-2 mb-md-0">
|
||||||
<NativeSelect
|
<NativeSelect
|
||||||
id="select-folder"
|
id="select-folder"
|
||||||
color="primary"
|
color="primary"
|
||||||
value={selectedFolderId}
|
value={selectedFolderId}
|
||||||
onChange={handleSelectFolder}
|
onChange={handleSelectFolder}
|
||||||
|
className="w-100"
|
||||||
|
fullWidth
|
||||||
>
|
>
|
||||||
<option value=""> Tous les dossiers... </option>
|
<option value="">Tous les dossiers...</option>
|
||||||
|
|
||||||
{folders.map((folder: FolderType) => (
|
{folders.map((folder: FolderType) => (
|
||||||
<option value={folder._id} key={folder._id}>
|
<option value={folder._id} key={folder._id}>
|
||||||
{' '}
|
{folder.title}
|
||||||
{folder.title}{' '}
|
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</NativeSelect>
|
</NativeSelect>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-md-4 col-lg-3">
|
||||||
<div className="actions">
|
<div className="d-flex justify-content-end gap-2">
|
||||||
<Tooltip title="Ajouter dossier" placement="top">
|
<Tooltip title="Ajouter dossier">
|
||||||
<IconButton color="primary" onClick={handleCreateFolder}>
|
<IconButton color="primary" onClick={handleCreateFolder} className="border">
|
||||||
{' '}
|
<Add />
|
||||||
<Add />{' '}
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip title="Renommer dossier">
|
||||||
<Tooltip title="Renommer dossier" placement="top">
|
|
||||||
<div>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={handleRenameFolder}
|
onClick={handleRenameFolder}
|
||||||
disabled={selectedFolderId == ''} // cannot action on all
|
disabled={selectedFolderId === ''}
|
||||||
|
className="border"
|
||||||
>
|
>
|
||||||
{' '}
|
<Edit />
|
||||||
<Edit />{' '}
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip title="Dupliquer dossier">
|
||||||
<Tooltip title="Dupliquer dossier" placement="top">
|
|
||||||
<div>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={handleDuplicateFolder}
|
onClick={handleDuplicateFolder}
|
||||||
disabled={selectedFolderId == ''} // cannot action on all
|
disabled={selectedFolderId === ''}
|
||||||
|
className="border"
|
||||||
>
|
>
|
||||||
{' '}
|
<FolderCopy />
|
||||||
<FolderCopy />{' '}
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip title="Supprimer dossier">
|
||||||
<Tooltip title="Supprimer dossier" placement="top">
|
|
||||||
<div>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="delete"
|
color="error"
|
||||||
color="primary"
|
|
||||||
onClick={handleDeleteFolder}
|
onClick={handleDeleteFolder}
|
||||||
disabled={selectedFolderId == ''} // cannot action on all
|
disabled={selectedFolderId === ''}
|
||||||
|
className="border"
|
||||||
>
|
>
|
||||||
{' '}
|
<DeleteOutline />
|
||||||
<DeleteOutline />{' '}
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="ajouter">
|
{/* Add Quiz and Import Buttons */}
|
||||||
|
<div className="row mb-4 g-2">
|
||||||
|
<div className="col-md-10">
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
startIcon={<Add />}
|
startIcon={<Add />}
|
||||||
onClick={handleCreateQuiz}
|
onClick={handleCreateQuiz}
|
||||||
|
className="w-100 py-2"
|
||||||
>
|
>
|
||||||
Ajouter un nouveau quiz
|
Ajouter un nouveau quiz
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
startIcon={<Upload />}
|
startIcon={<Upload />}
|
||||||
onClick={handleOnImport}
|
onClick={handleOnImport}
|
||||||
|
className="w-100 py-2"
|
||||||
>
|
>
|
||||||
Import
|
Importer
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="list">
|
</div>
|
||||||
|
|
||||||
|
{/* Quiz List */}
|
||||||
|
<div className="row">
|
||||||
{Object.keys(quizzesByFolder).map((folderName) => (
|
{Object.keys(quizzesByFolder).map((folderName) => (
|
||||||
<CustomCard key={folderName} className="folder-card">
|
<div className="col-12 mb-4" key={folderName}>
|
||||||
|
<CustomCard>
|
||||||
<div className="folder-tab">{folderName}</div>
|
<div className="folder-tab">{folderName}</div>
|
||||||
<CardContent>
|
<CardContent className="p-3">
|
||||||
{quizzesByFolder[folderName].map((quiz: QuizType) => (
|
{quizzesByFolder[folderName].map((quiz: QuizType) => (
|
||||||
<div className="quiz" key={quiz._id}>
|
<div className="d-flex align-items-center mb-3 p-2 rounded" key={quiz._id}>
|
||||||
<div className="title">
|
<div className="flex-grow-1 me-3 text-truncate">
|
||||||
<Tooltip title="Lancer quiz" placement="top">
|
|
||||||
<div>
|
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={() => handleLancerQuiz(quiz)}
|
onClick={() => handleLancerQuiz(quiz)}
|
||||||
disabled={!validateQuiz(quiz.content)}
|
disabled={!validateQuiz(quiz.content)}
|
||||||
|
className="w-100 text-truncate text-start py-2"
|
||||||
>
|
>
|
||||||
{`${quiz.title} (${quiz.content.length} question${
|
{`${quiz.title} (${quiz.content.length} question${quiz.content.length > 1 ? 's' : ''
|
||||||
quiz.content.length > 1 ? 's' : ''
|
|
||||||
})`}
|
})`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
<div className="d-flex gap-1">
|
||||||
</div>
|
<Tooltip title="Télécharger quiz">
|
||||||
|
|
||||||
<div className="actions">
|
|
||||||
<Tooltip title="Télécharger quiz" placement="top">
|
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => downloadTxtFile(quiz)}
|
onClick={() => downloadTxtFile(quiz)}
|
||||||
|
className="border"
|
||||||
>
|
>
|
||||||
{' '}
|
<FileDownload />
|
||||||
<FileDownload />{' '}
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip title="Modifier quiz">
|
||||||
<Tooltip title="Modifier quiz" placement="top">
|
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => handleEditQuiz(quiz)}
|
onClick={() => handleEditQuiz(quiz)}
|
||||||
|
className="border"
|
||||||
>
|
>
|
||||||
{' '}
|
<Edit />
|
||||||
<Edit />{' '}
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip title="Dupliquer quiz">
|
||||||
<Tooltip title="Dupliquer quiz" placement="top">
|
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => handleDuplicateQuiz(quiz)}
|
onClick={() => handleDuplicateQuiz(quiz)}
|
||||||
|
className="border"
|
||||||
>
|
>
|
||||||
{' '}
|
<ContentCopy />
|
||||||
<ContentCopy />{' '}
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip title="Supprimer quiz">
|
||||||
<Tooltip title="Supprimer quiz" placement="top">
|
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="delete"
|
color="error"
|
||||||
color="primary"
|
|
||||||
onClick={() => handleRemoveQuiz(quiz)}
|
onClick={() => handleRemoveQuiz(quiz)}
|
||||||
|
className="border"
|
||||||
>
|
>
|
||||||
{' '}
|
<DeleteOutline />
|
||||||
<DeleteOutline />{' '}
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip title="Partager quiz">
|
||||||
<Tooltip title="Partager quiz" placement="top">
|
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => handleShareQuiz(quiz)}
|
onClick={() => handleShareQuiz(quiz)}
|
||||||
|
className="border"
|
||||||
>
|
>
|
||||||
{' '}
|
<Share />
|
||||||
<Share />{' '}
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -658,8 +651,10 @@ const Dashboard: React.FC = () => {
|
||||||
))}
|
))}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</CustomCard>
|
</CustomCard>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ImportModal
|
<ImportModal
|
||||||
open={showImportModal}
|
open={showImportModal}
|
||||||
handleOnClose={() => setShowImportModal(false)}
|
handleOnClose={() => setShowImportModal(false)}
|
||||||
|
|
@ -670,11 +665,13 @@ const Dashboard: React.FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Dashboard;
|
// Helper function
|
||||||
function addFolderTitleToQuizzes(folderQuizzes: string | QuizType[], folderName: string) {
|
function addFolderTitleToQuizzes(folderQuizzes: string | QuizType[], folderName: string) {
|
||||||
if (Array.isArray(folderQuizzes))
|
if (Array.isArray(folderQuizzes)) {
|
||||||
folderQuizzes.forEach((quiz) => {
|
folderQuizzes.forEach((quiz) => {
|
||||||
quiz.folderName = folderName;
|
quiz.folderName = folderName;
|
||||||
console.log(`quiz: ${quiz.title} folder: ${quiz.folderName}`);
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
.dashboard {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-width: 100%;
|
|
||||||
gap: 30px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard .folder {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard .folder .select {
|
|
||||||
flex-grow: 8;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Select the selector to make it 100% width */
|
|
||||||
div:has(> #select-folder) {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard .folder .actions {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard .ajouter {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard .ajouter button:first-child {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard .list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard .list .quiz {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
margin-bottom: 10px;
|
|
||||||
box-sizing: content-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard .list .quiz .title {
|
|
||||||
flex-grow: 8;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
/* reset title css */
|
|
||||||
font-size: large;
|
|
||||||
margin: 0;
|
|
||||||
font-weight: 100;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.dashboard .list .quiz .title button {
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
display: block;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard .list .quiz .actions {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard .list .quiz .actions {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.folder-card {
|
|
||||||
position: relative;
|
|
||||||
/* margin: 40px 0 20px 0; /* Add top margin to make space for the tab */
|
|
||||||
border-radius: 8px;
|
|
||||||
color: #f9f9f9;
|
|
||||||
--outline-color: #e1e1e1;
|
|
||||||
border: 2px solid var(--outline-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.folder-tab {
|
|
||||||
position: absolute;
|
|
||||||
top: -33px;
|
|
||||||
left: 9px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 8px 8px 0 0;
|
|
||||||
font-weight: bold;
|
|
||||||
white-space: nowrap; /* Prevent text from wrapping */
|
|
||||||
display: inline-block; /* Ensure the tab width is based on content */
|
|
||||||
border: 2px solid var(--outline-color);
|
|
||||||
border-bottom-style: none;
|
|
||||||
background-color: white; /* Optional: background color to match the card */
|
|
||||||
color: #3f51b5; /* Text color to match the outline */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* .folder-card:nth-child(odd) {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.folder-card:nth-child(even) {
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
} */
|
|
||||||
|
|
@ -1,22 +1,17 @@
|
||||||
// EditorQuiz.tsx
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import React, { useState, useEffect, useRef, CSSProperties } from 'react';
|
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { FolderType } from '../../../Types/FolderType';
|
import { FolderType } from '../../../Types/FolderType';
|
||||||
|
|
||||||
import Editor from 'src/components/Editor/Editor';
|
import Editor from 'src/components/Editor/Editor';
|
||||||
import GiftCheatSheet from 'src/components/GIFTCheatSheet/GiftCheatSheet';
|
import GiftCheatSheet from 'src/components/GIFTCheatSheet/GiftCheatSheet';
|
||||||
import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview';
|
import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview';
|
||||||
|
|
||||||
import { QuizType } from '../../../Types/QuizType';
|
import { QuizType } from '../../../Types/QuizType';
|
||||||
|
|
||||||
import './editorQuiz.css';
|
|
||||||
import { Button, TextField, NativeSelect, Divider, Dialog, DialogTitle, DialogActions, DialogContent } from '@mui/material';
|
import { Button, TextField, NativeSelect, Divider, Dialog, DialogTitle, DialogActions, DialogContent } from '@mui/material';
|
||||||
import ReturnButton from 'src/components/ReturnButton/ReturnButton';
|
import ReturnButton from 'src/components/ReturnButton/ReturnButton';
|
||||||
|
|
||||||
import ApiService from '../../../services/ApiService';
|
import ApiService from '../../../services/ApiService';
|
||||||
import { escapeForGIFT } from '../../../utils/giftUtils';
|
import { escapeForGIFT } from '../../../utils/giftUtils';
|
||||||
import { Upload } from '@mui/icons-material';
|
import { Upload } from '@mui/icons-material';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
import SaveIcon from '@mui/icons-material/Save';
|
||||||
|
|
||||||
interface EditQuizParams {
|
interface EditQuizParams {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -27,7 +22,6 @@ const QuizForm: React.FC = () => {
|
||||||
const [quizTitle, setQuizTitle] = useState('');
|
const [quizTitle, setQuizTitle] = useState('');
|
||||||
const [selectedFolder, setSelectedFolder] = useState<string>('');
|
const [selectedFolder, setSelectedFolder] = useState<string>('');
|
||||||
const [filteredValue, setFilteredValue] = useState<string[]>([]);
|
const [filteredValue, setFilteredValue] = useState<string[]>([]);
|
||||||
|
|
||||||
const { id } = useParams<EditQuizParams>();
|
const { id } = useParams<EditQuizParams>();
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
const [isNewQuiz, setNewQuiz] = useState(false);
|
const [isNewQuiz, setNewQuiz] = useState(false);
|
||||||
|
|
@ -35,9 +29,6 @@ const QuizForm: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [folders, setFolders] = useState<FolderType[]>([]);
|
const [folders, setFolders] = useState<FolderType[]>([]);
|
||||||
const [imageLinks, setImageLinks] = useState<string[]>([]);
|
const [imageLinks, setImageLinks] = useState<string[]>([]);
|
||||||
const handleSelectFolder = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
|
||||||
setSelectedFolder(event.target.value);
|
|
||||||
};
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||||
|
|
@ -48,25 +39,15 @@ const QuizForm: React.FC = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (window.scrollY > 300) {
|
setShowScrollButton(window.scrollY > 300);
|
||||||
setShowScrollButton(true);
|
|
||||||
} else {
|
|
||||||
setShowScrollButton(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('scroll', handleScroll);
|
window.addEventListener('scroll', handleScroll);
|
||||||
return () => {
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
window.removeEventListener('scroll', handleScroll);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const scrollToImagesSection = (event: { preventDefault: () => void; }) => {
|
const scrollToImagesSection = (event: { preventDefault: () => void; }) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const section = document.getElementById('images-section');
|
document.getElementById('images-section')?.scrollIntoView({ behavior: 'smooth' });
|
||||||
if (section) {
|
|
||||||
section.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -74,7 +55,6 @@ const QuizForm: React.FC = () => {
|
||||||
const userFolders = await ApiService.getUserFolders();
|
const userFolders = await ApiService.getUserFolders();
|
||||||
setFolders(userFolders as FolderType[]);
|
setFolders(userFolders as FolderType[]);
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -87,116 +67,83 @@ const QuizForm: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const quiz = await ApiService.getQuiz(id) as QuizType;
|
const quiz = await ApiService.getQuiz(id) as QuizType;
|
||||||
|
|
||||||
if (!quiz) {
|
if (!quiz) {
|
||||||
window.alert(`Une erreur est survenue.\n Le quiz ${id} n'a pas été trouvé\nVeuillez réessayer plus tard`)
|
window.alert(`Une erreur est survenue.\n Le quiz ${id} n'a pas été trouvé\nVeuillez réessayer plus tard`);
|
||||||
console.error('Quiz not found for id:', id);
|
|
||||||
navigate('/teacher/dashboard');
|
navigate('/teacher/dashboard');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setQuiz(quiz as QuizType);
|
setQuiz(quiz);
|
||||||
const { title, content, folderId } = quiz;
|
setQuizTitle(quiz.title);
|
||||||
|
setSelectedFolder(quiz.folderId);
|
||||||
setQuizTitle(title);
|
setFilteredValue(quiz.content);
|
||||||
setSelectedFolder(folderId);
|
|
||||||
setFilteredValue(content);
|
|
||||||
setValue(quiz.content.join('\n\n'));
|
setValue(quiz.content.join('\n\n'));
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
|
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`);
|
||||||
console.error('Error fetching quiz:', error);
|
|
||||||
navigate('/teacher/dashboard');
|
navigate('/teacher/dashboard');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [id]);
|
}, [id, navigate]);
|
||||||
|
|
||||||
function handleUpdatePreview(value: string) {
|
function handleUpdatePreview(value: string) {
|
||||||
if (value !== '') {
|
if (value !== '') {
|
||||||
setValue(value);
|
setValue(value);
|
||||||
}
|
|
||||||
|
|
||||||
// split value when there is at least one blank line
|
|
||||||
const linesArray = value.split(/\n{2,}/);
|
const linesArray = value.split(/\n{2,}/);
|
||||||
|
|
||||||
// if the first item in linesArray is blank, remove it
|
|
||||||
if (linesArray[0] === '') linesArray.shift();
|
if (linesArray[0] === '') linesArray.shift();
|
||||||
|
|
||||||
if (linesArray[linesArray.length - 1] === '') linesArray.pop();
|
if (linesArray[linesArray.length - 1] === '') linesArray.pop();
|
||||||
|
|
||||||
setFilteredValue(linesArray);
|
setFilteredValue(linesArray);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleQuizTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleQuizTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setQuizTitle(event.target.value);
|
setQuizTitle(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSelectFolder = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
setSelectedFolder(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
const handleQuizSave = async () => {
|
const handleQuizSave = async () => {
|
||||||
try {
|
try {
|
||||||
// check if everything is there
|
if (quizTitle === '') {
|
||||||
if (quizTitle == '') {
|
|
||||||
alert("Veuillez choisir un titre");
|
alert("Veuillez choisir un titre");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (selectedFolder === '') {
|
||||||
if (selectedFolder == '') {
|
|
||||||
alert("Veuillez choisir un dossier");
|
alert("Veuillez choisir un dossier");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNewQuiz) {
|
if (isNewQuiz) {
|
||||||
await ApiService.createQuiz(quizTitle, filteredValue, selectedFolder);
|
await ApiService.createQuiz(quizTitle, filteredValue, selectedFolder);
|
||||||
} else {
|
} else if (quiz) {
|
||||||
if (quiz) {
|
|
||||||
await ApiService.updateQuiz(quiz._id, quizTitle, filteredValue);
|
await ApiService.updateQuiz(quiz._id, quizTitle, filteredValue);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
navigate('/teacher/dashboard');
|
navigate('/teacher/dashboard');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
|
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`);
|
||||||
console.log(error)
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// I do not know what this does but do not remove
|
|
||||||
if (!isNewQuiz && !quiz) {
|
|
||||||
return <div>Chargement...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSaveImage = async () => {
|
const handleSaveImage = async () => {
|
||||||
try {
|
try {
|
||||||
const inputElement = document.getElementById('file-input') as HTMLInputElement;
|
const inputElement = document.getElementById('file-input') as HTMLInputElement;
|
||||||
|
|
||||||
if (!inputElement?.files || inputElement.files.length === 0) {
|
if (!inputElement?.files || inputElement.files.length === 0) {
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inputElement.files || inputElement.files.length === 0) {
|
|
||||||
window.alert("Veuillez d'abord choisir une image à téléverser.")
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageUrl = await ApiService.uploadImage(inputElement.files[0]);
|
const imageUrl = await ApiService.uploadImage(inputElement.files[0]);
|
||||||
|
if (imageUrl.indexOf("ERROR") >= 0) {
|
||||||
// Check for errors
|
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`);
|
||||||
if(imageUrl.indexOf("ERROR") >= 0) {
|
|
||||||
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setImageLinks(prevLinks => [...prevLinks, imageUrl]);
|
setImageLinks(prevLinks => [...prevLinks, imageUrl]);
|
||||||
|
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||||
// Reset the file input element
|
|
||||||
if (fileInputRef.current) {
|
|
||||||
fileInputRef.current.value = '';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.alert(`Une erreur est survenue.\n${error}\nVeuillez réessayer plus tard.`)
|
window.alert(`Une erreur est survenue.\n${error}\nVeuillez réessayer plus tard.`);
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -204,22 +151,27 @@ const QuizForm: React.FC = () => {
|
||||||
navigator.clipboard.writeText(link);
|
navigator.clipboard.writeText(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (!isNewQuiz && !quiz) {
|
||||||
<div className='quizEditor'>
|
return <div>Chargement...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
<div className='editHeader'>
|
return (
|
||||||
|
<div className="container-fluid p-4">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div className="w-25">
|
||||||
<ReturnButton
|
<ReturnButton
|
||||||
askConfirm
|
askConfirm
|
||||||
message={`Êtes-vous sûr de vouloir quitter l'éditeur sans sauvegarder le questionnaire?`}
|
message="Êtes-vous sûr de vouloir quitter l'éditeur sans sauvegarder le questionnaire?"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<div className='title'>Éditeur de quiz</div>
|
<h1 className="text-center flex-grow-1">Éditeur de quiz</h1>
|
||||||
|
<div className="w-25"></div> {/* Spacer for balance */}
|
||||||
<div className='dumb'></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <h2 className="subtitle">Éditeur</h2> */}
|
{/* Quiz Info */}
|
||||||
|
<div className="row mb-4">
|
||||||
|
<div className="col-md-8 mb-3 mb-md-0">
|
||||||
<TextField
|
<TextField
|
||||||
onChange={handleQuizTitleChange}
|
onChange={handleQuizTitleChange}
|
||||||
value={quizTitle}
|
value={quizTitle}
|
||||||
|
|
@ -227,55 +179,65 @@ const QuizForm: React.FC = () => {
|
||||||
label="Titre du quiz"
|
label="Titre du quiz"
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<label>Choisir un dossier:
|
</div>
|
||||||
|
<div className="row mb-4">
|
||||||
|
<div className="col-md-4 d-flex align-items-center">
|
||||||
|
<label className="me-2">Choisir un dossier:</label>
|
||||||
<NativeSelect
|
<NativeSelect
|
||||||
id="select-folder"
|
id="select-folder"
|
||||||
color="primary"
|
color="primary"
|
||||||
value={selectedFolder}
|
value={selectedFolder}
|
||||||
onChange={handleSelectFolder}
|
onChange={handleSelectFolder}
|
||||||
disabled={!isNewQuiz}
|
disabled={!isNewQuiz}
|
||||||
style={{ marginBottom: '16px' }} // Ajout de marge en bas
|
className="flex-grow-1"
|
||||||
>
|
>
|
||||||
<option disabled value=""> Choisir un dossier... </option>
|
<option disabled value="">Choisir un dossier...</option>
|
||||||
|
|
||||||
{folders.map((folder: FolderType) => (
|
{folders.map((folder: FolderType) => (
|
||||||
<option value={folder._id} key={folder._id}> {folder.title} </option>
|
<option value={folder._id} key={folder._id}>{folder.title}</option>
|
||||||
))}
|
))}
|
||||||
</NativeSelect></label>
|
</NativeSelect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button variant="contained" onClick={handleQuizSave}>
|
<Button variant="contained" onClick={handleQuizSave} className="mb-4" startIcon={<SaveIcon />} >
|
||||||
Enregistrer
|
Enregistrer
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Divider style={{ margin: '16px 0' }} />
|
<Divider className="my-4" />
|
||||||
|
|
||||||
<div className='editSection'>
|
{/* Editor Section */}
|
||||||
|
<div className="row g-4">
|
||||||
<div className='edit'>
|
{/* Editor Column */}
|
||||||
|
<div className="col-lg-6 d-flex flex-column" style={{ height: '78vh', overflow: 'auto' }}>
|
||||||
<Editor
|
<Editor
|
||||||
label="Contenu GIFT du quiz:"
|
label="Contenu GIFT du quiz:"
|
||||||
initialValue={value}
|
initialValue={value}
|
||||||
onEditorChange={handleUpdatePreview} />
|
onEditorChange={handleUpdatePreview}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className='images'>
|
{/* Images Section */}
|
||||||
<div className='upload'>
|
<div className="mt-4 p-3 border rounded">
|
||||||
<label className="dropArea">
|
<div className="d-flex flex-column align-items-center mb-3">
|
||||||
<input type="file" id="file-input" className="file-input"
|
<input
|
||||||
|
type="file"
|
||||||
|
id="file-input"
|
||||||
|
className="d-none"
|
||||||
accept="image/jpeg, image/png"
|
accept="image/jpeg, image/png"
|
||||||
multiple
|
multiple
|
||||||
ref={fileInputRef} />
|
ref={fileInputRef}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
aria-label='Téléverser'
|
aria-label='Téléverser'
|
||||||
onClick={handleSaveImage}>
|
onClick={handleSaveImage}
|
||||||
Téléverser <Upload />
|
startIcon={<Upload />}
|
||||||
|
>
|
||||||
|
Téléverser
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</label>
|
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)}>
|
||||||
<Dialog
|
|
||||||
open={dialogOpen}
|
|
||||||
onClose={() => setDialogOpen(false)} >
|
|
||||||
<DialogTitle>Erreur</DialogTitle>
|
<DialogTitle>Erreur</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
Veuillez d'abord choisir une image à téléverser.
|
Veuillez d'abord choisir une image à téléverser.
|
||||||
|
|
@ -286,27 +248,28 @@ const QuizForm: React.FC = () => {
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4>Mes images :</h4>
|
<h4>Mes images :</h4>
|
||||||
<div>
|
<div className="mb-3">
|
||||||
<div>
|
<div className="mb-2">
|
||||||
<div style={{ display: "inline" }}>(Voir section </div>
|
(Voir section{' '}
|
||||||
<a href="#images-section"style={{ textDecoration: "none" }} onClick={scrollToImagesSection}>
|
<a href="#images-section" style={{ textDecoration: "none" }} onClick={scrollToImagesSection}>
|
||||||
<u><em><h4 style={{ display: "inline" }}> 9. Images </h4></em></u>
|
<u><em>9. Images</em></u>
|
||||||
</a>
|
</a>{' '}
|
||||||
<div style={{ display: "inline" }}> ci-dessous</div>
|
ci-dessous)
|
||||||
<div style={{ display: "inline" }}>)</div>
|
|
||||||
<br />
|
<br />
|
||||||
<em> - Cliquez sur un lien pour le copier</em>
|
<em> - Cliquez sur un lien pour le copier</em>
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul className="list-unstyled">
|
||||||
{imageLinks.map((link, index) => {
|
{imageLinks.map((link, index) => {
|
||||||
const imgTag = `} "texte de l'infobulle")`;
|
const imgTag = `} "texte de l'infobulle")`;
|
||||||
return (
|
return (
|
||||||
<li key={index}>
|
<li key={index} className="mb-2">
|
||||||
<code
|
<code
|
||||||
onClick={() => handleCopyToClipboard(imgTag)}>
|
onClick={() => handleCopyToClipboard(imgTag)}
|
||||||
|
className="p-1 bg-light rounded"
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
{imgTag}
|
{imgTag}
|
||||||
</code>
|
</code>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -317,26 +280,31 @@ const QuizForm: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GiftCheatSheet />
|
<GiftCheatSheet />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='preview'>
|
{/* Preview Column */}
|
||||||
<div className="preview-column">
|
<div className="col-lg-6" style={{ height: '78vh', overflow: 'auto' }}>
|
||||||
|
<div className="p-3">
|
||||||
<h4>Prévisualisation</h4>
|
<h4>Prévisualisation</h4>
|
||||||
<div>
|
|
||||||
<GIFTTemplatePreview questions={filteredValue} />
|
<GIFTTemplatePreview questions={filteredValue} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
{/* Scroll to Top Button */}
|
||||||
|
|
||||||
{showScrollButton && (
|
{showScrollButton && (
|
||||||
<Button
|
<Button
|
||||||
onClick={scrollToTop}
|
onClick={scrollToTop}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
style={scrollToTopButtonStyle}
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: '40px',
|
||||||
|
right: '50px',
|
||||||
|
padding: '10px',
|
||||||
|
fontSize: '16px',
|
||||||
|
zIndex: 1000,
|
||||||
|
}}
|
||||||
title="Scroll to top"
|
title="Scroll to top"
|
||||||
>
|
>
|
||||||
↑
|
↑
|
||||||
|
|
@ -346,17 +314,4 @@ const QuizForm: React.FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollToTopButtonStyle: CSSProperties = {
|
|
||||||
position: 'fixed',
|
|
||||||
bottom: '40px',
|
|
||||||
right: '50px',
|
|
||||||
padding: '10px',
|
|
||||||
fontSize: '16px',
|
|
||||||
color: 'white',
|
|
||||||
backgroundColor: '#5271ff',
|
|
||||||
border: 'none',
|
|
||||||
cursor: 'pointer',
|
|
||||||
zIndex: 1000,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QuizForm;
|
export default QuizForm;
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
.quizEditor .editHeader {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-content: stretch
|
|
||||||
}
|
|
||||||
.quizEditor .editHeader .returnButton {
|
|
||||||
flex-basis: 10%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quizEditor .editHeader .title {
|
|
||||||
flex-basis: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quizEditor .editHeader .dumb {
|
|
||||||
flex-basis: 10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quizEditor .editSection {
|
|
||||||
width: 100%;
|
|
||||||
height: 78vh;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quizEditor .editSection .edit {
|
|
||||||
flex: 50%;
|
|
||||||
padding: 5px;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 5px;
|
|
||||||
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
.quizEditor .editSection .edit code {
|
|
||||||
cursor: pointer;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quizEditor .editSection .edit .upload {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: right;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.upload .dropArea {
|
|
||||||
flex-direction: column;
|
|
||||||
/* align-items: stretch; */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="file"] {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quizEditor .editSection .edit .upload .dropArea {
|
|
||||||
display: flex;
|
|
||||||
border: 1px dotted;
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
/* justifyContent: center;
|
|
||||||
alignItems: center;
|
|
||||||
backgroundColor: dragIsOver ? "lightgray" : "white; */
|
|
||||||
}
|
|
||||||
|
|
||||||
.quizEditor .editSection .preview {
|
|
||||||
flex: 50%;
|
|
||||||
padding: 5px;
|
|
||||||
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
.login-links {
|
|
||||||
padding-top: 10px;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-links a {
|
|
||||||
padding: 4px;
|
|
||||||
color: #333;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-links a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +1,36 @@
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { TextField, Button, CircularProgress } from '@mui/material';
|
||||||
import './Login.css';
|
import LoginContainer from 'src/components/LoginContainer/LoginContainer';
|
||||||
import { TextField } from '@mui/material';
|
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
|
||||||
|
|
||||||
import LoginContainer from 'src/components/LoginContainer/LoginContainer'
|
|
||||||
import ApiService from '../../../services/ApiService';
|
import ApiService from '../../../services/ApiService';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
import LoginIcon from '@mui/icons-material/Login';
|
||||||
|
|
||||||
const Login: React.FC = () => {
|
const Login: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
|
|
||||||
const [connectionError, setConnectionError] = useState<string>('');
|
const [connectionError, setConnectionError] = useState<string>('');
|
||||||
const [isConnecting] = useState<boolean>(false);
|
const [isConnecting, setIsConnecting] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
// Cleanup if needed
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
|
setIsConnecting(true);
|
||||||
|
try {
|
||||||
const result = await ApiService.login(email, password);
|
const result = await ApiService.login(email, password);
|
||||||
|
|
||||||
if (typeof result === "string") {
|
if (typeof result === "string") {
|
||||||
setConnectionError(result);
|
setConnectionError(result);
|
||||||
return;
|
} else {
|
||||||
|
navigate("/teacher/Dashboard");
|
||||||
}
|
}
|
||||||
else {
|
} finally {
|
||||||
navigate("/teacher/Dashboard")
|
setIsConnecting(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReturnKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleReturnKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
|
@ -43,55 +40,58 @@ const Login: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginContainer
|
<LoginContainer title='Login' error={connectionError}>
|
||||||
title='Login'
|
{/* Email Field */}
|
||||||
error={connectionError}>
|
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Email"
|
label="Email"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
className="mb-3 w-100"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder="Adresse courriel"
|
placeholder="Adresse courriel"
|
||||||
sx={{ marginBottom: '1rem' }}
|
fullWidth
|
||||||
fullWidth={true}
|
onKeyDown={handleReturnKey}
|
||||||
onKeyDown={handleReturnKey} // Add this line as well
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Password Field */}
|
||||||
<TextField
|
<TextField
|
||||||
label="Mot de passe"
|
label="Mot de passe"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
type="password"
|
type="password"
|
||||||
|
className="mb-3 w-100"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="Mot de passe"
|
placeholder="Mot de passe"
|
||||||
sx={{ marginBottom: '1rem' }}
|
fullWidth
|
||||||
fullWidth={true}
|
onKeyDown={handleReturnKey}
|
||||||
onKeyDown={handleReturnKey} // Add this line as well
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LoadingButton
|
{/* Login Button */}
|
||||||
loading={isConnecting}
|
<Button
|
||||||
onClick={login}
|
|
||||||
variant="contained"
|
variant="contained"
|
||||||
sx={{ marginBottom: `${connectionError && '2rem'}` }}
|
className={`w-100 mb-${connectionError ? '4' : '3'}`}
|
||||||
disabled={!email || !password}
|
onClick={login}
|
||||||
|
disabled={!email || !password || isConnecting}
|
||||||
|
startIcon={isConnecting ? <CircularProgress size={20} /> : <LoginIcon/>}
|
||||||
>
|
>
|
||||||
Login
|
Login
|
||||||
</LoadingButton>
|
</Button>
|
||||||
|
|
||||||
<div className="login-links">
|
{/* Links Section */}
|
||||||
|
<div className="d-flex flex-column align-items-center pt-3">
|
||||||
<Link to="/teacher/resetPassword">
|
<Link
|
||||||
|
to="/teacher/resetPassword"
|
||||||
|
className="mb-2 text-decoration-none text-primary"
|
||||||
|
>
|
||||||
Réinitialiser le mot de passe
|
Réinitialiser le mot de passe
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
<Link to="/teacher/register">
|
to="/teacher/register"
|
||||||
|
className="text-decoration-none text-primary"
|
||||||
|
>
|
||||||
Créer un compte
|
Créer un compte
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</LoginContainer>
|
</LoginContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import webSocketService, {
|
||||||
} from '../../../services/WebsocketService';
|
} from '../../../services/WebsocketService';
|
||||||
import { QuizType } from '../../../Types/QuizType';
|
import { QuizType } from '../../../Types/QuizType';
|
||||||
import GroupIcon from '@mui/icons-material/Group';
|
import GroupIcon from '@mui/icons-material/Group';
|
||||||
import './manageRoom.css';
|
|
||||||
import { ENV_VARIABLES } from 'src/constants';
|
import { ENV_VARIABLES } from 'src/constants';
|
||||||
import { StudentType, Answer } from '../../../Types/StudentType';
|
import { StudentType, Answer } from '../../../Types/StudentType';
|
||||||
import LoadingCircle from 'src/components/LoadingCircle/LoadingCircle';
|
import LoadingCircle from 'src/components/LoadingCircle/LoadingCircle';
|
||||||
|
|
@ -20,6 +19,7 @@ import ApiService from '../../../services/ApiService';
|
||||||
import { QuestionType } from 'src/Types/QuestionType';
|
import { QuestionType } from 'src/Types/QuestionType';
|
||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
import { checkIfIsCorrect } from './useRooms';
|
import { checkIfIsCorrect } from './useRooms';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css'; // Add Bootstrap CSS import
|
||||||
|
|
||||||
const ManageRoom: React.FC = () => {
|
const ManageRoom: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -343,13 +343,13 @@ const ManageRoom: React.FC = () => {
|
||||||
|
|
||||||
if (!formattedRoomName) {
|
if (!formattedRoomName) {
|
||||||
return (
|
return (
|
||||||
<div className="center">
|
<div className="d-flex flex-column justify-content-center align-items-center vh-100">
|
||||||
{!connectingError ? (
|
{!connectingError ? (
|
||||||
<LoadingCircle text="Veuillez attendre la connexion au serveur..." />
|
<LoadingCircle text="Veuillez attendre la connexion au serveur..." />
|
||||||
) : (
|
) : (
|
||||||
<div className="center-v-align">
|
<div className="d-flex flex-column align-items-center gap-3">
|
||||||
<Error sx={{ padding: 0 }} />
|
<Error sx={{ padding: 0 }} />
|
||||||
<div className="text-base">{connectingError}</div>
|
<div className="text-center">{connectingError}</div>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
startIcon={<Refresh />}
|
startIcon={<Refresh />}
|
||||||
|
|
@ -364,68 +364,39 @@ const ManageRoom: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="room">
|
<div className="container-fluid p-3">
|
||||||
<h1>Salle : {formattedRoomName}</h1>
|
<h1 className="text-center mb-4">Salle : {formattedRoomName}</h1>
|
||||||
<div className="roomHeader">
|
|
||||||
|
{/* Room Header */}
|
||||||
|
<div className="d-flex justify-content-between align-items-center mb-4">
|
||||||
<DisconnectButton
|
<DisconnectButton
|
||||||
onReturn={handleReturn}
|
onReturn={handleReturn}
|
||||||
askConfirm
|
askConfirm
|
||||||
message={`Êtes-vous sûr de vouloir quitter?`}
|
message={`Êtes-vous sûr de vouloir quitter?`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div className="d-flex align-items-center ms-auto">
|
||||||
className="headerContent"
|
<GroupIcon className="me-2" />
|
||||||
style={{
|
<span className="text-muted">{students.length}/60</span>
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
width: '100%'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{(
|
|
||||||
<div
|
|
||||||
className="userCount subtitle smallText"
|
|
||||||
style={{ display: "flex", justifyContent: "flex-end" }}
|
|
||||||
>
|
|
||||||
<GroupIcon style={{ marginRight: '5px' }} />
|
|
||||||
{students.length}/60
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="dumb"></div>
|
{/* Main Content */}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* the following breaks the css (if 'room' classes are nested) */}
|
|
||||||
<div className="">
|
|
||||||
{quizQuestions ? (
|
{quizQuestions ? (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
<div className="d-flex flex-column">
|
||||||
<div className="title center-h-align mb-2">{quiz?.title}</div>
|
<h2 className="text-center mb-3">{quiz?.title}</h2>
|
||||||
{!isNaN(Number(currentQuestion?.question.id)) && (
|
{!isNaN(Number(currentQuestion?.question.id)) && (
|
||||||
<strong className="number of questions">
|
<p className="text-center fw-bold mb-3">
|
||||||
Question {Number(currentQuestion?.question.id)}/
|
Question {Number(currentQuestion?.question.id)}/{quizQuestions?.length}
|
||||||
{quizQuestions?.length}
|
</p>
|
||||||
</strong>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{quizMode === 'teacher' && (
|
<div className="mb-4" style={{ height: '70vh', overflow: 'auto' }}>
|
||||||
<div className="mb-1">
|
<div className="d-flex flex-column gap-4">
|
||||||
{/* <QuestionNavigation
|
|
||||||
currentQuestionId={Number(currentQuestion?.question.id)}
|
|
||||||
questionsLength={quizQuestions?.length}
|
|
||||||
previousQuestion={previousQuestion}
|
|
||||||
nextQuestion={nextQuestion}
|
|
||||||
/> */}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mb-2 flex-column-wrapper">
|
|
||||||
<div className="preview-and-result-container">
|
|
||||||
{currentQuestion && (
|
{currentQuestion && (
|
||||||
<QuestionDisplay
|
<QuestionDisplay
|
||||||
showAnswer={false}
|
showAnswer={false}
|
||||||
question={currentQuestion?.question as Question}
|
question={currentQuestion?.question as Question}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -435,37 +406,31 @@ const ManageRoom: React.FC = () => {
|
||||||
questions={quizQuestions}
|
questions={quizQuestions}
|
||||||
showSelectedQuestion={showSelectedQuestion}
|
showSelectedQuestion={showSelectedQuestion}
|
||||||
students={students}
|
students={students}
|
||||||
></LiveResultsComponent>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{quizMode === 'teacher' && (
|
{quizMode === 'teacher' && (
|
||||||
<div
|
<div className="d-flex justify-content-center gap-3 mb-4">
|
||||||
className="questionNavigationButtons"
|
|
||||||
style={{ display: 'flex', justifyContent: 'center' }}
|
|
||||||
>
|
|
||||||
<div className="previousQuestionButton">
|
|
||||||
<Button
|
<Button
|
||||||
onClick={previousQuestion}
|
onClick={previousQuestion}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={Number(currentQuestion?.question.id) <= 1}
|
disabled={Number(currentQuestion?.question.id) <= 1}
|
||||||
|
className="px-4"
|
||||||
>
|
>
|
||||||
Question précédente
|
Question précédente
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
<div className="nextQuestionButton">
|
|
||||||
<Button
|
<Button
|
||||||
onClick={nextQuestion}
|
onClick={nextQuestion}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={
|
disabled={
|
||||||
Number(currentQuestion?.question.id) >=
|
Number(currentQuestion?.question.id) >= quizQuestions.length
|
||||||
quizQuestions.length
|
|
||||||
}
|
}
|
||||||
|
className="px-4"
|
||||||
>
|
>
|
||||||
Prochaine question
|
Prochaine question
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -476,7 +441,6 @@ const ManageRoom: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,185 +0,0 @@
|
||||||
|
|
||||||
.room .roomHeader {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-content: stretch
|
|
||||||
}
|
|
||||||
.room .roomHeader .returnButton {
|
|
||||||
flex-basis: 10%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.room .roomHeader .centerTitle {
|
|
||||||
flex-basis: auto;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: flex-end;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.room .roomHeader .dumb {
|
|
||||||
flex-basis: 10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.room .room {
|
|
||||||
width: 100%;
|
|
||||||
height: 70vh;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
overflow: auto;
|
|
||||||
justify-content: center;
|
|
||||||
/* align-items: center; */
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* .create-room-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.manage-room-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quiz-setup-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quiz-mode-selection {
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 10px;
|
|
||||||
height: 15vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.users-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
flex-grow: 1;
|
|
||||||
gap: 2vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.launch-quiz-btn {
|
|
||||||
width: 20vw;
|
|
||||||
height: 11vh;
|
|
||||||
margin-top: 2vh;
|
|
||||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-choice {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 20vw;
|
|
||||||
margin-top: 2vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user {
|
|
||||||
background-color: #e7dad1;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border: 1px solid black;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-btn {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 2vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.room-container {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 60vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-device-width: 768px) {
|
|
||||||
.room-container {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.room-wrapper {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.room-name-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: end;
|
|
||||||
}
|
|
||||||
.user-item {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-column-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 85vh;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-and-result-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nextQuestionButton {
|
|
||||||
align-self: flex-end;
|
|
||||||
margin-bottom: 5rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-device-height: 4000px) {
|
|
||||||
.flex-column-wrapper {
|
|
||||||
height: 60vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-device-height: 1079px) {
|
|
||||||
.flex-column-wrapper {
|
|
||||||
height: 50vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-device-height: 741px) {
|
|
||||||
.flex-column-wrapper {
|
|
||||||
height: 40vh;
|
|
||||||
}
|
|
||||||
} */
|
|
||||||
|
|
@ -1,15 +1,10 @@
|
||||||
// EditorQuiz.tsx
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { FolderType } from '../../../Types/FolderType';
|
import { FolderType } from '../../../Types/FolderType';
|
||||||
|
|
||||||
|
|
||||||
import './share.css';
|
|
||||||
import { Button, NativeSelect } from '@mui/material';
|
import { Button, NativeSelect } from '@mui/material';
|
||||||
import ReturnButton from 'src/components/ReturnButton/ReturnButton';
|
import ReturnButton from 'src/components/ReturnButton/ReturnButton';
|
||||||
|
|
||||||
import ApiService from '../../../services/ApiService';
|
import ApiService from '../../../services/ApiService';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
|
||||||
const Share: React.FC = () => {
|
const Share: React.FC = () => {
|
||||||
console.log('Component rendered');
|
console.log('Component rendered');
|
||||||
|
|
@ -18,7 +13,6 @@ const Share: React.FC = () => {
|
||||||
|
|
||||||
const [quizTitle, setQuizTitle] = useState('');
|
const [quizTitle, setQuizTitle] = useState('');
|
||||||
const [selectedFolder, setSelectedFolder] = useState<string>('');
|
const [selectedFolder, setSelectedFolder] = useState<string>('');
|
||||||
|
|
||||||
const [folders, setFolders] = useState<FolderType[]>([]);
|
const [folders, setFolders] = useState<FolderType[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -68,7 +62,6 @@ const Share: React.FC = () => {
|
||||||
|
|
||||||
const handleQuizSave = async () => {
|
const handleQuizSave = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (selectedFolder == '') {
|
if (selectedFolder == '') {
|
||||||
alert("Veuillez choisir un dossier");
|
alert("Veuillez choisir un dossier");
|
||||||
return;
|
return;
|
||||||
|
|
@ -91,41 +84,39 @@ const Share: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='quizImport'>
|
<div className="container-fluid p-4">
|
||||||
|
{/* Header */}
|
||||||
<div className='importHeader'>
|
<div className="d-flex justify-content-between align-items-center mb-4">
|
||||||
<ReturnButton />
|
<ReturnButton />
|
||||||
|
<h2 className="text-center mb-0 flex-grow-1">Importer quiz: {quizTitle}</h2>
|
||||||
<div className='title'>Importer quiz: {quizTitle}</div>
|
<div style={{ width: '10%' }}></div> {/* Spacer for alignment */}
|
||||||
|
|
||||||
<div className='dumb'></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='editSection'>
|
{/* Main Content */}
|
||||||
|
<div className="d-flex flex-column align-items-center">
|
||||||
<div>
|
<div className="w-100 mb-3">
|
||||||
|
|
||||||
<NativeSelect
|
<NativeSelect
|
||||||
id="select-folder"
|
id="select-folder"
|
||||||
color="primary"
|
color="primary"
|
||||||
value={selectedFolder}
|
value={selectedFolder}
|
||||||
onChange={handleSelectFolder}
|
onChange={handleSelectFolder}
|
||||||
|
className="w-100 mb-3"
|
||||||
>
|
>
|
||||||
<option disabled value=""> Choisir un dossier... </option>
|
<option disabled value="">Choisir un dossier...</option>
|
||||||
|
|
||||||
{folders.map((folder: FolderType) => (
|
{folders.map((folder: FolderType) => (
|
||||||
<option value={folder._id} key={folder._id}> {folder.title} </option>
|
<option value={folder._id} key={folder._id}>{folder.title}</option>
|
||||||
))}
|
))}
|
||||||
</NativeSelect>
|
</NativeSelect>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button variant="contained" onClick={handleQuizSave}>
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleQuizSave}
|
||||||
|
className="w-100"
|
||||||
|
>
|
||||||
Enregistrer
|
Enregistrer
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
.quizImport .importHeader {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-content: stretch
|
|
||||||
}
|
|
||||||
.quizImport .importHeader .returnButton {
|
|
||||||
flex-basis: 10%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quizImport .importHeader .title {
|
|
||||||
flex-basis: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quizImport .importHeader .dumb {
|
|
||||||
flex-basis: 10%;
|
|
||||||
}
|
|
||||||
|
|
@ -50,7 +50,7 @@ describe('Users', () => {
|
||||||
password: 'hashedPassword',
|
password: 'hashedPassword',
|
||||||
created_at: expect.any(Date),
|
created_at: expect.any(Date),
|
||||||
});
|
});
|
||||||
expect(users.folders.create).toHaveBeenCalledWith('Dossier par Défaut', expect.any(String));
|
expect(users.folders.create).toHaveBeenCalledWith('Dossier par défaut', expect.any(String));
|
||||||
expect(result.insertedId).toBeDefined(); // Ensure result has insertedId
|
expect(result.insertedId).toBeDefined(); // Ensure result has insertedId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ class Users {
|
||||||
let created_user = await userCollection.insertOne(newUser);
|
let created_user = await userCollection.insertOne(newUser);
|
||||||
let user = await this.getById(created_user.insertedId)
|
let user = await this.getById(created_user.insertedId)
|
||||||
|
|
||||||
const folderTitle = "Dossier par Défaut";
|
const folderTitle = "Dossier par défaut";
|
||||||
|
|
||||||
const userId = newUser._id ? newUser._id.toString() : 'x';
|
const userId = newUser._id ? newUser._id.toString() : 'x';
|
||||||
await this.folders.create(folderTitle, userId);
|
await this.folders.create(folderTitle, userId);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue