feat: Ajout de la certitude d'un score

This commit is contained in:
Simon 2025-05-27 11:44:37 +02:00
parent 6e42e8c800
commit e5665a24e7
6 changed files with 308 additions and 26 deletions

2
.gitignore vendored
View File

@ -28,7 +28,7 @@ coverage
*.sw?
# app
src/data.json
src/*.json
public/answers
public/homepage.webp
public/logo.png

View File

@ -30,6 +30,32 @@ async function fetchAsset(uuid) {
return fetch(url);
}
async function fetchCertitudesData() {
const fields = ["*", "translations.*"];
const url = `/items/certitudes?${fields
.map((item) => `fields[]=${item}`)
.join("&")}`;
let certitudes = (await fetchJSONApi(url)).data;
await fs.writeFile(
"./src/certitudes.json",
JSON.stringify(certitudes),
"utf8",
);
}
async function fetchCertitudesResultsData() {
const fields = ["*", "translations.*"];
const url = `/items/certitudes_results?${fields
.map((item) => `fields[]=${item}`)
.join("&")}`;
let certitudes = (await fetchJSONApi(url)).data;
await fs.writeFile(
"./src/certitudesResults.json",
JSON.stringify(certitudes),
"utf8",
);
}
async function fetchScoresData() {
const fields = [
"*",
@ -312,6 +338,8 @@ async function fetchHomepageData() {
async function fetchData() {
await fetchHomepageData();
await fetchCertitudesData();
await fetchCertitudesResultsData();
await fetchScoresData();
}

View File

@ -123,3 +123,7 @@ header svg.color-text [stroke] {
header svg.color-text [fill]:not([fill=none]) {
fill: var(--color-header-text)
}
strong {
font-weight: bold;
}

View File

@ -0,0 +1,184 @@
<script setup>
import { ref, computed, watchEffect } from 'vue';
import { useStore } from "@/stores"; // adapte le chemin si besoin
const store = useStore();
const props = defineProps({
certitude: {
type: Object,
required: true,
},
});
const emits = defineEmits(['answerSelected', 'nextQuestion']);
const selectedWeight = ref(null);
// Utilise la langue du store
const language = computed(() => store.language || 'fr-FR');
// Recherche la bonne traduction selon la langue courante
const translation = computed(() => {
return (
props.certitude.translations.find(
(t) => t.languages_code === language.value
) ||
// Fallback en français
props.certitude.translations.find((t) => t.languages_code === 'fr-FR') ||
// Fallback générique
props.certitude.translations[0]
);
});
// Regroupe les réponses avec leurs poids
const answers = computed(() => {
return [1, 2, 3].map((i) => ({
id: i,
title: translation.value[`answer${i}`],
weight: props.certitude[`weight${i}`],
}));
});
function selectAnswer(answer) {
selectedWeight.value = answer.weight;
emits("answerSelected", props.certitude, selectedWeight.value);
}
watchEffect(() => {
if (answers.value.length && selectedWeight.value === null) {
selectAnswer(answers.value[0]);
}
})
</script>
<template>
<div class="main">
<div class="center">
<legend>{{ translation.title }}</legend>
<div class="description" v-html="translation.description"></div>
<ul class="choices">
<li class="choice" v-for="answer in answers" :key="answer.id">
<input
type="radio"
:id="`certitude_${certitude.id}_answer_${answer.id}`"
:name="`certitude_${certitude.id}`"
:value="answer.weight"
v-model="selectedWeight"
@change="selectAnswer(answer)"
@click="$emit('nextQuestion')"
/>
<label :for="`certitude_${certitude.id}_answer_${answer.id}`">
<div>{{ answer.title }}</div>
</label>
</li>
</ul>
</div>
</div>
<div class="btns">
<button class="btn next" @click="$emit('nextQuestion')">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="40" height="40">
<path d="m15.5 0.932-4.3 4.38 14.5 14.6-14.5 14.5 4.3 4.4 14.6-14.6 4.4-4.3-4.4-4.4-14.6-14.6z"></path>
</svg>
</button>
</div>
</template>
<style scoped lang="sass">
legend
text-align: center
font-size: 1.4rem
line-height: 2rem
font-weight: bold
width: 100%
.description
text-align: center
padding: 1rem
font-size: 1rem
.choices
list-style-type: none
text-align: left
display: inline-block
padding-left: 0
label
cursor: pointer
display: block
input[type=radio]
display: none
& + label > div
position: relative
padding: .2rem .2rem .2rem 2rem
& + label > div::before,
& + label > div::after
display: block
position: absolute
box-sizing: border-box
content:''
border-radius: 1rem
& + label > div::before
top: .5rem
left: 0
background-color: var(--color-green)
width: 1rem
height: 1rem
& + label > div::after
top: calc(3px + .5rem)
left: 3px
width: calc(1rem - 6px)
height: calc(1rem - 6px)
&:checked + label > div
text-shadow: -0.06ex 0 0 currentColor, 0.06ex 0 0 currentColor
&:checked + label > div::before
background-color: var(--color-green)
&:checked + label > div::after
background-color: white
.main
height: 100%
max-width: 100%
display: flex
flex-direction: column
justify-content: space-around
align-items: center
.center
width: 100%
text-align: center
.btns
width: 400px
max-width: 100%
min-width: 280px
position: relative
margin: 0 auto
.next
background: var(--color-highlight-background)
bottom: 1rem
right: 1rem
width: 3rem
height: 3rem
opacity: .8
&:hover
opacity: 1
svg
width: 80%
height: 80%
transform: rotate(90deg)
fill: var(--color-highlight-text)
</style>

View File

@ -298,9 +298,9 @@ legend
height: calc(1rem - 6px)
&:checked + label > div
text-shadow: -0.06ex 0 0 currentColor, 0.06ex 0 0 currentColor
&:not(:checked) + label > div::before
&:checked + label > div::before
background-color: var(--color-green)
&:not(:checked) + label > div::after
&:checked + label > div::after
background-color: white
.main

View File

@ -1,12 +1,15 @@
<script setup>
import data from "@/data.json";
import certitudes from "@/certitudes.json";
import certitudesResults from "@/certitudesResults.json";
import { ref, computed } from "vue";
import { ref, computed, watch } from "vue";
import { useStore } from "@/stores";
import { Splide, SplideSlide } from "@splidejs/vue-splide";
import Question from "./Question.vue";
import Certitude from "./Certitude.vue";
import "@splidejs/splide/dist/css/splide.min.css";
import ScoreHeader from "./ScoreHeader.vue";
import { toPng } from "html-to-image";
@ -115,7 +118,7 @@ function answerSelected(question, answerWeight) {
function nextQuestion() {
setTimeout(() => {
slides.value.go(">");
console.log(slides);
// console.log(slides);
}, 100);
}
@ -141,27 +144,56 @@ const saveAs = (blob, fileName) => {
elem.remove();
};
const sharing = ref(false);
async function share() {
sharing.value = true;
const filter = (node) => {
const exclusionClasses = ["btn"];
return !exclusionClasses.some((classname) =>
node.classList?.contains(classname),
// const sharing = ref(false);
// async function share() {
// sharing.value = true;
// const filter = (node) => {
// const exclusionClasses = ["btn"];
// return !exclusionClasses.some((classname) =>
// node.classList?.contains(classname),
// );
// };
// const body = document.querySelector("body");
// body.classList.add("print");
// const dataUrl = await toPng(body, { filter: filter });
// body.classList.remove("print");
// const fileName = new Date()
// .toISOString()
// .replace(/T/, "_")
// .replace(/\..+/, "")
// .replaceAll(":", "-");
// saveAs(dataUrl, `Ceiba-score-${fileName}.png`);
// sharing.value = false;
// }
const displayCertitude = ref(false);
const weightCertitudes = ref(new Array(certitudes.length).fill(0));
const weightAllCertitudes = ref(0);
const certitudeResult = ref(selectCertitudeResult());
function selectCertitudeResult() {
return certitudesResults
.find(
(result) =>
result.weight_min <= weightAllCertitudes.value &&
result.weight_max >= weightAllCertitudes.value,
)
?.translations.find(
(translation) => translation.languages_code == language,
);
};
const body = document.querySelector("body");
body.classList.add("print");
const dataUrl = await toPng(body, { filter: filter });
body.classList.remove("print");
const fileName = new Date()
.toISOString()
.replace(/T/, "_")
.replace(/\..+/, "")
.replaceAll(":", "-");
saveAs(dataUrl, `Ceiba-score-${fileName}.png`);
sharing.value = false;
}
function displayCertitudeQuestions() {
displayCertitude.value = !displayCertitude.value;
}
function answerSelectedCertitude(question, answerWeight) {
weightCertitudes.value[question.sort - 1] = answerWeight;
weightAllCertitudes.value = weightCertitudes.value.reduce(
(accumulator, curr) => accumulator + curr,
0,
);
}
watch(weightAllCertitudes, () => {
certitudeResult.value = selectCertitudeResult();
});
</script>
<template>
@ -187,6 +219,15 @@ async function share() {
@nextQuestion="nextQuestion"
/>
</SplideSlide>
<template v-if="displayCertitude">
<SplideSlide v-for="certitude in certitudes" :key="certitude.id" >
<Certitude
:certitude="certitude"
@answerSelected="answerSelectedCertitude"
@nextQuestion="nextQuestion"
/>
</SplideSlide>
</template>
<SplideSlide class="latest">
<template v-if="displayScoreResult && result">
<div>
@ -216,6 +257,20 @@ async function share() {
</li>
</ul>
</div>
<div v-if="displayCertitude" class="certitude_result">
<button @click="displayCertitudeQuestions">
<span>Niveau de certitude :
{{ certitudeResult?.niveau }}<br />
<span v-html="certitudeResult?.description"></span>
</span>
<span class="cross"></span>
</button>
</div>
<div v-else class="certitude_result">
<button @click="displayCertitudeQuestions">
Ajout un niveau de certitude
</button>
</div>
<!--button class="btn download" @click="() => share()" v-if="!sharing">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36">
<path
@ -236,9 +291,9 @@ async function share() {
/>
</svg>
</button-->
<button class="btn spin" v-if="sharing">
<!-- <button class="btn spin" v-if="sharing">
<img src="/spin.svg" />
</button>
</button> -->
</div>
</template>
<template v-else>
@ -271,6 +326,17 @@ async function share() {
</template>
<style lang="sass" scoped>
.certitude_result
text-align: center
button
display: flex
justify-content: center
flex-direction: row
align-items: center
margin: 1rem auto 0
padding: .3rem
.cross
margin-left: .5rem
.spin
bottom: 1.5rem
right: 1.5rem