feat: review backend and frontend
- update to the latest version of Java/SpringBoot - update to the latest version NuxtJS - add account/password update - add account creation - add account password reset - add bundle to regroup questions and add default questions on user creation - add bundle creation
This commit is contained in:
		
							
								
								
									
										182
									
								
								frontend/pages/bundle/create.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								frontend/pages/bundle/create.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| <script lang="ts" setup> | ||||
|  | ||||
| import {type QuestionCreation, useBundleStore} from "~/store/bundle"; | ||||
| import {useNotificationStore} from "~/store/notification"; | ||||
| import type {ApiError} from "~/composables/fetch-api"; | ||||
| import {type Axe, useAxeStore} from "~/store/axe"; | ||||
| import {type Question, useQuestionStore} from "~/store/question"; | ||||
|  | ||||
| const axes = ref<Axe[]>(); | ||||
| const label = ref(); | ||||
| const presentation = ref(); | ||||
| const questions = ref<Map<number, QuestionCreation[]>>(new Map()); | ||||
| const questionsExample = ref<Map<number, QuestionCreation[]>>(new Map()); | ||||
| const modalVisible = ref(false); | ||||
| const currentAxe = ref<Axe>(); | ||||
| const currentQuestions = ref<Question[]>(); | ||||
|  | ||||
| onMounted(() => { | ||||
|   useAxeStore().findAxes().then(response => { | ||||
|     response.forEach(axe => { | ||||
|       useQuestionStore().findDefaults(axe.id).then(response => { | ||||
|         questions.value.set(axe.id, response); | ||||
|       }); | ||||
|       useQuestionStore().findAll(axe.id).then(response => { | ||||
|         questionsExample.value.set(axe.id, response); | ||||
|       }); | ||||
|     }); | ||||
|     axes.value = response; | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| function createBundle() { | ||||
|   const newQuestions = []; | ||||
|   let errors = []; | ||||
|   questions.value.forEach((value, axeId) => { | ||||
|     if (value.length === 0) { | ||||
|       const axeNumber = axes.value.filter(a => a.id === axeId)[0].identifier; | ||||
|       errors.push(`L'axe ${axeNumber} n'a pas de question.`); | ||||
|     } | ||||
|     value.forEach((q: Question, index)=> { | ||||
|       newQuestions.push({ | ||||
|         axeId: axeId, | ||||
|         label: q.label, | ||||
|         description: q.description, | ||||
|         index: index+1 | ||||
|       }); | ||||
|       if (!q.label.trim()) { | ||||
|         const axeNumber = axes.value.filter(a => a.id === axeId)[0].identifier; | ||||
|         errors.push(`L'une des question de l'axe ${axeNumber} contient un libellé non rempli.`); | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
|   if (errors.length > 0) { | ||||
|     useNotificationStore().pushNotification("warn", | ||||
|       {message: `La boussole contient des erreurs !`, details: errors}); | ||||
|   }else { | ||||
|     useBundleStore() | ||||
|       .create({ | ||||
|         label: label.value, | ||||
|         presentation: presentation.value, | ||||
|         questions: newQuestions | ||||
|       }) | ||||
|       .then(() => { | ||||
|         useNotificationStore().pushNotification("success",{message: "La boussole a bien été créée."}); | ||||
|         navigateTo("/bundle"); | ||||
|       }) | ||||
|       .catch((apiError: ApiError) => { | ||||
|         let details; | ||||
|         if (apiError.fieldErrors) { | ||||
|           details = apiError.fieldErrors.map(error => `${error.fields!.join(", ")} ${error.detail}`); | ||||
|         } | ||||
|         useNotificationStore().pushNotification("warn",{message: apiError.message, details}); | ||||
|       }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| function css(axe: Axe) { | ||||
|   return { | ||||
|     '--color': axe.color | ||||
|   } | ||||
| } | ||||
|  | ||||
| function showAxeModal(axe: Axe) { | ||||
|   currentAxe.value = axe; | ||||
|   modalVisible.value = true; | ||||
|   currentQuestions.value = questions.value.get(axe.id); | ||||
|   document.body.style.overflowY = "hidden"; | ||||
| } | ||||
|  | ||||
| function hideAxeModal() { | ||||
|   currentAxe.value = undefined; | ||||
|   currentQuestions.value = undefined; | ||||
|   modalVisible.value = false; | ||||
|   document.body.style.overflowY = "auto"; | ||||
| } | ||||
|  | ||||
| function onQuestionsChange({axeId, newQuestions}) { | ||||
|   questions.value.set(axeId, newQuestions); | ||||
| } | ||||
|  | ||||
| function numberOfQuestions(axe: Axe) { | ||||
|   const q = questions.value.get(axe.id) | ||||
|   return q ? q.length: 0; | ||||
| } | ||||
|  | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <h1>Créer une nouvelle Boussole</h1> | ||||
|   <section> | ||||
|     <form class="form" @submit.prevent="createBundle"> | ||||
|       <label for="label">Nom de la boussole *</label> | ||||
|       <input id="label" v-model="label" type="text" required maxlength="50"> | ||||
|       <label for="label">Présentation</label> | ||||
|       <input id="label" v-model="presentation" type="text" maxlength="100"> | ||||
|       <ul class="axe-list"> | ||||
|         <li class="axe-list__item" v-for="axe in axes" :style="css(axe)"> | ||||
|           <h2>{{ axe.identifier }} - {{ axe.shortTitle }}</h2> | ||||
|           <div class="axe-list__item__content"> | ||||
|             <div class="axe-list__item__content-text"> | ||||
|               <p>{{ axe.title }}</p> | ||||
|               <p>{{ numberOfQuestions(axe) }} question{{numberOfQuestions(axe) > 1? 's': ''}} configurée{{numberOfQuestions(axe) > 1? 's': ''}}</p> | ||||
|             </div> | ||||
|             <button class="button blue" type="button" @click="showAxeModal(axe)"> | ||||
|               Configurer | ||||
|             </button> | ||||
|           </div> | ||||
|         </li> | ||||
|       </ul> | ||||
|       <div class="button-container"> | ||||
|         <nuxt-link class="button gray button-back" to="/bundle" aria-label="Précédent">❮</nuxt-link> | ||||
|         <button class="button orange">Valider</button> | ||||
|       </div> | ||||
|     </form> | ||||
|   </section> | ||||
|   <bundle-axe-modal v-if="currentAxe" :visible="modalVisible" | ||||
|                     :axe="currentAxe" | ||||
|                     :questions="questions.get(currentAxe.id)" | ||||
|                     :questions-example="questionsExample.get(currentAxe.id)" | ||||
|                     @close="hideAxeModal()" | ||||
|                     @changed="(axeId, newQuestions) => onQuestionsChange(axeId, newQuestions)"/> | ||||
| </template> | ||||
|  | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import "assets/css/color"; | ||||
| @import "assets/css/spacing"; | ||||
| @import "assets/css/font"; | ||||
|  | ||||
| .axe-list { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   list-style: none; | ||||
|   gap: $xxx_small; | ||||
|   margin: 0; | ||||
|  | ||||
|   &__item { | ||||
|     padding: $xx_small 0; | ||||
|     border-bottom: 2px solid var(--color); | ||||
|  | ||||
|     h2 { | ||||
|       font-size: $secondary-font-size; | ||||
|       margin: 0; | ||||
|     } | ||||
|  | ||||
|     &__content { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       justify-content: space-between; | ||||
|       gap: $xx_small; | ||||
|  | ||||
|       &-text p + p { | ||||
|        margin: $xxx_small 0; | ||||
|       } | ||||
|       &-text p:last-child { | ||||
|         font-style: italic; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| </style> | ||||
							
								
								
									
										126
									
								
								frontend/pages/bundle/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								frontend/pages/bundle/index.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,126 @@ | ||||
| <script lang="ts" setup> | ||||
|  | ||||
| import {type Bundle, useBundleStore} from "~/store/bundle"; | ||||
|  | ||||
| const showModal = ref(false); | ||||
| const bundles = ref<Bundle[]>([]); | ||||
| const loading = ref(false); | ||||
|  | ||||
| onMounted(() => { | ||||
|   loading.value = true; | ||||
|   useBundleStore().findAll().then((response: Quiz[]) => { | ||||
|     bundles.value = response; | ||||
|   }).finally(() => { | ||||
|     loading.value = false; | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| function navigateToDashboard(bundle: Bundle) { | ||||
|   useBundleStore().setCurrentBundle(bundle); | ||||
|   navigateTo("/dashboard"); | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| function navigateToQuiz(bundle: Bundle) { | ||||
|   useBundleStore().setCurrentBundle(bundle); | ||||
|   navigateTo("/quiz"); | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <section v-if="!loading" class="section"> | ||||
|     <h1>Bienvenue sur votre espace d’auto-évaluation Boussole !</h1> | ||||
|     <p>Vous allez pouvoir dès maintenant évaluer votre projet de Production Locale Utile Solidaire et Soutenable au | ||||
|       regard des <button class="button-link" @click="showModal = true">10 balises du référentiel</button>. | ||||
|     </p> | ||||
|     <p>Cliquez ci-dessous sur la <strong>Boussole de référence</strong>, pour évaluer votre projet sur des questions | ||||
|       déjà configurées. Pour configurez vous-même vos questions, cliquez sur <strong>Personnaliser votre Boussole</strong>.</p> | ||||
|     <p>Ce tableau de bord vous permet de suivre vos différentes auto-évaluations sur tous vos projets !</p> | ||||
|     <ul class="bundle-list"> | ||||
|       <li v-for="bundle in bundles"> | ||||
|         <article class="bundle-list__item"> | ||||
|           <main> | ||||
|             <h1>{{ bundle.label }}</h1> | ||||
|             <p>{{ bundle.presentation }}</p> | ||||
|             <dl class="bundle-list__item__attribute"> | ||||
|               <dt>Dernière auto-évaluation réalisée</dt> | ||||
|               <dd>{{ bundle.lastQuizzDate ? formatDate(bundle.lastQuizzDate) : 'NA' }}</dd> | ||||
|               <dt>Nombre d'auto-évaluation</dt> | ||||
|               <dd>{{ bundle.numberOfQuizzes || 0 }}</dd> | ||||
|             </dl> | ||||
|           </main> | ||||
|           <footer class="bundle-list__item__button-container"> | ||||
|             <button class="button blue" @click="navigateToDashboard(bundle)">Voir mes évaluations</button> | ||||
|             <button class="button orange" @click="navigateToQuiz(bundle)">S'évaluer</button> | ||||
|           </footer> | ||||
|         </article> | ||||
|       </li> | ||||
|     </ul> | ||||
|     <div class="button-container"> | ||||
|       <nuxt-link to="/bundle/create">Personnaliser votre Boussole</nuxt-link> | ||||
|     </div> | ||||
|   </section> | ||||
|   <loader v-else/> | ||||
|   <axe-definition-modal :visible="showModal" @close="showModal=false" /> | ||||
| </template> | ||||
|  | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
|  | ||||
| .section { | ||||
|   margin-block: $medium; | ||||
| } | ||||
|  | ||||
| .bundle-list { | ||||
|   display: grid; | ||||
|   grid-template-columns: repeat(auto-fill, minmax(22.5rem, 1fr)); | ||||
|   grid-gap: $small; | ||||
|  | ||||
|   list-style: none; | ||||
|   margin-top: $x_medium; | ||||
|  | ||||
|   &__item { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     height: 100%; | ||||
|     gap: $small; | ||||
|     padding: $small; | ||||
|  | ||||
|     border-radius: 20px; | ||||
|  | ||||
|     @include border-shadow(); | ||||
|  | ||||
|     h1 { | ||||
|       font-size: $title-font-size; | ||||
|       margin: 0 0 $xxx_small 0; | ||||
|     } | ||||
|     p { | ||||
|       margin: 0 0 $xxx_small 0; | ||||
|     } | ||||
|  | ||||
|     &__attribute { | ||||
|       dt { | ||||
|         font-weight: bold; | ||||
|         float: left; | ||||
|         clear: left; | ||||
|  | ||||
|         &:after { | ||||
|           content: ' :'; | ||||
|           margin-right: $xxxx_small; | ||||
|         } | ||||
|       } | ||||
|       dd { | ||||
|         padding-bottom: $xxxx_small; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     &__button-container { | ||||
|       display: flex; | ||||
|       justify-content: center; | ||||
|       gap: $xxx_small; | ||||
|       margin-top: auto; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
		Reference in New Issue
	
	Block a user