feat: review backend and frontend

- update to the latest version of Java/SpringBoot
- update to the latest version NuxtJS
- add account/password update
- add account creation
- add account password reset
- add bundle to regroup questions and add default questions on user creation
- add bundle creation
This commit is contained in:
2024-07-03 15:55:34 +02:00
parent f86d794239
commit c89deaf007
205 changed files with 5149 additions and 40454 deletions

View 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>

View File

@@ -0,0 +1,12 @@
<template>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Architecto consequatur, consequuntur et expedita libero
non soluta sunt ullam vel velit voluptatibus voluptatum? Accusamus blanditiis est obcaecati temporibus velit. Dicta
doloribus eveniet id incidunt suscipit. Accusamus ad aspernatur at aut, beatae laboriosam modi natus nemo, officia
perspiciatis porro quisquam, totam vel.
</p>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. A asperiores atque autem, culpa est, et facilis illum
minima, numquam odio pariatur quaerat quas quo! Adipisci, cum deleniti dolorem enim fugit hic, illum impedit in iure
magnam nam numquam omnis quo? Ab dignissimos distinctio eligendi facere molestiae nam non, odit quae quidem tempora!
Commodi earum excepturi exercitationem ipsa mollitia repellendus, veniam.</p>
</template>

View 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>CGU</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>

View File

@@ -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>

View File

@@ -1,29 +1,45 @@
<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, cest quoi ?</h1>
<p>
« Une application de valorisation des ressources mobilisées dans les projets locaux. Le petit plus cest
lidentification 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 vous en
êtes !
Le principe : il sagit dauto-é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 dune 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>
<section>
<h1>Contribuer à la Boussole</h1>
<p>
Le projet actuel est prévu pour travailler sur le thème de la production locale.<br/>
Cet outil peut servir à évaluer des collectifs sur dautres thématiques. Vous êtes encouragé à y réfléchir et à y
travailler. La licence AGPL version 3 prévoit un partage des améliorations que vous porterez à loutil. Si vous
désirez faire un fork de loutil, contactez-nous : <nuxt-link to="mailto:contact@apes-hdf.org">contact@apes-hdf.org</nuxt-link>
</p>
</section>
<div class="button-container">
<nuxt-link class="button orange" to="/login">Démarrer</nuxt-link>
</div>
</template>
<script setup lang="ts">
</script>

View File

@@ -19,9 +19,6 @@
<style lang="scss" scoped>
@import "assets/css/color";
@import "assets/css/spacing";
ol {
margin: 0;
position: relative;

View 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">CGU</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>

View File

@@ -0,0 +1,40 @@
<template>
<header class="header">
<div class="secondary-logo">
<nuxt-link to="https://www.apes-hdf.org" target="_blank">
<img src="/images/logo/logo_apes.svg" height="50px" alt="Logo APES"/>
</nuxt-link>
</div>
<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;
.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>

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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";

View File

@@ -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="/" 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>
{{ 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>

View 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>