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:
2024-07-03 15:55:34 +02:00
parent f86d794239
commit dd932823f3
204 changed files with 4885 additions and 40341 deletions

View File

@@ -0,0 +1,81 @@
<script lang="ts" setup>
import {useNotificationStore} from "~/store/notification";
import {useAccountStore} from "~/store/account";
import type {ApiError} from "~/composables/fetch-api";
const email = ref();
const username = ref();
const emailConfirmation = ref();
const password = ref();
const confirmationPassword = ref();
const conditionChecked = ref(false);
function createAccount() {
if (emailConfirmation.value !== email.value) {
useNotificationStore().pushNotification("warn", {message: "Les e-mails ne correspondent pas."});
} else if (password.value !== confirmationPassword.value) {
useNotificationStore().pushNotification("warn", {message: "Les mots de passes ne correspondent pas."});
} else {
useAccountStore().create(username.value, email.value, password.value)
.then(() => {
useNotificationStore().pushNotification("success",{message: "Votre compte a bien été créer."});
navigateTo("/login");
})
.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});
});
}
}
</script>
<template>
<div>
<section>
<h1>Créer un compte</h1>
<form class="form" @submit.prevent="createAccount">
<label for="username">Nom de l'équipe *</label>
<input id="username" v-model="username" type="text" autocomplete="username" required>
<label for="email">E-mail *</label>
<input id="email" v-model="email" type="email" autocomplete="email" required>
<label for="emailConfirmation">Confirmation de l'e-mail *</label>
<input id="emailConfirmation" v-model="emailConfirmation" type="email" autocomplete="email" required>
<p class="form__help">Votre mot de passe doit fait au moins 8 caractères, et être composé dau moins une
majuscule, une minuscule,
un chiffre de 0 à 9, et un caractère spécial parmi @?!#;</p>
<label for="newPassword">Mot de passe *</label>
<input id="newPassword" v-model="password" type="password" autocomplete="newPassword" required>
<label for="confirmationPassword">Confirmation du mot de passe *</label>
<input id="confirmationPassword" v-model="confirmationPassword" type="password" autocomplete="newPassword"
required>
<label>
<input v-model="conditionChecked" type="checkbox" required/>
En continuant, jaccepte
<nuxt-link to="/cgu">les conditions d'utilisation de Boussole PLUSS et jai lu la politique de
confidentialité
</nuxt-link>
</label>
<div class="button-container">
<nuxt-link class="button gray button-back" to="/login" aria-label="Retour à la page précédente"></nuxt-link>
<button class="button orange" type="submit" :disabled="!conditionChecked">Enregistrer</button>
</div>
</form>
</section>
</div>
</template>
<style lang="scss" scoped>
@import "assets/css/spacing";
.form {
input[type="checkbox"] {
grid-column: span 2 / 3;
margin-top: $small;
}
}
</style>

View File

@@ -0,0 +1,65 @@
<script lang="ts" setup>
import {useAccountStore} from "~/store/account";
import {useNotificationStore} from "~/store/notification";
import {useAuthStore} from "~/store/auth";
import type {ApiError} from "~/composables/fetch-api";
const email = ref(useAuthStore().user.email);
const username = ref(useAuthStore().user.username);
const emailConfirmation = ref();
function updateAccount() {
if (emailConfirmation.value !== email.value) {
useNotificationStore().pushNotification("warn",{message: "Les e-mails ne correspondent pas."});
} else {
useAccountStore().update(username.value, email.value)
.then(() => {
useNotificationStore().pushNotification("success",{message: "Votre compte a bien été mis à jour."});
useAuthStore().user.username = username.value;
useAuthStore().user.email = email.value;
navigateTo("/dashboard");
})
.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});
});
}
}
</script>
<template>
<div>
<section>
<h1>Mon compte</h1>
<form class="form" @submit.prevent="updateAccount">
<label for="username">Nom de l'équipe *</label>
<input id="username" v-model="username" type="text" autocomplete="username" required>
<label for="email">E-mail *</label>
<input id="email" v-model="email" type="email" autocomplete="email" required>
<label for="emailConfirmation">Confirmation de l'e-mail *</label>
<input id="emailConfirmation" v-model="emailConfirmation" type="email" autocomplete="email" required>
<button class="button orange" type="submit">Enregistrer</button>
</form>
<div class="button-container">
<nuxt-link class="button gray button-back" to="/dashboard" aria-label="Retour à la page précédente"></nuxt-link>
<nuxt-link to="/account/password" class="button blue" type="submit">Modifier mon mot de passe</nuxt-link>
</div>
</section>
</div>
</template>
<style lang="scss" scoped>
@import "assets/css/spacing";
.form {
& [type="submit"] {
grid-column: span 2 / 3;
margin-top: $x_small;
}
}
</style>

View File

@@ -0,0 +1,56 @@
<script lang="ts" setup>
import {useAccountStore} from "~/store/account";
import type {ApiError} from "~/composables/fetch-api";
import {useNotificationStore} from "~/store/notification";
definePageMeta({
layout: 'main-header'
});
const email = ref();
const newPassword = ref();
const confirmationPassword = ref();
onMounted(() => {
if (!useRoute().query.token) {
navigateTo("/");
}
});
function resetPassword() {
const token = useRoute().query.token;
useAccountStore().resetPassword(token, email.value, newPassword.value, confirmationPassword.value)
.then(() => {
useNotificationStore().pushNotification("success",{message: "Votre mot de passe a bien été ré-initialisé."});
navigateTo("/login");
})
.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});
});
}
</script>
<template>
<section>
<h1>-initialisé mon mot de passe</h1>
<form class="form" @submit.prevent="resetPassword">
<label for="email">Mon e-mail</label>
<input id="email" v-model="email" type="email" autocomplete="username" required>
<p class="form__help">Votre mot de passe doit fait au moins 8 caractères, et être composé dau moins une
majuscule, une minuscule, un chiffre de 0 à 9, et un caractère spécial parmi @?!#;</p>
<label for="newPassword">Nouveau mot de passe</label>
<input id="newPassword" v-model="newPassword" type="password" autocomplete="newPassword" required>
<label for="confirmationPassword">Confirmation du mot de passe</label>
<input id="confirmationPassword" v-model="confirmationPassword" type="password" autocomplete="newPassword"
required>
<div class="button-container">
<button class="button orange" type="submit">Enregistrer</button>
</div>
</form>
</section>
</template>

View File

@@ -0,0 +1,47 @@
<script lang="ts" setup>
import {useAccountStore} from "~/store/account";
import type {ApiError} from "~/composables/fetch-api";
import {useNotificationStore} from "~/store/notification";
const currentPassword = ref();
const newPassword = ref();
const confirmationPassword = ref();
function updatePassword() {
useAccountStore().updatePassword(currentPassword.value, newPassword.value, confirmationPassword.value)
.then(() => {
useNotificationStore().pushNotification("success",{message: "Votre mot de passe a bien été modifié."});
navigateTo("/account");
})
.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});
});
}
</script>
<template>
<section>
<h1>Modifier mon mot de passe</h1>
<form class="form" @submit.prevent="updatePassword">
<label for="currentPassword">Mot de passe actuel</label>
<input id="currentPassword" v-model="currentPassword" type="password" autocomplete="currentPassword" required>
<p class="form__help">Votre mot de passe doit fait au moins 8 caractères, et être composé dau moins une
majuscule, une minuscule,
un chiffre de 0 à 9, et un caractère spécial parmi @?!#;</p>
<label for="newPassword">Nouveau mot de passe</label>
<input id="newPassword" v-model="newPassword" type="password" autocomplete="newPassword" required>
<label for="confirmationPassword">Confirmation du mot de passe</label>
<input id="confirmationPassword" v-model="confirmationPassword" type="password" autocomplete="newPassword"
required>
<div class="button-container">
<nuxt-link class="button gray button-back" to="/account" aria-label="Retour à la page précédente"></nuxt-link>
<button class="button orange" type="submit">Enregistrer</button>
</div>
</form>
</section>
</template>

View File

@@ -0,0 +1,58 @@
<script lang="ts" setup>
import {useAccountStore} from "~/store/account";
import {useNotificationStore} from "~/store/notification";
import type {ApiError} from "~/composables/fetch-api";
definePageMeta({
layout: 'main-header'
});
const email = ref();
const loading = ref(false);
function sendEmail() {
loading.value = true;
useAccountStore()
.requestPasswordReset(email.value)
.then(() => {
useNotificationStore().pushNotification("success",{message: "Consultez vos emails pour réinitialiser votre mot de passe."})
navigateTo("login");
})
.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});
})
.finally(() => {
loading.value = false;
});
}
</script>
<template>
<section>
<h1>Mot de passe oublié</h1>
<form class="form" @submit.prevent="sendEmail">
<p>Entrez votre email pour recevoir un lien permettant de réinitialiser le mot de passe associé à votre
compte.</p>
<input v-model="email" type="email" placeholder="E-mail" required/>
<div class="button-container">
<nuxt-link class="button gray button-back" to="/login" aria-label="Retour à la page de login"></nuxt-link>
<button class="button orange" type="submit">Envoyer l'e-mail</button>
</div>
<loader v-if="loading"/>
</form>
</section>
</template>
<style lang="scss" scoped>
@import "assets/css/spacing";
.form {
display: flex;
flex-direction: column;
gap: $x_small;
}
</style>

View File

@@ -0,0 +1,150 @@
<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 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.content);
});
});
axes.value = response;
});
});
function createBundle() {
useBundleStore()
.create({
label: label.value,
questions: questions.value
})
.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});
});
}
// const questionsInput = computed<boolean>(() => {
// let emptyAxe = 0;
// for (const [key, value] of questions.value.entries()) {
// if (value.length === 0) {
// emptyAxe += 1;
// }
// console.log(`${key}: ${value.length}`);
// }
// return emptyAxe === questions.value.keys().length;
// });
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);
}
</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>
{{ questions }}
<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">
<p>{{ axe.title }}</p>
<button class="button blue" type="button" @click="showAxeModal(axe)">Configurer les
questions
</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;
}
}
}
</style>

View File

@@ -0,0 +1,95 @@
<script lang="ts" setup>
import {type Bundle, useBundleStore} from "~/store/bundle";
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 selectBundle(bundleId: number) {
useBundleStore().selectedBundle = bundleId;
navigateTo("/dashboard");
}
</script>
<template>
<section v-if="!loading">
<ul class="bundle-list">
<li v-for="bundle in bundles">
<article class="bundle-list__item">
<main>
<h1>{{ bundle.label }}</h1>
<dl class="bundle-list__item__attribute">
<dt>Dernière auto-évaluation réalisée</dt>
<dd>{{ new Date(bundle.lastQuizzDate).toLocaleDateString() || 'NA' }}</dd>
</dl>
<dl class="bundle-list__item__attribute">
<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="selectBundle(bundle.id)">Voir</button>
</footer>
</article>
</li>
</ul>
<div class="button-container">
<nuxt-link to="/bundle/create">Nouvelle boussole</nuxt-link>
</div>
</section>
<loader v-else/>
</template>
<style lang="scss" scoped>
.bundle-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(22.5rem, 1fr));
grid-gap: $x_small;
list-style: none;
margin-top: $medium;
&__item {
display: flex;
flex-direction: column;
gap: $x_small;
padding: $x_small;
border-radius: 20px;
@include border-shadow();
h1 {
margin: 0 0 $x_small 0;
}
&__attribute {
display: flex;
gap: $xxx_small;
dt {
font-weight: bold;
&:after {
content: ' :';
}
}
}
&__button-container {
display: flex;
justify-content: center;
}
}
}
</style>

20
frontend/pages/cgu.vue Normal file
View File

@@ -0,0 +1,20 @@
<script lang="ts" setup>
definePageMeta({
layout: 'main-header'
});
</script>
<template>
<section>
<h1>CGU</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Architecto consequatur, consequuntur et expedita libero
non soluta sunt ullam vel velit voluptatibus voluptatum? Accusamus blanditiis est obcaecati temporibus velit. Dicta
doloribus eveniet id incidunt suscipit. Accusamus ad aspernatur at aut, beatae laboriosam modi natus nemo, officia
perspiciatis porro quisquam, totam vel.
</p>
<div class="button-container">
<nuxt-link class="button gray button-back" to="/account/create" aria-label="Retour à la page précédente">
</nuxt-link>
</div>
</section>
</template>

View File

@@ -1,74 +1,65 @@
<template>
<div class="content">
<team-header/>
<hr/>
<div v-if="loading" class="center" >
<loader/>
</div>
<section v-if="currentResult" class="last-quiz">
<div class="last-quiz-header">
<span class="date"> {{ currentResult.createdDate | formatDate }}</span>
<nuxt-link
class="link" :to="{ path: '/details', query: { quiz: currentResult.id }}">
+ Voir le détail
</nuxt-link>
</div>
<div v-if="currentResult && currentResult.scores" class="chart-area">
<polar-area-chart :data="currentResult.scores.map(value => value.scoreAvg)"/>
<Legend/>
</div>
</section>
<section v-if="quizzes.length > 0" class="history" >
<div v-for="q in quizzes" :key="q.id">
<button @click="setCurrent(q)"><span>Boussole - {{ q.createdDate | formatDate }}</span><span></span></button>
</div>
</section>
<section v-else-if="!loading" class="center">
Aucune auto-évaluation n'a été faite. Veuillez en réaliser une première.
</section>
<div class="button-container">
<nuxt-link class="button orange" to="/quiz">Nouveau</nuxt-link>
</div>
</div>
</template>
<script lang="ts" setup>
<script lang="ts">
import {Component, Vue} from "vue-property-decorator";
import {AxiosResponse} from "axios";
import {RepositoryFactory} from "~/repositories/RepositoryFactory";
import {RestResponse} from "~/repositories/models/rest-response.model";
import {Quiz} from "~/repositories/models/quiz.model";
import {type Quiz, useQuizStore} from "~/store/quiz";
import {useBundleStore} from "~/store/bundle";
@Component
export default class History extends Vue {
const quizzes = ref<Quiz[]>([]);
const currentResult = ref<Quiz>();
readonly quizRepository = RepositoryFactory.get('quiz');
onMounted(() => {
useQuizStore().findQuizzes(useBundleStore().selectedBundle).then((response: Page<Quiz>) => {
quizzes.value = response.content;
currentResult.value = quizzes.value.length > 0 ? quizzes.value[0] : null;
});
});
private quizzes: Quiz[] = [];
private currentResult: Quiz | null = null;
private loading = true;
function toDisplayDate(q: Quizz) {
const date = new Date(q.createdDate);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
}
async mounted() {
await this.quizRepository.findMine().then((response: AxiosResponse<RestResponse<Quiz>>) => {
this.quizzes = response.data._embedded.quizzes;
});
this.currentResult = this.quizzes.length > 0 ? this.quizzes[0] : null;
this.loading = false;
}
private setCurrent(quiz: Quiz) {
this.currentResult = quiz;
}
function setCurrent(quiz: Quiz) {
currentResult.value = quiz;
}
</script>
<style lang="scss" scoped>
@import "assets/css/color";
@import "assets/css/spacing";
@import "assets/css/font";
<template>
<section v-if="currentResult" class="last-quiz">
<div class="last-quiz-header">
<span class="date"> {{ toDisplayDate(currentResult) }}</span>
<nuxt-link
class="link" :to="{ path: '/details', query: { quiz: currentResult.id }}">
+ Voir le détail
</nuxt-link>
</div>
<div v-if="currentResult && currentResult.axes" class="chart-area">
<polar-area-chart :data="currentResult.axes.map(value => value.average)"/>
<Legend/>
</div>
</section>
<section v-if="quizzes.length > 0">
<ul class="history">
<li class="history__item" v-for="q in quizzes">
<input :id="q.id" type="radio" @change="setCurrent(q)" :checked="q === currentResult" name="quiz"/>
<label :for="q.id">
<span>Boussole - {{ toDisplayDate(q) }}</span><span></span>
</label>
</li>
</ul>
</section>
<section v-else class="center">
Aucune auto-évaluation n'a été faite. Veuillez en réaliser une première.
</section>
<div class="button-container">
<nuxt-link class="button gray button-back" to="/bundle" aria-label="Retour à la page précédente"></nuxt-link>
<nuxt-link class="button orange" to="/quiz">Nouveau</nuxt-link>
</div>
</template>
<style lang="scss" scoped>
.last-quiz {
margin-bottom: $x_small 0;
margin: $x_small 0;
}
.last-quiz-header {
@@ -90,37 +81,45 @@ export default class History extends Vue {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
margin: $x_small 0;
gap: $x_small;
}
.history {
margin-top: $x_small;
}
.history button {
display: flex;
width: 100%;
justify-content: space-between;
padding: 16px;
margin: $xxx_small 0;
flex-direction: column;
gap: $xxx_small;
list-style: none;
background: $gray_1;
border-radius: 8px;
border: none;
color: $gray_4;
font-weight: 700;
font-size: $tertiary-font-size;
.history__item {
&:hover {
text-decoration: none;
cursor: pointer;
background: $gray_3;
color: $gray_1;
input {
position: absolute;
opacity: 0;
&:checked + label {
outline: 3px solid $gray_4;
}
}
label {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: $gray_1;
border-radius: 8px;
color: $gray_4;
font-weight: 700;
font-size: $tertiary-font-size;
&:hover {
text-decoration: none;
cursor: pointer;
background: $gray_3;
color: $gray_1;
}
}
}
}
.button-container {
margin-bottom: $x_small;
}
</style>

View File

@@ -1,91 +1,76 @@
<script lang="ts" setup>
import {type AxeResponses, type QuizResponse, useQuizStore} from "~/store/quiz";
import Quiz from "~/pages/quiz.vue";
import {type Axe, useAxeStore} from "~/store/axe";
const loading = ref(true);
const quiz = ref<Quiz>();
const axes = ref<Axe[]>();
onMounted(() => {
if (!useRoute().query.quiz) {
navigateTo("/dashboard");
}
loading.value = true;
const quizId = Number.parseInt(useRoute().query.quiz as string);
useQuizStore().findById(quizId)
.then((result: Quiz) => {
quiz.value = result;
})
.then(() => {
useAxeStore().findAxes().then(result => {
axes.value = result.filter(axe => {
return quiz.value.axes.filter(axeResponse => axeResponse.axeIdentifier === axe.identifier).length > 0;
});
});
})
.finally(() => {
loading.value = false;
});
});
function getAverage(axe: Axe): number {
const axeResponses = quiz.value.axes.filter((response: AxeResponses) => response.axeIdentifier === axe.identifier);
if (axeResponses.length === 1) {
return axeResponses[0].average;
}
return 0;
}
function getResponses(axe: Axe): QuizResponse[] {
const axeResponses = quiz.value.axes.filter((response: AxeResponses) => response.axeIdentifier === axe.identifier);
if (axeResponses.length === 1) {
return axeResponses[0].responses;
}
return [];
}
function print() {
window.print();
}
</script>
<template>
<div class="content">
<team-header/>
<hr/>
<div v-if="!loading">
<span class="date">{{ quiz.createdDate | formatDate }}</span>
<quiz-axe-details
v-for="axe in axes"
:key="axe.identifier"
:axe="axe"
:score="getScore(axe)"
:responses="getResponses(axe)"/>
</div>
<loader v-else class="center"/>
<div class="button-container">
<nuxt-link class="button orange" to="/dashboard">Retour à l'accueil</nuxt-link>
</div>
<section v-if="!loading">
<span class="date">{{ new Date(quiz.createdDate).toLocaleDateString() }}</span>
<quiz-axe-details
v-for="axe in axes"
:axe="axe"
:average="getAverage(axe)"
:responses="getResponses(axe)"/>
</section>
<loader v-else class="center"/>
<div class="button-container">
<nuxt-link class="button gray button-back" to="/dashboard" aria-label="Retour à l'accueil"></nuxt-link>
<button class="button orange button-print" @click="print">Imprimer</button>
</div>
</template>
<script lang="ts">
import {Component, Vue} from "nuxt-property-decorator";
import {AxiosResponse} from "axios";
import {RepositoryFactory} from "~/repositories/RepositoryFactory";
import {Quiz, ResponseWithQuestion, Score} from "~/repositories/models/quiz.model";
import QuizAxeDetails from "~/components/QuizAxeDetails.vue";
import {Axe} from "~/repositories/models/axe.model";
import {RestResponse} from "~/repositories/models/rest-response.model";
@Component({
components: {QuizAxeDetails}
})
export default class Result extends Vue {
readonly axeRepository = RepositoryFactory.get('axe');
readonly quizRepository = RepositoryFactory.get('quiz');
private axes: Axe[] = [];
private quiz: Quiz | null = null;
private responses: ResponseWithQuestion[] = [];
private loading = false;
created() {
if (!this.$route.query.quiz) {
this.$router.push("/dashboard");
}
try {
this.loading = true;
const quizId = Number.parseInt(this.$route.query.quiz as string);
this.quizRepository.findById(quizId)
.then((response: AxiosResponse<Quiz>) => {
this.quiz = response.data;
this.responses = response.data._embedded.responses;
return response;
})
.then(() => {
return this.axeRepository.findAll()
.then((response: AxiosResponse<RestResponse<Axe>>) => {
this.axes = response.data._embedded.axes;
return response;
});
})
.finally(() => {
this.loading = false;
});
} catch (e: any) {
console.info("error", e);
this.loading = false;
}
}
getScore(axe: Axe): Score {
const responses = this.getResponses(axe);
return {
axeIdentifier: axe.identifier,
scoreAvg: responses.reduce((total, response) => total + response.score, 0) / responses.length
};
}
getResponses(axe: Axe) {
return this.responses.filter((response: ResponseWithQuestion) => response.axeIdentifier === axe.identifier);
}
}
</script>
<style lang="scss" scoped>
@import "assets/css/color";
@import "assets/css/font";
section {
margin: $x_small 0;
}
.date {
font-weight: 700;
@@ -93,4 +78,10 @@ export default class Result extends Vue {
color: $gray_4;
}
@media print {
.button-container {
display: none;
}
}
</style>

14
frontend/pages/error.vue Normal file
View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { NuxtError } from '#app'
const props = defineProps({
error: Object as () => NuxtError
})
</script>
<template>
<div>
<h1>{{ error.statusCode }}</h1>
<nuxt-link to="/">Go back home</nuxt-link>
</div>
</template>

View File

@@ -1,3 +1,9 @@
<script setup lang="ts">
definePageMeta({
layout: 'main-header'
});
</script>
<template>
<Home />
<home />
</template>

View File

@@ -1,80 +1,61 @@
<template>
<div>
<Header/>
<form id="login_form" class="content login" @submit.prevent="authenticate">
<input v-model="username" type="text" required autocomplete="username" placeholder="Nom de l'équipe" aria-label="Nom de l'équipe"/>
<input id="code" v-model="password" type="password" required autocomplete="current-password" placeholder="Code" aria-label="Code"/>
<div>
<label>
<input v-model="conditionChecked" type="checkbox" required/>
En continuant, jaccepte les conditions d'utilisation de Boussole PLUSS et jai lu la politique de
confidentialité
</label>
</div>
<button class="button blue" type="submit" :disabled="!conditionChecked">
Continuer
</button>
<div class="error-container">{{ error }}</div>
</form>
</div>
</template>
<script lang="ts" setup>
import {useAuthStore} from "~/store/auth";
import {useNotificationStore} from "~/store/notification";
<script lang="ts">
import {Component, Vue} from "nuxt-property-decorator";
import {HTTPResponse} from "@nuxtjs/auth-next/dist";
definePageMeta({
layout: 'main-header'
});
@Component
export default class Login extends Vue {
private username = "";
private password = "";
private conditionChecked: boolean = false;
private error = "";
const email = ref("nicolas.doby@gmail.com");
const password = ref("Password123;");
async authenticate() {
try {
this.error = "";
const response = await this.$auth.loginWith('local', {
data: {
username: this.username,
password: this.password
}
}) as HTTPResponse;
if (response && response.data) {
const { token } = response.data;
this.$axios.defaults.headers.common = { Authorization: `Bearer ${token}` };
} else {
console.error("Unable to login, no data in HTTP response")
function authenticate() {
useAuthStore()
.login(email.value, password.value)
.then(() => {
if (useAuthStore().authenticated) {
navigateTo("/bundle");
}
} catch (e: any) {
this.error = e;
console.info("error", e);
}
}
})
.catch(e => {
console.dir(e)
useNotificationStore().pushNotification("warn", e);
});
}
</script>
<template>
<form class="login" @submit.prevent="authenticate">
<input v-model="email" type="email" required autocomplete="username" placeholder="E-mail" aria-label="E-mail"/>
<input id="code" v-model="password" type="password" required autocomplete="current-password"
placeholder="Mot de passe" aria-label="Mot de passe"/>
<button class="button orange" type="submit">
Continuer
</button>
<nuxt-link class="link-forget-password" to="/account/password/reset">J'ai oublié mon mot de passe</nuxt-link>
<nuxt-link class="button blue" to="/account/create">Créer un compte</nuxt-link>
</form>
</template>
<style lang="scss" scoped>
@import "assets/css/spacing";
form.login {
input[type="text"] {
margin: $x_small 0;
}
input[type="password"] {
margin-bottom: $small;
}
display: flex;
flex-direction: column;
gap: $xxx_small;
padding: $medium 0;
button {
margin: $medium 0;
margin-block: $medium 0;
}
}
.error-container {
display: flex;
justify-content: center;
color: red;
.link-forget-password {
align-self: center;
margin-block: $xx_small $x_medium;
}
}
</style>

View File

@@ -1,8 +1,71 @@
<template>
<div class="content">
<team-header/>
<hr/>
<script lang="ts" setup>
const currentAxe = ref<Axe>();
const currentAxeIdentifier = ref(1);
const questions = computed(() => useQuizStore().questions);
const axes = computed(() => useQuizStore().axes);
const loading = ref(true);
const saving = ref(false);
const isFullRated = ref(false);
import {type Axe, useQuizStore} from "~/store/quiz";
const store = useQuizStore();
onMounted(() => {
loading.value = true;
store.initialize().finally(()=> {
initializeState();
store.resetResponses();
loading.value = false;
});
});
function showPrevious() {
if (currentAxeIdentifier.value > 1) {
currentAxeIdentifier.value--;
initializeState();
}
}
function showNext() {
if (currentAxeIdentifier.value < axes.value.length) {
currentAxeIdentifier.value++;
initializeState();
setTimeout(() => {
scrollTop();
}, 50)
}
}
function initializeState() {
currentAxe.value = axes.value.find(value => value.identifier === currentAxeIdentifier.value);
const questions = store.questionsRatedPerAxe.get(currentAxeIdentifier.value);
const unratedQuestions = questions ? questions.filter(value => !value.rated) : [];
isFullRated.value = unratedQuestions.length === 0;
}
function scrollTop() {
window.scrollTo({
top: 60,
behavior: "smooth",
});
}
function saveResult() {
store.save().then((response) => {
navigateTo({path: "result", query: {quiz: response.id + ""}});
});
}
function onRate(event: { isFullRated: boolean }) {
isFullRated.value = event.isFullRated;
}
</script>
<template>
<div v-if="!loading">
<quiz-part
:key="currentAxe.identifier" :axe-number="currentAxe.identifier" :total-axes="axes.length"
@@ -17,18 +80,18 @@
<div class="button-container">
<nuxt-link
v-if="currentAxeIdentifier <= 1"
class="button gray"
class="button gray button-back"
to="/dashboard" aria-label="Précédent">
</nuxt-link>
<button v-if="currentAxeIdentifier > 1" class="button gray" @click="showPrevious"></button>
<button
v-if="currentAxeIdentifier < axes.length" class="button blue" :disabled="!isFilled"
v-if="currentAxeIdentifier < axes.length" class="button blue" :disabled="!isFullRated"
@click="showNext"
>Suivant
</button>
<button
v-if="currentAxeIdentifier >= axes.length" class="button orange"
:disabled="!isFilled || saving" @click="saveResult()"
:disabled="!isFullRated || saving" @click="saveResult()"
>Valider
</button>
</div>
@@ -36,137 +99,4 @@
<div v-else class="center">
<loader/>
</div>
</div>
</template>
<script lang="ts">
import {AxiosResponse} from "axios";
import {Component, Vue} from "nuxt-property-decorator";
import {RepositoryFactory} from "~/repositories/RepositoryFactory";
import {Axe} from "~/repositories/models/axe.model";
import {RestResponse} from "~/repositories/models/rest-response.model";
import {Question} from "~/repositories/models/question.model";
import {Quiz} from "~/repositories/models/quiz.model";
import {quizStore} from "~/utils/store-accessor";
@Component
export default class Login extends Vue {
readonly axeRepository = RepositoryFactory.get("axe");
readonly questionRepository = RepositoryFactory.get("question");
private axes: Axe[] = [];
private currentAxe?: Axe | undefined;
private currentAxeIdentifier = 1;
private questions: Map<number, Question[]> = new Map<number, []>();
private loading = true;
private saving = false;
private isFullRated = false;
mounted() {
this.loading = true;
this.axeRepository
.findAll()
.then((response: AxiosResponse<RestResponse<Axe>>) => {
this.axes = response.data._embedded.axes;
const promises: any[] = [];
this.axes.forEach(axe => {
promises.push(
this.questionRepository
.findAllByAxeId(axe.identifier)
.then((response: AxiosResponse<RestResponse<Question>>) => {
return {
axeId: axe.identifier,
questions: response.data._embedded.questions
};
}));
});
Promise.all(promises).then((axeQuestions) => {
axeQuestions.forEach(axeQuestion => {
this.questions.set(axeQuestion.axeId, axeQuestion.questions)
});
quizStore.initialize(this.questions);
this.initializeState();
this.loading = false;
});
});
}
showPrevious() {
if (this.currentAxeIdentifier > 1) {
this.currentAxeIdentifier--;
this.initializeState();
}
}
showNext() {
if (this.currentAxeIdentifier < this.axes.length) {
this.currentAxeIdentifier++;
this.initializeState();
setTimeout(() => {
this.scrollTop();
}, 50)
}
}
initializeState() {
this.currentAxe = this.axes.find(value => value.identifier === this.currentAxeIdentifier);
const questions = quizStore.questionsRatedPerAxe.get(this.currentAxeIdentifier);
const unratedQuestions = questions ? questions.filter(value => !value.rated) : [];
this.isFullRated = unratedQuestions.length === 0;
}
scrollTop() {
window.scrollTo({
top: 60,
behavior: "smooth",
});
}
saveResult() {
const responsesFormatted: { score: number; comment?: string; questionId: number }[] = [];
quizStore.responses.forEach((value, key) => {
responsesFormatted.push({
score: value.score ? value.score : 0,
comment: value.comment,
questionId: key
});
});
this.saving = true;
RepositoryFactory.get('quiz').save(responsesFormatted).then((response: AxiosResponse<Quiz>) => {
this.saving = false;
quizStore.reset();
this.$router.push({path: "/result", query: {quiz: response.data.id + ""}});
});
}
onRate(event: { isFullRated: boolean }) {
this.isFullRated = event.isFullRated;
}
get isFilled() {
return this.isFullRated;
}
}
</script>
<style lang="scss" scoped>
.button-container {
display: flex;
flex-direction: row;
justify-content: center;
> a {
margin-right: 5px;
}
> button {
margin-left: 5px;
}
}
</style>

View File

@@ -1,20 +0,0 @@
<template>
<div>
Redirect
</div>
</template>
<script lang="ts">
import {Component, Vue} from "nuxt-property-decorator";
@Component
export default class Redirect extends Vue {
created(){
if (this.$auth.user) {
this.$router.push(`/dashboard`);
} else {
window.location.reload()
}
}
}
</script>

View File

@@ -1,48 +1,32 @@
<template>
<div class="content">
<team-header/>
<hr/>
<section>
<h1>Bravo !</h1>
<p class="text-center">Merci pour votre contribution à la production locale et bravo pour votre implication.</p>
<div v-if="!loading" class="chart-area">
<polar-area-chart :data="scores"/>
<Legend/>
</div>
<loader v-else class="center"/>
<nuxt-link class="button orange" to="/dashboard">Retour à l'accueil</nuxt-link>
<script lang="ts" setup>
</section>
</div>
import {type Quiz, useQuizStore} from "~/store/quiz";
const scores = ref<number[]>();
const loading = ref(true);
onMounted(() => {
loading.value = true;
useQuizStore().findById(useRoute().query.quiz).then((quiz: Quiz) => {
scores.value = quiz.axes.map(value => value.average);
})
.finally(() => loading.value = false);
});
</script>
<template>
<section>
<h1>Bravo !</h1>
<p class="text-center">Merci pour votre contribution à la production locale et bravo pour votre implication.</p>
<div v-if="!loading" class="chart-area">
<polar-area-chart :data="scores"/>
<Legend/>
</div>
<loader v-else class="center"/>
<nuxt-link class="button orange" to="/dashboard">Retour à l'accueil</nuxt-link>
</section>
</template>
<script lang="ts">
import {Component, Vue} from "nuxt-property-decorator";
import {AxiosResponse} from "axios";
import {RepositoryFactory} from "~/repositories/RepositoryFactory";
import { Score} from "~/repositories/models/quiz.model";
@Component
export default class Result extends Vue {
readonly quizRepository = RepositoryFactory.get('quiz');
private scores: number[] = [];
private loading = false;
mounted() {
this.loading = true;
try {
this.quizRepository.findScores(this.$route.query.quiz).then((response: AxiosResponse<Score[]>) => {
this.scores = response.data.map(value => value.scoreAvg);
});
} catch (e: any) {
console.info("error", e);
} finally {
this.loading = false;
}
}
}
</script>
<style lang="scss" scoped>