This commit is contained in:
lanahria 2025-04-07 17:48:15 +00:00 committed by GitHub
commit 3da334967e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 1034 additions and 1738 deletions

1
.gitignore vendored
View file

@ -132,3 +132,4 @@ launch.json
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*
db-backup/ db-backup/
/.vs

View file

@ -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",

View file

@ -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",

View file

@ -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 finissantes de l&apos;ETS <div className="mb-2">
Réalisé avec à Montréal par des finissantes de l&apos;ÉTS
</div>
<div className="d-flex justify-content-center align-items-center">
<a
href="https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/"
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 className="footer-links"> </footer>
<a href="https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/">GitHub</a>
<span className="divider">|</span>
<a href="https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/wiki">Wiki GitHub</a>
</div>
</div>
); );
}; };

View file

@ -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;
}

View file

@ -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&apos;é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&apos;éditeur</h2>
<p className="mb-4">
L&apos;éditeur utilise le format GIFT (General Import Format Template) créé pour la L&apos;é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&nbsp;: syntaxe pour chaque type de question&nbsp;:
</span> </p>
<div className="question-type">
<h4>1. Questions Vrai/Faux</h4>
<pre>
<code className="selectable-text">
{QuestionVraiFaux}
</code>
</pre> {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((section) => (
<button onClick={() => copyToClipboard(QuestionVraiFaux)}>Copier</button> <div key={section} className="mb-4">
</div> {section === 1 && (
<>
<h4 className="mt-3">1. Questions Vrai/Faux</h4>
<pre className="bg-white p-3 border rounded">
<code className="font-monospace">
{QuestionVraiFaux}
</code>
</pre>
<Button variant="contained" onClick={() => copyToClipboard(QuestionVraiFaux)} className="mb-4" startIcon={<FileCopyIcon />} >
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 variant="contained" onClick={() => copyToClipboard(QuestionChoixMul)} className="mb-4" startIcon={<FileCopyIcon />} >
<button onClick={() => copyToClipboard(QuestionChoixMul)}>Copier</button> Copier
</div> </Button>
<div className="question-type"> </>
<h4>3. Questions à choix multiple avec plusieurs réponses</h4> )}
<pre>
<code className="question-code-block selectable-text">
{
QuestionChoixMulMany
}
</code>
</pre>
<button onClick={() => copyToClipboard(QuestionChoixMulMany)}>Copier</button>
</div>
<div className="question-type"> {section === 3 && (
<h4>4. Questions à réponse courte</h4> <>
<pre> <h4 className="mt-3">3. Questions à choix multiple avec plusieurs réponses</h4>
<code className="question-code-block selectable-text"> <pre className="bg-white p-3 border rounded">
{QuestionCourte} <code className="font-monospace">
</code> {QuestionChoixMulMany}
</pre> </code>
<button onClick={() => copyToClipboard(QuestionCourte)}>Copier</button> </pre>
</div> <Button variant="contained" onClick={() => copyToClipboard(QuestionChoixMulMany)} className="mb-4" startIcon={<FileCopyIcon />} >
Copier
</Button>
</>
)}
<div className="question-type"> {section === 4 && (
<h4> 5. Questions numériques </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">
QuestionNum {QuestionCourte}
} </code>
</code> </pre>
</pre> <Button variant="contained" onClick={() => copyToClipboard(QuestionCourte)} className="mb-4" startIcon={<FileCopyIcon />} >
<button onClick={() => copyToClipboard(QuestionNum)}>Copier</button> Copier
</div> </Button>
</>
)}
<div className="question-type"> {section === 5 && (
<h4> 6. Paramètres optionnels </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">
{'::Titre:: '} <code className="font-monospace">
<span className="code-comment selectable-text"> {QuestionNum}
{' // Ajoute un titre à une question'} </code>
</span> </pre>
<br /> <Button variant="contained" onClick={() => copyToClipboard(QuestionNum)} className="mb-4" startIcon={<FileCopyIcon />} >
{'# Feedback '} Copier
<span className="code-comment selectable-text"> </Button>
{' // Feedback pour UNE réponse'} </>
</span> )}
<br />
{'// Commentaire '}
<span className="code-comment selectable-text">
{' // Commentaire non apparent'}
</span>
<br />
{'#### Feedback général '}
<span className="code-comment selectable-text">
{' // Feedback général pour une question'}
</span>
<br />
{'%50% '}
<span className="code-comment selectable-text">
{" // Poids d'une réponse (peut être négatif)"}
</span>
</code>
</pre>
</div>
<div className="question-type"> {section === 6 && (
<h4> 7. Caractères spéciaux </h4> <>
<p> <h4 className="mt-3">6. Paramètres optionnels</h4>
Si vous souhaitez utiliser certains caractères spéciaux dans vos énoncés, <pre className="bg-white p-3 border rounded">
réponses ou feedback, vous devez «échapper» ces derniers en ajoutant un \ <code className="font-monospace">
devant: {'::Titre:: '}
</p> <span className="text-success">
<pre> {' // Ajoute un titre à une question'}
<code className="question-code-block selectable-text"> </span>
{'\\~ \n\\= \n\\# \n\\{ \n\\} \n\\:'} <br />
</code> {'# Feedback '}
</pre> <span className="text-success">
</div> {' // Feedback pour UNE réponse'}
</span>
<br />
{'// Commentaire '}
<span className="text-success">
{' // Commentaire non apparent'}
</span>
<br />
{'#### Feedback général '}
<span className="text-success">
{' // Feedback général pour une question'}
</span>
<br />
{'%50% '}
<span className="text-success">
{" // Poids d'une réponse (peut être négatif)"}
</span>
</code>
</pre>
</>
)}
<div className="question-type"> {section === 7 && (
<h4> 8. LaTeX et Markdown</h4> <>
<p> <h4 className="mt-3">7. Caractères spéciaux</h4>
Les formats LaTeX et Markdown sont supportés dans cette application. Vous devez cependant penser <p>
à «échapper» les caractères spéciaux mentionnés plus haut. Si vous souhaitez utiliser certains caractères spéciaux dans vos énoncés,
</p> réponses ou feedback, vous devez «échapper» ces derniers en ajoutant un \
<p>Exemple d&apos;équation:</p> devant:
<pre> </p>
<code className="question-code-block selectable-text">{'$$x\\= \\frac\\{y^2\\}\\{4\\}$$'}</code> <pre className="bg-white p-3 border rounded">
<code className="question-code-block selectable-text">{'\n$x\\= \\frac\\{y^2\\}\\{4\\}$'}</code> <code className="font-monospace">
</pre> {'\\~ \n\\= \n\\# \n\\{ \n\\} \n\\:'}
<p>Exemple de texte Markdown:</p> </code>
<pre> </pre>
<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> </>
</pre> )}
</div>
<div className="question-type" id="images-section"> {section === 8 && (
<h4> 9. Images </h4> <>
<p>Il est possible d&apos;insérer une image dans une question, une réponse (choix multiple) et dans une rétroaction. D&apos;abord, <strong>le format de l&apos;élément doit être [markdown]</strong>. Ensuite utilisez la syntaxe suivante&nbsp;:</p> <h4 className="mt-3">8. LaTeX et Markdown</h4>
<pre> <p>
<code className="question-code-block"> 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.
<span className="code-comment">{`text alternatif`}</span> </p>
{']('} <p>Exemple d&apos;équation:</p>
<span className="code-comment">{`URL-de-l'image`}</span> <pre className="bg-white p-3 border rounded">
{' "'} <code className="font-monospace">{'$$x\\= \\frac\\{y^2\\}\\{4\\}$$'}</code>
<span className="code-comment">{`texte de l'infobulle`}</span> <code className="font-monospace">{'\n$x\\= \\frac\\{y^2\\}\\{4\\}$'}</code>
{'")'} </pre>
</code> <p>Exemple de texte Markdown:</p>
</pre> <pre className="bg-white p-3 border rounded">
<p>Exemple d&apos;une question Vrai/Faux avec l&apos;image d&apos;un chat:</p> <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>
<code className="question-code-block"> </>
{'[markdown]Ceci est un chat: \n![Image de chat](https\\://www.example.com\\:8000/chat.jpg "Chat mignon")\n{T}'} )}
</code>
</pre> {section === 9 && (
<p>Exemple d&apos;une question à choix multiple avec l&apos;image d&apos;un chat dans une rétroaction&nbsp;:</p> <>
<pre> <h4 className="mt-3">9. Images</h4>
<code className="question-code-block"> <p>Il est possible d&apos;insérer une image dans une question, une réponse (choix multiple) et dans une rétroaction. D&apos;abord, <strong>le format de l&apos;élément doit être [markdown]</strong>. Ensuite utilisez la syntaxe suivante&nbsp;:</p>
{`[markdown]Qui a initié le développement d'ÉvalueTonSavoir {=ÉTS#OUI! ![](https\\://www.etsmtl.ca/assets/img/ets.svg "\\=50px") <pre className="bg-white p-3 border rounded">
~EPFL#Non...}`} <code className="font-monospace">
</code> {'!['}
</pre> <span className="text-success">{`text alternatif`}</span>
<p>Note&nbsp;: les images étant spécifiées avec la syntaxe Markdown dans GIFT, on doit échapper les caractères spéciales (:) dans l&apos;URL de l&apos;image.</p> {']('}
<p style={{ color: 'red' }}> <span className="text-success">{`URL-de-l'image`}</span>
Attention: l&apos;ancienne fonctionnalité avec les balises <code>{'<img>'}</code> n&apos;est plus {' "'}
supportée. <span className="text-success">{`texte de l'infobulle`}</span>
</p> {'")'}
</code>
</pre>
<p>Exemple d&apos;une question Vrai/Faux avec l&apos;image d&apos;un chat:</p>
<pre className="bg-white p-3 border rounded">
<code className="font-monospace">
{'[markdown]Ceci est un chat: \n![Image de chat](https\\://www.example.com\\:8000/chat.jpg "Chat mignon")\n{T}'}
</code>
</pre>
<p>Exemple d&apos;une question à choix multiple avec l&apos;image d&apos;un chat dans une rétroaction&nbsp;:</p>
<pre className="bg-white p-3 border rounded">
<code className="font-monospace">
{`[markdown]Qui a initié le développement d'ÉvalueTonSavoir {=ÉTS#OUI! ![](https\\://www.etsmtl.ca/assets/img/ets.svg "\\=50px")
~EPFL#Non...}`}
</code>
</pre>
<p>Note&nbsp;: les images étant spécifiées avec la syntaxe Markdown dans GIFT, on doit échapper les caractères spéciales (:) dans l&apos;URL de l&apos;image.</p>
<p className="text-danger">
Attention: l&apos;ancienne fonctionnalité avec les balises <code>{'<img>'}</code> n&apos;est plus
supportée.
</p>
</>
)}
{section === 10 && (
<>
<h4 className="mt-3">10. Informations supplémentaires</h4>
<p>
GIFT supporte d&apos;autres formats de questions que nous ne gérons pas sur cette
application.
</p>
<p>Vous pouvez retrouver la Documentation de GIFT (en anglais):</p>
<a
href="https://ethan-ou.github.io/vscode-gift-docs/docs/questions"
className="btn btn-link"
>
Documentation de GIFT
</a>
</>
)}
</div> </div>
<div className="question-type"> ))}
<h4> 10. Informations supplémentaires </h4>
<p>
GIFT supporte d&apos;autres formats de questions que nous ne gérons pas sur cette
application.
</p>
<p>Vous pouvez retrouver la Documentation de GIFT (en anglais):</p>
<a href="https://ethan-ou.github.io/vscode-gift-docs/docs/questions">
Documentation de GIFT
</a>
</div>
</div> </div>
); );
}; };

View file

@ -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;
}

View file

@ -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>
<Button {isLoggedIn ? (
variant="outlined" <Button
color="primary" variant="outlined"
onClick={() => { onClick={() => {
handleLogout(); handleLogout();
navigate('/'); navigate('/');
}} }}
> className="mb-4"
Logout startIcon={<LogoutIcon />}
</Button> >
)} Déconnexion
</Button>
{!isLoggedIn && ( ) : (
<div className="auth-selection-btn"> <Link to="/login" className="text-decoration-none">
<Link to="/login"> <Button variant="contained" className="mb-4" startIcon={<LoginIcon />} >
<button className="auth-btn">Connexion</button> Connexion
</Link> </Button>
</div> </Link>
)} )}
</div> </div>
</header>
); );
}; };

View file

@ -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;
}

View file

@ -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;
@ -28,7 +26,7 @@ interface Props {
selectedFolder: string; selectedFolder: string;
} }
const DragAndDrop: React.FC<Props> = ({ handleOnClose, handleOnImport, open, selectedFolder }) => { const DragAndDrop: React.FC<Props> = ({ handleOnClose, handleOnImport, open, selectedFolder }) => {
const [droppedFiles, setDroppedFiles] = useState<DroppedFile[]>([]); const [droppedFiles, setDroppedFiles] = useState<DroppedFile[]>([]);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
@ -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&apos;explorateur des fichiers cliquez pour ouvrir l&apos;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

View file

@ -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;
}

View file

@ -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>
); );

View file

@ -1,5 +0,0 @@
.loading-circle {
display: flex;
flex-direction: column;
align-items: center;
}

View file

@ -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';

View file

@ -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>
); );
}; };

View file

@ -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;
}

View file

@ -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';

View file

@ -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';

View file

@ -1,59 +1,62 @@
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') && (
{Object.keys(authData).map((providerKey) => { <div className="d-flex flex-column my-3 w-100" style={{ maxWidth: '400px' }}>
const providerType = authData[providerKey].type; {Object.keys(authData).map((providerKey) => {
if (providerType === 'oidc' || providerType === 'oauth') { const providerType = authData[providerKey].type;
return ( if (providerType === 'oidc' || providerType === 'oauth') {
<ButtonAuth return (
key={providerKey} <ButtonAuth
providerName={providerKey} key={providerKey}
providerType={providerType} providerName={providerKey}
/> providerType={providerType}
); />
} );
return null; }
})} return null;
</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>
); );
}; };

View file

@ -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;
}

View file

@ -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}
Continuer avec {providerName} className={`btn btn-outline-secondary w-100 ${providerType}-btn`}
</button> onClick={() => handleAuthLogin(providerName)}
</div> >
</> Continuer avec {providerName}
</button>
</div>
); );
}; };

View file

@ -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>
); );
}; };

View file

@ -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>
); );
}; };

View file

@ -1,66 +1,70 @@
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 () => {
const result = await ApiService.resetPassword(email); setIsConnecting(true);
try {
const result = await ApiService.resetPassword(email);
if (!result) { if (!result) {
setConnectionError(result.toString()); setConnectionError(result.toString());
return; return;
}
navigate("/login");
} finally {
setIsConnecting(false);
} }
navigate("/login")
}; };
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>
); );
}; };

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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>
); );
} }

View file

@ -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;
}
} */

View file

@ -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,31 +415,38 @@ 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">
<label htmlFor="select-room">Sélectionner une salle: </label>
<select value={selectedRoom?._id || ''} onChange={(e) => handleSelectRoom(e)}>
<option value="" disabled>
-- Sélectionner une salle --
</option>
{rooms.map((room) => (
<option key={room._id} value={room._id}>
{room.title}
</option>
))}
<option value="add-room">Ajouter salle</option>
</select>
{/* Room Selection */}
<div className="row mb-4">
<div className="col-md-6">
<div className="d-flex align-items-center">
<label htmlFor="select-room" className="me-2 fw-medium">Sélectionner une salle:</label>
<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) => (
<option key={room._id} value={room._id}>
{room.title}
</option>
))}
<option value="add-room">Ajouter salle</option>
</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,193 +472,189 @@ const Dashboard: React.FC = () => {
</DialogActions> </DialogActions>
</Dialog> </Dialog>
<div className="search-bar"> {/* Search Bar */}
<TextField <div className="row mb-4">
onChange={handleSearch} <div className="col-12">
value={searchTerm} <TextField
placeholder="Rechercher un quiz par son titre" onChange={handleSearch}
fullWidth value={searchTerm}
InputProps={{ placeholder="Rechercher un quiz par son titre"
endAdornment: ( fullWidth
<InputAdornment position="end"> className="w-100"
<IconButton> InputProps={{
<Search /> endAdornment: (
</IconButton> <InputAdornment position="end">
</InputAdornment> <IconButton>
) <Search />
}} </IconButton>
/> </InputAdornment>
)
}}
/>
</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">
<IconButton
<Tooltip title="Renommer dossier" placement="top"> color="primary"
<div> onClick={handleRenameFolder}
<IconButton disabled={selectedFolderId === ''}
color="primary" className="border"
onClick={handleRenameFolder} >
disabled={selectedFolderId == ''} // cannot action on all <Edit />
> </IconButton>
{' '} </Tooltip>
<Edit />{' '} <Tooltip title="Dupliquer dossier">
</IconButton> <IconButton
</div> color="primary"
</Tooltip> onClick={handleDuplicateFolder}
disabled={selectedFolderId === ''}
<Tooltip title="Dupliquer dossier" placement="top"> className="border"
<div> >
<IconButton <FolderCopy />
color="primary" </IconButton>
onClick={handleDuplicateFolder} </Tooltip>
disabled={selectedFolderId == ''} // cannot action on all <Tooltip title="Supprimer dossier">
> <IconButton
{' '} color="error"
<FolderCopy />{' '} onClick={handleDeleteFolder}
</IconButton> disabled={selectedFolderId === ''}
</div> className="border"
</Tooltip> >
<DeleteOutline />
<Tooltip title="Supprimer dossier" placement="top"> </IconButton>
<div> </Tooltip>
<IconButton </div>
aria-label="delete"
color="primary"
onClick={handleDeleteFolder}
disabled={selectedFolderId == ''} // cannot action on all
>
{' '}
<DeleteOutline />{' '}
</IconButton>
</div>
</Tooltip>
</div> </div>
</div> </div>
<div className="ajouter"> {/* Add Quiz and Import Buttons */}
<Button <div className="row mb-4 g-2">
variant="outlined" <div className="col-md-10">
color="primary" <Button
startIcon={<Add />} variant="contained"
onClick={handleCreateQuiz} color="primary"
> startIcon={<Add />}
Ajouter un nouveau quiz onClick={handleCreateQuiz}
</Button> className="w-100 py-2"
>
<Button Ajouter un nouveau quiz
variant="outlined" </Button>
color="primary" </div>
startIcon={<Upload />} <div className="col-md-2">
onClick={handleOnImport} <Button
> variant="outlined"
Import color="primary"
</Button> startIcon={<Upload />}
onClick={handleOnImport}
className="w-100 py-2"
>
Importer
</Button>
</div>
</div> </div>
<div className="list">
{/* 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}>
<div className="folder-tab">{folderName}</div> <CustomCard>
<CardContent> <div className="folder-tab">{folderName}</div>
{quizzesByFolder[folderName].map((quiz: QuizType) => ( <CardContent className="p-3">
<div className="quiz" key={quiz._id}> {quizzesByFolder[folderName].map((quiz: QuizType) => (
<div className="title"> <div className="d-flex align-items-center mb-3 p-2 rounded" key={quiz._id}>
<Tooltip title="Lancer quiz" placement="top"> <div className="flex-grow-1 me-3 text-truncate">
<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">
<Tooltip title="Télécharger quiz">
<IconButton
color="primary"
onClick={() => downloadTxtFile(quiz)}
className="border"
>
<FileDownload />
</IconButton>
</Tooltip>
<Tooltip title="Modifier quiz">
<IconButton
color="primary"
onClick={() => handleEditQuiz(quiz)}
className="border"
>
<Edit />
</IconButton>
</Tooltip>
<Tooltip title="Dupliquer quiz">
<IconButton
color="primary"
onClick={() => handleDuplicateQuiz(quiz)}
className="border"
>
<ContentCopy />
</IconButton>
</Tooltip>
<Tooltip title="Supprimer quiz">
<IconButton
color="error"
onClick={() => handleRemoveQuiz(quiz)}
className="border"
>
<DeleteOutline />
</IconButton>
</Tooltip>
<Tooltip title="Partager quiz">
<IconButton
color="primary"
onClick={() => handleShareQuiz(quiz)}
className="border"
>
<Share />
</IconButton>
</Tooltip>
</div>
</div> </div>
))}
<div className="actions"> </CardContent>
<Tooltip title="Télécharger quiz" placement="top"> </CustomCard>
<IconButton </div>
color="primary"
onClick={() => downloadTxtFile(quiz)}
>
{' '}
<FileDownload />{' '}
</IconButton>
</Tooltip>
<Tooltip title="Modifier quiz" placement="top">
<IconButton
color="primary"
onClick={() => handleEditQuiz(quiz)}
>
{' '}
<Edit />{' '}
</IconButton>
</Tooltip>
<Tooltip title="Dupliquer quiz" placement="top">
<IconButton
color="primary"
onClick={() => handleDuplicateQuiz(quiz)}
>
{' '}
<ContentCopy />{' '}
</IconButton>
</Tooltip>
<Tooltip title="Supprimer quiz" placement="top">
<IconButton
aria-label="delete"
color="primary"
onClick={() => handleRemoveQuiz(quiz)}
>
{' '}
<DeleteOutline />{' '}
</IconButton>
</Tooltip>
<Tooltip title="Partager quiz" placement="top">
<IconButton
color="primary"
onClick={() => handleShareQuiz(quiz)}
>
{' '}
<Share />{' '}
</IconButton>
</Tooltip>
</div>
</div>
))}
</CardContent>
</CustomCard>
))} ))}
</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;

View file

@ -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;
} */

View file

@ -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);
const linesArray = value.split(/\n{2,}/);
if (linesArray[0] === '') linesArray.shift();
if (linesArray[linesArray.length - 1] === '') linesArray.pop();
setFilteredValue(linesArray);
} }
// split value when there is at least one blank line
const linesArray = value.split(/\n{2,}/);
// if the first item in linesArray is blank, remove it
if (linesArray[0] === '') linesArray.shift();
if (linesArray[linesArray.length - 1] === '') linesArray.pop();
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,109 +151,125 @@ const QuizForm: React.FC = () => {
navigator.clipboard.writeText(link); navigator.clipboard.writeText(link);
} }
if (!isNewQuiz && !quiz) {
return <div>Chargement...</div>;
}
return ( return (
<div className='quizEditor'> <div className="container-fluid p-4">
{/* Header */}
<div className='editHeader'> <div className="d-flex justify-content-between align-items-center mb-4">
<ReturnButton <div className="w-25">
askConfirm <ReturnButton
message={`Êtes-vous sûr de vouloir quitter l'éditeur sans sauvegarder le questionnaire?`} askConfirm
/> message="Êtes-vous sûr de vouloir quitter l'éditeur sans sauvegarder le questionnaire?"
/>
<div className='title'>Éditeur de quiz</div> </div>
<h1 className="text-center flex-grow-1">Éditeur de quiz</h1>
<div className='dumb'></div> <div className="w-25"></div> {/* Spacer for balance */}
</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
onChange={handleQuizTitleChange}
value={quizTitle}
placeholder="Titre du quiz"
label="Titre du quiz"
fullWidth
/>
</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
id="select-folder"
color="primary"
value={selectedFolder}
onChange={handleSelectFolder}
disabled={!isNewQuiz}
className="flex-grow-1"
>
<option disabled value="">Choisir un dossier...</option>
{folders.map((folder: FolderType) => (
<option value={folder._id} key={folder._id}>{folder.title}</option>
))}
</NativeSelect>
</div>
</div>
</div>
<TextField <Button variant="contained" onClick={handleQuizSave} className="mb-4" startIcon={<SaveIcon />} >
onChange={handleQuizTitleChange}
value={quizTitle}
placeholder="Titre du quiz"
label="Titre du quiz"
fullWidth
/>
<label>Choisir un dossier:
<NativeSelect
id="select-folder"
color="primary"
value={selectedFolder}
onChange={handleSelectFolder}
disabled={!isNewQuiz}
style={{ marginBottom: '16px' }} // Ajout de marge en bas
>
<option disabled value=""> Choisir un dossier... </option>
{folders.map((folder: FolderType) => (
<option value={folder._id} key={folder._id}> {folder.title} </option>
))}
</NativeSelect></label>
<Button variant="contained" onClick={handleQuizSave}>
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 />}
</Button> >
Téléverser
</label> </Button>
<Dialog
open={dialogOpen}
onClose={() => setDialogOpen(false)} >
<DialogTitle>Erreur</DialogTitle>
<DialogContent>
Veuillez d&apos;abord choisir une image à téléverser.
</DialogContent>
<DialogActions>
<Button onClick={() => setDialogOpen(false)} color="primary">
OK
</Button>
</DialogActions>
</Dialog>
</div> </div>
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)}>
<DialogTitle>Erreur</DialogTitle>
<DialogContent>
Veuillez d&apos;abord choisir une image à téléverser.
</DialogContent>
<DialogActions>
<Button onClick={() => setDialogOpen(false)} color="primary">
OK
</Button>
</DialogActions>
</Dialog>
<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 = `![alt_text](${escapeForGIFT(link)} "texte de l'infobulle")`; const imgTag = `![alt_text](${escapeForGIFT(link)} "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;

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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 () => {
const result = await ApiService.login(email, password); setIsConnecting(true);
try {
if (typeof result === "string") { const result = await ApiService.login(email, password);
setConnectionError(result); if (typeof result === "string") {
return; setConnectionError(result);
} else {
navigate("/teacher/Dashboard");
}
} finally {
setIsConnecting(false);
} }
else {
navigate("/teacher/Dashboard")
}
}; };
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>
); );
}; };

View file

@ -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,12 +19,13 @@ 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();
const [socket, setSocket] = useState<Socket | null>(null); const [socket, setSocket] = useState<Socket | null>(null);
const [students, setStudents] = useState<StudentType[]>([]); const [students, setStudents] = useState<StudentType[]>([]);
const { quizId = '', roomName = '' } = useParams<{ quizId: string, roomName: string }>(); const { quizId = '', roomName = '' } = useParams<{ quizId: string, roomName: string }>();
const [quizQuestions, setQuizQuestions] = useState<QuestionType[] | undefined>(); const [quizQuestions, setQuizQuestions] = useState<QuestionType[] | undefined>();
const [quiz, setQuiz] = useState<QuizType | null>(null); const [quiz, setQuiz] = useState<QuizType | null>(null);
const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher'); const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher');
@ -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,118 +364,82 @@ 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', </div>
justifyContent: 'space-between', </div>
alignItems: 'center',
width: '100%' {/* Main Content */}
}} {quizQuestions ? (
> <div className="d-flex flex-column">
{( <h2 className="text-center mb-3">{quiz?.title}</h2>
<div {!isNaN(Number(currentQuestion?.question.id)) && (
className="userCount subtitle smallText" <p className="text-center fw-bold mb-3">
style={{ display: "flex", justifyContent: "flex-end" }} Question {Number(currentQuestion?.question.id)}/{quizQuestions?.length}
> </p>
<GroupIcon style={{ marginRight: '5px' }} /> )}
{students.length}/60
<div className="mb-4" style={{ height: '70vh', overflow: 'auto' }}>
<div className="d-flex flex-column gap-4">
{currentQuestion && (
<QuestionDisplay
showAnswer={false}
question={currentQuestion?.question as Question}
/>
)}
<LiveResultsComponent
quizMode={quizMode}
socket={socket}
questions={quizQuestions}
showSelectedQuestion={showSelectedQuestion}
students={students}
/>
</div>
</div>
{quizMode === 'teacher' && (
<div className="d-flex justify-content-center gap-3 mb-4">
<Button
onClick={previousQuestion}
variant="contained"
disabled={Number(currentQuestion?.question.id) <= 1}
className="px-4"
>
Question précédente
</Button>
<Button
onClick={nextQuestion}
variant="contained"
disabled={
Number(currentQuestion?.question.id) >= quizQuestions.length
}
className="px-4"
>
Prochaine question
</Button>
</div> </div>
)} )}
</div> </div>
) : (
<div className="dumb"></div> <StudentWaitPage
</div> students={students}
launchQuiz={launchQuiz}
{/* the following breaks the css (if 'room' classes are nested) */} setQuizMode={setQuizMode}
<div className=""> />
{quizQuestions ? ( )}
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div className="title center-h-align mb-2">{quiz?.title}</div>
{!isNaN(Number(currentQuestion?.question.id)) && (
<strong className="number of questions">
Question {Number(currentQuestion?.question.id)}/
{quizQuestions?.length}
</strong>
)}
{quizMode === 'teacher' && (
<div className="mb-1">
{/* <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 && (
<QuestionDisplay
showAnswer={false}
question={currentQuestion?.question as Question}
/>
)}
<LiveResultsComponent
quizMode={quizMode}
socket={socket}
questions={quizQuestions}
showSelectedQuestion={showSelectedQuestion}
students={students}
></LiveResultsComponent>
</div>
</div>
{quizMode === 'teacher' && (
<div
className="questionNavigationButtons"
style={{ display: 'flex', justifyContent: 'center' }}
>
<div className="previousQuestionButton">
<Button
onClick={previousQuestion}
variant="contained"
disabled={Number(currentQuestion?.question.id) <= 1}
>
Question précédente
</Button>
</div>
<div className="nextQuestionButton">
<Button
onClick={nextQuestion}
variant="contained"
disabled={
Number(currentQuestion?.question.id) >=
quizQuestions.length
}
>
Prochaine question
</Button>
</div>
</div>
)}
</div>
) : (
<StudentWaitPage
students={students}
launchQuiz={launchQuiz}
setQuizMode={setQuizMode}
/>
)}
</div>
</div> </div>
); );
}; };

View file

@ -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;
}
} */

View file

@ -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>
<Button variant="contained" onClick={handleQuizSave}>
Enregistrer
</Button>
</div> </div>
<Button
variant="contained"
onClick={handleQuizSave}
className="w-100"
>
Enregistrer
</Button>
</div> </div>
</div> </div>
); );
}; };

View file

@ -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%;
}

View file

@ -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
}); });

View file

@ -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);