front_foundation #9

Closed
mohamed_maoulainine wants to merge 181 commits from front_foundation into main
8 changed files with 339 additions and 153 deletions
Showing only changes of commit 28b0e69da1 - Show all commits

View File

@ -1,63 +0,0 @@
{
"entrepreneurs": [
{ "id": 1, "name": "Alice", "email": "alice@example.com" },
{ "id": 2, "name": "Bob", "email": "bob@example.com" },
{ "id": 3, "name": "Charlie", "email": "charlie@example.com" }
],
"data": [
{
"projectId": 1,
"title": 1,
"title_text": "1. Problème",
"description": "3 problèmes essentiels à résoudre pour le client"
},
{
"projectId": 1,
"title": 2,
"title_text": "2. Segments",
"description": "Les segments de clientèle visés"
},
{
"projectId": 1,
"title": 3,
"title_text": "3. Valeur",
"description": "La proposition de valeur"
},
{
"projectId": 1,
"title": 4,
"title_text": "4. Solution",
"description": "Les solutions proposées"
},
{
"projectId": 1,
"title": 5,
"title_text": "5. Avantage",
"description": "Les avantages concurrentiels"
},
{
"projectId": 1,
"title": 6,
"title_text": "6. Canaux",
"description": "Les canaux de distribution"
},
{
"projectId": 1,
"title": 7,
"title_text": "7. Indicateurs",
"description": "Les indicateurs clés de performance"
},
{
"projectId": 1,
"title": 8,
"title_text": "8. Coûts",
"description": "Les coûts associés"
},
{
"projectId": 1,
"title": 9,
"title_text": "9. Revenus",
"description": "Les sources de revenus"
}
]
}

View File

@ -1,2 +0,0 @@
#!/usr/bin/bash
json-server --watch db.json --port 5000

View File

@ -1,6 +1,12 @@
<template> <template>
<header> <header>
<img src="./icons/logo inpulse.png" alt="INPulse" /> <a
href="https://www.bordeaux-inp.fr/fr/lincubateur-bordeaux-inpulse"
target="_blank"
rel="noopener"
>
<img src="./icons/logo inpulse.png" alt="INPulse Logo" class="logo" />
</a>
</header> </header>
</template> </template>

View File

@ -4,7 +4,7 @@ import { useRouter } from "vue-router";
import { jwtDecode } from "jwt-decode"; // i hope this doesn't break the code later import { jwtDecode } from "jwt-decode"; // i hope this doesn't break the code later
import { store } from "../main.ts"; import { store } from "../main.ts";
import { callApi } from "@/services/api.ts"; import { callApi } from "@/services/api.ts";
import Header from "@/components/HeaderComponent.vue";
const router = useRouter(); const router = useRouter();
type TokenPayload = { type TokenPayload = {
@ -43,6 +43,7 @@ const callApiWithLoading = async (path: string) => {
</script> </script>
<template> <template>
<Header />
<error-wrapper></error-wrapper> <error-wrapper></error-wrapper>
<div class="auth-container"> <div class="auth-container">
<div class="auth-card"> <div class="auth-card">

View File

@ -2,67 +2,72 @@
<div :class="['cell', { expanded }]" @click="handleClick"> <div :class="['cell', { expanded }]" @click="handleClick">
<h3 class="fs-5 fw-medium">{{ titleText }}</h3> <h3 class="fs-5 fw-medium">{{ titleText }}</h3>
<div <div class="tooltip-explain">{{ description }}</div>
v-for="(desc, index) in currentDescriptions"
:key="index"
class="section-bloc"
>
<!-- ADMIN -------------------------------------------------------------------------------------------->
<template v-if="IS_ADMIN">
<div class="description">
<p class="m-0">{{ desc }}</p>
</div>
</template>
<!-- ENTREP ------------------------------------------------------------------------------------------->
<template v-if="!IS_ADMIN">
<!-- Mode affichage -->
<template v-if="!isEditing[index]">
<div class="description">
<p class="m-0">{{ desc }}</p>
</div>
<div class="button-container">
<button
v-if="expanded"
class="edit-button"
@click.stop="startEditing(index)"
>
Éditer
</button>
</div>
</template>
<!-- Mode édition -->
<template v-else>
<textarea
v-model="editedDescriptions[index]"
class="edit-input"
></textarea>
<div class="button-container">
<button
class="save-button"
@click.stop="saveEdit(index)"
>
Enregistrer
</button>
<button
class="cancel-button"
@click.stop="cancelEdit(index)"
>
Annuler
</button>
</div>
</template>
</template>
</div>
<!---------------------------------------------------------------------------------------------------->
<template v-if="expanded"> <template v-if="expanded">
<div class="canvas-exit-hint"> <div class="explain">
Cliquez n'importe pour quitter le canvas <p>
</div> {{ description }}
</p>
</div>
</template> </template>
<div class="description-wrapper custom-flow">
<div
v-for="(desc, index) in currentDescriptions"
:key="index"
:class="['section-bloc', index % 2 === 0 ? 'from-left' : 'from-right']"
class="section-bloc"
>
<!-- ADMIN -------------------------------------------------------------------------------------------->
<template v-if="IS_ADMIN">
<div class="description">
<p class="m-0">{{ desc }}</p>
</div>
</template>
<!-- ENTREP ------------------------------------------------------------------------------------------->
<template v-if="!IS_ADMIN">
<!-- Mode affichage -->
<template v-if="!isEditing[index]">
<div class="description">
<p class="m-0">{{ desc }}</p>
</div>
<div class="button-container">
<button
v-if="expanded"
class="edit-button"
@click.stop="startEditing(index)"
>
Éditer
</button>
</div>
</template>
<!-- Mode édition -->
<template v-else>
<div class="edit-row">
<textarea
v-model="editedDescriptions[index]"
class="edit-input"
></textarea>
<div class="button-container">
<button class="save-button" @click.stop="saveEdit(index)">Enregistrer</button>
<button class="cancel-button" @click.stop="cancelEdit(index)">Annuler</button>
</div>
</div>
</template>
</template>
</div>
<!---------------------------------------------------------------------------------------------------->
<template v-if="expanded">
<div class="canvas-exit-hint">
Cliquez n'importe où pour quitter le canvas (terminez d'abord vos modifications)
</div>
</template>
</div>
</div> </div>
</template> </template>
@ -165,24 +170,72 @@ const mockFetch = async (projectId: number, title: number, date: string) => {
`Mock fetch pour projectId: ${projectId}, title: ${title}, date: ${date}` `Mock fetch pour projectId: ${projectId}, title: ${title}, date: ${date}`
); );
const leanCanvasData: Record<number, string[]> = {
1: [
"Les clients ont du mal à trouver des produits écoresponsables abordables.",
"Le processus d'achat en ligne est trop complexe.",
"Manque de transparence sur lorigine des produits.",
"Peu dalternatives locales et durables sur le marché."
],
2: [
"Jeunes urbains engagés dans la cause écologique.",
"Familles à revenu moyen voulant consommer responsable.",
"Entreprises soucieuses de leur empreinte carbone."
],
3: [
"Une plateforme centralisée avec des produits écologiques certifiés.",
"Un service client humain et réactif.",
"Livraison éco-responsable avec suivi."
],
4: [
"Application intuitive avec suggestions personnalisées.",
"Emballages recyclables et réutilisables.",
],
5: [
"Algorithme exclusif de recommandations durables.",
"Forte communauté engagée sur les réseaux.",
],
6: [
"Canaux digitaux : réseaux sociaux, SEO.",
adnane marked this conversation as resolved Outdated
Outdated
Review

Ce saveEdit utile étant donnée celle définie en dessous ?

Ce saveEdit utile étant donnée celle définie en dessous ?
"Partenariats avec influenceurs écoresponsables.",
"Boutique physique en pop-up stores."
],
adnane marked this conversation as resolved Outdated
Outdated
Review

axios again

axios again
Outdated
Review

And the path is not right

And the path is not right
7: [
"Taux de rétention client mensuel.",
"Taux de satisfaction utilisateur (NPS)."
],
8: [
"Coût du développement logiciel initial.",
"Campagnes publicitaires et communication.",
"Frais logistiques (emballages, transport)."
],
9: [
"Ventes directes sur la plateforme.",
"Abonnement mensuel premium pour livraison gratuite.",
"Revenus via partenariats de marque."
]
};
// On extrait les descriptions pour la section demandée
const section = leanCanvasData[title] || ["Aucune donnée disponible."];
adnane marked this conversation as resolved Outdated
Outdated
Review

same

same
// On garde tous les éléments, dans l'ordre
const result = section.map((txt) => ({ txt }));
return new Promise<{ txt: string }[]>((resolve) => { return new Promise<{ txt: string }[]>((resolve) => {
setTimeout(() => { setTimeout(() => resolve(result), 500);
resolve([
{ txt: "Ceci est une description 1 pour tester le front." },
{ txt: "Deuxième description." },
{ txt: "Troisième description." },
]);
}, 500); // Simule un délai réseau de 500ms
}); });
}; };
// Utilisation du mock dans handleClick pour tester sans serveur // Utilisation du mock dans handleClick pour tester sans serveur
const handleClick = async () => { const handleClick = async () => {
if (!expanded.value) { if (!expanded.value) {
await fetchData(props.projectId, props.title, "NaN", IS_MOCK_MODE); await fetchData(props.projectId, props.title, "NaN", IS_MOCK_MODE);
} else if (!isEditing.value.includes(true)) { } else if (!isEditing.value.includes(true)) {
// Réinitialiser les descriptions si aucune édition n'est en cours // Réinitialiser les descriptions si aucune édition n'est en cours
currentDescriptions.value = [props.description]; //currentDescriptions.value = [props.description];
editedDescriptions.value = [props.description]; editedDescriptions.value = [props.description];
} }
@ -256,6 +309,20 @@ const cancelEdit = (index: number) => {
editedDescriptions.value[index] = currentDescriptions.value[index]; editedDescriptions.value[index] = currentDescriptions.value[index];
isEditing.value[index] = false; isEditing.value[index] = false;
}; };
const randomStyle = () => {
const offsetX = Math.floor(Math.random() * 20) - 10; // entre -10 et +10px
const offsetY = Math.floor(Math.random() * 20) - 10;
return {
transform: `translate(${offsetX}px, ${offsetY}px)`,
transition: 'transform 0.3s ease',
};
};
const styleClasses = ['float-up', 'float-left', 'float-right', 'wiggle', 'tilt'];
const getRandomClass = () => {
return styleClasses[Math.floor(Math.random() * styleClasses.length)];
};
</script> </script>
<style scoped> <style scoped>
@ -276,6 +343,27 @@ const cancelEdit = (index: number) => {
justify-content: flex-start !important; justify-content: flex-start !important;
} }
.tooltip-explain {
position: absolute;
bottom: 101%; /* au-dessus de la carte */
left: 50%;
transform: translateX(-50%);
background-color: #333;
color: #fff;
padding: 6px 12px;
font-size: 13px;
border-radius: 6px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
z-index: 10;
}
.cell:not(.expanded):hover .tooltip-explain {
opacity: 0.9;
}
.cell:not(.expanded):hover { .cell:not(.expanded):hover {
transform: scale(1.05); transform: scale(1.05);
box-shadow: 0 8px 9px rgba(0, 0, 0, 0.2); box-shadow: 0 8px 9px rgba(0, 0, 0, 0.2);
@ -309,16 +397,11 @@ const cancelEdit = (index: number) => {
} }
.description { .description {
display: flex; font-size: 5px;
align-items: center; color: #333;
justify-content: center; word-break: break-word;
text-align: center; width: 90%;
width: 100%; margin: 5px 0;
height: 100%;
font-size: 16px;
margin-top: 10px;
margin-left: 2%;
margin-right: 4%;
} }
.description + .p { .description + .p {
@ -330,14 +413,18 @@ const cancelEdit = (index: number) => {
.edit-input { .edit-input {
width: 100%; width: 100%;
height: 100%; height: 100%;
min-height: 100px;
padding: 10px; padding: 10px;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 5px; border-radius: 5px;
margin-top: 10px; margin-top: 10px;
box-sizing: border-box; box-sizing: border-box;
margin-left: 2%; margin-left: 2%;
max-height: none;
overflow: hidden;
} }
.button-container { .button-container {
display: block; display: block;
margin-top: 20px; margin-top: 20px;
@ -347,7 +434,25 @@ const cancelEdit = (index: number) => {
padding-right: 1%; padding-right: 1%;
} }
.section-bloc, .section-bloc {
background-color: #f3f3f3;
border-radius: 8px;
padding: 10px 12px;
font-family: "Arial", sans-serif;
color: #333;
word-break: break-word;
flex-shrink: 0;
cursor: default;
max-width: 100%;
width: fit-content;
overflow-wrap: break-word;
box-sizing: border-box;
min-width: 120px;
}
.editing-section-bloc { .editing-section-bloc {
width: 100%; width: 100%;
justify-content: center; justify-content: center;
@ -368,6 +473,10 @@ const cancelEdit = (index: number) => {
margin-right: 20px; margin-right: 20px;
} }
.description p {
font-size: 12px;
}
.save-button, .save-button,
.cancel-button { .cancel-button {
width: 100px; width: 100px;
@ -417,4 +526,86 @@ const cancelEdit = (index: number) => {
text-align: center; text-align: center;
z-index: 1000; z-index: 1000;
} }
.description-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 10px;
overflow: hidden;
max-height: 100%;
width: 100%;
box-sizing: border-box;
width: 100%;
overflow-x: hidden;
max-height: 100%;
box-sizing: border-box;
}
.custom-flow {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: flex-start;
align-items: flex-start;
padding: 10px;
width: 100%;
box-sizing: border-box;
overflow-x: hidden;
}
.container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
grid-auto-rows: min-content;
grid-auto-flow: dense;
gap: 1rem;
}
.float-up { transform: translateY(-10px); }
.float-left { transform: translateX(-10px); }
.float-right { transform: translateX(10px); }
.wiggle { transform: rotate(1deg); }
.tilt { transform: rotate(-1deg); }
.from-left {
align-self: flex-start;
}
.from-right {
align-self: flex-end;
}
.section-bloc.from-left {
margin-right: auto;
margin-left: 20%;
}
.section-bloc.from-right {
margin-left: auto;
margin-right: 20%;
}
.explain {
font-size: 16px;
color: #444; /* gris doux pour le texte */
background-color: #f9f9f9; /* fond léger pour contraster */
padding: 7px;
border-left: 4px solid #0d6efd; /* petite bande à gauche type "info" */
border-right: 4px solid #0d6efd;
border-radius: 6px;
margin-bottom: 20px;
margin-top: 20px;
line-height: 1.6;
font-family: "Segoe UI", sans-serif;
}
</style> </style>

View File

@ -1,6 +1,14 @@
<template> <template>
adnane marked this conversation as resolved
Review

Doesn't this header overlap with the main app canvas ?

Doesn't this header overlap with the main app canvas ?
<header class="header"> <header class="header">
<img src="../icons/logo inpulse.png" alt="INPulse Logo" class="logo" /> <a
href="https://www.bordeaux-inp.fr/fr/lincubateur-bordeaux-inpulse"
target="_blank"
rel="noopener"
>
<img src="../icons/logo inpulse.png" alt="INPulse Logo" class="logo" />
</a>
<div class="header-actions"> <div class="header-actions">
<div ref="dropdownRef" class="dropdown-wrapper"> <div ref="dropdownRef" class="dropdown-wrapper">

View File

@ -112,16 +112,24 @@ onMounted(() => {
.canvas { .canvas {
display: grid; display: grid;
grid-template-columns: repeat(10, 1fr); grid-template-columns: repeat(10, minmax(0, 1fr));
grid-template-rows: repeat(6, 1fr); grid-auto-rows: min-content;
gap: 12px; gap: 12px;
padding: 30px; padding: 30px;
/*background-color: #f8f9fa;*/
position: relative; position: relative;
height: 90vh; height: auto; /* autorise la hauteur à s'ajuster selon le contenu */
overflow: auto; max-height: none; /* enlève la limite de hauteur */
box-sizing: border-box;
overflow: visible; /* autorise le débordement visible */
} }
@media (max-width: 768px) {
.canvas {
grid-template-columns: repeat(1, 1fr);
}
}
.Probleme { .Probleme {
grid-column: 1 / 3; grid-column: 1 / 3;
grid-row: 1 / 5; grid-row: 1 / 5;
@ -164,4 +172,33 @@ onMounted(() => {
border: 1px solid #dee2e6; border: 1px solid #dee2e6;
border-radius: 0.5rem; border-radius: 0.5rem;
} }
.Probleme {
background-color: #ffdddd;
}
.Segments {
background-color: #ddffdd;
}
.Valeur {
background-color: #ddddff;
}
.Solution {
background-color: #fff0b3;
}
.Avantage {
background-color: #d1c4e9;
}
.Canaux {
background-color: #b2ebf2;
}
.Indicateurs {
background-color: #ffe082;
}
.Couts {
background-color: #ffcdd2;
}
.Revenus {
background-color: #c8e6c9;
}
</style> </style>

View File

@ -1,4 +1,5 @@
<template> <template>
<div> <div>
<header> <header>
<HeaderCanvas :project-id="1" /> <HeaderCanvas :project-id="1" />
@ -6,13 +7,11 @@
</div> </div>
<div> <div>
<h1 class="page-title">PAGE CANVAS</h1> <h1 class="page-title">PAGE CANVAS</h1>
<p class="canvas-help-text"> <p class="canvas-help-text">
Cliquez sur un champ du tableau pour afficher son contenu en détail Cliquez sur un champ du tableau pour afficher son contenu en détail
ci-dessous. ci-dessous.
</p> </p>
<LeanCanvas :is-admin="isAdmin" /> <LeanCanvas :is-admin="isAdmin" />
<div class="info-box"> <div class="info-box">
<p> <p>
Responsable : Responsable :
@ -26,7 +25,7 @@
<a href="tel:{{ admin.phoneNumber }}">{{ <a href="tel:{{ admin.phoneNumber }}">{{
admin.phoneNumber admin.phoneNumber
}}</a> }}</a>
</p> </p> <div class="main"></div>
</div> </div>
</div> </div>
</template> </template>
@ -138,4 +137,13 @@ onMounted(() => {
.info-box a:hover { .info-box a:hover {
text-decoration: underline; text-decoration: underline;
} }
.canvas-help-text {
margin-top: 20px;
margin-bottom: -10px;
}
div:last-child {
margin-bottom: 60px;
}
</style> </style>