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