feat: add frontend

This commit is contained in:
2022-10-07 16:15:53 +02:00
parent abfaf19c47
commit 97059d4c6e
72 changed files with 47026 additions and 0 deletions

View File

@@ -0,0 +1,137 @@
<template>
<div class="content">
<team-header/>
<hr/>
<div v-if="loading" class="center" >
<loader/>
</div>
<section v-if="currentResult" class="last-quiz">
<div class="last-quiz-header">
<span class="date"> {{ currentResult.createdDate | formatDate }}</span>
<!-- FIXME remove this dirty trick -->
<nuxt-link
class="link"
:to="{ path: '/details', query: { quiz: currentResult._links.self.href.replace('http://localhost:8080/quizzes/','') }}">
+ Voir le détail
</nuxt-link>
</div>
<div v-if="currentResult && currentResult.scores" class="chart-area">
<polar-area-chart :data="currentResult.scores.map(value => value.scoreAvg)"/>
<Legend/>
</div>
</section>
<section v-if="quizzes.length > 0" class="history" >
<div v-for="q in quizzes" :key="q.id">
<button @click="setCurrent(q)"><span>Bousolle - {{ q.createdDate | formatDate }}</span><span></span></button>
</div>
</section>
<section v-else-if="!loading" class="center">
Aucune auto-évaluation n'a été faite. Veuillez en réaliser une première.
</section>
<div class="button-container">
<nuxt-link class="button orange" to="/quiz">Nouveau</nuxt-link>
</div>
</div>
</template>
<script lang="ts">
import {Component, Vue} from "vue-property-decorator";
import {AxiosResponse} from "axios";
import {RepositoryFactory} from "~/repositories/RepositoryFactory";
import {RestResponse} from "~/repositories/models/rest-response.model";
import {Quiz, Score} from "~/repositories/models/quiz.model";
@Component
export default class History extends Vue {
readonly quizRepository = RepositoryFactory.get('quiz');
private quizzes: Quiz[] = [];
private currentResult: Quiz | null = null;
private loading = true;
async mounted() {
await this.quizRepository.findMine().then((response: AxiosResponse<RestResponse<Quiz>>) => {
this.quizzes = response.data._embedded.quizzes;
});
for (const quiz of this.quizzes) {
// FIXME ugly : replace when scores will be handled as a REST resource
console.info(quiz);
const id = Number.parseInt(quiz._links.self.href.replace("http://localhost:8080/quizzes/", ""));
await this.quizRepository.findScores(id)
.then((value: AxiosResponse<Score[]>) => {
quiz.scores = value.data;
});
}
this.currentResult = this.quizzes.length > 0 ? this.quizzes[0] : null;
this.loading = false;
}
private setCurrent(quiz: Quiz) {
this.currentResult = quiz;
}
}
</script>
<style lang="scss" scoped>
@import "assets/css/color";
@import "assets/css/spacing";
@import "assets/css/font";
.last-quiz {
margin-bottom: $x_small 0;
}
.last-quiz-header {
display: flex;
justify-content: space-between;
font-size: $tertiary-font-size;
.date {
color: $gray_4;
font-weight: 700;
}
.link {
color: $blue;
}
}
.chart-area {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
margin: $x_small 0;
}
.history {
margin-top: $x_small;
}
.history button {
display: flex;
width: 100%;
justify-content: space-between;
padding: 16px;
margin: $xxx_small 0;
background: $gray_1;
border-radius: 8px;
border: none;
color: $gray_4;
font-weight: 700;
font-size: $tertiary-font-size;
&:hover {
text-decoration: none;
cursor: pointer;
background: $gray_3;
color: $gray_1;
}
}
.button-container {
margin-bottom: $x_small;
}
</style>

106
frontend/pages/details.vue Normal file
View File

@@ -0,0 +1,106 @@
<template>
<div class="content">
<team-header/>
<hr/>
<div v-if="!loading">
<span class="date">{{ quiz.createdDate | formatDate }}</span>
<quiz-axe-details
v-for="axe in axes"
:key="axe.identifier"
:axe="axe"
:score="getScore(axe)"
:responses="getResponses(axe)"/>
</div>
<loader v-else class="center"/>
<div class="button-container">
<nuxt-link class="button orange" to="/dashboard">Retour à l'accueil</nuxt-link>
</div>
</div>
</template>
<script lang="ts">
import {Component, Vue} from "nuxt-property-decorator";
import {AxiosResponse} from "axios";
import {RepositoryFactory} from "~/repositories/RepositoryFactory";
import {Quiz, ResponseWithQuestion, Score} from "~/repositories/models/quiz.model";
import QuizAxeDetails from "~/components/QuizAxeDetails.vue";
import {Axe} from "~/repositories/models/axe.model";
import {RestResponse} from "~/repositories/models/rest-response.model";
@Component({
components: {QuizAxeDetails}
})
export default class Result extends Vue {
readonly axeRepository = RepositoryFactory.get('axe');
readonly quizRepository = RepositoryFactory.get('quiz');
private axes: Axe[] = [];
private quiz: Quiz | null = null;
private scores: Score[] = [];
private responses: ResponseWithQuestion[] = [];
private loading = false;
created() {
if (!this.$route.query.quiz) {
this.$router.push("/dashboard");
}
try {
this.loading = true;
const quizId = Number.parseInt(this.$route.query.quiz as string);
this.quizRepository.findById(quizId)
.then((response: AxiosResponse<Quiz>) => {
this.quiz = response.data;
return response;
})
.then(() => {
return this.quizRepository.findScores(quizId)
.then((response: AxiosResponse<Score[]>) => {
this.scores = response.data;
return response;
})
})
.then(() => {
return this.quizRepository.findResponses(quizId)
.then((response: AxiosResponse<RestResponse<ResponseWithQuestion>>) => {
this.responses = response.data._embedded.responses;
return response;
})
})
.then(() => {
return this.axeRepository.findAll()
.then((response: AxiosResponse<RestResponse<Axe>>) => {
this.axes = response.data._embedded.axes;
return response;
});
})
.finally(() => {
this.loading = false;
});
} catch (e: any) {
console.info("error", e);
this.loading = false;
}
}
getScore(axe: Axe) {
return this.scores.find(value => value.axeIdentifier === axe.identifier);
}
getResponses(axe: Axe) {
console.info(this.responses)
return this.responses.filter((response: ResponseWithQuestion) => response.axeIdentifier === axe.identifier);
}
}
</script>
<style lang="scss" scoped>
@import "assets/css/color";
@import "assets/css/font";
.date {
font-weight: 700;
font-size: $tertiary-font-size;
color: $gray_4;
}
</style>

3
frontend/pages/index.vue Normal file
View File

@@ -0,0 +1,3 @@
<template>
<Home />
</template>

80
frontend/pages/login.vue Normal file
View File

@@ -0,0 +1,80 @@
<template>
<div>
<Header/>
<form id="login_form" class="content login" @submit.prevent="authenticate">
<input v-model="username" type="text" required placeholder="Nom de l'équipe" aria-label="Nom de l'équipe"/>
<input id="code" v-model="password" type="password" required placeholder="Code" aria-label="Code"/>
<div>
<label>
<input v-model="conditionChecked" type="checkbox" required/>
En continuant, jaccepte les conditions d'utilisation de Boussole PLUSS et jai lu la politique de
confidentialité
</label>
</div>
<button class="button blue" type="submit" :disabled="!conditionChecked">
Continuer
</button>
<div class="error-container">{{ error }}</div>
</form>
</div>
</template>
<script lang="ts">
import {Component, Vue} from "nuxt-property-decorator";
import {HTTPResponse} from "@nuxtjs/auth-next/dist";
@Component
export default class Login extends Vue {
private username = "";
private password = "";
private conditionChecked: boolean = false;
private error = "";
async authenticate() {
try {
this.error = "";
const response = await this.$auth.loginWith('local', {
data: {
username: this.username,
password: this.password
}
}) as HTTPResponse;
if (response && response.data) {
const { token } = response.data;
this.$axios.defaults.headers.common = { Authorization: `Bearer ${token}` };
} else {
console.error("Unable to login, no data in HTTP response")
}
} catch (e: any) {
this.error = e;
console.info("error", e);
}
}
}
</script>
<style lang="scss" scoped>
@import "assets/css/spacing";
form.login {
input[type="text"] {
margin: $x_small 0;
}
input[type="password"] {
margin-bottom: $small;
}
button {
margin: $medium 0;
}
}
.error-container {
display: flex;
justify-content: center;
color: red;
}
</style>

171
frontend/pages/quiz.vue Normal file
View File

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

View File

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

58
frontend/pages/result.vue Normal file
View File

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