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:
		
							
								
								
									
										47
									
								
								frontend/store/account.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								frontend/store/account.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| import {defineStore} from 'pinia'; | ||||
|  | ||||
| export const useAccountStore = defineStore('account', { | ||||
|   state: () => ({}), | ||||
|   actions: { | ||||
|     update(username: string, email: string) { | ||||
|       return useApi("/account", { | ||||
|         method: "PUT", | ||||
|         body: { | ||||
|           email, username | ||||
|         } | ||||
|       }); | ||||
|     }, | ||||
|     updatePassword(currentPassword: string, newPassword: string, confirmationPassword: string) { | ||||
|       return useApi("/account/password", { | ||||
|         method: "PUT", | ||||
|         body: { | ||||
|           currentPassword, newPassword, confirmationPassword | ||||
|         } | ||||
|       }); | ||||
|     }, | ||||
|     requestPasswordReset(email: string) { | ||||
|       return useApi("/account/password/notify-reset-request", { | ||||
|         method: "POST", | ||||
|         body: { | ||||
|           email | ||||
|         } | ||||
|       }, false); | ||||
|     }, | ||||
|     resetPassword(token: string, email: string, newPassword: string, confirmationPassword: string) { | ||||
|       return useApi("/account/password/reset", { | ||||
|         method: "POST", | ||||
|         body: { | ||||
|           token, email, newPassword, confirmationPassword | ||||
|         } | ||||
|       }, false); | ||||
|     }, | ||||
|     create(username: string, email: string, password: string) { | ||||
|       return useApi("/auth/register", { | ||||
|         method: "POST", | ||||
|         body: { | ||||
|           username, email, password | ||||
|         } | ||||
|       }, false); | ||||
|     } | ||||
|   }, | ||||
| }); | ||||
							
								
								
									
										76
									
								
								frontend/store/auth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								frontend/store/auth.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| import {defineStore} from 'pinia'; | ||||
| import {useApi} from "~/composables/fetch-api"; | ||||
|  | ||||
| export interface Auth { | ||||
|   token: { | ||||
|     type: string; | ||||
|     value: string; | ||||
|     expireAt: Date; | ||||
|   }, | ||||
|   refreshToken: string; | ||||
| } | ||||
|  | ||||
| export interface User { | ||||
|   id: number | ||||
|   email: string; | ||||
|   username: string; | ||||
| } | ||||
|  | ||||
| export const useAuthStore = defineStore('auth', { | ||||
|   state: () => ({ | ||||
|     authenticated: ref<boolean>(useCookie("auth").value !== undefined), | ||||
|     auth: ref<Auth>(useCookie("auth").value), | ||||
|     user: ref<User>(useCookie("user").value) | ||||
|   }), | ||||
|   getters: { | ||||
|   }, | ||||
|   actions: { | ||||
|     async login(email: string, password: string) { | ||||
|       return useApi('auth/login', { | ||||
|         method: 'post', | ||||
|         body: { | ||||
|           email, | ||||
|           password, | ||||
|         }, | ||||
|       }, false).then(data => { | ||||
|         useCookie('auth').value = JSON.stringify(data); | ||||
|         this.authenticated = true; | ||||
|         this.auth = data; | ||||
|         useApi('auth/me').then(data => { | ||||
|           this.user = data; | ||||
|           useCookie("user").value = JSON.stringify(data) | ||||
|         }); | ||||
|       }); | ||||
|     }, | ||||
|     logout() { | ||||
|       useApi('auth/logout', { | ||||
|         method: 'post', | ||||
|         body: { | ||||
|           userId: this.user.id, | ||||
|         }, | ||||
|       }).finally(() => { | ||||
|         this.authenticated = false; | ||||
|         useCookie('auth').value = undefined; | ||||
|         useCookie("user").value = undefined; | ||||
|       }); | ||||
|     }, | ||||
|     refreshSession() { | ||||
|       // Use useFetch to not call | ||||
|       return useFetch('auth/refresh-token', { | ||||
|         baseURL: useRuntimeConfig().public.baseURL, | ||||
|         method: 'post', | ||||
|         body: { | ||||
|           refreshToken: this.auth.refreshToken, | ||||
|         }, | ||||
|       }, false).then((response) => { | ||||
|         this.authenticated = true; | ||||
|         this.auth.token = response.data; | ||||
|         useCookie('auth').value = JSON.stringify(this.auth); | ||||
|       }).catch(() => { | ||||
|         this.authenticated = false; | ||||
|         useCookie('auth').value = undefined; | ||||
|         useCookie("user").value = undefined; | ||||
|       }); | ||||
|     } | ||||
|   }, | ||||
| }); | ||||
							
								
								
									
										19
									
								
								frontend/store/axe.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								frontend/store/axe.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| import {defineStore} from 'pinia'; | ||||
|  | ||||
| export interface Axe { | ||||
|   id: number; | ||||
|   identifier: number; | ||||
|   shortTitle: string; | ||||
|   description: string; | ||||
|   title: string; | ||||
|   color: string; | ||||
| } | ||||
|  | ||||
| export const useAxeStore = defineStore('axe', { | ||||
|   state: () => ({}), | ||||
|   actions: { | ||||
|     findAxes() { | ||||
|       return useApi("axes"); | ||||
|     } | ||||
|   } | ||||
| }); | ||||
							
								
								
									
										45
									
								
								frontend/store/bundle.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								frontend/store/bundle.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| import {defineStore} from 'pinia'; | ||||
|  | ||||
| export interface Bundle { | ||||
|   id: number; | ||||
|   label: string; | ||||
|   presentation: string; | ||||
|   lastQuizzDate: string; | ||||
|   numberOfQuizzes: number; | ||||
| } | ||||
|  | ||||
| export interface QuestionCreation { | ||||
|   label: string; | ||||
|   description: string; | ||||
|   axeId: number; | ||||
|   index: number; | ||||
| } | ||||
|  | ||||
| export interface BundleCreationRequest { | ||||
|   label: string; | ||||
|   presentation: string; | ||||
|   questions: QuestionCreation[]; | ||||
| } | ||||
|  | ||||
| export const useBundleStore = defineStore('bundle', { | ||||
|   state: () => ({ | ||||
|     selectedBundle: ref<Bundle>() | ||||
|   }), | ||||
|   actions: { | ||||
|  | ||||
|     findAll(): Bundle[] { | ||||
|       return useApi("bundles"); | ||||
|     }, | ||||
|  | ||||
|     setCurrentBundle(bundle: Bundle) { | ||||
|       this.selectedBundle = bundle; | ||||
|     }, | ||||
|  | ||||
|     create(request: BundleCreationRequest) { | ||||
|       return useApi("bundles", { | ||||
|         method: "POST", | ||||
|         body: request | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| }); | ||||
| @@ -1,5 +0,0 @@ | ||||
| import { Store } from 'vuex' | ||||
| import { initialiseStores } from '~/utils/store-accessor' | ||||
| const initializer = (store: Store<any>) => initialiseStores(store) | ||||
| export const plugins = [initializer] | ||||
| export * from '~/utils/store-accessor' | ||||
							
								
								
									
										29
									
								
								frontend/store/notification.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								frontend/store/notification.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| import {defineStore} from 'pinia'; | ||||
|  | ||||
| interface Notification { | ||||
|   message?: string; | ||||
|   details?: string[] | string; | ||||
| } | ||||
|  | ||||
| export const useNotificationStore = defineStore('notification', { | ||||
|   state: () => ({ | ||||
|     hasNotification: false, | ||||
|     type: "info", | ||||
|     notification: {}, | ||||
|   }), | ||||
|   actions: { | ||||
|     pushNotification(type: 'warn' | 'info' | 'success', notification: Notification) { | ||||
|       this.notification = notification; | ||||
|       this.hasNotification = true; | ||||
|       this.type = type; | ||||
|       setTimeout(() => { | ||||
|         this.clearNotification(); | ||||
|       }, 5000); | ||||
|     }, | ||||
|     clearNotification() { | ||||
|       this.notification = {}; | ||||
|       this.type = 'info'; | ||||
|       this.hasNotification = false; | ||||
|     } | ||||
|   } | ||||
| }); | ||||
							
								
								
									
										29
									
								
								frontend/store/question.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								frontend/store/question.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| import {defineStore} from 'pinia'; | ||||
|  | ||||
| export interface Question { | ||||
|   id: number; | ||||
|   label: string; | ||||
|   description: string; | ||||
| } | ||||
|  | ||||
| export const useQuestionStore = defineStore('question', { | ||||
|   state: () => ({}), | ||||
|   actions: { | ||||
|  | ||||
|     findDefaults(axeId: number): Promise<Question> { | ||||
|       return useApi("/questions/search/defaults", { | ||||
|         params: { | ||||
|           axeId | ||||
|         } | ||||
|       }); | ||||
|     }, | ||||
|  | ||||
|     findAll(axeId: number): Promise<Question[]> { | ||||
|       return useApi("/questions/search", { | ||||
|         params: { | ||||
|           axeId, | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| }); | ||||
| @@ -1,76 +1,155 @@ | ||||
| import {Module, VuexModule, Mutation} from 'vuex-module-decorators' | ||||
| import {Question} from "~/repositories/models/question.model"; | ||||
| import {QuizRate, Response} from "~/repositories/models/quiz.model"; | ||||
| import {defineStore} from 'pinia'; | ||||
| import {useBundleStore} from "~/store/bundle"; | ||||
| import {Axe, useAxeStore} from "~/store/axe"; | ||||
| import type {Question} from "~/store/question"; | ||||
|  | ||||
| @Module({ | ||||
|   name: 'quiz', | ||||
|   stateFactory: true, | ||||
|   namespaced: true, | ||||
| }) | ||||
| export default class Quiz extends VuexModule { | ||||
|  | ||||
|   responses = new Map<number, QuizRate>; | ||||
|   questionsRatedPerAxe = new Map<number, { questionId: number; rated: boolean }[]>; | ||||
|  | ||||
|   @Mutation | ||||
|   initialize(questions: Map<number, Question[]>) { | ||||
|     questions.forEach((questions, axeId) => this.questionsRatedPerAxe.set(axeId, questions.map(value => { | ||||
|       return { | ||||
|         questionId: value.id, | ||||
|         rated: this.responses.has(value.id) | ||||
|       } | ||||
|     }))); | ||||
|   } | ||||
|  | ||||
|   @Mutation | ||||
|   reset() { | ||||
|     this.responses.clear(); | ||||
|     this.questionsRatedPerAxe.forEach((questions) => { | ||||
|       questions | ||||
|         .map(value => { | ||||
|           value.rated = false; | ||||
|           return value; | ||||
|         }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @Mutation | ||||
|   updateScoreResponse(response: Response) { | ||||
|     const previous = this.responses.get(response.questionId); | ||||
|     if (previous) { | ||||
|       this.responses.set(response.questionId, { | ||||
|         comment: previous.comment, | ||||
|         score: response.score | ||||
|       }); | ||||
|     } else { | ||||
|       this.responses.set(response.questionId, { | ||||
|         score: response.score | ||||
|       }); | ||||
|     } | ||||
|     const questionsRated = this.questionsRatedPerAxe.get(response.axeId); | ||||
|     if (questionsRated) { | ||||
|       questionsRated | ||||
|         .filter(value => value.questionId === response.questionId) | ||||
|         .map(value => { | ||||
|           value.rated = true; | ||||
|           return value; | ||||
|         }); | ||||
|     } | ||||
|     // else should not happen | ||||
|   } | ||||
|  | ||||
|   @Mutation | ||||
|   updateCommentResponse(response: Response) { | ||||
|     const previous = this.responses.get(response.questionId); | ||||
|     if (previous) { | ||||
|       this.responses.set(response.questionId, { | ||||
|         score: previous.score, | ||||
|         comment: response.comment | ||||
|       }); | ||||
|     } else { | ||||
|       this.responses.set(response.questionId, { | ||||
|         comment: response.comment | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| export interface QuizResponse { | ||||
|   question: string; | ||||
|   score: number; | ||||
|   comment: string; | ||||
| } | ||||
|  | ||||
| export interface AxeResponses { | ||||
|   axeIdentifier: number; | ||||
|   average: number; | ||||
|   responses: QuizResponse[]; | ||||
| } | ||||
|  | ||||
| export interface Quiz { | ||||
|   id: number; | ||||
|   createdDate: string; | ||||
|   axes: AxeResponses[] | ||||
| } | ||||
|  | ||||
| export interface Response { | ||||
|   axeId: number; | ||||
|   questionId: number; | ||||
|   score?: number; | ||||
|   comment?: string; | ||||
| } | ||||
|  | ||||
| export interface QuizRate { // ? | ||||
|   score?: number; | ||||
|   comment?: string; | ||||
| } | ||||
|  | ||||
| export const useQuizStore = defineStore('quiz', { | ||||
|   state: () => ({ | ||||
|     axes: ref<Axe[]>([]), | ||||
|     questions: ref<Map<number, Question[]>>(new Map()), | ||||
|     responses: ref<Map<number, QuizRate>>(new Map()), | ||||
|     questionsRatedPerAxe: ref<Map<number, Array<{ questionId: number; rated: boolean }>>>(new Map()) | ||||
|   }), | ||||
|   actions: { | ||||
|  | ||||
|     initialize() { | ||||
|       const bundle = useBundleStore().selectedBundle; | ||||
|       return useAxeStore().findAxes().then(axes => { | ||||
|         const promises: any[] = []; | ||||
|         this.axes = axes; | ||||
|         axes.forEach(axe => { | ||||
|           promises.push( | ||||
|             useApi(`/bundles/${bundle.id}/questions/search`, { | ||||
|               params: { | ||||
|                 axeId: axe.id | ||||
|               } | ||||
|             }) | ||||
|               .then((questions: Question[]) => { | ||||
|                 return { | ||||
|                   axeId: axe.identifier, | ||||
|                   questions: questions | ||||
|                 }; | ||||
|               })); | ||||
|         }); | ||||
|         Promise.all(promises).then((axeQuestions) => { | ||||
|           axeQuestions.forEach(axeQuestion => { | ||||
|             this.questions.set(axeQuestion.axeId, axeQuestion.questions) | ||||
|           }); | ||||
|           this.questions.forEach((questions, axeId) => this.questionsRatedPerAxe.set(axeId, questions.map(value => { | ||||
|             return { | ||||
|               questionId: value.id, | ||||
|               rated: this.responses.has(value.id) | ||||
|             } | ||||
|           }))); | ||||
|         }); | ||||
|       }); | ||||
|     }, | ||||
|  | ||||
|     resetResponses() { | ||||
|       this.responses.clear(); | ||||
|       this.questionsRatedPerAxe.forEach((questions) => { | ||||
|         questions | ||||
|           .map(value => { | ||||
|             value.rated = false; | ||||
|             return value; | ||||
|           }); | ||||
|       }); | ||||
|     }, | ||||
|  | ||||
|     updateScoreResponse(response: Response) { | ||||
|       const previous = this.responses.get(response.questionId); | ||||
|       if (previous) { | ||||
|         this.responses.set(response.questionId, { | ||||
|           comment: previous.comment, | ||||
|           score: response.score | ||||
|         }); | ||||
|       } else { | ||||
|         this.responses.set(response.questionId, { | ||||
|           score: response.score | ||||
|         }); | ||||
|       } | ||||
|       const questionsRated = this.questionsRatedPerAxe.get(response.axeId); | ||||
|       if (questionsRated) { | ||||
|         questionsRated | ||||
|           .filter(value => value.questionId === response.questionId) | ||||
|           .map(value => { | ||||
|             value.rated = true; | ||||
|             return value; | ||||
|           }); | ||||
|       } | ||||
|       // else should not happen | ||||
|     }, | ||||
|  | ||||
|     updateCommentResponse(response: Response) { | ||||
|       const previous = this.responses.get(response.questionId); | ||||
|       if (previous) { | ||||
|         this.responses.set(response.questionId, { | ||||
|           score: previous.score, | ||||
|           comment: response.comment | ||||
|         }); | ||||
|       } else { | ||||
|         this.responses.set(response.questionId, { | ||||
|           comment: response.comment | ||||
|         }); | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     findQuizzes(bundleId: number) { | ||||
|       return useApi("/quizzes/search", { | ||||
|         params: { | ||||
|           bundleId: bundleId, | ||||
|           sort: "createdDate,desc" | ||||
|         } | ||||
|       }); | ||||
|     }, | ||||
|  | ||||
|     findById(quizId: number) { | ||||
|       return useApi(`/quizzes/${quizId}`); | ||||
|     }, | ||||
|  | ||||
|     save() { | ||||
|       const responses = []; | ||||
|       this.responses.forEach((value, key) => { | ||||
|         responses.push({ | ||||
|           score: value.score ? value.score : 0, | ||||
|           comment: value.comment, | ||||
|           questionId: key | ||||
|         }); | ||||
|       }); | ||||
|       return useApi("/quizzes", { | ||||
|         method: "POST", | ||||
|           body: {responses, bundleId: useBundleStore().selectedBundle.id} | ||||
|       }).finally(() => this.resetResponses()); | ||||
|     } | ||||
|   } | ||||
| }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user