21 Commits

Author SHA1 Message Date
a1322c64ca test front 2025-03-21 02:06:49 +01:00
8394bf02f2 test front 2025-03-19 03:09:43 +01:00
f48b570494 fix: to composition api 2025-02-26 03:39:59 +01:00
0733f8d5af Merge branch 'front_foundation' of https://gitea.piair.dev/piair/MyINPulse into front_foundation 2025-02-25 22:53:26 +01:00
8071c01c5d fix: fixing the issues regarding the use of href 2025-02-25 22:53:15 +01:00
4ee3d9bc44 fix: data from api is now correctly displayed 2025-02-19 12:15:39 +01:00
d75d45e204 fix + feat: api test + error fix 2025-02-19 11:02:15 +01:00
9f3754776f fix: fixed the code for AdminMain 2025-02-18 22:17:57 +01:00
651fb2b1a1 feat: adding first fake api (use this cmd in fake_data reportory: json-server --watch db.json --port 5000) + fetching witch axios for now (npm install axios) 2025-02-18 07:20:58 +01:00
aa5988ce75 fix: emptyed the app.vue and did some code reorganisation 2025-02-17 23:48:28 +01:00
9ae18e1e4b feat : adding an agenda, not included to the main page yet 2025-02-17 21:51:27 +01:00
22ebb0e1f4 feat: enhancing canvass layout 2025-02-11 20:47:37 +01:00
09e4b3262f feat: switching to composition API standard 2025-02-11 19:56:52 +01:00
6a3d4239ab feat: canvas are now generic 2025-02-10 22:46:59 +01:00
9d71c93b5b feat: layout changes 2025-02-10 15:53:10 +01:00
5145b833ae feat: rendez-vous agenda for admin and user 2025-02-10 15:15:58 +01:00
4080cee818 Meeeerge...
Merge branch 'main' of https://gitea.piair.dev/piair/MyINPulse
2025-02-08 22:40:42 +01:00
f4d73654d1 some minor changes on the main page 2025-02-08 22:40:35 +01:00
4fda5513a9 error corrected 2025-02-08 20:33:03 +01:00
32407b0e8f conflits... 2025-02-08 20:23:43 +01:00
b30e1196f4 canvas included in the main page, still shiting with vue 2025-02-08 20:18:44 +01:00
34 changed files with 1229 additions and 1866 deletions

View File

@ -1,24 +0,0 @@
name: CI
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install everything
working-directory: ./front/MyINPulse-front
run: npm i
- name: Run ESLint
working-directory: ./front/MyINPulse-front
run: npx eslint
- name: Run prettier
working-directory: ./front/MyINPulse-front
run: npx prettier src --check
- name: Build frontend
working-directory: ./front/MyINPulse-front
run: npm run build

View File

@ -36,4 +36,4 @@ playwright-report/
# Custom
.installed
./package-lock.json
package-lock.json

View File

@ -1,7 +0,0 @@
{
"useTabs":false,
"semi":true,
"trailingComma":"es5",
"arrowParens":"always",
"tabWidth":4
}

View File

@ -1,29 +0,0 @@
import eslint from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
import eslintPluginVue from "eslint-plugin-vue";
import globals from "globals";
import typescriptEslint from "typescript-eslint";
export default typescriptEslint.config(
{ ignores: ["*.d.ts", "**/coverage", "**/dist"] },
{
extends: [
eslint.configs.recommended,
...typescriptEslint.configs.recommended,
...eslintPluginVue.configs["flat/recommended"],
],
files: ["**/*.{ts,vue}"],
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
globals: globals.browser,
parserOptions: {
parser: typescriptEslint.parser,
},
},
rules: {
// your rules
},
},
eslintConfigPrettier
);

View File

@ -0,0 +1,10 @@
{
"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": [
{ "canva_data": "this is a fake data to test api" }
]
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -26,15 +26,8 @@
"@types/node": "^22.10.7",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.20.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-vue": "^9.32.0",
"globals": "^15.14.0",
"jiti": "^2.4.2",
"npm-run-all2": "^7.0.2",
"prettier": "3.5.0",
"typescript": "~5.7.3",
"typescript-eslint": "^8.23.0",
"vite": "^6.0.11",
"vite-plugin-vue-devtools": "^7.7.0",
"vue-tsc": "^2.2.0"

View File

@ -1,47 +1,15 @@
<script setup lang="ts">
import { RouterView } from "vue-router";
import { RouterLink, RouterView } from 'vue-router'
import ErrorWrapper from "@/views/errorWrapper.vue";
import ProjectComponent from "@/components/ProjectComponent.vue";
</script>
<template>
<HeaderComponent />
<error-wrapper></error-wrapper>
<div id="main">
<ProjectComponent
v-for="(project, index) in projects"
:key="index"
:project-name="project.name"
/>
</div>
<Header />
<RouterLink to="/">Home</RouterLink> |
<RouterLink to="/canvas">Canvas</RouterLink>
<RouterView />
</template>
<script lang="ts">
import HeaderComponent from "@/components/HeaderComponent.vue";
export default {
name: "App",
components: {
HeaderComponent,
},
data() {
return {
projects: [
{
name: "Projet Alpha",
//link: './project-alpha.html',
//members: ['Alice', 'Bob', 'Charlie'],
},
{
name: "Projet Beta",
//link: './project-beta.html',
//members: ['David', 'Eve', 'Frank'],
},
],
};
},
};
</script>
<style scoped></style>

View File

@ -0,0 +1,75 @@
<template>
<div id="agenda">
<h3>Rendez-vous</h3>
<table>
<tbody>
<tr v-for=" (p, index) in projectRDV" :key="index" >
<td>{{ p.projectName }} </td> <td>{{ p.date }}</td> <td>{{ p.lieu }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
import { defineProps } from "vue";
interface rendezVous{
projectName: String,
date: String,
lieu: String,
}
const props = defineProps<{
projectRDV: rendezVous[]
}>();
</script>
<style scoped>
#agenda {
padding: 20px;
}
/* Table Styling */
table {
width: 100%;
border-collapse: collapse;
font-family: Arial, sans-serif;
text-align: left;
margin-top: 20px;
border: 1px solid #ccc;
}
/* Header Row (if exists) */
th {
background-color: #f4f4f4;
padding: 12px;
font-weight: bold;
border: 1px solid #ccc;
}
/* Table Body Rows */
tbody tr {
border-bottom: 1px solid #ddd;
transition: background-color 0.2s ease; /* Smooth hover effect */
}
tbody tr:hover {
background-color: #f9f9f9; /* Highlight row on hover */
}
/* Cells Styling */
td {
padding: 10px;
border: 1px solid #eee;
font-size: 14px;
vertical-align: middle; /* Align text to middle */
}
/* First Column Styling */
td:first-child {
text-align: center;
width: 50px; /* Adjust width as needed */
}
</style>

View File

@ -1,26 +1,26 @@
<template>
<header>
<img src="./icons/logo inpulse.png" alt="INPulse" />
<img src="./icons/logo inpulse.png" alt="INPulse" />
</header>
</template>
<script lang="ts">
export default {
name: "HeaderComponent",
};
</script>
<style scoped>
header img {
</template>
<script lang="ts">
export default {
name: 'Header',
};
</script>
<style scoped>
header img {
width: 100px;
}
}
header {
header{
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: #fff;
border-bottom: 2px solid #ddd;
}
</style>
}
</style>

View File

@ -0,0 +1,129 @@
<template>
<div @click="goToLink" class="project">
<div class="project-header">
<h2 >{{ projectName }}</h2>
<div class="project-buttons">
<button class="contact-btn">Contact</button>
</div>
</div>
<div class="project-body">
<ul>
<li v-for="(name, index) in listName" :key="index">{{ name }}</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps } from "vue";
import { useRouter } from 'vue-router'
const props = defineProps<{
projectName: string;
listName: string[];
projectLink: string;
}>();
const router = useRouter();
const goToLink = () => {
if (props.projectLink) {
router.push(props.projectLink);
}
};
</script>
<style scoped>
.project {
background: linear-gradient(to right, #f4f4f4, #ffffff);
border: 1px solid #ddd;
border-radius: 10px;
padding: 20px;
margin: 20px 0;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
font-family: Arial, sans-serif;
}
/* Header Styling */
.project-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.project-header h2 {
font-size: 20px;
color: #333;
margin: 0;
}
/* Button Container */
.project-buttons {
display: flex;
gap: 10px;
}
.info-btn {
background-color: #4CAF50;
color: #fff;
}
.info-btn:hover {
background-color: #45a049;
transform: scale(1.05);
}
.contact-btn {
background-color: #007BFF;
color: #fff;
}
.contact-btn:hover {
background-color: #0056b3;
transform: scale(1.05);
}
.project-body {
margin-top: 15px;
}
.project-body p {
font-size: 16px;
color: #555;
margin-bottom: 10px;
}
.project-body ul {
list-style-type: disc;
margin: 0;
padding-left: 20px;
}
.project-body ul li {
font-size: 14px;
color: #666;
line-height: 1.6;
}
button {
padding: 10px 15px;
background-color: #007bff;
color: white;
border: none;
cursor: pointer;
border-radius: 5px;
}
button:hover {
background-color: #0056b3;
}
</style>

View File

@ -1,21 +0,0 @@
<template>
<div class="project">
<div class="project-header">
<h2>{{ projectName }}</h2>
</div>
</div>
</template>
<script lang="ts">
import type { PropType } from "vue";
export default {
name: "ProjectComponent",
props: {
projectName: {
type: Object as PropType<string>,
required: true,
},
},
};
</script>

View File

@ -0,0 +1,88 @@
<template>
<div :class="['cell', { expanded }]"
@click="toggleExpand"
:style="{ justifyContent: expanded ? 'flex-start' : 'center' }"> <!-- Looking for finding a way
to make this style in the toggleExpand event -->
<h3>{{ title }}</h3>
<p>{{ currentDescription }}</p>
</div>
</template>
<script setup lang="ts">
import { ref, defineProps, onMounted } from "vue";
import axios from "axios";
const props = defineProps<{
title: string;
description: string;
}>();
const expanded = ref(false);
const currentDescription = ref(props.description);
const fetchData = async () => {
try {
const response = await axios.get("http://localhost:5000/data"); // Update the URL if needed
currentDescription.value = response.data[0].canva_data;
} catch (error) {
console.error("Erreur lors de la récupération des données :", error);
}
};
const toggleExpand = async () => {
if (!expanded.value) {
await fetchData();
} else {
currentDescription.value = props.description;
}
expanded.value = !expanded.value;
};
</script>
<style scoped>
@import "@/components/canvas/style-project.css";
.cell {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
transition: all 0.3s ease;
cursor: pointer;
box-shadow: 0 4px 5px rgba(0, 0, 0, 0.1);
}
.cell:not(.expanded):hover {
transform: scale(1.05);
box-shadow: 0 8px 9px rgba(0, 0, 0, 0.2);
}
.cell h3 {
font-size: 20px;
font-weight: 500;
/*margin-bottom: 10px;*/
}
.cell p {
font-size: 14px;
color: #666;
}
.expanded {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: white;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
</style>

View File

@ -0,0 +1,120 @@
<template>
<header>
<img src="../icons/logo inpulse.png" alt="INPulse Logo">
<div class="header-buttons">
<div class="menu">
<button class="contact-button" @click="toggleDropdown">Contact</button>
<div class="contact-dropdown" v-bind:class="{ 'dropdown-visible': isDropdownOpen }">
<button @click="contactAll">Contact All</button>
<button v-for="(email, index) in entrepreneurEmails" :key="index">
{{ email }}
</button>
</div>
<button class="return-button">
<RouterLink to="/">Return to list project</RouterLink>
</button>
</div>
</div>
</header>
</template>
<script setup>
import { ref, onMounted } from "vue";
import axios from "axios";
const isDropdownOpen = ref(false);
const entrepreneurEmails = ref([]);
const toggleDropdown = () => {
isDropdownOpen.value = !isDropdownOpen.value;
console.log("Dropdown toggled:", isDropdownOpen.value); // for debug purposes
};
const fetchEntrepreneurs = async () => {
try {
const response = await axios.get("http://localhost:5000/entrepreneurs");
entrepreneurEmails.value = response.data.map(e => e.email);
} catch (error) {
console.error("Erreur lors de la récupération des entrepreneurs:", error);
}
};
const contactAll = () => {
alert("Contacter tous les entrepreneurs : " + entrepreneurEmails.value.join(", "));
};
onMounted(fetchEntrepreneurs);
</script>
<style scoped>
@import "@/components/canvas/style-project.css";
header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 10px;
}
.header-buttons {
display: flex;
align-items: flex-start;
}
.menu {
display: flex;
flex-direction: column;
gap: 10px;
}
.contact-button, .return-button {
background-color: #000;
color: white;
border: none;
padding: 10px;
cursor: pointer;
font-size: 14px;
text-align: center;
}
.return-button a {
color: white;
text-decoration: none;
}
.contact-dropdown {
display: none;
position: absolute;
background-color: white;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 10px;
margin-top: 5px;
z-index: 1000;
}
.contact-dropdown button {
display: block;
width: 100%;
padding: 5px;
text-align: left;
border: none;
background: none;
cursor: pointer;
}
.contact-dropdown button:hover {
background-color: #f0f0f0;
}
.dropdown-visible {
display: block;
}
header img {
width: 100px;
height: auto;
}
</style>

View File

@ -0,0 +1,56 @@
<template>
<div class="canvas">
<CanvasItem
v-for="(item, index) in items"
:key="index"
:title="item.title"
:description="item.description"
:class="item.class"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import CanvasItem from "@/components/canvas/CanvasItem.vue";
const items = ref([
{ title: "1. Problème", description: "3 problèmes essentiels à résoudre pour le client", class: "Probleme" },
{ title: "2. Segments", description: "Les segments de clientèle visés", class: "Segments" },
{ title: "3. Valeur", description: "La proposition de valeur", class: "Valeur" },
{ title: "4. Solution", description: "Les solutions proposées", class: "Solution" },
{ title: "5. Avantage", description: "Les avantages concurrentiels", class: "Avantage" },
{ title: "6. Canaux", description: "Les canaux de distribution", class: "Canaux" },
{ title: "7. Indicateurs", description: "Les indicateurs clés de performance", class: "Indicateurs" },
{ title: "8. Coûts", description: "Les coûts associés", class: "Couts" },
{ title: "9. Revenus", description: "Les sources de revenus", class: "Revenus" }
]);
</script>
<style scoped>
@import "@/components/canvas/style-project.css";
.canvas {
display: grid;
grid-template-columns: repeat(10, 1fr);
grid-template-rows: repeat(6, 1fr);
gap: 10px;
padding: 10px;
max-width: 1200px;
margin: 20px auto;
background-color: #fff;
position: relative;
height: 80vh;
overflow: auto;
}
.Probleme { grid-column: 1 / 3; grid-row: 1 / 5; }
.Segments { grid-column: 9 / 11; grid-row: 1 / 5; }
.Valeur { grid-column: 5 / 7; grid-row: 1 / 5; }
.Solution { grid-column: 3 / 5; grid-row: 1 / 3; }
.Avantage { grid-column: 7 / 9; grid-row: 1 / 3; }
.Canaux { grid-column: 7 / 9; grid-row: 3 / 5; }
.Indicateurs { grid-column: 3 / 5; grid-row: 3 / 5; }
.Couts { grid-column: 1 / 6; grid-row: 5 / 7; }
.Revenus { grid-column: 6 / 11; grid-row: 5 / 7; }
</style>

View File

@ -0,0 +1,156 @@
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 10px;
}
.row {
display: flex;
}
.cell {
flex: 1;
border: 1px solid #ddd;
padding: 10px;
text-align: center;
background-color: #f1f1f1;
}
.produit {
background-color: #f9e4e4;
}
.marche {
background-color: #e4f1f9;
}
.valeur {
background-color: #f9f4e4;
}
h3 {
margin: 0;
font-size: 18px;
color: #333;
}
p {
margin: 5px 0 0;
font-size: 14px;
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f9f9f9;
}
h1 img {
height: 80px;
margin: 40px;
text-align: center;
}
.row {
display: flex;
margin-bottom: 10px;
}
#ade {
max-width: 1200px;
margin: 20px auto;
padding: 20px;
text-align: center;
background-color: #e8f5e9;
border: 2px solid #4caf50;
border-radius: 10px;
}
#ade h3 {
color: #2e7d32;
}
#ade p {
margin: 10px 0;
font-size: 16px;
color: #333;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: #fff;
border-bottom: 2px solid #ddd;
}
header img {
height: 60px;
}
header .contact-menu {
position: relative;
}
.contact-button, .return {
padding: 10px 15px;
border: none;
border-radius: 4px;
background-color: #2196f3;
color: #fff;
cursor: pointer;
}
.contact-button:hover, .return:hover {
background-color: #1976d2;
}
/* Dropdown styling */
.contact-dropdown {
position: absolute;
right: 0;
top: 50px;
display: none;
flex-direction: column;
gap: 10px;
padding: 15px;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
z-index: 10;
}
.contact-dropdown button {
padding: 8px 12px;
border: none;
border-radius: 4px;
background-color: #4caf50;
color: #fff;
cursor: pointer;
}
.contact-dropdown button:hover {
background-color: #388e3c;
}
.return {
background-color: #f44336;
}
.return:hover {
background-color: #d32f2f;
}
.header-buttons {
display: flex;
align-items: center;
gap: 15px;
}
a{
color: white;
}

View File

@ -1,43 +1,17 @@
<script setup lang="ts">
import { color } from "@/services/popupDisplayer.ts";
import type { PropType } from "vue";
defineProps({
errorMessage: {
type: Object as PropType<string>,
required: true,
},
errorColor: {
type: Object as PropType<color>,
default: color.Red,
},
});
const props = defineProps(['data']);
</script>
<template>
<div
:class="['red', 'yellow', 'blue', 'green'][errorColor]"
class="error-modal"
>
<p>
{{
["Erreur :(", "Warning :|", "Info :)", "Succes ;)"][errorColor]
}}
</p>
<p>{{ errorMessage }}</p>
<div
class="loading"
:class="
['red-loader', 'yellow-loader', 'blue-loader', 'green-loader'][
errorColor
]
"
></div>
</div>
<div :class='["red", "yellow", "blue", "green"][data.type]' class="error-modal">
<p>{{["Erreur :(", "Warning :|", "Info :)", "Succes ;)"][data.type]}}</p>
<p>{{data.errorMessage}}</p>
<div class="loading" :class='["red-loader", "yellow-loader", "blue-loader", "green-loader"][data.type]'></div>
</div>
</template>
<style scoped>
.error-modal {
.error-modal{
margin-bottom: 1em;
padding: 1em;
border-radius: 1em;
@ -45,45 +19,41 @@ defineProps({
overflow: hidden;
position: relative;
animation: disappear 5s linear forwards;
}
}
.red {
.red{
background-color: #ee6055;
color: white;
}
.red-loader {
}
.red-loader {
background-color: #fa8383;
}
}
.yellow {
background-color: #ff9d23;
color: white;
}
.yellow-loader {
.yellow{
background-color: #FF9D23;
color: white;
}
.yellow-loader{
background-color: #ffbf81;
}
}
.blue {
.blue {
background-color: #809bce;
color: white;
}
.blue-loader {
}
.blue-loader{
background-color: #95b8d1;
}
}
.green {
.green {
background-color: green;
color: white;
}
.green-loader {
}
.green-loader {
background-color: darkgreen;
}
}
.loading {
.loading {
box-sizing: border-box;
position: absolute;
padding: 0;
@ -92,33 +62,33 @@ defineProps({
height: 1em;
width: 0;
animation: loading 4s linear forwards;
}
}
/* Animation for the loading bar */
@keyframes loading {
/* Animation for the loading bar */
@keyframes loading {
0% {
width: 100%;
width: 100%;
}
100% {
width: 0;
width: 0;
}
}
}
@keyframes disappear {
@keyframes disappear {
0% {
height: 100px;
padding: 1em;
margin: 1em;
height: 100px;
padding: 1em;
margin: 1em;
}
80% {
height: 100px;
padding: 1em;
margin: 1em;
height: 100px;
padding: 1em;
margin: 1em;
}
100% {
height: 0;
margin: 0;
padding: 0;
height: 0;
margin: 0;
padding: 0;
}
}
</style>
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -1,31 +1,77 @@
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router/router.ts";
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import keycloakService from "./services/keycloak";
import { type AuthStore, useAuthStore } from "@/stores/authStore.ts";
let store: AuthStore;
import { createApp } from 'vue'
import App from './App.vue'
import router from './router/router.ts'
import {createPinia} from "pinia";
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import AuthStorePlugin from './plugins/authStore';
import keycloakService from './services/keycloak';
import {useAuthStore} from "@/stores/authStore.ts";
let store: any;
keycloakService.CallInit(() => {
try {
const app = createApp(App);
const app = createApp(App)
// Setup pinia store, allowing user to keep logged in status after refresh
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
app.use(pinia);
app.use(AuthStorePlugin, { pinia });
store = useAuthStore();
keycloakService.CallInitStore(store);
app.use(router);
app.use(router)
app.mount("#app");
app.mount('#app');
} catch (e) {
console.error("Error while initiating Keycloak.");
console.error(e);
createApp(App).mount("#app");
console.error("Error while initiating Keycloak.")
console.error(e)
createApp(App).mount('#app');
}
});
export { store };
})
// this shit made by me so i can run the canva vue app
createApp(App).use(router).mount('#app');
// TODO: fix the comment
/*
function tokenInterceptor () {
axios.interceptors.request.use(config => {
const keycloak = useKeycloak()
if (keycloak.authenticated) {
// Note that this is a simple example.
// you should be careful not to leak tokens to third parties.
// in this example the token is added to all usage of axios.
config.headers.Authorization = `Bearer ${keycloak.token}`
}
return config
}, error => {
console.error("tokenInterceptor: Rejected")
return Promise.reject(error)
})
}
*/
/*
app.use(VueKeyCloak,{
onReady: (keycloak) => {
console.log("Ready !")
tokenInterceptor()
},
init: {
onLoad: 'login-required',
checkLoginIframe: false,
},
config: {
realm: 'test',
url: 'http://localhost:7080',
clientId: 'myinpulse'
}
} );
*/
export {store};

View File

@ -0,0 +1,14 @@
// file: src/plugins/authStore.js
import { useAuthStore } from "@/stores/authStore.ts";
import keycloakService from '@/services/keycloak';
// Setup auth store as a plugin so it can be accessed globally in our FE
const authStorePlugin = {
install(app: any, option: any) {
const store = useAuthStore(option.pinia);
app.config.globalProperties.$store = store;
keycloakService.CallInitStore(store);
}
}
export default authStorePlugin;

View File

@ -1,17 +1,30 @@
import { createRouter, createWebHistory } from "vue-router";
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/test",
name: "test",
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import("../views/testComponent.vue"),
},
],
});
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/test',
name: 'test',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/test.vue'),
},
export default router;
{
path: '/',
name: 'Admin-main',
component: () => import('../views/AdminMain.vue'),
},
// route pour les canvas (made by adnane), in fact the two vue apps are separated for now
{
path: '/canvas',
name: 'canvas',
component: () => import('../views/CanvasView.vue'),
},
],
})
export default router

View File

@ -1,36 +1,31 @@
import axios, { type AxiosError, type AxiosResponse } from "axios";
import { store } from "@/main.ts";
import { addNewMessage, color } from "@/services/popupDisplayer.ts";
import axios from "axios";
import {store} from "@/main.ts";
import {addNewMessage, color} from "@/services/popupDisplayer.ts";
const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_BACKEND_URL,
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
axiosInstance.interceptors.response.use(
(response) => response, // Directly return successful responses.
async (error) => {
response => response, // Directly return successful responses.
async error => {
const originalRequest = error.config;
if (
error.response.status === 401 &&
!originalRequest._retry &&
store.authenticated
) {
if (error.response.status === 401 && !originalRequest._retry && store.authenticated) {
originalRequest._retry = true; // Mark the request as retried to avoid infinite loops.
try {
await store.refreshUserToken();
// Update the authorization header with the new access token.
axiosInstance.defaults.headers.common["Authorization"] =
`Bearer ${store.user.token}`;
axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${store.user.token}`;
return axiosInstance(originalRequest); // Retry the original request with the new access token.
} catch (refreshError) {
// Handle refresh token errors by clearing stored tokens and redirecting to the login page.
console.error("Token refresh failed:", refreshError);
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
window.location.href = "/login";
console.error('Token refresh failed:', refreshError);
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
@ -39,29 +34,23 @@ axiosInstance.interceptors.response.use(
);
// TODO: spawn a error modal
function defaultApiErrorHandler(err: AxiosError) {
addNewMessage(err.message, color.Red);
function defaultApiErrorHandler(err: string){
addNewMessage(err, color.Red);
}
function defaultApiSuccessHandler(response: AxiosResponse) {
addNewMessage(response.data, color.Green);
function defaultApiSuccessHandler(response: any){
addNewMessage(response.data, color.green)
}
function callApi(endpoint: string, onSuccessHandler?: any, onErrorHandler?: any): void {
axiosInstance.get(endpoint).then(
onSuccessHandler == null ? defaultApiSuccessHandler : onSuccessHandler
).catch(
(err) => {
onErrorHandler == null ? defaultApiErrorHandler(err): onErrorHandler(err);
throw err;
}
)
}
function callApi(
endpoint: string,
onSuccessHandler?: (response: AxiosResponse) => void,
onErrorHandler?: (error: AxiosError) => void
): void {
axiosInstance
.get(endpoint)
.then(
onSuccessHandler == null
? defaultApiSuccessHandler
: onSuccessHandler
)
.catch(
onErrorHandler == null ? defaultApiErrorHandler : onErrorHandler
);
}
export { callApi };
export {callApi}

View File

@ -1,31 +1,33 @@
import Keycloak from "keycloak-js";
import type { AuthStore } from "@/stores/authStore.ts";
import Keycloak from 'keycloak-js';
const options = {
url: import.meta.env.VITE_KEYCLOAK_URL,
clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID,
realm: import.meta.env.VITE_KEYCLOAK_REALM,
};
realm: import.meta.env.VITE_KEYCLOAK_REALM
}
const keycloak = new Keycloak(options);
let authenticated: boolean | undefined;
let store = null;
async function login() {
async function login(){
try {
await keycloak.login(); // https://www.keycloak.org/securing-apps/javascript-adapter#:~:text=when%20initialization%20completes.-,login(options),-Redirects%20to%20login
await keycloak.login() // https://www.keycloak.org/securing-apps/javascript-adapter#:~:text=when%20initialization%20completes.-,login(options),-Redirects%20to%20login
return keycloak;
} catch (error) {
console.log(error);
console.log(error)
}
}
async function signup() {
async function signup(){
try {
await keycloak.login({ action: "register" }); // https://www.keycloak.org/securing-apps/javascript-adapter#:~:text=when%20initialization%20completes.-,login(options),-Redirects%20to%20login
await keycloak.login(
{action: "register"}
) // https://www.keycloak.org/securing-apps/javascript-adapter#:~:text=when%20initialization%20completes.-,login(options),-Redirects%20to%20login
return keycloak;
} catch (error) {
console.log(error);
console.log(error)
}
}
@ -40,33 +42,31 @@ async function init(onInitCallback: () => void) {
onLoad: "check-sso",
silentCheckSsoRedirectUri: `${location.origin}/silent-check-sso.htm`,
responseMode: "query",
});
onInitCallback();
})
onInitCallback()
} catch (error) {
console.error("Keycloak init failed");
console.error(error);
console.error("Keycloak init failed")
console.error(error)
}
}
};
/**
* Initializes store with Keycloak user data
*
*/
async function initStore(storeInstance: AuthStore) {
async function initStore(storeInstance: any) {
try {
store = storeInstance;
console.log(keycloak);
await store.initOauth(keycloak);
store = storeInstance
console.log(keycloak)
await store.initOauth(keycloak)
// Show alert if user is not authenticated
if (!authenticated) {
console.warn("not authenticated");
}
if (!authenticated) { console.warn("not authenticated") }
} catch (error) {
console.error("Keycloak init failed");
console.error(error);
console.error("Keycloak init failed")
console.error(error)
}
}
};
/**
* Logout user
@ -83,7 +83,7 @@ async function refreshToken() {
await keycloak.updateToken(480);
return keycloak;
} catch (error) {
console.error("Failed to refresh token");
console.error('Failed to refresh token');
console.error(error);
}
}
@ -97,4 +97,4 @@ const KeycloakService = {
callSignup: signup,
};
export default KeycloakService;
export default KeycloakService;

View File

@ -1,43 +1,19 @@
import { ref, type Ref } from "vue";
import {ref} from "vue";
enum color {Red, Yellow, Blue, green}
enum color {
Red,
Yellow,
Blue,
Green,
}
type ErrorMessageContent = {
message: string;
color: color;
id: number;
timeout: number;
};
let id: number = 0;
const getId = () => {
id = id + 1;
return id;
};
function addNewMessage(errorMessage: string, type?: color, timeout?: number) {
if (timeout == null) {
function addNewMessage(errorMessage: string, type?: color, timeout?: number){
if (timeout == null){
timeout = 5000;
}
if (type == null) {
if (type == null){
type = color.Red;
}
const data: ErrorMessageContent = {
message: errorMessage,
timeout: timeout,
color: type,
id: getId(),
};
errorList.value.push(data);
setTimeout(() => errorList.value.slice(0, 1), timeout);
const data = {errorMessage: errorMessage, timeout: timeout, type: type, uid: Math.random()*100000};
errorList.value.push(data)
setTimeout(() => errorList.value.slice(0, 1), timeout)
}
const errorList: Ref<ErrorMessageContent[]> = ref([]);
const errorList: any= ref([])
export { addNewMessage, errorList, color, type ErrorMessageContent };
export {addNewMessage, errorList, color}

View File

@ -1,61 +1,54 @@
import { defineStore } from "pinia";
import keycloakService from "@/services/keycloak";
import keycloakService from '@/services/keycloak';
import type Keycloak from "keycloak-js";
const useAuthStore = defineStore("storeAuth", {
export const useAuthStore = defineStore("storeAuth", {
state: () => {
return {
testv: true,
authenticated: false,
user: {
token: "",
refreshToken: "",
username: "",
},
};
}
},
persist: true,
getters: {},
actions: {
// Initialize Keycloak OAuth
async initOauth(keycloak: Keycloak, clearData = true) {
if (clearData) {
await this.clearUserData();
}
if(clearData) { await this.clearUserData(); }
this.authenticated = !!keycloak.authenticated; // the !! removes undefined
if (
this.authenticated &&
keycloak.token &&
keycloak.idTokenParsed &&
keycloak.refreshToken
) {
if (this.authenticated && keycloak.token && keycloak.idTokenParsed && keycloak.refreshToken){
this.user.username = keycloak.idTokenParsed.given_name;
this.user.token = keycloak.token;
this.user.refreshToken = keycloak.refreshToken;
}
},
async login() {
async login(){
try {
const keycloak = await keycloakService.callLogin();
if (keycloak) await this.initOauth(keycloak);
if (keycloak)
await this.initOauth(keycloak);
} catch (error) {
console.log(error);
console.log(error)
}
},
async signup() {
try {
const keycloak = await keycloakService.callSignup();
if (keycloak) await this.initOauth(keycloak);
if (keycloak)
await this.initOauth(keycloak);
} catch (error) {
console.log(error);
console.log(error)
}
},
// Logout user
async logout() {
try {
await keycloakService.CallLogout(
import.meta.env.VITE_APP_URL + "/test"
);
await keycloakService.CallLogout(import.meta.env.VITE_APP_URL + "/test");
await this.clearUserData();
} catch (error) {
console.error(error);
@ -65,23 +58,23 @@ const useAuthStore = defineStore("storeAuth", {
async refreshUserToken() {
try {
const keycloak = await keycloakService.CallTokenRefresh();
if (keycloak) await this.initOauth(keycloak, false);
if (keycloak)
await this.initOauth(keycloak, false);
} catch (error) {
console.error(error);
}
},
test() {
this.testv = !this.testv;
},
// Clear user's store data
clearUserData() {
this.authenticated = false;
this.user = {
token: "",
refreshToken: "",
username: "",
refreshToken: "",
username: "",
};
},
},
});
type AuthStore = ReturnType<typeof useAuthStore>;
export { useAuthStore, type AuthStore };
}
}
});

View File

@ -0,0 +1,15 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>

View File

@ -0,0 +1,68 @@
<template>
<Header />
<error-wrapper></error-wrapper>
<div id="container">
<div id="main">
<ProjectComp
v-for="(project, index) in projects"
:key="index"
:projectName="project.name"
:listName="project.members"
:projectLink="project.link"
/>
</div>
<Agenda :projectRDV="rendezVous" />
</div>
</template>
<script setup lang="ts">
import Header from '../components/Header.vue';
import Agenda from "../components/Agenda.vue"
import ProjectComp from '../components/Project-comp.vue';
import { ref } from "vue";
const projects = ref([
{
name: "Projet Alpha",
link: "/canvas", // to test
members: ["Alice", "Bob", "Charlie"],
},
{
name: "Projet Beta",
link: "./canvas", // to test
members: ["David", "Eve", "Frank"],
},
]);
const rendezVous = ref([
{ projectName: "Projet Alpha", date: "2025-03-10", lieu: "P106" },
{ projectName: "Projet Beta", date: "2025-04-15", lieu: "Td10" },
]);
</script>
<style scoped>
#container {
margin: 0;
display: grid;
grid-template-columns: 3fr 1fr; /* Main body takes 3/4, agenda 1/4 */
height: 100vh; /* Full viewport height */
}
button {
padding: 10px 15px;
background-color: #007bff;
color: white;
border: none;
cursor: pointer;
border-radius: 5px;
}
button:hover {
background-color: #0056b3;
}
</style>

View File

@ -0,0 +1,17 @@
<template>
<div>
<header>
<HeaderCanvas />
</header>
</div>
<div>
<h1>Page Canvas</h1>
<LeanCanvas />
</div>
</template>
<script setup lang="ts">
import HeaderCanvas from '../components/canvas/HeaderCanvas.vue';
import LeanCanvas from '../components/canvas/LeanCanvas.vue';
</script>

View File

@ -1,25 +1,22 @@
<script setup lang="ts">
import { errorList } from "@/services/popupDisplayer.ts";
import {errorList} from "@/services/popupDisplayer.ts";
import ErrorModal from "@/components/errorModal.vue";
</script>
<template>
<div class="error-wrapper">
<error-modal
v-for="elm in errorList"
:key="elm.id"
:error-message="elm.message"
:error-color="elm.color"
/>
</div>
<div class="error-wrapper">
<error-modal v-for="elm in errorList" :data=elm></error-modal>
</div>
</template>
<style scoped>
.error-wrapper {
position: absolute;
left: 70%;
//background-color: blue;
height: 100%;
width: 30%;
.error-wrapper{
position: absolute;
left: 70%;
//background-color: blue;
height: 100%;
width: 30%;
}
</style>
</style>

View File

@ -0,0 +1,75 @@
<script setup lang="ts">
import {store} from "../main.ts";
import {callApi} from "@/services/api.ts";
import ErrorModal from "@/components/errorModal.vue";
import {errorList} from "@/services/popupDisplayer.ts";
//import TempModal from "@/components/temp-modal.vue";
import ErrorWrapper from "@/App.vue";
function addResToTable(id: any){
return (req: any) => {
console.log(req)
}
}
</script>
<template>
<h1>Test page</h1>
<table class="test" style="width:100%">
<tbody>
<tr>
<td>Is Currently Authenticated ? </td>
<td>{{store.authenticated}}</td>
<td><button @click="store.login">Login</button></td>
<td><button @click="store.logout">Logout</button></td>
<td><button @click="store.signup">Signup</button></td>
</tr>
<tr>
<td>current token</td>
<td>{{store.user.token}}</td>
<td><button @click="store.refreshUserToken">Refresh</button></td>
</tr>
<tr>
<td>Current refresh token</td>
<td>{{store.user.refreshToken}}</td>
</tr>
<tr>
<td>Entrepreneur API call</td>
<td><button @click="callApi('random')">call</button></td>
<td>res</td>
<td></td>
</tr>
<tr>
<td>Admin API call</td>
<td><button @click="callApi('random2')">call</button></td>
<td>res</td>
<td></td>
</tr>
<tr>
<td>Unauth API call</td>
<td><button @click="callApi('random3')">call</button></td>
<td>res</td>
<td id="3"></td>
</tr>
</tbody>
</table>
<temp-modal></temp-modal>
</template>
<style scoped>
table {
width: 100px;
table-layout: fixed
}
tr {
width: 100%;
height: 100%;
}
td {
border: solid 1px black;
width: 20%;
height: 100%;
overflow: hidden;
}
</style>

View File

@ -1,79 +0,0 @@
<script lang="ts" setup>
import { store } from "../main.ts";
import { callApi } from "@/services/api.ts";
</script>
<template>
<h1>Test page</h1>
<table class="test" style="width: 100%">
<tbody>
<tr>
<td>Is Currently Authenticated ?</td>
<td>{{ store.authenticated }}</td>
<td>
<button @click="store.login">Login</button>
</td>
<td>
<button @click="store.logout">Logout</button>
</td>
<td>
<button @click="store.signup">Signup</button>
</td>
</tr>
<tr>
<td>current token</td>
<td>{{ store.user.token }}</td>
<td>
<button @click="store.refreshUserToken">Refresh</button>
</td>
</tr>
<tr>
<td>Current refresh token</td>
<td>{{ store.user.refreshToken }}</td>
</tr>
<tr>
<td>Entrepreneur API call</td>
<td>
<button @click="callApi('random')">call</button>
</td>
<td>res</td>
<td></td>
</tr>
<tr>
<td>Admin API call</td>
<td>
<button @click="callApi('random2')">call</button>
</td>
<td>res</td>
<td></td>
</tr>
<tr>
<td>Unauth API call</td>
<td>
<button @click="callApi('random3')">call</button>
</td>
<td>res</td>
<td id="3"></td>
</tr>
</tbody>
</table>
</template>
<style scoped>
table {
width: 100px;
table-layout: fixed;
}
tr {
width: 100%;
height: 100%;
}
td {
border: solid 1px black;
width: 20%;
height: 100%;
overflow: hidden;
}
</style>

19
tests/example.spec.js Normal file
View File

@ -0,0 +1,19 @@
// @ts-check
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Click the get started link.
await page.getByRole('link', { name: 'Get started' }).click();
// Expects page to have a heading with the name of Installation.
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});

123
tests/front.spec.js Normal file
View File

@ -0,0 +1,123 @@
import { test,expect } from '@playwright/test'
test('Title Page',async({page}) =>{
await page.goto('http://localhost:5173/')
await expect(page).toHaveTitle(/Vite App/)
})
test('Navigation between pages', async ({ page }) => {
// Aller à la page d'accueil
await page.goto('http://localhost:5173/');
// Vérifier que l'URL a changé pour la page Canvas
await page.click('text=Canvas');
await expect(page).toHaveURL(/canvas/);
// Vérifier que l'URL a changé pour la page Home
await page.click('text=Home');
await expect(page).toHaveURL(/\//);
});
test.describe('Tests de la Page Canvas', () => {
test('Vérifier que tous les blocs Canvas sont affichés', async ({ page }) => {
// Aller à la page Canvas
await page.goto('http://localhost:5173/canvas');
await expect(page.locator('h1')).toHaveText('Page Canvas');
const sections = [
'1. Problème', '2. Segments', '3. Valeur', '4. Solution',
'5. Avantage', '6. Canaux', '7. Indicateurs', '8. Coûts', '9. Revenus'
];
for (const section of sections) {
await expect(page.locator(`text=${section}`)).toBeVisible();
}
});
test('Vérifier le contenu de la page Canvas', async ({ page }) => {
await page.goto('http://localhost:5173/canvas');
// Attendre le chargement des éléments
await page.waitForSelector('.canvas');
// Vérifier le titre
await expect(page.locator('h1')).toHaveText('Page Canvas');
// Nouveau sélecteur plus précis
const canvasItems = page.locator('.canvas > div'); // Sélectionne les div directes dans .canvas
// Vérifier le nombre d'éléments
await expect(canvasItems).toHaveCount(9);
// Contenu attendu
const expectedContent = [
{ title: '1. Problème', description: '3 problèmes essentiels à résoudre pour le client' },
{ title: '2. Segments', description: 'Les segments de clientèle visés' },
{ title: '3. Valeur', description: 'La proposition de valeur' },
{ title: '4. Solution', description: 'Les solutions proposées' },
{ title: '5. Avantage', description: 'Les avantages concurrentiels' },
{ title: '6. Canaux', description: 'Les canaux de distribution' },
{ title: '7. Indicateurs', description: 'Les indicateurs clés de performance' },
{ title: '8. Coûts', description: 'Les coûts associés' },
{ title: '9. Revenus', description: 'Les sources de revenus' }
];
// Vérifier chaque élément
for (let i = 0; i < expectedContent.length; i++) {
const item = canvasItems.nth(i);
await expect(item.locator('h3')).toHaveText(expectedContent[i].title);
await expect(item.locator('p')).toHaveText(expectedContent[i].description);
}
});
});
test.describe('Tests de la page Home', () => {
test('Vérifier la présence des projets', async ({ page }) => {
await page.goto('http://localhost:5173');
// Vérifier les titres des projets avec getByRole pour éviter les doublons
await expect(page.getByRole('heading', { name: 'Projet Alpha' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Projet Beta' })).toBeVisible();
});
test('Vérifier les membres des projets', async ({ page }) => {
await page.goto('http://localhost:5173');
const membresAlpha = ['Alice', 'Bob', 'Charlie'];
const membresBeta = ['David', 'Eve', 'Frank'];
for (const membre of membresAlpha) {
await expect(page.getByText(membre)).toBeVisible();
}
for (const membre of membresBeta) {
await expect(page.getByText(membre)).toBeVisible();
}
});
test('Vérifier les boutons Contact', async ({ page }) => {
await page.goto('http://localhost:5173');
// Vérifier que les boutons "Contact" existent et sont visibles
const contactButtons = await page.locator('button:has-text("Contact")').count();
expect(contactButtons).toBe(2); // Vérifie qu'il y a bien 2 boutons Contact
});
test('Vérifier la table des rendez-vous', async ({ page }) => {
await page.goto('http://localhost:5173');
await expect(page.getByRole('heading', { name: 'Rendez-vous' })).toBeVisible();
// Vérifier la première ligne du tableau
await expect(page.locator('table').getByRole('cell', { name: 'Projet Alpha' })).toBeVisible();
await expect(page.getByText('2025-03-10')).toBeVisible();
await expect(page.getByText('P106')).toBeVisible();
// Vérifier la deuxième ligne du tableau
await expect(page.locator('table').getByRole('cell', { name: 'Projet Beta' })).toBeVisible();
await expect(page.getByText('2025-04-15')).toBeVisible();
await expect(page.getByText('Td10')).toBeVisible();
});
});