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:
128
frontend/pages/account/create.vue
Normal file
128
frontend/pages/account/create.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import {useNotificationStore} from "~/store/notification";
|
||||
import {useAccountStore} from "~/store/account";
|
||||
import type {ApiError} from "~/composables/fetch-api";
|
||||
|
||||
definePageMeta({
|
||||
layout: 'main-header'
|
||||
});
|
||||
|
||||
const email = ref();
|
||||
const username = ref();
|
||||
const emailConfirmation = ref();
|
||||
const password = ref();
|
||||
const confirmationPassword = ref();
|
||||
const conditionChecked = ref(false);
|
||||
const cguModalVisible = ref(false);
|
||||
|
||||
function createAccount() {
|
||||
if (emailConfirmation.value !== email.value) {
|
||||
useNotificationStore().pushNotification("warn", {message: "Saisir le même e-mail dans les champs 'E-mail' et 'Confirmation de l'e-mail'."});
|
||||
} else if (password.value !== confirmationPassword.value) {
|
||||
useNotificationStore().pushNotification("warn", {message: "Saisir le même mot de passe dans les champs 'Mot de passe' et 'Confirmation du mot de passe'."});
|
||||
} else {
|
||||
useAccountStore().create(username.value, email.value, password.value)
|
||||
.then(() => {
|
||||
useNotificationStore().pushNotification("success", {
|
||||
message: "Votre compte a bien été créé.",
|
||||
details: "Vous allez recevoir un e-mail."
|
||||
});
|
||||
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" required>
|
||||
<p class="form__help">Votre mot de passe doit fait au moins 8 caractères, et être composé d’au 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" required>
|
||||
<label for="confirmationPassword">Confirmation du mot de passe *</label>
|
||||
<input id="confirmationPassword" v-model="confirmationPassword" type="password" required>
|
||||
<label>
|
||||
<input type="checkbox" v-model="conditionChecked" required />
|
||||
En continuant, j’accepte
|
||||
<button class="button-link" @click="cguModalVisible = true" type="button">les conditions d'utilisation de Boussole PLUSS et j’ai lu la politique de
|
||||
confidentialité
|
||||
</button>
|
||||
</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>
|
||||
<cgu-modal :visible="cguModalVisible" @close="cguModalVisible = false; conditionChecked = false;" @validate="conditionChecked = true; cguModalVisible = false"/>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "assets/css/spacing";
|
||||
|
||||
.form {
|
||||
|
||||
//.checkbox {
|
||||
// input {
|
||||
// position: absolute;
|
||||
// opacity: 0;
|
||||
// cursor: pointer;
|
||||
// height: 0;
|
||||
// width: 0;
|
||||
// }
|
||||
// input:checked ~ &-checkmark:after {
|
||||
// display: block;
|
||||
// }
|
||||
//
|
||||
// &-checkmark {
|
||||
// position: absolute;
|
||||
// top: 0;
|
||||
// left: 0;
|
||||
// height: 25px;
|
||||
// width: 25px;
|
||||
// background-color: #eee;
|
||||
//
|
||||
// &:after {
|
||||
// left: 9px;
|
||||
// top: 5px;
|
||||
// width: 5px;
|
||||
// height: 10px;
|
||||
// border: solid white;
|
||||
// border-width: 0 3px 3px 0;
|
||||
// rotate: 45deg;
|
||||
// }
|
||||
// }
|
||||
// &-checkmark:after {
|
||||
// content: "";
|
||||
// position: absolute;
|
||||
// display: none;
|
||||
// }
|
||||
// &:hover input ~ &-checkmark {
|
||||
// background-color: #ccc;
|
||||
// }
|
||||
//}
|
||||
input[type="checkbox"] {
|
||||
grid-column: span 2 / 3;
|
||||
margin-top: $medium;
|
||||
}
|
||||
}
|
||||
</style>
|
68
frontend/pages/account/index.vue
Normal file
68
frontend/pages/account/index.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<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: "Saisir le même e-mail dans les champs 'E-mail' et 'Confirmation de l'e-mail'."});
|
||||
} else {
|
||||
useAccountStore().update(username.value, email.value)
|
||||
.then(async () => {
|
||||
if (useAuthStore().user.email !== email.value) {
|
||||
await useAuthStore().refreshSession();
|
||||
}
|
||||
useNotificationStore().pushNotification("success", {message: "Votre compte a bien été mis à jour."});
|
||||
useAuthStore().user.username = username.value;
|
||||
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});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
</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" required>
|
||||
<button class="button orange" type="submit">Enregistrer</button>
|
||||
</form>
|
||||
<div class="button-container">
|
||||
<nuxt-link to="/bundle" class="button gray button-back" aria-label="Retour à la page principale">❮
|
||||
</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: $small;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
55
frontend/pages/account/password/confirm-reset.vue
Normal file
55
frontend/pages/account/password/confirm-reset.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<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>Ré-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é d’au 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" required>
|
||||
<label for="confirmationPassword">Confirmation du mot de passe</label>
|
||||
<input id="confirmationPassword" v-model="confirmationPassword" type="password" required>
|
||||
<div class="button-container">
|
||||
<button class="button orange" type="submit">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</template>
|
||||
|
46
frontend/pages/account/password/index.vue
Normal file
46
frontend/pages/account/password/index.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<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é d’au 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" required>
|
||||
<label for="confirmationPassword">Confirmation du mot de passe</label>
|
||||
<input id="confirmationPassword" v-model="confirmationPassword" type="password" 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>
|
||||
|
61
frontend/pages/account/password/reset.vue
Normal file
61
frontend/pages/account/password/reset.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<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 class="loader" v-if="loading"/>
|
||||
</form>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $small;
|
||||
}
|
||||
|
||||
.loader {
|
||||
align-self: center;
|
||||
}
|
||||
</style>
|
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>
|
20
frontend/pages/cgu.vue
Normal file
20
frontend/pages/cgu.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
definePageMeta({
|
||||
layout: 'main-header'
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<h1>Conditions Générales d’Utilisation de la Boussole PLUSS</h1>
|
||||
<cgu />
|
||||
<nuxt-link class="button gray button-link" to="/">Retour à l'accueil</nuxt-link>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.button-link {
|
||||
margin: $medium 0;
|
||||
}
|
||||
</style>
|
@@ -1,74 +1,61 @@
|
||||
<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.id).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;
|
||||
|
||||
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"> {{ formatDateTime(currentResult.createdDate) }}</span>
|
||||
<nuxt-link
|
||||
class="button blue" :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>{{useBundleStore().selectedBundle.label }} - {{ formatDateTime(q.createdDate) }}</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">S'évaluer</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.last-quiz {
|
||||
margin-bottom: $x_small 0;
|
||||
margin: $small 0;
|
||||
}
|
||||
|
||||
.last-quiz-header {
|
||||
@@ -80,47 +67,51 @@ export default class History extends Vue {
|
||||
color: $gray_4;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-area {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
margin: $x_small 0;
|
||||
gap: $small;
|
||||
}
|
||||
|
||||
.history {
|
||||
margin-top: $x_small;
|
||||
}
|
||||
|
||||
.history button {
|
||||
margin-top: $small;
|
||||
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>
|
||||
|
@@ -1,91 +1,84 @@
|
||||
<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 notFound = ref(false);
|
||||
const quiz = ref<Quiz>();
|
||||
const axes = ref<Axe[]>();
|
||||
|
||||
onMounted(() => {
|
||||
if (!useRoute().query.quiz) {
|
||||
navigateTo("/dashboard");
|
||||
}
|
||||
loading.value = true;
|
||||
notFound.value = false;
|
||||
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;
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
notFound.value = true;
|
||||
})
|
||||
.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">{{ formatDateTime(quiz.createdDate) }}</span>
|
||||
<quiz-axe-details
|
||||
v-for="axe in axes"
|
||||
:axe="axe"
|
||||
:average="getAverage(axe)"
|
||||
:responses="getResponses(axe)"/>
|
||||
</section>
|
||||
<section v-else-if="notFound">
|
||||
<p class="center">Le quizz demandé n'existe pas !</p>
|
||||
</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: $small 0;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-weight: 700;
|
||||
@@ -93,4 +86,10 @@ export default class Result extends Vue {
|
||||
color: $gray_4;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.button-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
14
frontend/pages/error.vue
Normal file
14
frontend/pages/error.vue
Normal 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>
|
@@ -1,3 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'main-header'
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Home />
|
||||
<home />
|
||||
</template>
|
||||
|
@@ -1,80 +1,60 @@
|
||||
<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, j’accepte les conditions d'utilisation de Boussole PLUSS et j’ai 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("");
|
||||
const password = ref("");
|
||||
|
||||
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 => {
|
||||
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: $x_medium 0;
|
||||
|
||||
button {
|
||||
margin: $medium 0;
|
||||
margin-block: $x_medium 0;
|
||||
}
|
||||
}
|
||||
|
||||
.error-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: red;
|
||||
.link-forget-password {
|
||||
align-self: center;
|
||||
margin-block: $xx_small $xx_medium;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@@ -1,172 +1,105 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import type {Axe} from "~/store/axe";
|
||||
import {useQuizStore} from "~/store/quiz";
|
||||
|
||||
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);
|
||||
|
||||
const store = useQuizStore();
|
||||
|
||||
onMounted(() => {
|
||||
loading.value = true;
|
||||
store.initialize().finally(() => {
|
||||
store.resetResponses();
|
||||
initializeState();
|
||||
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);
|
||||
if (questions) {
|
||||
isFullRated.value = questions.filter(value => !value.rated).length == 0;
|
||||
} else {
|
||||
isFullRated.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
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 class="content">
|
||||
<team-header/>
|
||||
<hr/>
|
||||
<div v-if="!loading && questions && questions.get(currentAxe.identifier)">
|
||||
<quiz-part
|
||||
:key="currentAxe.identifier" :axe-number="currentAxe.identifier" :total-axes="axes.length"
|
||||
:title="currentAxe.title"
|
||||
:description="currentAxe.description"
|
||||
:color="currentAxe.color"
|
||||
:icon="'balise_' + currentAxe.identifier + '.svg'"
|
||||
:questions="questions.get(currentAxe.identifier)"
|
||||
@rate="onRate"
|
||||
/>
|
||||
|
||||
<div v-if="!loading">
|
||||
<quiz-part
|
||||
:key="currentAxe.identifier" :axe-number="currentAxe.identifier" :total-axes="axes.length"
|
||||
:title="currentAxe.title"
|
||||
:description="currentAxe.description"
|
||||
:color="currentAxe.color"
|
||||
:icon="'balise_' + currentAxe.identifier + '.svg'"
|
||||
:questions="questions.get(currentAxe.identifier)"
|
||||
@rate="onRate"
|
||||
/>
|
||||
|
||||
<div class="button-container">
|
||||
<nuxt-link
|
||||
v-if="currentAxeIdentifier <= 1"
|
||||
class="button gray"
|
||||
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"
|
||||
@click="showNext"
|
||||
>Suivant ❯
|
||||
</button>
|
||||
<button
|
||||
v-if="currentAxeIdentifier >= axes.length" class="button orange"
|
||||
:disabled="!isFilled || saving" @click="saveResult()"
|
||||
>Valider ❯
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="center">
|
||||
<loader/>
|
||||
<div class="button-container">
|
||||
<nuxt-link
|
||||
v-if="currentAxeIdentifier <= 1"
|
||||
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="!isFullRated"
|
||||
@click="showNext"
|
||||
>Suivant ❯
|
||||
</button>
|
||||
<button
|
||||
v-if="currentAxeIdentifier >= axes.length" class="button orange"
|
||||
:disabled="!isFullRated || saving" @click="saveResult()"
|
||||
>Valider ❯
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="center">
|
||||
<loader/>
|
||||
</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>
|
||||
|
@@ -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>
|
@@ -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>
|
||||
|
||||
@@ -52,7 +36,7 @@ export default class Result extends Vue {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
margin: $x_small 0;
|
||||
margin: $small 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
Reference in New Issue
Block a user