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: $x_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>
|
37
frontend/components/CguModal.vue
Normal file
37
frontend/components/CguModal.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<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">
|
||||
<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>
|
||||
</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,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, 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>
|
||||
<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 d’autres 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 à l’outil. Si vous
|
||||
désirez faire un fork de l’outil, 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>
|
||||
|
@@ -19,9 +19,6 @@
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@import "assets/css/color";
|
||||
@import "assets/css/spacing";
|
||||
|
||||
ol {
|
||||
margin: 0;
|
||||
position: relative;
|
||||
|
26
frontend/components/MainFooter.vue
Normal file
26
frontend/components/MainFooter.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<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>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.footer {
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
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>
|
40
frontend/components/MainHeader.vue
Normal file
40
frontend/components/MainHeader.vue
Normal 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: $xx_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>
|
@@ -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";
|
||||
|
@@ -1,3 +1,62 @@
|
||||
<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});
|
||||
this.emitRatingState();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function emitRatingState() {
|
||||
const questions = store.questionsRatedPerAxe.get(props.axeNumber);
|
||||
const unratedQuestions = questions ? questions.filter(value => !value.rated) : [];
|
||||
emit('rate', {
|
||||
isFullRated: unratedQuestions.length === 0
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article :style="cssVars">
|
||||
<header>
|
||||
@@ -11,102 +70,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,6 +111,7 @@ $size: 31px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
text-align: left;
|
||||
|
||||
> span {
|
||||
padding: 0 $x_small;
|
||||
}
|
||||
@@ -149,40 +130,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: $x_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: $small 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,6 +28,7 @@ export default class QuizProgress extends Vue {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.step {
|
||||
border-radius: 2px;
|
||||
border: 6px solid $gray_2;
|
||||
|
@@ -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="/" 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>
|
||||
|
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: $small $small $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