Compare commits

...

4 Commits

Author SHA1 Message Date
859924d6be feat: Modification du design des checkbox
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2025-06-20 14:33:14 +02:00
a26ec95983 feat: Modification du design du bouton de la certitude
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2025-06-20 01:01:55 +02:00
575cc28717 chore: lint
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2025-05-27 11:47:48 +02:00
e5665a24e7 feat: Ajout de la certitude d'un score 2025-05-27 11:44:52 +02:00
6 changed files with 368 additions and 66 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

@ -6,11 +6,16 @@
--color-black: #000000;
--color-green: rgb(118, 148, 67);
--header-size-small: 64px;
--header-size-small: 48px;
--header-size-big: 128px;
--header-size: var(--header-size-small);
--footer-size: 64px;
--footer-size: 48px;
--color-red: #c82606;
--color-orange: #df6a0f;
--color-light-green: #70bf41;
--color-dark-green: #00882b;
}
/* semantic color variables for this project */
@ -123,3 +128,12 @@ header svg.color-text [stroke] {
header svg.color-text [fill]:not([fill=none]) {
fill: var(--color-header-text)
}
strong {
font-weight: bold;
}
.bg-red { background: var(--color-red); }
.bg-orange { background: var(--color-orange); }
.bg-light-green { background: var(--color-light-green); }
.bg-dark-green { background: var(--color-dark-green); }

View File

@ -0,0 +1,191 @@
<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
&:not(:checked) + label > div::before
background-color: var(--color-green)
&:not(: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

@ -1,15 +1,18 @@
<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";
// import { toPng } from "html-to-image";
const props = defineProps({
id: {
@ -115,53 +118,82 @@ function answerSelected(question, answerWeight) {
function nextQuestion() {
setTimeout(() => {
slides.value.go(">");
console.log(slides);
// console.log(slides);
}, 100);
}
const saveAs = (blob, fileName) => {
var elem = window.document.createElement("a");
elem.href = blob;
elem.download = fileName;
elem.style = "display:none;";
(document.body || document.documentElement).appendChild(elem);
if (typeof elem.click === "function") {
elem.click();
} else {
elem.target = "_blank";
elem.dispatchEvent(
new MouseEvent("click", {
view: window,
bubbles: true,
cancelable: true,
}),
);
}
URL.revokeObjectURL(elem.href);
elem.remove();
};
// const saveAs = (blob, fileName) => {
// var elem = window.document.createElement("a");
// elem.href = blob;
// elem.download = fileName;
// elem.style = "display:none;";
// (document.body || document.documentElement).appendChild(elem);
// if (typeof elem.click === "function") {
// elem.click();
// } else {
// elem.target = "_blank";
// elem.dispatchEvent(
// new MouseEvent("click", {
// view: window,
// bubbles: true,
// cancelable: true,
// }),
// );
// }
// URL.revokeObjectURL(elem.href);
// 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 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 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() {
const certitude = certitudesResults.find(
(result) =>
result.weight_min <= weightAllCertitudes.value &&
result.weight_max >= weightAllCertitudes.value,
);
const certitudeTrad = certitude?.translations.find(
(translation) => translation.languages_code == language,
);
return { ...certitudeTrad, color: certitude.color };
}
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,15 @@ async function share() {
</li>
</ul>
</div>
<div class="certitude_result">
Niveau de certitude
<button
@click="displayCertitudeQuestions"
:class="'bg-' + certitudeResult.color"
>
<span>{{ certitudeResult?.niveau }} </span>
</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 +286,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 +321,27 @@ async function share() {
</template>
<style lang="sass" scoped>
.certitude_result
padding-top: 1rem
font-size: 1.1rem
text-align: center
color: var(--color-highlight-text)
& *
font-weight: 800
button
display: flex
width: 100%
color: var(--color-highlight-text)
justify-content: center
flex-direction: row
align-items: center
margin: .2rem auto 0
border-radius: 0
border: none
font-weight: 900 !important
font-size: 1.1rem
padding: .3rem
.spin
bottom: 1.5rem
right: 1.5rem
@ -352,7 +423,7 @@ label
justify-content: center
h2
font-size: 2rem
font-size: 1.5rem
font-weight: bold
line-height: 2.4rem
& + h2
@ -383,10 +454,10 @@ label
.gradient
padding: 0 1rem
height: 3rem
height: 2.5rem
background-image: linear-gradient(to right, red, red, rgb(255, 255, 0), rgb(255, 255, 0), green, green)
display: flex
margin: 2.5rem auto
margin: 1.5rem auto
align-items: center
border-radius: 3px
max-width: 30rem
@ -416,8 +487,8 @@ label
content: ""
border: 1px solid var(--color-highlight-text)
border-radius: 3px
top: -2.8rem
bottom: -1.5rem
top: -1rem
bottom: -1rem
position: absolute
width: 100%
font-weight: bold
@ -426,7 +497,7 @@ label
justify-content: center
&::after
content: attr(data-title)
content: ""
position: absolute
font-size: 1rem
top: -2rem

View File

@ -23,9 +23,7 @@ export default defineConfig({
},
// add this to cache all the
// static assets in the public folder
includeAssets: [
"**/*",
],
includeAssets: ["**/*"],
useCredentials: true,
manifest: {
theme_color: "#eb5252",
@ -34,16 +32,16 @@ export default defineConfig({
scope: "/",
icons: [
{
"src": "/images/pwa-icon-256.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
src: "/images/pwa-icon-256.png",
sizes: "192x192",
type: "image/png",
purpose: "any maskable",
},
{
"src": "/images/pwa-icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
src: "/images/pwa-icon-512.png",
sizes: "512x512",
type: "image/png",
purpose: "any maskable",
},
],
},