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:
243
frontend/components/BundleAxeModal.vue
Normal file
243
frontend/components/BundleAxeModal.vue
Normal file
@@ -0,0 +1,243 @@
|
||||
<script lang="ts" setup>
|
||||
import type {PropType} from "vue";
|
||||
import type {Axe} from "~/store/axe";
|
||||
import type {Question} from "~/store/question";
|
||||
|
||||
const questions = ref<Question[]>([]);
|
||||
const newQuestionMode = ref(false);
|
||||
const newQuestionLabel = ref("");
|
||||
const newQuestionDescription = ref("");
|
||||
|
||||
const emit = defineEmits(["changed", "close"]);
|
||||
|
||||
const props = defineProps({
|
||||
axe: {
|
||||
type: Object as PropType<Axe>,
|
||||
},
|
||||
questions: {
|
||||
type: Object as PropType<Question[]>,
|
||||
},
|
||||
questionsExample: {
|
||||
type: Object as PropType<Question[]>,
|
||||
default: [{
|
||||
label: "",
|
||||
description: ""
|
||||
}]
|
||||
},
|
||||
visible: Boolean
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
questions.value = props.questions ? props.questions.map(q => {
|
||||
return {
|
||||
label: q.label,
|
||||
description: q.description
|
||||
}
|
||||
}) : [];
|
||||
});
|
||||
|
||||
function removeQuestion(index: number) {
|
||||
questions.value.splice(index, 1);
|
||||
}
|
||||
|
||||
function moveQuestion(fromIndex, toIndex) {
|
||||
if (toIndex > questions.value.length || toIndex < 0) {
|
||||
return;
|
||||
}
|
||||
const element = questions.value[fromIndex];
|
||||
questions.value.splice(fromIndex, 1);
|
||||
questions.value.splice(toIndex, 0, element);
|
||||
}
|
||||
|
||||
function questionChanged(event, {index, newQuestion}) {
|
||||
questions.value[index] = newQuestion;
|
||||
}
|
||||
|
||||
function addNewQuestion() {
|
||||
questions.value.push({label: newQuestionLabel.value, description: newQuestionDescription.value});
|
||||
newQuestionMode.value = false;
|
||||
}
|
||||
|
||||
function changeToNewQuestionMode() {
|
||||
newQuestionLabel.value = "";
|
||||
newQuestionDescription.value = "";
|
||||
newQuestionMode.value = true;
|
||||
}
|
||||
|
||||
function validate() {
|
||||
emit("changed", {axeId: props.axe.id, newQuestions: questions.value});
|
||||
emit("close");
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="`modal${visible ? ' visible' : ''}`" v-if="axe">
|
||||
<section class="modal-content">
|
||||
<header class="modal-content-header">
|
||||
<h1>{{ axe.identifier }} - {{ axe.shortTitle }}</h1>
|
||||
<p>{{ axe.title }}</p>
|
||||
<p v-if="axe.description">{{ axe.description }}</p>
|
||||
<button class="close_modal" @click="$emit('close')">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.6 16L0 14.4L6.4 8L0 1.6L1.6 0L8 6.4L14.4 0L16 1.6L9.6 8L16 14.4L14.4 16L8 9.6L1.6 16Z"
|
||||
fill="#1C1B1F"/>
|
||||
</svg>
|
||||
<span>Fermer</span>
|
||||
</button>
|
||||
</header>
|
||||
<main class="modal-content-body">
|
||||
<div v-if="newQuestionMode" class="new-question">
|
||||
<div class="new-question__inputs">
|
||||
<label for="label-new-question">Libellé *</label>
|
||||
<input id="label-new-question" type="text" list="question-labels" v-model="newQuestionLabel" maxlength="200"/>
|
||||
<label for="description-new-question">Description</label>
|
||||
<input id="description-new-question" type="text" list="question-labels" v-model="newQuestionDescription" maxlength="500"/>
|
||||
</div>
|
||||
<h2>Historique des questions</h2>
|
||||
<ul class="new-question__example-list">
|
||||
<li v-for="(question, index) in questionsExample">
|
||||
<input :id="'example-'+index" type="radio" name="example-radio" @input="() => {newQuestionLabel = question.label;newQuestionDescription = question.description;}">
|
||||
<label :for="'example-'+index">
|
||||
<span class="new-question__example-list__item-label">{{ question.label }}</span>
|
||||
<span class="new-question__example-list__item-description" v-if="question.description">{{ question.description }}</span>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else>
|
||||
<ul class="question-list">
|
||||
<li v-for="(question, index) in questions" class="question-list__item">
|
||||
<div class="question-list__item__inputs">
|
||||
<span class="question-list__item__inputs__title">Question {{index+1}}</span>
|
||||
<label :for="'label-'+index">Libellé *</label>
|
||||
<input :id="'label-'+index" type="text" list="question-labels" :value="question.label" maxlength="200"
|
||||
@change="event => questionChanged(event, {index, newQuestion: {label: event.target.value, description: question.description}})"/>
|
||||
<label :for="'description-'+index">Description</label>
|
||||
<input :id="'description-'+index" type="text" list="question-descriptions" :value="question.description" maxlength="500"
|
||||
@change="event => questionChanged(event, {index, newQuestion: {label: question.label, description: event.target.value}})"/>
|
||||
|
||||
</div>
|
||||
<div class="question-list__item__buttons">
|
||||
<button class="button-icon button-up" aria-label="Déplacer vers le haut"
|
||||
@click="moveQuestion(index, index-1)" :disabled="index === 0">
|
||||
➜
|
||||
</button>
|
||||
<button class="button-icon button-down" aria-label="Déplacer vers le bas"
|
||||
@click="moveQuestion(index, index+1)" :disabled="index === questions.length -1">
|
||||
➜
|
||||
</button>
|
||||
<button class="button-icon" @click="removeQuestion(index)">
|
||||
🗑
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="modal-content-footer">
|
||||
<button v-if="newQuestionMode" class="button gray button-back"
|
||||
@click="newQuestionMode = false">❮
|
||||
</button>
|
||||
<button v-if="newQuestionMode" class="button orange" :disabled="!newQuestionLabel"
|
||||
@click="addNewQuestion()">Ajouter
|
||||
</button>
|
||||
<button v-if="!newQuestionMode" class="button blue" @click="changeToNewQuestionMode()">Ajouter une question
|
||||
</button>
|
||||
<button v-if="!newQuestionMode" class="button orange" @click="validate()">Valider</button>
|
||||
</footer>
|
||||
</section>
|
||||
<div class="modal-overlay" @click="$emit('close')"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "assets/css/modal";
|
||||
.modal-content-body {
|
||||
//overflow: unset;
|
||||
}
|
||||
.question-list,
|
||||
.new-question__example-list {
|
||||
//overflow-y: scroll;
|
||||
//overflow-x: hidden;
|
||||
//height: 30%;
|
||||
}
|
||||
.new-question {
|
||||
&__inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $xx_small;
|
||||
}
|
||||
h2 {
|
||||
margin: $xx_small 0;
|
||||
font-size: $secondary-font-size;
|
||||
}
|
||||
|
||||
&__example-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $xx_small;
|
||||
|
||||
&__item-description {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
input {
|
||||
//align-content: start;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
gap: $xxx_small;
|
||||
//align-items: start;
|
||||
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $xxxx_small;
|
||||
}
|
||||
}
|
||||
}
|
||||
.question-list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $small;
|
||||
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
gap: $xxx_small;
|
||||
|
||||
&__inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: $xxx_small;
|
||||
|
||||
&__title {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
//align-self: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: end;
|
||||
//gap: $xxx_small;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.button-up {
|
||||
rotate: -90deg;
|
||||
}
|
||||
|
||||
.button-down {
|
||||
rotate: 90deg;
|
||||
}
|
||||
</style>
|
149
frontend/components/Cgu.vue
Normal file
149
frontend/components/Cgu.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<h2>Bienvenue sur la Boussole PLUSS</h2>
|
||||
|
||||
<p>
|
||||
« Une application de valorisation des ressources mobilisées dans les projets locaux. Le petit plus c’est l’identification collective des axes à améliorer. »
|
||||
</p>
|
||||
<p>
|
||||
Vous souhaitez engager votre action ou votre entreprise dans une démarche responsable, utile, solidaire ? 10 Balises comme 10 repères pour vous aider à identifier les points à améliorer et visualiser concrètement où vous en êtes !
|
||||
Le principe : il s’agit d’auto-évaluer une dynamique territoriale ou un projet en répondant à des questions pré-formatées portant sur chacune des 10 Balises du référentiel « Agir pour une production locale ». Les participants choisissent 3 questions par balise.
|
||||
Cette application fait partie d’une boîte à outils plus vaste, initiée par l’Apes et ses adhérents. En savoir PLUSS
|
||||
</p>
|
||||
<p>
|
||||
Toute utilisation effectuée a quel que titre que ce soit de la plateforme implique obligatoirement l'acceptation sans réserve des présentes conditions générales d’utilisation (CGU)
|
||||
</p>
|
||||
<h2>1. Définitions</h2>
|
||||
<p>Les termes mentionnés ci-dessous ont, dans les présentes Conditions Générales d’Utilisation, la signification suivante, qu’ils soient utilisés au singulier ou au pluriel :</p>
|
||||
<ul>
|
||||
<li>Balise : il s’agit d’un axe du référentiel PLUSS accessible ici :</li>
|
||||
<li>Boussole : le présent outil d’évaluation accessible sur le site éponyme</li>
|
||||
<li>http://apes-hdf.org/_docs/Fichier/2022/21-220610094246.pdf</li>
|
||||
<li>Compte : désigne l’espace personnel de l’Utilisateur sur le Site. L’accès au Compte</li>
|
||||
<li>se fait grâce aux Identifiants.</li>
|
||||
<li>Équipe : il s’agit du collectif qui va utiliser la Boussole pour s’évaluer</li>
|
||||
<li>Identifiants : désigne l’adresse courriel de l’Utilisateur et le mot de passe associé à son Compte, nécessaires pour y accéder.</li>
|
||||
<li>Site : le présent site internet Boussole PLUSS</li>
|
||||
<li>Tableau de Bord : la page d’accueil accessible après identification sur laquelle l’Utilisateur retrouve toutes ses évaluations et peut en réaliser de nouvelles</li>
|
||||
<li>Utilisateur : désigne toute personne qui accède et navigue sur le Site</li>
|
||||
</ul>
|
||||
<h2>2. Objet</h2>
|
||||
Les présentes Conditions Générales d’Utilisation ont pour objet de définir les conditions et les
|
||||
modalités d’accès à la Boussole PLUSS
|
||||
|
||||
<h2>3. Acceptation des Conditions Générales d’Utilisation</h2>
|
||||
L’utilisation des fonctionnalités de la Plateforme implique l’acceptation des présentes CGU.
|
||||
L’Utilisateur s’engage à lire attentivement les présentes Conditions Générales d’Utilisation lors de
|
||||
l’accès au site et est invité à les télécharger ou les imprimer et à en conserver une copie.
|
||||
Les présentes CGU sont référencées sur la page d’accueil au moyen d’un lien
|
||||
hypertexte et peuvent ainsi être consultées à tout moment
|
||||
|
||||
|
||||
<h3>3. 1. Durée</h3>
|
||||
<p>Les Conditions Générales d'Utilisation entrent en vigueur à compter de leur publication sur le site et s’appliquent pendant toute la durée de l’utilisation des fonctionnalités du site (c’est-à-dire, notamment, tant que l’Utilisateur est titulaire d’un Compte sur le site).</p>
|
||||
<p>L'Utilisateur peut mettre fin à l'utilisation de son Compte à tout moment, sans frais et sans devoir
|
||||
donner de notice préalable, en demandant la suppression de son Compte à l’adresse suivante :
|
||||
contact@apes-hdf.org</p>
|
||||
<p>Les présentes CGU sont susceptibles d’évoluer dans le temps. Les Utilisateurs en seront informés par mail.</p>
|
||||
|
||||
<h2>4. Fonctionnalités de la Boussole</h2>
|
||||
<p>La Boussole est un outil d’auto-évaluation qui offre aux Utilisateurs les fonctionnalités suivantes :</p>
|
||||
<ul>
|
||||
<li>Créer un compte et pouvoir ensuite en modifier les accès et le nom d’Utilisateur</li>
|
||||
<li>Générer une évaluation à partir d’une série de questions programmée dans le Site, autant de fois que nécessaire</li>
|
||||
<li>Créer de nouveaux supports d’évaluation en créant de nouvelles questions et/ ou en modifiant et/ou sélectionnant des questions existantes</li>
|
||||
<li>Générer une évaluation à partir des nouveaux supports d’évaluations créés</li>
|
||||
<li>Suivre les évaluations faites sur tous les supports utilisés pour s’évaluer</li>
|
||||
</ul>
|
||||
<h3>4.1 Création d’un Compte, gestion et accès à l’espace « Mon Compte »</h3>
|
||||
<h4>4.1.1 Création d’un Compte</h4>
|
||||
<p>Sur la page d’accueil de la Boussole PLUSS il est possible de se créer un Compte en cliquant sur « créer un compte ». L’Utilisateur est alors invité à remplir les champs du formulaire : nom de l’équipe, adresse e-mail et mot de passe.
|
||||
Un mail automatique est envoyé à l’Utilisateur pour confirmer son inscription.
|
||||
L’Utilisateur est seul responsable de l’utilisation de ses Identifiants. Il doit veiller à conserver secret
|
||||
son mot de passe et à ne pas le divulguer.
|
||||
Il sera responsable de l’utilisation de ses Identifiants par des tiers, qu’elle soit frauduleuse ou non.</p>
|
||||
|
||||
<h4>4.1.2 Connexion à l’espace personnel si le Compte est déjà créé</h4>
|
||||
<p>Sur la page d’accueil de la Boussole PLUSS, l’Utilisateur peut se connecter si son Compte est déjà créé.
|
||||
</p>
|
||||
<h4>4.1.3 Gestion du Compte</h4>
|
||||
<p>Dans le menu « Mon compte » en haut à droite, l’utilisateur peut modifier le nom de son équipe, son adresse e-mail de connexion et son mot de passe.
|
||||
</p>
|
||||
<h4>4.1.4 Mot de passe oublié</h4>
|
||||
<p>Sur la page d’accueil de la Boussole PLUSS un lien « j’ai oublié mon mot de passe » permet de réinitialiser le mot de passe d’accès au Compte. L’e-mail utilisé lors de la création du Compte est demandé afin qu’un e-mail permettant la réinitialisation mot de passe y sera envoyé.
|
||||
</p>
|
||||
<h4>4.1.5 Suppression du Compte</h4>
|
||||
<p>Pour supprimer votre compte, merci de contacter l’administrateur à contact@apes-hdf.org</p>
|
||||
|
||||
<h3>4.2 Réalisation d’une évaluation</h3>
|
||||
|
||||
<h4>4.2.1 Réalisation d’une évaluation sur la Boussole Classique</h4>
|
||||
<p>Dès la première connexion l’Utilisateur peut réaliser une première évaluation en utilisant le lot de question existant. Pour y accéder il suffit de cliquer sur la tuile « Boussole Classique ». L’Utilisateur est alors invité à répondre aux questions. A la fin de l’évaluation, un radar lui permettra de mesurer sa position sur les 10 balises de la PLUSS. Les réponses aux questions sont accessibles en cliquant sur la date de l’évaluation.</p>
|
||||
<p>Les évaluations passées restent disponibles. Les nouvelles évaluations sur la même Boussole viennent s’ajouter sous forme de liste en-dessous de la première, permettant de mesurer son évolution. Pour réaliser une nouvelle évaluation sur la même Boussole cliquez sur le bouton « nouveau ».</p>
|
||||
<p>Vous pouvez sauvegarder en pdf ou imprimer les résultats de votre évaluation en cliquant sur « imprimer ».</p>
|
||||
|
||||
<h4>4.2.2 Création d’une nouvelle Boussole</h4>
|
||||
<p>La création d’une nouvelle Boussole suit les étapes suivantes :</p>
|
||||
<ul>
|
||||
<li>cliquez sur « Nouvelle Boussole » sur votre tableau de bord</li>
|
||||
<li>pour chaque Balise, cliquez sur « configurez les questions » afin d’accéder aux questions existantes, les modifier, ou en écrire de nouvelles. Pour chaque question vous pouvez écrire une description.</li>
|
||||
<li>le nombre maximale de questions par Balise est de 10 questions</li>
|
||||
<li>les Balises ne sont pas modifiables</li>
|
||||
<li>une fois toutes les Balises configurées cliquez sur valider</li>
|
||||
<li>la nouvelle Boussole est maintenant accessible sur votre tableau de bord</li>
|
||||
</ul>
|
||||
<p>Vous pouvez réaliser autant de nouvelles Boussoles que vous le désirez. Les Boussoles ne sont pas supprimables une fois configurées.
|
||||
</p>
|
||||
<h4>4.2.3 Réalisation d’une évaluation sur une nouvelle Boussole</h4>
|
||||
<p>
|
||||
Le mode opératoire est le même que pour la Boussole classique (voir 4.2.1).
|
||||
</p>
|
||||
|
||||
<h2>5. Mentions légales</h2>
|
||||
<p>Ce site a été produit et est administré par APES Hauts-de-France, association 1901, située au 235 boulevard Paul Painlevé, 59000 LILLE
|
||||
pour nous contacter : contact@apes-hdf.org</p>
|
||||
<p>Rédaction du site : Marie-Charlotte WOETS pour APES HDF</p>
|
||||
<p>Réalisation du site : It’s on us</p>
|
||||
<p>Hébergement : Cliss XXI</p>
|
||||
<p>Ces site est développé et maintenu sous licence AGPL 3. Il est disponible sur un dépôt public : <a href="https://git.itsonus.fr/client_projects/boussole-pluss">https://git.itsonus.fr/client_projects/boussole-pluss</a></p>
|
||||
|
||||
<h2>6. Protection des données personnelles</h2>
|
||||
<p>Les données personnelles collectées se limitent à l’adresse mail de l’Utilisateur, pour la création du Compte. Nous nous inscrivons dans le respect de la RGPD en vigueur.</p>
|
||||
<p>Les données produites lors de la création de Compte et de la réalisation d’évaluation sont hébergées par Cliss XXI sur un serveur interne au 23 avenue Jean Jaurès 62800 LIEVIN</p>
|
||||
<p>Ce site ne contient pas d’outil analytique ou de mesure d’audience.</p>
|
||||
|
||||
<h2>7. Accessibilité du site</h2>
|
||||
<p>L’Administration fait de son mieux pour s’assurer du bon fonctionnement du Site et des services y figurant, dans les limites de responsabilité des présentes conditions générales</p>
|
||||
<p>Le Site est en principe accessible 24 heures sur 24 et 7 jours sur 7, cependant, le Site décline toute responsabilité, dans les cas suivants, sans que cette liste soit limitative :</p>
|
||||
<ul>
|
||||
<li>interruption du Site pour des opérations de maintenance techniques ou d’actualisation des informations publiées.</li>
|
||||
<li>impossibilité momentanée d’accès au Site (et/ou aux sites internet et applications lui étant liés) en raison de problèmes techniques et ce quelles qu’en soient l’origine et la provenance.</li>
|
||||
<li>indisponibilité ou de surcharge ou toute autre cause empêchant le fonctionnement normal du réseau de téléphonie mobile utilisé pour accéder au Site</li>
|
||||
<li>contamination par des éventuels virus informatiques circulant sur le réseau.</li>
|
||||
<li>dommages directs ou indirects causés à l’Utilisateur, quelle qu’en soit la nature, résultant de l’accès, ou de l’utilisation du Site (et/ou des sites ou applications qui lui sont liés)</li>
|
||||
<li>utilisation anormale ou d’une exploitation illicite du Site</li>
|
||||
</ul>
|
||||
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
h2 {
|
||||
margin: $small 0;
|
||||
}
|
||||
h3 {
|
||||
font-size: $secondary-font-size;
|
||||
margin: $small 0;
|
||||
}
|
||||
h4 {
|
||||
margin: $xx_small 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-position: inside;
|
||||
margin: $xxx_small 0;
|
||||
li {
|
||||
padding: $xxxx_small 0;
|
||||
}
|
||||
//margin: $xx_small;
|
||||
}
|
||||
|
||||
</style>
|
32
frontend/components/CguModal.vue
Normal file
32
frontend/components/CguModal.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts" setup>
|
||||
const emit = defineEmits(["close", "validate"]);
|
||||
defineProps({
|
||||
visible: Boolean
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div :class="`modal${visible ? ' visible' : ''}`">
|
||||
<section class="modal-content">
|
||||
<header class="modal-content-header">
|
||||
<h1>Conditions Générales d’Utilisation de la Boussole PLUSS</h1>
|
||||
<button class="close_modal" @click="$emit('close')">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.6 16L0 14.4L6.4 8L0 1.6L1.6 0L8 6.4L14.4 0L16 1.6L9.6 8L16 14.4L14.4 16L8 9.6L1.6 16Z"
|
||||
fill="#1C1B1F"/>
|
||||
</svg>
|
||||
<span>Fermer</span>
|
||||
</button>
|
||||
</header>
|
||||
<main class="modal-content-body">
|
||||
<cgu />
|
||||
</main>
|
||||
<footer class="modal-content-footer">
|
||||
<button class="button orange" @click="emit('validate')">Accepter</button>
|
||||
</footer>
|
||||
</section>
|
||||
<div class="modal-overlay" @click="$emit('close')"></div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
@import "assets/css/modal";
|
||||
</style>
|
@@ -1,42 +0,0 @@
|
||||
<template>
|
||||
<div class="header">
|
||||
<nuxt-link to="/">
|
||||
<div class="secondary-logo">
|
||||
<img src="/logo/logo_apes.svg" height="50px" alt="Logo APES"/>
|
||||
</div>
|
||||
<div class="main-logo">
|
||||
<img src="/logo/main_logo.svg" width="245px" alt="Boussole PLUSS"/>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "assets/css/spacing";
|
||||
@import "assets/css/color";
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
height: 115px;
|
||||
margin-bottom: $xx_medium;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url("/decoration/background.svg");
|
||||
background-position: bottom center, 50%;
|
||||
background-size: cover;
|
||||
|
||||
.secondary-logo img {
|
||||
padding: 12px 0 0 12px;
|
||||
}
|
||||
|
||||
.main-logo {
|
||||
position: absolute;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
top: 40px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 225px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
@@ -1,29 +1,36 @@
|
||||
<template>
|
||||
<div>
|
||||
<Header/>
|
||||
<div class="content">
|
||||
<section>
|
||||
<h1>La démarche</h1>
|
||||
<p>
|
||||
Les crises successives que nous subissons révèlent encore plus la fragilité d'une économie productive
|
||||
prioritairement mondialisée. S'il est impératif que les producteurs locaux reprennent la main sur certaines
|
||||
filières, il faut aussi, pour favoriser la transition écologique et sociale, que la production locale soit
|
||||
avant tout utile, solidaire et soutenable.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h1>La boussole comme auto-évaluation</h1>
|
||||
<p>
|
||||
Nous proposons dix points de repères pour permettre aux différents écosystèmes de production locale sur un
|
||||
territoire d'agir conjointement et collectivement dans ce sens et faire en sorte que les acteurs locaux
|
||||
puissent innover, expérimenter et renforcer la production locale quantitativement et qualitativement.
|
||||
</p>
|
||||
</section>
|
||||
<div class="button-container">
|
||||
<nuxt-link class="button orange" to="/login">Démarrer</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
<section>
|
||||
<h1>La Boussole PLUSS, c’est quoi ?</h1>
|
||||
<p>
|
||||
« Une application de valorisation des ressources mobilisées dans les projets locaux. Le petit plus c’est
|
||||
l’identification collective des axes à améliorer. »
|
||||
</p>
|
||||
<p>
|
||||
Vous souhaitez engager votre action ou votre entreprise dans une démarche responsable, utile, solidaire ? 10
|
||||
balises comme 10 repères pour vous aider à identifier les points à améliorer et visualiser concrètement où vous en
|
||||
êtes !
|
||||
Le principe : il s’agit d’auto-évaluer une dynamique territoriale ou un projet en répondant à des questions
|
||||
pré-formatées portant sur chacune des 10 balises du référentiel <nuxt-link to="https://apes-hdf.org/_docs/Fichier/2022/21-220610094246.pdf" target="_blank">« Agir pour une production locale » (1,24 Mo)</nuxt-link>. Les
|
||||
participants choisissent 3 questions par balise.
|
||||
Cette application fait partie d’une boîte à outils plus vaste, initiée par l’<nuxt-link to="https://www.apes-hdf.org/page-0-0-0.html" target="_blank">Apes</nuxt-link> et ses adhérents. En savoir
|
||||
<nuxt-link to="https://pluss-hdf.org/" target="_blank">PLUSS</nuxt-link> ?
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h1>Comment ça marche ?</h1>
|
||||
<p>
|
||||
Après avoir créé un compte pour votre équipe, vous pouvez commencer votre évaluation sur un premier lot de
|
||||
questions disponible. Vous pouvez également décider de configurer vous même vos questions, et même de piocher
|
||||
parmi celles imaginées par les autres utilisateurs !
|
||||
Vous pouvez créer autant de boussoles que vous le désirez.<br/>
|
||||
Une fois la boussole configurée, vous pouvez réaliser autant d’évaluations que vous le souhaiter, afin de mesurer
|
||||
votre progression sur chacune des balises. Les évaluations passées restent disponibles.
|
||||
</p>
|
||||
</section>
|
||||
<div class="button-container">
|
||||
<nuxt-link class="button orange" to="/login">Démarrer</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
@@ -19,9 +19,6 @@
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@import "assets/css/color";
|
||||
@import "assets/css/spacing";
|
||||
|
||||
ol {
|
||||
margin: 0;
|
||||
position: relative;
|
||||
|
31
frontend/components/MainFooter.vue
Normal file
31
frontend/components/MainFooter.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<footer class="footer">
|
||||
<nuxt-link to="https://www.apes-hdf.org" target="_blank">
|
||||
<img src="/images/logo/logo_apes.png" height="125" width="200" alt="Logo APES"/>
|
||||
</nuxt-link>
|
||||
<nuxt-link to="/cgu">Conditions Générales d’Utilisation de la Boussole PLUSS</nuxt-link>
|
||||
<nuxt-link to="mailto:contact@apes.org">Contactez-nous !</nuxt-link>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.footer {
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $xxx_small;
|
||||
padding-bottom: $xx_small;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #ededf0;
|
||||
|
||||
margin-top: auto;
|
||||
|
||||
clear: both;
|
||||
height: $footer_height;
|
||||
|
||||
background: $white url(/images/decoration/cube.png) repeat left top;;
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
</script>
|
31
frontend/components/MainHeader.vue
Normal file
31
frontend/components/MainHeader.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<header class="header">
|
||||
<div class="main-logo">
|
||||
<nuxt-link :to="useRoute().name === 'index' ? '' : '/'">
|
||||
<img src="/images/logo/main_logo.svg" width="245px" alt="Boussole PLUSS"/>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
width: 100%;
|
||||
height: 115px;
|
||||
margin-bottom: $xxx_medium;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url("/images/decoration/background.svg");
|
||||
background-position: bottom center, 50%;
|
||||
background-size: cover;
|
||||
|
||||
.main-logo {
|
||||
position: absolute;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
top: 40px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 225px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,80 +1,15 @@
|
||||
<template>
|
||||
<PolarArea
|
||||
:chart-options="chartOptions"
|
||||
:chart-data="chartData"
|
||||
:chart-id="chartId"
|
||||
:dataset-id-key="datasetIdKey"
|
||||
:plugins="plugins"
|
||||
:css-classes="cssClasses"
|
||||
:styles="styles"
|
||||
:width="width"
|
||||
:height="height"
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {PolarArea} from 'vue-chartjs';
|
||||
import type {PropType} from "vue";
|
||||
|
||||
<script lang="ts">
|
||||
import {PolarArea} from 'vue-chartjs/legacy'
|
||||
|
||||
import {ArcElement, Chart as ChartJS, Legend, RadialLinearScale, Title, Tooltip} from 'chart.js'
|
||||
import {Component, Prop, Vue, Watch} from "nuxt-property-decorator";
|
||||
|
||||
ChartJS.register(Title, Tooltip, Legend, ArcElement, RadialLinearScale);
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
PolarArea
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object as PropType<number[]>
|
||||
}
|
||||
})
|
||||
export default class PolarAreaChart extends Vue {
|
||||
});
|
||||
|
||||
@Prop({
|
||||
default: 'polar-chart',
|
||||
type: String
|
||||
})
|
||||
public chartId!: string;
|
||||
|
||||
@Prop({
|
||||
default: 'label',
|
||||
type: String
|
||||
})
|
||||
public datasetIdKey!: string;
|
||||
|
||||
@Prop({
|
||||
default: 400,
|
||||
type: Number
|
||||
})
|
||||
public width!: number;
|
||||
|
||||
@Prop({
|
||||
default: 400,
|
||||
type: Number
|
||||
})
|
||||
public height!: number;
|
||||
|
||||
@Prop({
|
||||
default: '',
|
||||
type: String
|
||||
})
|
||||
public cssClasses!: string;
|
||||
|
||||
@Prop({
|
||||
type: Object
|
||||
})
|
||||
public styles!: Object;
|
||||
|
||||
@Prop({
|
||||
default: () => [],
|
||||
type: Array
|
||||
})
|
||||
public plugins!: [];
|
||||
|
||||
@Prop({
|
||||
default: () => [],
|
||||
type: Array<number>
|
||||
})
|
||||
public data!: number[];
|
||||
|
||||
readonly chartData = {
|
||||
const chartData = computed(() => {
|
||||
return {
|
||||
labels: [
|
||||
"1. Pouvoir d'agir",
|
||||
"2. Multi-secteur",
|
||||
@@ -101,36 +36,45 @@ export default class PolarAreaChart extends Vue {
|
||||
"#E9D280",
|
||||
"#7BD1F5"
|
||||
],
|
||||
data: this.data,
|
||||
data: props.data,
|
||||
borderWidth: 1,
|
||||
borderColor: "#666666"
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
readonly chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
// animation: false,
|
||||
scales: {
|
||||
r: {
|
||||
suggestedMin: 0,
|
||||
suggestedMax: 10,
|
||||
ticks: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
const chartOptions = ref({
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
scales: {
|
||||
r: {
|
||||
suggestedMin: 0,
|
||||
suggestedMax: 10,
|
||||
ticks: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Watch("data", {})
|
||||
private renderChart() {
|
||||
this.chartData.datasets[0].data = this.data;
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="chart-container">
|
||||
<polar-area
|
||||
:options="chartOptions"
|
||||
:data="chartData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.chart-container {
|
||||
position: relative;
|
||||
max-width: 60vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
@@ -1,10 +1,34 @@
|
||||
<script lang="ts" setup>
|
||||
import type {PropType} from "vue";
|
||||
import type {QuizResponse} from "~/store/quiz";
|
||||
import type {Axe} from "~/store/axe";
|
||||
|
||||
const props = defineProps({
|
||||
axe: {
|
||||
type: Object as PropType<Axe>,
|
||||
required: true
|
||||
},
|
||||
average: Number,
|
||||
responses: {
|
||||
type: Object as PropType<QuizResponse[]>,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const cssVars = computed(() => {
|
||||
return {
|
||||
'--color': props.axe ? props.axe.color : "#ffffff"
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section :style="cssVars" class="axe-details">
|
||||
<h2 class="title">
|
||||
<span><span class="upper">{{ axe.identifier }}</span><span>{{ axe.shortTitle }}</span></span>
|
||||
<span class="upper">{{ score.scoreAvg | formatRate }} / 10</span>
|
||||
<span class="upper">{{ Number((average).toFixed(1)) }} / 10</span>
|
||||
</h2>
|
||||
<div v-for="response in responses" :key="response._links.self.href">
|
||||
<div v-for="response in responses">
|
||||
<p class="question">
|
||||
<span>
|
||||
{{ response.question }}
|
||||
@@ -16,38 +40,6 @@
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from "nuxt-property-decorator";
|
||||
import {Axe} from "~/repositories/models/axe.model";
|
||||
import {ResponseWithQuestion, Score} from "~/repositories/models/quiz.model";
|
||||
|
||||
@Component
|
||||
export default class QuizAxeDetails extends Vue {
|
||||
|
||||
@Prop({
|
||||
type: Object,
|
||||
required: true
|
||||
})
|
||||
public axe!: Axe;
|
||||
|
||||
@Prop({
|
||||
type: Object,
|
||||
required: true
|
||||
})
|
||||
public score!: Score;
|
||||
|
||||
@Prop({
|
||||
type: Array
|
||||
})
|
||||
public responses!: ResponseWithQuestion[];
|
||||
|
||||
get cssVars() {
|
||||
return {
|
||||
'--color': this.axe ? this.axe.color : "#ffffff"
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "assets/css/font";
|
||||
@@ -55,7 +47,7 @@ export default class QuizAxeDetails extends Vue {
|
||||
@import "assets/css/spacing";
|
||||
|
||||
.axe-details {
|
||||
margin-top: $x_small;
|
||||
margin-top: $small;
|
||||
}
|
||||
|
||||
.title {
|
||||
|
@@ -1,3 +1,58 @@
|
||||
<script lang="ts" setup>
|
||||
import type {PropType} from "vue";
|
||||
import {useQuizStore} from "~/store/quiz";
|
||||
import type {Question} from "~/store/question";
|
||||
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
description: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
axeNumber: Number,
|
||||
totalAxes: Number,
|
||||
icon: String,
|
||||
color: String,
|
||||
questions: {
|
||||
type: Object as PropType<Question[]>,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["rate"]);
|
||||
|
||||
const store = useQuizStore();
|
||||
|
||||
const cssVars = computed(() => {
|
||||
return {
|
||||
'--color': props.color
|
||||
}
|
||||
});
|
||||
|
||||
function onRate(score: number, question: Question) {
|
||||
store.updateScoreResponse({axeId: props.axeNumber, questionId: question.id, score});
|
||||
const questions = store.questionsRatedPerAxe.get(props.axeNumber);
|
||||
const unratedQuestions = questions ? questions.filter(value => !value.rated) : [];
|
||||
emit('rate', {
|
||||
isFullRated: unratedQuestions.length === 0
|
||||
});
|
||||
}
|
||||
|
||||
function onComment(comment: string, question: Question) {
|
||||
store.updateCommentResponse({axeId: props.axeNumber, questionId: question.id, comment});
|
||||
}
|
||||
|
||||
function getCurrentScore(question: Question): number | undefined {
|
||||
const rate = store.responses.get(question.id) as QuizRate;
|
||||
return rate ? rate.score : undefined;
|
||||
}
|
||||
|
||||
function getCurrentComment(question: Question): string | undefined {
|
||||
const rate = store.responses.get(question.id) as QuizRate;
|
||||
return rate ? rate.comment : undefined;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article :style="cssVars">
|
||||
<header>
|
||||
@@ -11,102 +66,23 @@
|
||||
<img :src="icon" width="200px" alt="" aria-hidden="true"/>
|
||||
</div>
|
||||
</header>
|
||||
<section v-for="question in questions" :key="question._links.self.href" class="question">
|
||||
<section v-if="questions" v-for="question in questions" :key="question.id" class="question">
|
||||
<div class="title">
|
||||
{{ question.label }}
|
||||
</div>
|
||||
<details v-if="question.description">
|
||||
<summary> {{ question.label }}</summary>
|
||||
<summary>Description</summary>
|
||||
<p>{{ question.description }}</p>
|
||||
</details>
|
||||
<div v-else class="title">
|
||||
{{ question.label }}
|
||||
</div>
|
||||
<div class="rating">
|
||||
<rating :color="color" :initial-value="getCurrentScore(question)" @rate="rate => onRate(rate, question)" />
|
||||
<rating :color="color" :initial-value="getCurrentScore(question)" @rate="rate => onRate(rate, question)"/>
|
||||
</div>
|
||||
<input :value="getCurrentComment(question)" type="text" placeholder="Commentaire" maxlength="500" @input="event => onComment(event.target.value, question)"/>
|
||||
<input :value="getCurrentComment(question)" type="text" placeholder="Commentaire" maxlength="500"
|
||||
@input="event => onComment(event.target.value, question)"/>
|
||||
</section>
|
||||
</article>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from "nuxt-property-decorator";
|
||||
import {Question} from "~/repositories/models/question.model";
|
||||
import {quizStore} from "~/utils/store-accessor";
|
||||
import {QuizRate} from "~/repositories/models/quiz.model";
|
||||
|
||||
@Component
|
||||
export default class Quiz extends Vue {
|
||||
|
||||
@Prop({
|
||||
required: true
|
||||
})
|
||||
private title!: string;
|
||||
|
||||
@Prop({
|
||||
required: false
|
||||
})
|
||||
private description!: string;
|
||||
|
||||
@Prop({
|
||||
required: true
|
||||
})
|
||||
private axeNumber!: number;
|
||||
|
||||
@Prop({
|
||||
required: true
|
||||
})
|
||||
private totalAxes!: number;
|
||||
|
||||
@Prop({
|
||||
required: true,
|
||||
type: String,
|
||||
})
|
||||
public icon !: string;
|
||||
|
||||
@Prop({
|
||||
required: true,
|
||||
type: Array<Question>
|
||||
})
|
||||
private questions!: Array<Question>;
|
||||
|
||||
@Prop({
|
||||
required: true,
|
||||
type: String,
|
||||
})
|
||||
public color !: string;
|
||||
|
||||
get cssVars() {
|
||||
return {
|
||||
'--color': this.color
|
||||
}
|
||||
}
|
||||
|
||||
onRate(score: number, question: Question) {
|
||||
quizStore.updateScoreResponse({axeId: this.axeNumber, questionId: question.id, score});
|
||||
this.emitRatingState();
|
||||
}
|
||||
|
||||
onComment(comment: string, question: Question) {
|
||||
quizStore.updateCommentResponse({axeId: this.axeNumber, questionId: question.id, comment});
|
||||
}
|
||||
|
||||
getCurrentScore(question: Question): number | undefined {
|
||||
const rate = quizStore.responses.get(question.id) as QuizRate;
|
||||
return rate ? rate.score : undefined;
|
||||
}
|
||||
|
||||
getCurrentComment(question: Question): string | undefined {
|
||||
const rate = quizStore.responses.get(question.id) as QuizRate;
|
||||
return rate ? rate.comment : undefined;
|
||||
}
|
||||
|
||||
emitRatingState() {
|
||||
const questions = quizStore.questionsRatedPerAxe.get(this.axeNumber);
|
||||
const unratedQuestions = questions ? questions.filter(value => !value.rated) : [];
|
||||
this.$emit('rate', {
|
||||
isFullRated: unratedQuestions.length === 0
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@import "assets/css/color";
|
||||
@@ -131,14 +107,15 @@ $size: 31px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
text-align: left;
|
||||
|
||||
> span {
|
||||
padding: 0 $x_small;
|
||||
padding: 0 $small;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
text-align: center;
|
||||
margin: $small;
|
||||
margin: $medium;
|
||||
}
|
||||
|
||||
.description {
|
||||
@@ -149,40 +126,35 @@ $size: 31px;
|
||||
|
||||
.question {
|
||||
width: 100%;
|
||||
}
|
||||
.question details,
|
||||
.question .title {
|
||||
margin-bottom: $x_small;
|
||||
}
|
||||
.question details summary,
|
||||
.question .title {
|
||||
font-weight: 700;
|
||||
font-size: $title-font-size;
|
||||
}
|
||||
|
||||
.question details summary {
|
||||
margin-bottom: $xxx_small;
|
||||
}
|
||||
details,
|
||||
.title {
|
||||
margin-bottom: $small;
|
||||
}
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: $title-font-size;
|
||||
}
|
||||
|
||||
.question details p {
|
||||
font-size: $secondary-font-size;
|
||||
color: $gray_4;
|
||||
margin-left: $x_small;
|
||||
}
|
||||
details summary {
|
||||
list-style-position: inside;
|
||||
margin-bottom: $xxx_small;
|
||||
}
|
||||
|
||||
.question .rating {
|
||||
margin: 0 auto;
|
||||
max-width: 401px;
|
||||
}
|
||||
.rating {
|
||||
margin: 0 auto;
|
||||
max-width: 401px;
|
||||
}
|
||||
|
||||
.question input[type="text"] {
|
||||
width: 100%;
|
||||
border: 1px solid $gray_2;
|
||||
border-radius: 8px;
|
||||
background: none;
|
||||
margin: $small 0;
|
||||
font-weight: 400;
|
||||
font-size: $tertiary-font-size;
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
border: 1px solid $gray_2;
|
||||
border-radius: 8px;
|
||||
background: none;
|
||||
margin: $medium 0;
|
||||
font-weight: 400;
|
||||
font-size: $tertiary-font-size;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@@ -1,38 +1,24 @@
|
||||
<template>
|
||||
<div :style="cssVars" class="progress">
|
||||
<span v-for="step in numberSteps" :key="step" class="step" :class="{ active: step <= currentStep }"></span>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue} from "nuxt-property-decorator";
|
||||
<script lang="ts" setup>
|
||||
|
||||
@Component
|
||||
export default class QuizProgress extends Vue {
|
||||
const props = defineProps({
|
||||
numberSteps: Number,
|
||||
currentStep: Number,
|
||||
color: String
|
||||
});
|
||||
|
||||
@Prop({
|
||||
type: Number
|
||||
})
|
||||
private numberSteps!: number;
|
||||
|
||||
@Prop({
|
||||
required: true,
|
||||
type: Number
|
||||
})
|
||||
private currentStep!: number;
|
||||
|
||||
@Prop({
|
||||
required: true,
|
||||
type: String
|
||||
})
|
||||
private color!: string;
|
||||
|
||||
get cssVars() {
|
||||
return {
|
||||
'--color': this.color
|
||||
}
|
||||
const cssVars = computed(() => {
|
||||
return {
|
||||
'--color': props.color
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="cssVars" class="progress">
|
||||
<span v-for="step in numberSteps" :key="step" class="step" :class="{ active: step <= currentStep }"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@import "assets/css/color";
|
||||
@@ -42,10 +28,11 @@ export default class QuizProgress extends Vue {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.step {
|
||||
border-radius: 2px;
|
||||
border: 6px solid $gray_2;
|
||||
margin: $x_small $xxx_small;
|
||||
margin: $small $xxx_small;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
|
@@ -1,3 +1,76 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import {randomUUID} from "uncrypto";
|
||||
|
||||
const MAX_VALUE = 10;
|
||||
|
||||
const props = defineProps({
|
||||
initialValue: Number,
|
||||
color: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "#DC6A00"
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["rate"]);
|
||||
|
||||
const checkedValue = ref(0);
|
||||
const componentId = ref();
|
||||
|
||||
onMounted(() => {
|
||||
componentId.value = randomUUID();
|
||||
checkedValue.value = props.initialValue;
|
||||
});
|
||||
|
||||
const index = computed(() =>
|
||||
Array.apply(0, Array(MAX_VALUE)).map((_, b) => {
|
||||
return b + 1;
|
||||
}).reverse()
|
||||
);
|
||||
|
||||
const cssVars = computed(() => {
|
||||
return {
|
||||
'--color': props.color
|
||||
}
|
||||
});
|
||||
|
||||
function setChecked(index: number) {
|
||||
if (index < 1) {
|
||||
index = 1;
|
||||
} else if (index > MAX_VALUE) {
|
||||
index = MAX_VALUE;
|
||||
}
|
||||
checkedValue.value = index;
|
||||
emit('rate', index);
|
||||
}
|
||||
|
||||
function increment() {
|
||||
setChecked(checkedValue.value + 1);
|
||||
}
|
||||
|
||||
function decrement() {
|
||||
setChecked(checkedValue.value - 1);
|
||||
}
|
||||
|
||||
function isChecked(index: number) {
|
||||
return checkedValue.value >= index;
|
||||
}
|
||||
|
||||
function handleKeyUp(event: KeyboardEvent) {
|
||||
switch (event.key) {
|
||||
case "ArrowLeft":
|
||||
decrement();
|
||||
break;
|
||||
case "ArrowRight":
|
||||
increment();
|
||||
break;
|
||||
}
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="cssVars">
|
||||
<div class="rating" tabindex="0" @keyup="handleKeyUp">
|
||||
@@ -17,91 +90,9 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import {Component, Prop, Vue} from "nuxt-property-decorator";
|
||||
|
||||
@Component
|
||||
export default class Rating extends Vue {
|
||||
|
||||
@Prop({
|
||||
type: String,
|
||||
default: "#DC6A00"
|
||||
})
|
||||
readonly color !: string;
|
||||
|
||||
|
||||
@Prop({
|
||||
type: Number,
|
||||
required: false
|
||||
})
|
||||
readonly initialValue !: number | undefined;
|
||||
|
||||
readonly MAX_VALUE = 10;
|
||||
|
||||
created() {
|
||||
if (this.initialValue) {
|
||||
this.checkedValue = this.initialValue;
|
||||
}
|
||||
}
|
||||
|
||||
readonly index = Array.apply(0, Array(this.MAX_VALUE)).map((_, b) => {
|
||||
return b + 1;
|
||||
}).reverse();
|
||||
|
||||
public checkedValue = 0;
|
||||
private _uid: any;
|
||||
|
||||
public setChecked(index: number) {
|
||||
if (index < 1) {
|
||||
index = 1;
|
||||
} else if (index > this.MAX_VALUE) {
|
||||
index = this.MAX_VALUE;
|
||||
}
|
||||
this.checkedValue = index;
|
||||
this.$emit('rate', index);
|
||||
}
|
||||
|
||||
public increment() {
|
||||
this.setChecked(this.checkedValue + 1);
|
||||
}
|
||||
|
||||
public decrement() {
|
||||
this.setChecked(this.checkedValue - 1);
|
||||
}
|
||||
|
||||
public isChecked(index: number) {
|
||||
return this.checkedValue >= index;
|
||||
}
|
||||
|
||||
get componentId() {
|
||||
return this._uid;
|
||||
}
|
||||
|
||||
handleKeyUp(event: KeyboardEvent) {
|
||||
switch (event.key) {
|
||||
case "ArrowLeft":
|
||||
this.decrement();
|
||||
break;
|
||||
case "ArrowRight":
|
||||
this.increment();
|
||||
break;
|
||||
}
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
get cssVars() {
|
||||
return {
|
||||
'--color': this.color
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@import "assets/css/color";
|
||||
@import "assets/css/spacing";
|
||||
@import "assets/css/font";
|
||||
|
||||
|
@@ -1,56 +1,105 @@
|
||||
<script lang="ts" setup>
|
||||
import {useAuthStore} from "~/store/auth";
|
||||
|
||||
function logout() {
|
||||
useAuthStore().logout();
|
||||
navigateTo("/");
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<nuxt-link to="/" class="title">
|
||||
<svg width="13" height="16" viewBox="0 0 13 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<nuxt-link to="/bundle" class="title">
|
||||
<svg width="26" height="32" viewBox="0 0 13 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M0 7.07348C0 6.81265 0.101912 6.56214 0.284 6.37538L5.784 0.73436C6.17647 0.331827 6.82353 0.331826 7.216 0.734359L12.716 6.37538C12.8981 6.56214 13 6.81265 13 7.07348V15C13 15.5523 12.5523 16 12 16H1C0.447715 16 0 15.5523 0 15V7.07348Z"
|
||||
fill="#8BCDCD"/>
|
||||
</svg>
|
||||
Boussole <span class="bold">PLUSS</span>
|
||||
</nuxt-link>
|
||||
<div class="menu-container">
|
||||
<button class="button-icon">
|
||||
<svg class="svg-icon" width="32" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M0 7.07348C0 6.81265 0.101912 6.56214 0.284 6.37538L5.784 0.73436C6.17647 0.331827 6.82353 0.331826 7.216 0.734359L12.716 6.37538C12.8981 6.56214 13 6.81265 13 7.07348V15C13 15.5523 12.5523 16 12 16H1C0.447715 16 0 15.5523 0 15V7.07348Z"
|
||||
d="M723.43 508.6c-54.123 47.75-125.977 77.056-205.163 77.056-80.364 0-153.4-30.259-207.765-79.421C184.05 539.325 105.81 652.308 105.81 787.277v68.782c0 160.968 812.39 160.968 812.39 0v-68.782c-0.005-131.415-74.22-242.509-194.77-278.677z m-205.163 28.13c140.165 0 254.095-109.44 254.095-244.64S658.668 47.218 518.267 47.218c-139.93 0-253.855 109.675-253.855 244.874 0 135.204 113.925 244.639 253.855 244.639z m0 0"
|
||||
fill="#8BCDCD"/>
|
||||
</svg>
|
||||
Boussole <span class="bold">PLUSS</span>
|
||||
</nuxt-link>
|
||||
<span class="team">
|
||||
Équipe : {{ team }}
|
||||
</span>
|
||||
</svg>
|
||||
</button>
|
||||
<ul class="menu-container__content">
|
||||
<li v-if="useAuthStore().user">
|
||||
{{ useAuthStore().user.username }}
|
||||
</li>
|
||||
<li>
|
||||
<nuxt-link to="/account">Mon compte</nuxt-link>
|
||||
</li>
|
||||
<li>
|
||||
<button class="button-link" @click="logout">Me déconnecter</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Vue} from "nuxt-property-decorator";
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@Component
|
||||
export default class TeamHeader extends Vue {
|
||||
header {
|
||||
padding: 0 $xx_small;
|
||||
color: $black;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
//position: absolute;
|
||||
//height: $header_height;
|
||||
border-bottom: 2px solid $gray_3;
|
||||
}
|
||||
|
||||
get team() {
|
||||
return this.$auth.user ? this.$auth.user.username : "Non connecté";
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $xxx_small;
|
||||
text-transform: uppercase;
|
||||
color: $black;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title svg {
|
||||
margin-right: $xxx_small;
|
||||
}
|
||||
|
||||
.team {
|
||||
font-size: $small-font-size;
|
||||
}
|
||||
|
||||
.menu-container {
|
||||
position: relative;
|
||||
|
||||
& > .menu-container__content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: -16px;
|
||||
top: 47px;
|
||||
background: $white;
|
||||
}
|
||||
|
||||
&:focus-within > .menu-container__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.menu-container__content {
|
||||
gap: $xx_small;
|
||||
align-items: flex-end;
|
||||
list-style: none;
|
||||
min-width: 150px;
|
||||
padding: $xx_small $xx_small;
|
||||
|
||||
border: 2px solid $gray_3;
|
||||
|
||||
& li {
|
||||
//margin: $xxx_small;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "assets/css/color";
|
||||
@import "assets/css/spacing";
|
||||
@import "assets/css/font";
|
||||
|
||||
header {
|
||||
margin-top: $xx_small;
|
||||
color: $black;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-transform: uppercase;
|
||||
color: $black;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title svg {
|
||||
margin-right: $xxx_small;
|
||||
}
|
||||
|
||||
.team {
|
||||
font-size: $small-font-size;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
91
frontend/components/Toaster.vue
Normal file
91
frontend/components/Toaster.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script lang="ts" setup>
|
||||
import type {PropType} from "vue";
|
||||
|
||||
defineProps({
|
||||
"title": {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
"type": {
|
||||
type: String as PropType<'warn' | 'info' | 'success'>,
|
||||
default: 'info'
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside :class="'toaster ' + type">
|
||||
<div class="toaster-content">
|
||||
<div class="toaster-content-title">{{ title }}</div>
|
||||
<div class="toaster-content-body">
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.toaster {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
translate: -50% 0;
|
||||
padding: $medium $medium $xx_small;
|
||||
gap: $xxx_small;
|
||||
|
||||
border-radius: 0 0 16px 16px;
|
||||
|
||||
animation-name: fadeInRight;
|
||||
animation-duration: 300ms;
|
||||
animation-fill-mode: both;
|
||||
|
||||
@include border-shadow();
|
||||
|
||||
&.info {
|
||||
color: $info_text;
|
||||
background: $info_background;
|
||||
}
|
||||
&.warn {
|
||||
color: $warn_text;
|
||||
background: $warn_background;
|
||||
}
|
||||
&.success {
|
||||
color: $success_text;
|
||||
background: $success_background;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInRight {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, -100%, 0);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.toaster-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $xxx_small;
|
||||
|
||||
font-size: $small-font-size;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.toaster-content-title {
|
||||
font-weight: bold;
|
||||
line-height: 120%;
|
||||
font-size: $tertiary-font-size;
|
||||
}
|
||||
|
||||
.toaster-content-body {
|
||||
line-height: 150%;
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user