feat: add frontend
This commit is contained in:
137
frontend/pages/dashboard.vue
Normal file
137
frontend/pages/dashboard.vue
Normal 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
106
frontend/pages/details.vue
Normal 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
3
frontend/pages/index.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<Home />
|
||||
</template>
|
80
frontend/pages/login.vue
Normal file
80
frontend/pages/login.vue
Normal 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, j’accepte les conditions d'utilisation de Boussole PLUSS et j’ai lu la politique de
|
||||
confidentialité
|
||||
</label>
|
||||
</div>
|
||||
<button class="button blue" type="submit" :disabled="!conditionChecked">
|
||||
Continuer
|
||||
</button>
|
||||
<div class="error-container">{{ error }}</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
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
171
frontend/pages/quiz.vue
Normal 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>
|
20
frontend/pages/redirect.vue
Normal file
20
frontend/pages/redirect.vue
Normal 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
58
frontend/pages/result.vue
Normal 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>
|
Reference in New Issue
Block a user