Compare commits
No commits in common. "main" and "notifications-comments-26" have entirely different histories.
main
...
notificati
91 changed files with 362 additions and 14680 deletions
|
|
@ -1,73 +0,0 @@
|
|||
name: Deploy Preprod
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- preprod
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
name: Build and Deploy to Preprod
|
||||
runs-on: docker
|
||||
container:
|
||||
image: forgejo-ci-node:latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
run: |
|
||||
git clone --depth 1 --branch preprod https://forge.studio-variable.com/${{ github.repository }}.git .
|
||||
ls -la
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Build frontend (preprod)
|
||||
run: npm run build:preprod
|
||||
|
||||
- name: Install composer dependencies
|
||||
run: |
|
||||
cd dist
|
||||
composer install --no-dev --optimize-autoloader
|
||||
|
||||
- name: Deploy via rsync
|
||||
env:
|
||||
USERNAME: ${{ secrets.USERNAME }}
|
||||
PASSWORD: ${{ secrets.PASSWORD }}
|
||||
HOST: ${{ secrets.HOST }}
|
||||
PREPROD_PATH: ${{ secrets.PREPROD_PATH }}
|
||||
run: |
|
||||
cd dist
|
||||
|
||||
echo "Deploying site/"
|
||||
sshpass -p "$PASSWORD" rsync -az --delete -O --no-perms --no-owner --no-group \
|
||||
--include 'accounts/' \
|
||||
--exclude 'accounts/*' \
|
||||
--include 'sessions/' \
|
||||
--exclude 'sessions/*' \
|
||||
--include 'cache/' \
|
||||
--exclude 'cache/*' \
|
||||
-e 'ssh -p 2244 -o StrictHostKeyChecking=no' \
|
||||
site/ $USERNAME@$HOST:$PREPROD_PATH/site/
|
||||
|
||||
echo "Deploying vendor/"
|
||||
sshpass -p "$PASSWORD" rsync -az --delete -O \
|
||||
-e 'ssh -p 2244 -o StrictHostKeyChecking=no' \
|
||||
vendor/ $USERNAME@$HOST:$PREPROD_PATH/vendor/
|
||||
|
||||
echo "Deploying kirby/"
|
||||
sshpass -p "$PASSWORD" rsync -az --delete -O \
|
||||
-e 'ssh -p 2244 -o StrictHostKeyChecking=no' \
|
||||
kirby/ $USERNAME@$HOST:$PREPROD_PATH/kirby/
|
||||
|
||||
echo "Deploying assets/"
|
||||
sshpass -p "$PASSWORD" rsync -az --delete -O \
|
||||
-e 'ssh -p 2244 -o StrictHostKeyChecking=no' \
|
||||
assets/ $USERNAME@$HOST:$PREPROD_PATH/assets/
|
||||
|
||||
echo "Deploying .user.ini"
|
||||
sshpass -p "$PASSWORD" rsync -az -O \
|
||||
-e 'ssh -p 2244 -o StrictHostKeyChecking=no' \
|
||||
.user.ini $USERNAME@$HOST:$PREPROD_PATH/.user.ini
|
||||
|
||||
echo "Fix ACL mask on writable directories"
|
||||
sshpass -p "$PASSWORD" ssh -p 2244 -o StrictHostKeyChecking=no $USERNAME@$HOST \
|
||||
"setfacl -m mask::rwx $PREPROD_PATH/site/accounts $PREPROD_PATH/site/cache $PREPROD_PATH/site/sessions $PREPROD_PATH/content $PREPROD_PATH/media"
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
name: Deploy Production
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
name: Build and Deploy to Production
|
||||
runs-on: docker
|
||||
container:
|
||||
image: forgejo-ci-node:latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
run: |
|
||||
git clone --depth 1 --branch main https://forge.studio-variable.com/${{ github.repository }}.git .
|
||||
ls -la
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Build frontend (production)
|
||||
run: npm run build
|
||||
|
||||
- name: Install composer dependencies
|
||||
run: |
|
||||
cd dist
|
||||
composer install --no-dev --optimize-autoloader
|
||||
|
||||
- name: Deploy via rsync
|
||||
env:
|
||||
USERNAME: ${{ secrets.USERNAME }}
|
||||
PASSWORD: ${{ secrets.PASSWORD }}
|
||||
HOST: ${{ secrets.HOST }}
|
||||
PROD_PATH: ${{ secrets.PROD_PATH }}
|
||||
run: |
|
||||
cd dist
|
||||
|
||||
echo "Deploying site/"
|
||||
sshpass -p "$PASSWORD" rsync -az --delete -O \
|
||||
--exclude 'accounts/*' \
|
||||
--exclude 'cache/*' \
|
||||
--exclude 'sessions/*' \
|
||||
--include 'accounts/' \
|
||||
--include 'cache/' \
|
||||
--include 'sessions/' \
|
||||
-e 'ssh -p 2244 -o StrictHostKeyChecking=no' \
|
||||
site/ $USERNAME@$HOST:$PROD_PATH/site/
|
||||
|
||||
echo "Deploying vendor/"
|
||||
sshpass -p "$PASSWORD" rsync -az --delete -O \
|
||||
-e 'ssh -p 2244 -o StrictHostKeyChecking=no' \
|
||||
vendor/ $USERNAME@$HOST:$PROD_PATH/vendor/
|
||||
|
||||
echo "Deploying kirby/"
|
||||
sshpass -p "$PASSWORD" rsync -az --delete -O \
|
||||
-e 'ssh -p 2244 -o StrictHostKeyChecking=no' \
|
||||
kirby/ $USERNAME@$HOST:$PROD_PATH/kirby/
|
||||
|
||||
echo "Deploying assets/"
|
||||
sshpass -p "$PASSWORD" rsync -az --delete -O \
|
||||
-e 'ssh -p 2244 -o StrictHostKeyChecking=no' \
|
||||
assets/ $USERNAME@$HOST:$PROD_PATH/assets/
|
||||
|
||||
echo "Deploying .user.ini"
|
||||
sshpass -p "$PASSWORD" rsync -az -O \
|
||||
-e 'ssh -p 2244 -o StrictHostKeyChecking=no' \
|
||||
.user.ini $USERNAME@$HOST:$PROD_PATH/.user.ini
|
||||
|
||||
echo "Fix ACL mask on writable directories"
|
||||
sshpass -p "$PASSWORD" ssh -p 2244 -o StrictHostKeyChecking=no $USERNAME@$HOST \
|
||||
"setfacl -m mask::rwx $PROD_PATH/site/accounts $PROD_PATH/site/cache $PROD_PATH/site/sessions $PROD_PATH/content $PROD_PATH/media"
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -62,21 +62,18 @@ public/.idea
|
|||
|
||||
public/site/cache/*
|
||||
!public/site/cache/index.html
|
||||
!public/site/cache/.gitkeep
|
||||
|
||||
# Accounts
|
||||
# ---------------
|
||||
|
||||
public/site/accounts/*
|
||||
!public/site/accounts/index.html
|
||||
!public/site/accounts/.gitkeep
|
||||
|
||||
# Sessions
|
||||
# ---------------
|
||||
|
||||
public/site/sessions/*
|
||||
!public/site/sessions/index.html
|
||||
!public/site/sessions/.gitkeep
|
||||
|
||||
# License
|
||||
# ---------------
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ deploy_preprod:
|
|||
build_prod:
|
||||
stage: build
|
||||
only:
|
||||
- main
|
||||
- prod
|
||||
image: composer:2
|
||||
script:
|
||||
- apk add --no-cache nodejs npm
|
||||
|
|
|
|||
|
|
@ -309,36 +309,3 @@ npm run build:preprod # Staging (avec sourcemaps)
|
|||
3. **Commentaires**: Positionnés en % (x, y) + pageIndex pour les PDFs multi-pages.
|
||||
4. **Notifications**: Stockées par projet, trackées par user UUID dans `readby[]`.
|
||||
5. **Virtual Sample**: Pistes dynamiques = pages enfants, pistes statiques = fichiers.
|
||||
|
||||
---
|
||||
|
||||
## Préférences de code et standards
|
||||
|
||||
### Principes généraux
|
||||
|
||||
**Clarté avant tout**
|
||||
- Privilégier des noms de variables et fonctions explicites
|
||||
- Éviter les commentaires sauf en cas de nécessité absolue
|
||||
- Le code doit être auto-documenté par des noms clairs
|
||||
|
||||
**Organisation du code**
|
||||
- Factoriser le code en petites fonctions bien nommées
|
||||
- Éviter les blocs de code longs et complexes dans les hooks de lifecycle
|
||||
- Une fonction = une responsabilité claire
|
||||
|
||||
**Ce qui est considéré comme "bricolage" (à éviter)**
|
||||
- Paramètres d'URL pour gérer des états applicatifs
|
||||
- Commentaires qui expliquent ce que fait le code au lieu de nommer correctement
|
||||
- Abstractions prématurées ou sur-ingénierie
|
||||
|
||||
### Style de communication
|
||||
|
||||
**Feedback direct**
|
||||
- Pas de politesses inutiles
|
||||
- Dire immédiatement si quelque chose ne va pas
|
||||
- Tester tout de suite et signaler les problèmes
|
||||
|
||||
**Qualité attendue**
|
||||
- Standards élevés sur la qualité du code
|
||||
- Pas de code médiocre accepté
|
||||
- Exigence de clarté et d'explicité constante
|
||||
65
package-lock.json
generated
65
package-lock.json
generated
|
|
@ -19,7 +19,6 @@
|
|||
"three": "^0.168.0",
|
||||
"uniqid": "^5.4.0",
|
||||
"vue": "^3.5.6",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -543,50 +542,6 @@
|
|||
"@swc/helpers": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "11.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.2.8.tgz",
|
||||
"integrity": "sha512-nBq6Y1tVkjIUsLsdOjDSJj4AsjvD0UG3zsg9Fyc+OivwlA/oMHSKooUy9tpKj0HqZ+NWFifweHavdljlBLTwdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/message-compiler": "11.2.8",
|
||||
"@intlify/shared": "11.2.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "11.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.2.8.tgz",
|
||||
"integrity": "sha512-A5n33doOjmHsBtCN421386cG1tWp5rpOjOYPNsnpjIJbQ4POF0QY2ezhZR9kr0boKwaHjbOifvyQvHj2UTrDFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "11.2.8",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "11.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.2.8.tgz",
|
||||
"integrity": "sha512-l6e4NZyUgv8VyXXH4DbuucFOBmxLF56C/mqh2tvApbzl2Hrhi1aTDcuv5TKdxzfHYmpO3UB0Cz04fgDT9vszfw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
|
|
@ -1902,26 +1857,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "11.2.8",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.2.8.tgz",
|
||||
"integrity": "sha512-vJ123v/PXCZntd6Qj5Jumy7UBmIuE92VrtdX+AXr+1WzdBHojiBxnAxdfctUFL+/JIN+VQH4BhsfTtiGsvVObg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "11.2.8",
|
||||
"@intlify/shared": "11.2.8",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@
|
|||
"three": "^0.168.0",
|
||||
"uniqid": "^5.4.0",
|
||||
"vue": "^3.5.6",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -42,14 +42,4 @@ tabs:
|
|||
type: files
|
||||
multiple: false
|
||||
uploads: pdf
|
||||
description:
|
||||
type: textarea
|
||||
size: tiny
|
||||
buttons: false
|
||||
moodboard:
|
||||
label: Images
|
||||
type: files
|
||||
template: image
|
||||
layout: cards
|
||||
size: medium
|
||||
files: tabs/files
|
||||
|
|
|
|||
|
|
@ -85,10 +85,6 @@ tabs:
|
|||
query: page.logo.toFile
|
||||
layout: cardlets
|
||||
required: true
|
||||
users:
|
||||
label: Utilisateurs assignés
|
||||
type: users
|
||||
multiple: true
|
||||
|
||||
- width: 2/3
|
||||
sections:
|
||||
|
|
|
|||
|
|
@ -30,13 +30,13 @@ sections:
|
|||
fr: Ouvrir la plateforme
|
||||
en: Open platform
|
||||
value: Design to Pack
|
||||
link: https://designtopack.groupe-pochet.fr/
|
||||
link: https://designtopack.morphozbygroupepochet.com/
|
||||
icon: open
|
||||
- label:
|
||||
fr: Projet(s) en cours
|
||||
en: Current project(s)
|
||||
link: https://designtopack.groupe-pochet.fr/
|
||||
value: "{{ user.currentProjects.count }}"
|
||||
link: https://designtopack.morphozbygroupepochet.com/
|
||||
value: "{{ user.projects.toPages.count }}"
|
||||
icon: folder
|
||||
content:
|
||||
label: ' '
|
||||
|
|
@ -52,3 +52,8 @@ sections:
|
|||
layout: cardlets
|
||||
required: true
|
||||
width: 1/2
|
||||
projects:
|
||||
label: Projets
|
||||
type: pages
|
||||
query: page('projects').children
|
||||
width: 1/2
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ fields:
|
|||
- Sales Manager
|
||||
default: Project Panager
|
||||
width: 1/4
|
||||
hiddenProjects:
|
||||
label: Projets masqués
|
||||
projects:
|
||||
label: Projets
|
||||
type: pages
|
||||
query: page('projects').children
|
||||
width: 3/4
|
||||
|
|
|
|||
0
public/site/cache/.gitkeep
vendored
0
public/site/cache/.gitkeep
vendored
|
|
@ -25,14 +25,11 @@ return [
|
|||
'css' => 'assets/css/panel.css',
|
||||
'favicon' => 'favicon.svg',
|
||||
'menu' => require(__DIR__ . '/menu.php'),
|
||||
'install' => 'true'
|
||||
],
|
||||
'routes' => [
|
||||
require(__DIR__ . '/routes/en-locale.php'),
|
||||
require(__DIR__ . '/routes/logout.php'),
|
||||
require(__DIR__ . '/routes/login.php'),
|
||||
require(__DIR__ . '/routes/toggle-favorite.php'),
|
||||
require(__DIR__ . '/routes/toggle-hidden-project.php'),
|
||||
require(__DIR__ . '/routes/upload-images.php'),
|
||||
require(__DIR__ . '/routes/save-page.php'),
|
||||
require(__DIR__ . '/routes/save-file.php'),
|
||||
|
|
@ -44,7 +41,6 @@ return [
|
|||
require(__DIR__ . '/routes/request-project-creation.php'),
|
||||
require(__DIR__ . '/routes/request-optimization-appointment.php'),
|
||||
require(__DIR__ . '/routes/migrate-notifications.php'),
|
||||
require(__DIR__ . '/routes/migrate-user-projects.php'),
|
||||
],
|
||||
'hooks' => [
|
||||
'page.create:after' => require_once(__DIR__ . '/hooks/create-steps.php'),
|
||||
|
|
|
|||
|
|
@ -59,15 +59,6 @@ $menu = [
|
|||
}
|
||||
],
|
||||
'-',
|
||||
'analytics' => [
|
||||
'label' => 'Analytics',
|
||||
'icon' => 'chart',
|
||||
'link' => 'pages/analytics',
|
||||
'current' => function (string $current): bool {
|
||||
$path = Kirby\Cms\App::instance()->path();
|
||||
return Str::contains($path, 'pages/analytics');
|
||||
}
|
||||
],
|
||||
'-',
|
||||
'users',
|
||||
'system'
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Route catch-all pour le préfixe /en/
|
||||
* Permet d'accéder aux pages avec /en/... pour la version anglaise
|
||||
*/
|
||||
|
||||
return [
|
||||
'pattern' => 'en/(:all?)',
|
||||
'action' => function ($uri = '') {
|
||||
// Si l'URI se termine par .json, chercher la page et retourner le JSON
|
||||
if (str_ends_with($uri, '.json')) {
|
||||
$uri = str_replace('.json', '', $uri);
|
||||
$page = page($uri);
|
||||
|
||||
if (!$page) {
|
||||
// Si pas de page trouvée, essayer avec 'home'
|
||||
if ($uri === '' || $uri === '/') {
|
||||
$page = page('home');
|
||||
} else {
|
||||
return false; // 404
|
||||
}
|
||||
}
|
||||
|
||||
// Retourner la page pour que Kirby serve le template .json.php
|
||||
return $page;
|
||||
}
|
||||
|
||||
// Pour les URLs normales (sans .json), chercher la page
|
||||
$page = page($uri);
|
||||
|
||||
if (!$page) {
|
||||
// Si pas de page trouvée, essayer avec 'home'
|
||||
if ($uri === '' || $uri === '/') {
|
||||
$page = page('home');
|
||||
} else {
|
||||
return false; // 404
|
||||
}
|
||||
}
|
||||
|
||||
// Retourner la page pour que Kirby serve le template .php
|
||||
return $page;
|
||||
}
|
||||
];
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Script de migration : inverse la relation User→Projects en Project→Users.
|
||||
*
|
||||
* Pour chaque user (non-admin) ayant un champ projects non vide,
|
||||
* ajoute ce user dans le champ `users` du projet correspondant.
|
||||
*
|
||||
* Idempotent : pas de doublons si exécuté plusieurs fois.
|
||||
* À supprimer après migration.
|
||||
*
|
||||
* Usage: POST /migrate-user-projects.json
|
||||
*/
|
||||
|
||||
return [
|
||||
'pattern' => 'migrate-user-projects.json',
|
||||
'method' => 'POST',
|
||||
'action' => function () {
|
||||
ini_set('memory_limit', '512M');
|
||||
$user = kirby()->user();
|
||||
|
||||
if (!$user || $user->role()->id() !== 'admin') {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Cette action nécessite les droits administrateur.'
|
||||
];
|
||||
}
|
||||
|
||||
$migrated = [];
|
||||
$errors = [];
|
||||
|
||||
$nonAdminUsers = kirby()->users()->filter(fn($u) => $u->role()->id() !== 'admin');
|
||||
|
||||
foreach ($nonAdminUsers as $u) {
|
||||
if (!$u->projects()->exists() || $u->projects()->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$userProjects = $u->projects()->toPages();
|
||||
|
||||
foreach ($userProjects as $project) {
|
||||
try {
|
||||
$currentUsers = $project->users()->yaml();
|
||||
|
||||
$userUuid = $u->uuid()->toString();
|
||||
|
||||
if (in_array($userUuid, $currentUsers)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$currentUsers[] = $userUuid;
|
||||
$project->update(['users' => $currentUsers]);
|
||||
|
||||
$migrated[] = [
|
||||
'user' => $u->name()->value(),
|
||||
'email' => $u->email(),
|
||||
'project' => $project->title()->value(),
|
||||
];
|
||||
} catch (\Throwable $th) {
|
||||
$errors[] = [
|
||||
'user' => $u->email(),
|
||||
'project' => $project->title()->value(),
|
||||
'error' => $th->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'success',
|
||||
'message' => count($migrated) . ' assignations migrées.',
|
||||
'migrated' => $migrated,
|
||||
'errors' => $errors
|
||||
];
|
||||
}
|
||||
];
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'pattern' => '/toggle-hidden-project.json',
|
||||
'method' => 'POST',
|
||||
'action' => function() {
|
||||
$json = file_get_contents("php://input");
|
||||
$data = json_decode($json);
|
||||
|
||||
try {
|
||||
$user = kirby()->user();
|
||||
$projectUuid = $data->projectUuid;
|
||||
|
||||
// Récupérer la collection des projets masqués actuels
|
||||
$hiddenProjects = $user->hiddenProjects()->toPages();
|
||||
|
||||
// Trouver le projet à toggle
|
||||
$projectPage = Find::page($projectUuid);
|
||||
|
||||
if (!$projectPage) {
|
||||
throw new Exception('Projet introuvable');
|
||||
}
|
||||
|
||||
// Toggle: ajouter ou retirer le projet de la collection
|
||||
if ($hiddenProjects->has($projectPage)) {
|
||||
$hiddenProjects->remove($projectPage);
|
||||
$action = 'shown';
|
||||
} else {
|
||||
$hiddenProjects->add($projectPage);
|
||||
$action = 'hidden';
|
||||
}
|
||||
|
||||
// Convertir la collection en array puis en YAML
|
||||
$array = $hiddenProjects->toArray();
|
||||
$yaml = Data::encode($array, 'yaml');
|
||||
|
||||
// Mettre à jour l'utilisateur
|
||||
$user->update([
|
||||
'hiddenProjects' => $yaml
|
||||
]);
|
||||
|
||||
// Retourner les UUIDs pour le frontend
|
||||
$hiddenProjectsUuids = [];
|
||||
foreach ($hiddenProjects as $project) {
|
||||
$hiddenProjectsUuids[] = $project->uuid();
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'success',
|
||||
'action' => $action,
|
||||
'hiddenProjects' => $hiddenProjectsUuids
|
||||
];
|
||||
} catch (\Throwable $th) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Impossible de modifier les projets masqués : ' . $th->getMessage() . ' in file ' . $th->getFile() . ' line ' . $th->getLine()
|
||||
];
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
@ -27,28 +27,15 @@ return function ($page, $kirby, $site) {
|
|||
}
|
||||
}
|
||||
|
||||
$userProjects = $kirby->user()->currentProjects()->merge(
|
||||
$kirby->user()->archivedProjects()
|
||||
);
|
||||
|
||||
if ($userProjects->count() > 0) {
|
||||
$userData['projects'] = $userProjects->map(function ($project) {
|
||||
if ($kirby->user()->projects()->exists() && $kirby->user()->projects()->isNotEmpty()) {
|
||||
$userData['projects'] = $kirby->user()->projects()->toPages()->map(function ($project) {
|
||||
return [
|
||||
"title" => (string) $project->title(),
|
||||
"uri" => (string) $project->uri(),
|
||||
"step" => (string) $project->currentStep(),
|
||||
"uuid" => (string) $project->uuid(),
|
||||
"step" => (string) $project->getStepLabel(),
|
||||
];
|
||||
})->data;
|
||||
}
|
||||
|
||||
if ($kirby->user()->hiddenProjects()->exists() && $kirby->user()->hiddenProjects()->isNotEmpty()) {
|
||||
$userData['hiddenProjects'] = $kirby->user()->hiddenProjects()->toPages()->map(function ($project) {
|
||||
return (string) $project->uuid();
|
||||
})->values();
|
||||
} else {
|
||||
$userData['hiddenProjects'] = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ class ProjectPage extends NotificationsPage {
|
|||
}
|
||||
|
||||
return [
|
||||
'label' => $child->title()->value(),
|
||||
'id' => $child->stepName()->value(),
|
||||
'slug' => $child->slug(),
|
||||
'index' => intval($child->stepIndex()->value()),
|
||||
|
|
@ -161,30 +162,6 @@ class ProjectPage extends NotificationsPage {
|
|||
}
|
||||
}
|
||||
|
||||
// Récupérer l'ordre des groupes depuis le champ groups
|
||||
$orderedGroups = $child->groups()->split();
|
||||
|
||||
// Réorganiser $files['dynamic'] selon l'ordre défini
|
||||
if (!empty($orderedGroups)) {
|
||||
$orderedDynamic = [];
|
||||
|
||||
foreach ($orderedGroups as $group) {
|
||||
if (isset($files['dynamic'][$group])) {
|
||||
$orderedDynamic[$group] = $files['dynamic'][$group];
|
||||
}
|
||||
}
|
||||
|
||||
// Ajouter les groupes non définis dans le champ à la fin
|
||||
foreach ($files['dynamic'] as $group => $tracks) {
|
||||
if (!isset($orderedDynamic[$group])) {
|
||||
$orderedDynamic[$group] = $tracks;
|
||||
}
|
||||
}
|
||||
|
||||
$files['dynamic'] = $orderedDynamic;
|
||||
}
|
||||
|
||||
// Toujours mettre "Autres pistes" à la fin
|
||||
if (isset($files['dynamic']['Autres pistes'])) {
|
||||
$others = $files['dynamic']['Autres pistes'];
|
||||
unset($files['dynamic']['Autres pistes']);
|
||||
|
|
@ -216,18 +193,30 @@ class ProjectPage extends NotificationsPage {
|
|||
}
|
||||
}
|
||||
|
||||
public function getStepLabel() {
|
||||
$stepsLabel = [
|
||||
"clientBrief" => "brief",
|
||||
"proposal" => "offre commerciale",
|
||||
"extendedBrief" => "brief enrichi",
|
||||
"industrialIdeation" => "idéation industrielle",
|
||||
"virtualSample" => "échantillon virtuel",
|
||||
"physicalSample" => "échantillon physique",
|
||||
];
|
||||
|
||||
return $stepsLabel[$this->currentStep()->value()];
|
||||
}
|
||||
|
||||
// public function printManagers() {
|
||||
// return A::implode($this->managers()->toUsers()->pluck('name'), ', ');
|
||||
// }
|
||||
|
||||
public function managers() {
|
||||
if ($this->users()->isEmpty()) {
|
||||
return kirby()->users()->filterBy('role', 'admin');
|
||||
return kirby()->users()->filter(function($user) {
|
||||
if ($user->role() != 'admin' && $user->projects()->isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
$projectUsers = $this->users()->toUsers();
|
||||
return kirby()->users()->filter(function($user) use ($projectUsers) {
|
||||
return $user->role() == 'admin' || $projectUsers->has($user);
|
||||
|
||||
return $user->role() == 'admin' || $user->projects()->toPages()->has($this);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
title: Analytics
|
||||
icon: chart
|
||||
buttons: false
|
||||
options:
|
||||
changeTitle: false
|
||||
|
||||
status:
|
||||
- unlisted
|
||||
|
||||
columns:
|
||||
- width: 1/1
|
||||
sections:
|
||||
dashboard:
|
||||
type: fields
|
||||
fields:
|
||||
analytics:
|
||||
type: analytics-dashboard
|
||||
label: false
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace adrienpayet\analytics;
|
||||
|
||||
use Kirby\Cms\Page;
|
||||
|
||||
class AnalyticsPage extends Page
|
||||
{
|
||||
public function getAnalyticsData(array $filters = []): array
|
||||
{
|
||||
$user = kirby()->user();
|
||||
|
||||
if (!$user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($user->isAdmin()) {
|
||||
return AnalyticsStore::getAggregatedData($filters);
|
||||
}
|
||||
|
||||
$allowedProjects = $user->currentProjects();
|
||||
$allowedEmails = [];
|
||||
foreach ($allowedProjects as $project) {
|
||||
$users = kirby()->users();
|
||||
foreach ($users as $u) {
|
||||
if ($u->currentProjects()->has($project)) {
|
||||
$allowedEmails[] = $u->email()->value();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$filters['emails'] = array_unique($allowedEmails);
|
||||
|
||||
return AnalyticsStore::getAggregatedData($filters);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace adrienpayet\analytics;
|
||||
|
||||
use Kirby\Data\Yaml;
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
class AnalyticsStore
|
||||
{
|
||||
private static function getFilePath(): string
|
||||
{
|
||||
return kirby()->root('content') . '/analytics/visits.yml';
|
||||
}
|
||||
|
||||
private static function ensureFileExists(): void
|
||||
{
|
||||
$filePath = self::getFilePath();
|
||||
$dirPath = dirname($filePath);
|
||||
|
||||
if (!is_dir($dirPath)) {
|
||||
F::mkdir($dirPath, true);
|
||||
}
|
||||
|
||||
if (!F::exists($filePath)) {
|
||||
F::write($filePath, Yaml::encode(['visits' => []]));
|
||||
}
|
||||
}
|
||||
|
||||
public static function addVisit(Visit $visit): void
|
||||
{
|
||||
self::ensureFileExists();
|
||||
|
||||
$filePath = self::getFilePath();
|
||||
$data = Yaml::decode(F::read($filePath));
|
||||
|
||||
$visits = $data['visits'] ?? [];
|
||||
$visits[] = $visit->toArray();
|
||||
|
||||
// Limiter à 10000 visites max
|
||||
if (count($visits) > 10000) {
|
||||
$visits = array_slice($visits, -10000);
|
||||
}
|
||||
|
||||
$data['visits'] = $visits;
|
||||
F::write($filePath, Yaml::encode($data));
|
||||
}
|
||||
|
||||
public static function getVisits(array $filters = []): array
|
||||
{
|
||||
self::ensureFileExists();
|
||||
|
||||
$filePath = self::getFilePath();
|
||||
$data = Yaml::decode(F::read($filePath));
|
||||
$visits = $data['visits'] ?? [];
|
||||
|
||||
// Convertir en objets Visit
|
||||
$visits = array_map(fn($v) => Visit::fromArray($v), $visits);
|
||||
|
||||
// Filtrer par daterange
|
||||
if (!empty($filters['startDate']) || !empty($filters['endDate'])) {
|
||||
$visits = array_filter($visits, function($visit) use ($filters) {
|
||||
$timestamp = strtotime($visit->timestamp);
|
||||
|
||||
if (!empty($filters['startDate'])) {
|
||||
$startDate = strtotime($filters['startDate'] . ' 00:00:00');
|
||||
if ($timestamp < $startDate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($filters['endDate'])) {
|
||||
$endDate = strtotime($filters['endDate'] . ' 23:59:59');
|
||||
if ($timestamp > $endDate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Filtrer par projet
|
||||
if (!empty($filters['project'])) {
|
||||
$projectId = $filters['project'];
|
||||
$visits = array_filter($visits, function($visit) use ($projectId) {
|
||||
return str_contains($visit->pageUrl, "/projects/{$projectId}");
|
||||
});
|
||||
}
|
||||
|
||||
// Filtrer par email (permissions)
|
||||
if (!empty($filters['emails'])) {
|
||||
$allowedEmails = $filters['emails'];
|
||||
$visits = array_filter($visits, function($visit) use ($allowedEmails) {
|
||||
return in_array($visit->email, $allowedEmails);
|
||||
});
|
||||
}
|
||||
|
||||
// Filtrer par nom de page
|
||||
if (!empty($filters['pageNames'])) {
|
||||
$pageNames = $filters['pageNames'];
|
||||
$visits = array_filter($visits, function($visit) use ($pageNames) {
|
||||
$name = $visit->pageName ?: $visit->pageUrl;
|
||||
return in_array($name, $pageNames);
|
||||
});
|
||||
}
|
||||
|
||||
return array_values($visits);
|
||||
}
|
||||
|
||||
public static function getAggregatedData(array $filters = []): array
|
||||
{
|
||||
self::ensureFileExists();
|
||||
|
||||
// Visits sans filtre pageNames → pour construire la liste des pages (options)
|
||||
$filtersWithoutPages = $filters;
|
||||
unset($filtersWithoutPages['pageNames']);
|
||||
$allVisits = self::getVisits($filtersWithoutPages);
|
||||
|
||||
$visitsByPage = [];
|
||||
foreach ($allVisits as $visit) {
|
||||
$page = $visit->pageName ?: $visit->pageUrl;
|
||||
$visitsByPage[$page] = ($visitsByPage[$page] ?? 0) + 1;
|
||||
}
|
||||
arsort($visitsByPage);
|
||||
|
||||
// Visits avec tous les filtres → pour les stats affichées
|
||||
$visits = !empty($filters['pageNames'])
|
||||
? self::getVisits($filters)
|
||||
: $allVisits;
|
||||
|
||||
$visitsByDay = [];
|
||||
foreach ($visits as $visit) {
|
||||
$day = date('Y-m-d', strtotime($visit->timestamp));
|
||||
$visitsByDay[$day] = ($visitsByDay[$day] ?? 0) + 1;
|
||||
}
|
||||
ksort($visitsByDay);
|
||||
|
||||
$uniqueSessions = count(array_unique(array_map(fn($v) => $v->sessionId, $visits)));
|
||||
|
||||
// Visites par jour par page (toutes les pages)
|
||||
$visitsByDayByPage = [];
|
||||
foreach ($visits as $visit) {
|
||||
$page = $visit->pageName ?: $visit->pageUrl;
|
||||
$day = date('Y-m-d', strtotime($visit->timestamp));
|
||||
$visitsByDayByPage[$page][$day] = ($visitsByDayByPage[$page][$day] ?? 0) + 1;
|
||||
}
|
||||
foreach ($visitsByDayByPage as &$days) {
|
||||
ksort($days);
|
||||
}
|
||||
|
||||
return [
|
||||
'totalVisits' => count($visits),
|
||||
'uniqueSessions' => $uniqueSessions,
|
||||
'visitsByDay' => $visitsByDay,
|
||||
'visitsByPage' => array_slice($visitsByPage, 0, 10, true),
|
||||
'visitsByDayByPage' => $visitsByDayByPage,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace adrienpayet\analytics;
|
||||
|
||||
class Visit
|
||||
{
|
||||
public string $id;
|
||||
public string $email;
|
||||
public ?string $country;
|
||||
public string $timestamp;
|
||||
public string $sessionId;
|
||||
public string $pageUrl;
|
||||
public string $pageType;
|
||||
public ?string $pageName;
|
||||
|
||||
public function __construct(array $data)
|
||||
{
|
||||
$this->id = $data['id'] ?? uniqid('visit_', true);
|
||||
$this->email = $data['email'];
|
||||
$this->country = $data['country'] ?? null;
|
||||
$this->timestamp = $data['timestamp'] ?? date('Y-m-d H:i:s');
|
||||
$this->sessionId = $data['sessionId'];
|
||||
$this->pageUrl = $data['pageUrl'];
|
||||
$this->pageType = $data['pageType'];
|
||||
$this->pageName = $data['pageName'] ?? null;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'email' => $this->email,
|
||||
'country' => $this->country,
|
||||
'timestamp' => $this->timestamp,
|
||||
'sessionId' => $this->sessionId,
|
||||
'pageUrl' => $this->pageUrl,
|
||||
'pageType' => $this->pageType,
|
||||
'pageName' => $this->pageName,
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self($data);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'props' => [
|
||||
'value' => function ($value = null) {
|
||||
return null;
|
||||
}
|
||||
],
|
||||
'computed' => [
|
||||
'analyticsData' => function () {
|
||||
$page = $this->model();
|
||||
if (method_exists($page, 'getAnalyticsData')) {
|
||||
return $page->getAnalyticsData();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
]
|
||||
];
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
|
||||
.k-analytics-filters {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
.k-analytics-filters label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.k-date-inputs-wrapper {
|
||||
display: flex;
|
||||
column-gap: 1rem;
|
||||
}
|
||||
.k-analytics-filters input[type='date'] {
|
||||
padding: 0.375rem 0.5rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--rounded);
|
||||
font-size: 0.875rem;
|
||||
background: var(--color-background);
|
||||
}
|
||||
.k-analytics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.5rem;
|
||||
margin: 3rem 0;
|
||||
}
|
||||
.k-analytics-user-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
.k-field-name-user,
|
||||
.k-field-name-page {
|
||||
min-width: 15rem;
|
||||
}
|
||||
.k-field-name-top-pages {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.k-analytics-page-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
.k-analytics-card {
|
||||
background: var(--color-background);
|
||||
border-radius: var(--rounded);
|
||||
}
|
||||
.k-analytics-card h3 {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-light);
|
||||
margin: 0 0 0.5rem 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.k-analytics-metric {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
line-height: 1;
|
||||
}
|
||||
.k-analytics-chart-container {
|
||||
background: var(--color-background);
|
||||
border-radius: var(--rounded);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.k-analytics-chart-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.k-analytics-chart-header h3 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
.k-analytics-chart-container canvas {
|
||||
max-height: 300px;
|
||||
}
|
||||
.k-analytics-empty {
|
||||
background: var(--color-background);
|
||||
border-radius: var(--rounded);
|
||||
padding: 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
.k-analytics-empty p {
|
||||
margin: 0;
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
.k-analytics-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.k-analytics-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.375rem 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.k-analytics-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.k-analytics-list-label {
|
||||
color: var(--color-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
.k-analytics-list-value {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
|
||||
use adrienpayet\analytics\Visit;
|
||||
use adrienpayet\analytics\AnalyticsPage;
|
||||
use adrienpayet\analytics\AnalyticsStore;
|
||||
|
||||
F::loadClasses([
|
||||
"adrienpayet\\analytics\\Visit" => __DIR__ . "/classes/Visit.php",
|
||||
"adrienpayet\\analytics\\AnalyticsPage" => __DIR__ . "/classes/AnalyticsPage.php",
|
||||
"adrienpayet\\analytics\\AnalyticsStore" => __DIR__ . "/classes/AnalyticsStore.php",
|
||||
]);
|
||||
|
||||
Kirby::plugin("adrienpayet/analytics", [
|
||||
"pageModels" => [
|
||||
"analytics" => AnalyticsPage::class,
|
||||
],
|
||||
"blueprints" => [
|
||||
"pages/analytics" => __DIR__ . "/blueprints/pages/analytics.yml",
|
||||
],
|
||||
"routes" => [
|
||||
require(__DIR__ . "/routes/track.php"),
|
||||
require(__DIR__ . "/routes/get-data.php"),
|
||||
],
|
||||
"fields" => [
|
||||
"analytics-dashboard" => require(__DIR__ . "/fields/dashboard.php"),
|
||||
],
|
||||
]);
|
||||
30
public/site/plugins/analytics/package-lock.json
generated
30
public/site/plugins/analytics/package-lock.json
generated
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"name": "analytics",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"chart.js": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@kurkle/color": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"pnpm": ">=8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"scripts": {
|
||||
"dev": "npx -y kirbyup src/index.js --watch",
|
||||
"build": "npx -y kirbyup src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"chart.js": "^4.4.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'pattern' => 'analytics-data.json',
|
||||
'method' => 'GET',
|
||||
'action' => function () {
|
||||
$kirby = kirby();
|
||||
$user = $kirby->user();
|
||||
|
||||
// Seuls les admins peuvent accéder aux données analytics
|
||||
if (!$user || !$user->isAdmin()) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Unauthorized'
|
||||
];
|
||||
}
|
||||
|
||||
$analyticsPage = $kirby->page('analytics');
|
||||
|
||||
if (!$analyticsPage) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Analytics page not found'
|
||||
];
|
||||
}
|
||||
|
||||
$request = $kirby->request();
|
||||
$filters = [];
|
||||
|
||||
if ($startDate = $request->query()->get('startDate')) {
|
||||
$filters['startDate'] = $startDate;
|
||||
}
|
||||
|
||||
if ($endDate = $request->query()->get('endDate')) {
|
||||
$filters['endDate'] = $endDate;
|
||||
}
|
||||
|
||||
if ($project = $request->query()->get('project')) {
|
||||
$filters['project'] = $project;
|
||||
}
|
||||
|
||||
if (!empty($_GET['emails'])) {
|
||||
$filters['emails'] = explode(',', $_GET['emails']);
|
||||
}
|
||||
|
||||
if (!empty($_GET['pageNames'])) {
|
||||
$filters['pageNames'] = explode(',', $_GET['pageNames']);
|
||||
}
|
||||
|
||||
$data = $analyticsPage->getAnalyticsData($filters);
|
||||
|
||||
$users = [];
|
||||
foreach ($kirby->users() as $u) {
|
||||
$email = (string) $u->email();
|
||||
$name = $u->name()->isNotEmpty() ? (string) $u->name() : $email;
|
||||
$label = $name;
|
||||
|
||||
$clientField = $u->content()->get('client');
|
||||
if ($clientField && $clientField->isNotEmpty()) {
|
||||
$clientPage = $clientField->toPage();
|
||||
if ($clientPage) {
|
||||
$label .= ' (' . $clientPage->title() . ')';
|
||||
}
|
||||
}
|
||||
|
||||
$users[] = [
|
||||
'email' => $email,
|
||||
'label' => $label,
|
||||
];
|
||||
}
|
||||
|
||||
usort($users, fn($a, $b) => strcasecmp($a['label'], $b['label']));
|
||||
|
||||
return [
|
||||
'status' => 'success',
|
||||
'data' => $data,
|
||||
'users' => $users
|
||||
];
|
||||
}
|
||||
];
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
<?php
|
||||
|
||||
use adrienpayet\analytics\Visit;
|
||||
use adrienpayet\analytics\AnalyticsStore;
|
||||
|
||||
return [
|
||||
'pattern' => 'track-visit.json',
|
||||
'method' => 'POST',
|
||||
'action' => function () {
|
||||
$kirby = kirby();
|
||||
$user = $kirby->user();
|
||||
|
||||
// Seuls les utilisateurs connectés peuvent tracker
|
||||
if (!$user) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Unauthorized'
|
||||
];
|
||||
}
|
||||
|
||||
$data = $kirby->request()->body()->toArray();
|
||||
|
||||
// Détection du pays
|
||||
$country = null;
|
||||
|
||||
// 1. Header Cloudflare
|
||||
if (isset($_SERVER['HTTP_CF_IPCOUNTRY'])) {
|
||||
$country = $_SERVER['HTTP_CF_IPCOUNTRY'];
|
||||
}
|
||||
// 2. Fallback : API ipapi.co (optionnel, peut être désactivé)
|
||||
elseif (!empty($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] !== '127.0.0.1') {
|
||||
try {
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
$response = @file_get_contents("https://ipapi.co/{$ip}/country/", false, stream_context_create([
|
||||
'http' => ['timeout' => 1]
|
||||
]));
|
||||
if ($response) {
|
||||
$country = trim($response);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Ignorer les erreurs de géolocalisation
|
||||
}
|
||||
}
|
||||
|
||||
$visit = new Visit([
|
||||
'email' => (string) $user->email(),
|
||||
'country' => $country,
|
||||
'sessionId' => $data['sessionId'] ?? '',
|
||||
'pageUrl' => $data['pageUrl'] ?? '',
|
||||
'pageType' => $data['pageType'] ?? 'unknown',
|
||||
'pageName' => $data['pageName'] ?? null,
|
||||
]);
|
||||
|
||||
AnalyticsStore::addVisit($visit);
|
||||
|
||||
return [
|
||||
'status' => 'success',
|
||||
'message' => 'Visit tracked'
|
||||
];
|
||||
}
|
||||
];
|
||||
|
|
@ -1,491 +0,0 @@
|
|||
<template>
|
||||
<div class="k-analytics-dashboard">
|
||||
<div class="k-analytics-filters">
|
||||
<div class="k-analytics-date-filter">
|
||||
<header class="k-field-header">
|
||||
<label class="k-label k-field-label" title="Filtrer par dates">
|
||||
<span class="k-label-text">Filtrer par dates </span>
|
||||
</label>
|
||||
</header>
|
||||
<div class="k-date-inputs-wrapper">
|
||||
<label>
|
||||
Du
|
||||
<input type="date" v-model="startDate" @change="fetchData" />
|
||||
</label>
|
||||
<label>
|
||||
Au
|
||||
<input type="date" v-model="endDate" @change="fetchData" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="k-analytics-user-filter">
|
||||
<k-multiselect-field
|
||||
:options="userOptions"
|
||||
:value="selectedEmails"
|
||||
label="Filtrer par utilisateur(s)"
|
||||
search="true"
|
||||
name="user"
|
||||
@input="onUserSelectionChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="k-analytics-page-filter">
|
||||
<k-multiselect-field
|
||||
:options="pageOptions"
|
||||
:value="selectedPages"
|
||||
label="Filtrer par page(s)"
|
||||
search="true"
|
||||
name="page"
|
||||
@input="onPageSelectionChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!hasData" class="k-analytics-empty">
|
||||
<p>Aucune donnée à afficher</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="k-analytics-grid">
|
||||
<div class="k-analytics-card">
|
||||
<h3>Sessions uniques</h3>
|
||||
<div class="k-analytics-metric">{{ data.uniqueSessions }}</div>
|
||||
</div>
|
||||
<div class="k-analytics-card">
|
||||
<h3>Pages vues</h3>
|
||||
<div class="k-analytics-metric">{{ data.totalVisits }}</div>
|
||||
</div>
|
||||
<div class="k-analytics-card">
|
||||
<h3>Pages / session</h3>
|
||||
<div class="k-analytics-metric">{{ pagesPerSession }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="k-analytics-chart-container">
|
||||
<div class="k-analytics-chart-header">
|
||||
<h3>Visites par jour</h3>
|
||||
<k-toggle-field
|
||||
v-if="selectedPages.length === 0"
|
||||
:value="showTopPagesOnly"
|
||||
before="Moyenne"
|
||||
after="Pages les + visitées"
|
||||
text=" "
|
||||
name="top-pages"
|
||||
@input="onToggleTopPages"
|
||||
/>
|
||||
</div>
|
||||
<canvas ref="chartCanvas"></canvas>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="k-analytics-card"
|
||||
v-if="data.visitsByPage && Object.keys(data.visitsByPage).length"
|
||||
>
|
||||
<h3>Pages les plus visitées</h3>
|
||||
<ul class="k-analytics-list">
|
||||
<li v-for="(count, page) in data.visitsByPage" :key="page">
|
||||
<span class="k-analytics-list-label">{{ page }}</span>
|
||||
<span class="k-analytics-list-value">{{ count }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
Chart,
|
||||
LineController,
|
||||
LineElement,
|
||||
PointElement,
|
||||
LinearScale,
|
||||
CategoryScale,
|
||||
Filler,
|
||||
Tooltip,
|
||||
Legend,
|
||||
} from 'chart.js';
|
||||
|
||||
Chart.register(
|
||||
LineController,
|
||||
LineElement,
|
||||
PointElement,
|
||||
LinearScale,
|
||||
CategoryScale,
|
||||
Filler,
|
||||
Tooltip,
|
||||
Legend
|
||||
);
|
||||
|
||||
const CHART_COLORS = [
|
||||
'#4271ae',
|
||||
'#c82829',
|
||||
'#8959a8',
|
||||
'#4d9a0f',
|
||||
'#f5871f',
|
||||
'#3e999f',
|
||||
'#718c00',
|
||||
'#a3685a',
|
||||
'#525252',
|
||||
'#eab700',
|
||||
];
|
||||
|
||||
export default {
|
||||
props: {
|
||||
analyticsData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
data: this.analyticsData || {},
|
||||
chart: null,
|
||||
users: [],
|
||||
selectedEmails: [],
|
||||
selectedPages: [],
|
||||
showTopPagesOnly: true,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
hasData() {
|
||||
return this.data && this.data.totalVisits > 0;
|
||||
},
|
||||
pagesPerSession() {
|
||||
if (!this.data?.uniqueSessions) return '0';
|
||||
return (this.data.totalVisits / this.data.uniqueSessions).toFixed(1);
|
||||
},
|
||||
userOptions() {
|
||||
return this.users.map((u) => ({ value: u.email, text: u.label }));
|
||||
},
|
||||
pageOptions() {
|
||||
if (!this.data?.visitsByPage) return [];
|
||||
return Object.keys(this.data.visitsByPage)
|
||||
.map((p) => ({ value: p, text: p }))
|
||||
.sort((a, b) => a.text.localeCompare(b.text));
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
analyticsData(newVal) {
|
||||
this.data = newVal || {};
|
||||
this.renderChart();
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.setDefaultDates();
|
||||
this.fetchData();
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
this.destroyChart();
|
||||
},
|
||||
|
||||
methods: {
|
||||
setDefaultDates() {
|
||||
const now = new Date();
|
||||
const thirtyDaysAgo = new Date(now);
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
this.endDate = now.toISOString().split('T')[0];
|
||||
this.startDate = thirtyDaysAgo.toISOString().split('T')[0];
|
||||
},
|
||||
|
||||
onUserSelectionChange(emails) {
|
||||
this.selectedEmails = emails;
|
||||
this.fetchData();
|
||||
},
|
||||
|
||||
onPageSelectionChange(pages) {
|
||||
this.selectedPages = pages;
|
||||
this.fetchData();
|
||||
},
|
||||
|
||||
onToggleTopPages(value) {
|
||||
this.showTopPagesOnly = value;
|
||||
this.renderChart();
|
||||
},
|
||||
|
||||
async fetchData() {
|
||||
const params = new URLSearchParams();
|
||||
if (this.startDate) params.set('startDate', this.startDate);
|
||||
if (this.endDate) params.set('endDate', this.endDate);
|
||||
if (this.selectedEmails.length) {
|
||||
params.set('emails', this.selectedEmails.join(','));
|
||||
}
|
||||
if (this.selectedPages.length) {
|
||||
params.set('pageNames', this.selectedPages.join(','));
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/analytics-data.json?${params}`);
|
||||
const json = await response.json();
|
||||
|
||||
console.log(json.data);
|
||||
|
||||
if (json.status === 'success') {
|
||||
this.data = json.data;
|
||||
}
|
||||
|
||||
if (json.users && !this.users.length) {
|
||||
this.users = json.users;
|
||||
}
|
||||
|
||||
this.renderChart();
|
||||
} catch (e) {
|
||||
console.error('Analytics fetch error:', e);
|
||||
this.renderChart();
|
||||
}
|
||||
},
|
||||
|
||||
destroyChart() {
|
||||
if (this.chart) {
|
||||
this.chart.destroy();
|
||||
this.chart = null;
|
||||
}
|
||||
},
|
||||
|
||||
formatDateFR(isoDate) {
|
||||
const [y, m, d] = isoDate.split('-');
|
||||
return `${d}/${m}/${y}`;
|
||||
},
|
||||
|
||||
renderChart() {
|
||||
this.destroyChart();
|
||||
|
||||
const canvas = this.$refs.chartCanvas;
|
||||
if (!canvas || !this.data?.visitsByDay) return;
|
||||
|
||||
const allDays = Object.keys(this.data.visitsByDay);
|
||||
if (!allDays.length) return;
|
||||
|
||||
const labels = allDays.map((d) => this.formatDateFR(d));
|
||||
const showPerPage =
|
||||
this.selectedPages.length === 0 &&
|
||||
this.showTopPagesOnly &&
|
||||
this.data.visitsByDayByPage;
|
||||
|
||||
let datasets;
|
||||
let maxValue;
|
||||
|
||||
if (showPerPage) {
|
||||
const topPages = this.data.visitsByPage
|
||||
? Object.keys(this.data.visitsByPage)
|
||||
: [];
|
||||
const pages = topPages.sort((a, b) => a.localeCompare(b));
|
||||
datasets = pages.map((page, i) => {
|
||||
const color = CHART_COLORS[i % CHART_COLORS.length];
|
||||
const values = allDays.map(
|
||||
(day) => this.data.visitsByDayByPage[page]?.[day] || 0
|
||||
);
|
||||
return {
|
||||
label: page,
|
||||
data: values,
|
||||
borderColor: color,
|
||||
backgroundColor: 'transparent',
|
||||
fill: false,
|
||||
tension: 0.3,
|
||||
pointRadius: 2,
|
||||
pointHoverRadius: 4,
|
||||
borderWidth: 2,
|
||||
};
|
||||
});
|
||||
maxValue = Math.max(...datasets.flatMap((ds) => ds.data));
|
||||
} else {
|
||||
const values = Object.values(this.data.visitsByDay);
|
||||
datasets = [
|
||||
{
|
||||
label: 'Visites',
|
||||
data: values,
|
||||
borderColor: '#4271ae',
|
||||
backgroundColor: 'rgba(66, 113, 174, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.3,
|
||||
pointRadius: 3,
|
||||
pointHoverRadius: 5,
|
||||
},
|
||||
];
|
||||
maxValue = Math.max(...values);
|
||||
}
|
||||
|
||||
this.chart = new Chart(canvas, {
|
||||
type: 'line',
|
||||
data: { labels, datasets },
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
legend: {
|
||||
display: showPerPage,
|
||||
position: 'bottom',
|
||||
labels: { boxWidth: 12, padding: 12 },
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: maxValue + 3,
|
||||
ticks: { precision: 0 },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.k-analytics-filters {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.k-analytics-filters label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.k-date-inputs-wrapper {
|
||||
display: flex;
|
||||
column-gap: 1rem;
|
||||
}
|
||||
|
||||
.k-analytics-filters input[type='date'] {
|
||||
padding: 0.375rem 0.5rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--rounded);
|
||||
font-size: 0.875rem;
|
||||
background: var(--color-background);
|
||||
}
|
||||
|
||||
.k-analytics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.5rem;
|
||||
margin: 3rem 0;
|
||||
}
|
||||
|
||||
.k-analytics-user-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
.k-field-name-user,
|
||||
.k-field-name-page {
|
||||
min-width: 15rem;
|
||||
}
|
||||
|
||||
.k-field-name-top-pages {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.k-analytics-page-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
.k-analytics-card {
|
||||
background: var(--color-background);
|
||||
border-radius: var(--rounded);
|
||||
}
|
||||
|
||||
.k-analytics-card h3 {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-light);
|
||||
margin: 0 0 0.5rem 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.k-analytics-metric {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.k-analytics-chart-container {
|
||||
background: var(--color-background);
|
||||
border-radius: var(--rounded);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.k-analytics-chart-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.k-analytics-chart-header h3 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.k-analytics-chart-container canvas {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.k-analytics-empty {
|
||||
background: var(--color-background);
|
||||
border-radius: var(--rounded);
|
||||
padding: 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.k-analytics-empty p {
|
||||
margin: 0;
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
.k-analytics-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.k-analytics-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.375rem 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.k-analytics-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.k-analytics-list-label {
|
||||
color: var(--color-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.k-analytics-list-value {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import AnalyticsDashboard from "./components/AnalyticsDashboard.vue";
|
||||
|
||||
window.panel.plugin("adrienpayet/analytics", {
|
||||
fields: {
|
||||
"analytics-dashboard": AnalyticsDashboard
|
||||
}
|
||||
});
|
||||
|
|
@ -94,6 +94,7 @@ function processDTLProposals($page) {
|
|||
],
|
||||
"path" => "/projects/" . $page->slug() . "?dialog=proposal&fileIndex=" . $index,
|
||||
"date" => $proposalFile->modified("d/MM/Y"),
|
||||
"stepLabel" => "Proposition commerciale",
|
||||
];
|
||||
}
|
||||
break;
|
||||
|
|
@ -111,6 +112,7 @@ function processDTLProposals($page) {
|
|||
],
|
||||
"path" => "/projects/" . $page->slug() . "?dialog=industrial-ideation",
|
||||
"date" => $proposalFile->modified("d/MM/Y"),
|
||||
"stepLabel" => "Idéation industrielle",
|
||||
];
|
||||
}
|
||||
break;
|
||||
|
|
@ -127,6 +129,7 @@ function processDTLProposals($page) {
|
|||
],
|
||||
"path" => "/projects/" . $page->slug() . "?dialog=virtual-sample",
|
||||
"date" => $proposalPage->modified("d/MM/Y"),
|
||||
"stepLabel" => "Échantillon virtuel - piste dynamique",
|
||||
];
|
||||
}
|
||||
break;
|
||||
|
|
@ -144,6 +147,7 @@ function processDTLProposals($page) {
|
|||
],
|
||||
"path" => "/projects/" . $page->slug() . "?dialog=virtual-sample",
|
||||
"date" => $proposalFile->modified("d/MM/Y"),
|
||||
"stepLabel" => "Échantillon virtuel - piste statique",
|
||||
];
|
||||
}
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ return [
|
|||
if ($user->role()->name() === 'admin') {
|
||||
$projects = page('projects')->children()->toArray();
|
||||
} else {
|
||||
$projects = $user->currentProjects()->toArray();
|
||||
$projects = $user->projects()->toPages()->toArray();
|
||||
}
|
||||
|
||||
$count = $collector->markAllAsRead($projects, $user);
|
||||
|
|
|
|||
|
|
@ -3,21 +3,18 @@
|
|||
Kirby::plugin('adrienpayet/pdc-authorized-projects', [
|
||||
'userMethods' => [
|
||||
'currentProjects' => function() {
|
||||
$listed = page('projects')->children()->listed();
|
||||
if ($this->role() == 'admin' && $this->hasNoAssignedProjects()) {
|
||||
return $listed;
|
||||
if ($this->role() == 'admin') {
|
||||
return page('projects')->children()->listed();
|
||||
} else {
|
||||
return $this->projects()->toPages()->listed();
|
||||
}
|
||||
return $listed->filter(fn($project) => $project->users()->toUsers()->has($this));
|
||||
},
|
||||
'archivedProjects' => function() {
|
||||
$unlisted = page('projects')->children()->unlisted();
|
||||
if ($this->role() == 'admin' && $this->hasNoAssignedProjects()) {
|
||||
return $unlisted;
|
||||
if ($this->role() == 'admin') {
|
||||
return page('projects')->children()->unlisted();
|
||||
} else {
|
||||
return $this->projects()->toPages()->unlisted();
|
||||
}
|
||||
return $unlisted->filter(fn($project) => $project->users()->toUsers()->has($this));
|
||||
},
|
||||
'hasNoAssignedProjects' => function() {
|
||||
return page('projects')->children()->filter(fn($p) => $p->users()->toUsers()->has($this))->isEmpty();
|
||||
},
|
||||
]
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
|
||||
<script>
|
||||
if (location.href.includes('goguely')) {
|
||||
location.href = 'https://designtopack.groupe-pochet.fr' + location.pathname
|
||||
location.href = 'https://designtopack.morphozbygroupepochet.com' + location.pathname
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
|
|
|||
|
|
@ -11,12 +11,11 @@ function getProjectData($project, $user)
|
|||
{
|
||||
// Utiliser getNotificationsLight() avec cache pour optimiser les performances
|
||||
$notifications = [];
|
||||
if ($project instanceof ProjectPage) {
|
||||
try {
|
||||
$notifications = $project->getNotificationsLight($user);
|
||||
} catch (\Throwable $e) {
|
||||
error_log("Error getting notifications for project {$project->uri()}: " . $e->getMessage());
|
||||
}
|
||||
$notifications = [];
|
||||
}
|
||||
|
||||
$data = [
|
||||
|
|
@ -45,12 +44,9 @@ function getProjectData($project, $user)
|
|||
$currentUser = $kirby->user();
|
||||
|
||||
try {
|
||||
if ($currentUser->role() == 'admin' && $currentUser->hasNoAssignedProjects()) {
|
||||
$projectCollection = $page->childrenAndDrafts();
|
||||
} else {
|
||||
$projectCollection = $currentUser->currentProjects()->merge($currentUser->archivedProjects());
|
||||
}
|
||||
$children = $projectCollection->map(fn($project) => getProjectData($project, $currentUser))->values();
|
||||
$children = $currentUser->role() == 'admin'
|
||||
? $page->childrenAndDrafts()->map(fn($project) => getProjectData($project, $currentUser))->values()
|
||||
: $currentUser->projects()->toPages()->map(fn($project) => getProjectData($project, $currentUser))->values();
|
||||
} catch (\Throwable $th) {
|
||||
throw new Exception($th->getMessage() . ' line ' . $th->getLine() . ' in file ' . $th->getFile(), 1);
|
||||
$children = [];
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
}
|
||||
|
||||
.comments.empty::after {
|
||||
content: attr(data-empty-message);
|
||||
content: "Partagez vos idées en ajoutant des commentaires";
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
|
|
|||
|
|
@ -29,22 +29,7 @@
|
|||
<div v-if="isExpanded" id="menu" class="flex | rounded-xl">
|
||||
<header class="w-full | flex">
|
||||
<!-- TODO: à dynamiser en récupérant le $site->title() -->
|
||||
<p :lang="currentLocale">Design to Pack</p>
|
||||
<div class="lang-toggle">
|
||||
<button
|
||||
@click="switchLocale('fr')"
|
||||
:class="{ active: currentLocale === 'fr' }"
|
||||
>
|
||||
FR
|
||||
</button>
|
||||
<span class="slash">/</span>
|
||||
<button
|
||||
@click="switchLocale('en')"
|
||||
:class="{ active: currentLocale === 'en' }"
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
</div>
|
||||
<p lang="en">Design to Pack</p>
|
||||
</header>
|
||||
<nav class="w-full | flow">
|
||||
<ul class="flex">
|
||||
|
|
@ -66,23 +51,25 @@
|
|||
>{{ mainItem.title }}</router-link
|
||||
>
|
||||
<span
|
||||
v-if="
|
||||
mainItem.title === t('menu.inspirations') && page?.newInspirations
|
||||
"
|
||||
v-if="mainItem.title === 'Inspirations' && page?.newInspirations"
|
||||
class="pill pill--secondary"
|
||||
>{{ t('menu.news') }}</span
|
||||
>{{ 'Nouveautés' }}</span
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<details :class="{ skeleton: !currentProjects }" open>
|
||||
<summary>{{ t('menu.currentProjects') }}</summary>
|
||||
<summary>Projets en cours</summary>
|
||||
<ul v-if="currentProjects.length > 0">
|
||||
<li
|
||||
v-for="project in currentProjects"
|
||||
:class="{ active: isCurrent(project) }"
|
||||
>
|
||||
<router-link
|
||||
:to="getProjectPath(project)"
|
||||
:to="
|
||||
isEmptyBrief(project)
|
||||
? project.uri + '/client-brief'
|
||||
: project.uri
|
||||
"
|
||||
:class="hasUnreadNotification(project) ? 'new' : undefined"
|
||||
:data-dtl="project.isDTLEnabled ? 'true' : undefined"
|
||||
@click="collapse()"
|
||||
|
|
@ -92,13 +79,13 @@
|
|||
</ul>
|
||||
</details>
|
||||
<details v-if="archivedProjects.length">
|
||||
<summary>{{ t('menu.archivedProjects') }}</summary>
|
||||
<summary>Projets archivés</summary>
|
||||
<ul>
|
||||
<li
|
||||
v-for="project in archivedProjects"
|
||||
:class="{ active: isCurrent(project) }"
|
||||
>
|
||||
<router-link :to="getProjectPath(project)" @click="collapse()">{{
|
||||
<router-link :to="project.uri" @click="collapse()">{{
|
||||
project.title
|
||||
}}</router-link>
|
||||
</li>
|
||||
|
|
@ -109,19 +96,13 @@
|
|||
<ul class="flex">
|
||||
<li data-icon="user">
|
||||
<a
|
||||
:href="
|
||||
user.role === 'admin'
|
||||
? '/panel/account'
|
||||
: currentLocale === 'en'
|
||||
? '/en/account'
|
||||
: '/account'
|
||||
"
|
||||
:href="user.role === 'admin' ? '/panel/account' : '/account'"
|
||||
@click="collapse()"
|
||||
>{{ t('menu.profile') }}</a
|
||||
>Profil</a
|
||||
>
|
||||
</li>
|
||||
<li data-icon="logout">
|
||||
<a href="/logout" @click="collapse()">{{ t('menu.logout') }}</a>
|
||||
<a href="/logout" @click="collapse()">Déconnexion</a>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
||||
|
|
@ -132,23 +113,17 @@
|
|||
import { storeToRefs } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useProjectsStore } from '../stores/projects';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useUserStore } from '../stores/user';
|
||||
import { usePageStore } from '../stores/page';
|
||||
import { useProjectStore } from '../stores/project';
|
||||
import { useLocaleStore } from '../stores/locale';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const isExpanded = ref(true);
|
||||
const { user, notifications } = storeToRefs(useUserStore());
|
||||
const { currentProjects, archivedProjects } = storeToRefs(useProjectsStore());
|
||||
const { isEmptyBrief } = useProjectStore();
|
||||
const { page } = storeToRefs(usePageStore());
|
||||
const localeStore = useLocaleStore();
|
||||
const { currentLocale } = storeToRefs(localeStore);
|
||||
const { t } = useI18n();
|
||||
|
||||
const unreadNotificationsCount = computed(() => {
|
||||
if (!user.value) return 0;
|
||||
|
|
@ -160,37 +135,34 @@ const unreadNotificationsCount = computed(() => {
|
|||
return count === 0 ? 0 : count;
|
||||
});
|
||||
|
||||
const mainItems = computed(() => {
|
||||
const prefix = currentLocale.value === 'en' ? '/en' : '';
|
||||
return [
|
||||
const mainItems = [
|
||||
{
|
||||
title: t('menu.home'),
|
||||
path: prefix + '/',
|
||||
title: 'Home',
|
||||
path: '/',
|
||||
icon: 'home',
|
||||
},
|
||||
{
|
||||
title: t('menu.notifications'),
|
||||
path: prefix + '/notifications',
|
||||
title: 'Notifications',
|
||||
path: '/notifications',
|
||||
icon: 'megaphone',
|
||||
},
|
||||
{
|
||||
title: t('menu.meetings'),
|
||||
path: prefix + '/reunions',
|
||||
title: 'Réunions',
|
||||
path: '/reunions',
|
||||
icon: 'calendar',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
title: t('menu.designToLight'),
|
||||
path: prefix + '/design-to-light',
|
||||
title: 'Design to Light',
|
||||
path: '/design-to-light',
|
||||
icon: 'leaf',
|
||||
},
|
||||
{
|
||||
title: t('menu.inspirations'),
|
||||
path: prefix + '/inspirations',
|
||||
title: 'Inspirations',
|
||||
path: '/inspirations',
|
||||
icon: 'inspiration',
|
||||
},
|
||||
];
|
||||
});
|
||||
];
|
||||
|
||||
function toggleExpand() {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
|
|
@ -220,33 +192,6 @@ function collapse() {
|
|||
isExpanded.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function switchLocale(newLocale) {
|
||||
if (newLocale === currentLocale.value) return;
|
||||
|
||||
let newPath = route.path;
|
||||
|
||||
if (newLocale === 'en') {
|
||||
if (!newPath.startsWith('/en')) {
|
||||
newPath = '/en' + newPath;
|
||||
}
|
||||
} else {
|
||||
if (newPath.startsWith('/en')) {
|
||||
newPath = newPath.replace(/^\/en/, '') || '/';
|
||||
}
|
||||
}
|
||||
|
||||
localeStore.setLocale(newLocale);
|
||||
router.push(newPath);
|
||||
}
|
||||
|
||||
function getProjectPath(project) {
|
||||
const prefix = currentLocale.value === 'en' ? '/en' : '';
|
||||
const path = isEmptyBrief(project)
|
||||
? project.uri + '/client-brief'
|
||||
: project.uri;
|
||||
return prefix + path;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
@ -298,25 +243,6 @@ button[aria-controls='menu'][aria-expanded='false']
|
|||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Lang toggle */
|
||||
.lang-toggle {
|
||||
font-family: var(--font-sans);
|
||||
height: 1.65rem;
|
||||
}
|
||||
|
||||
.lang-toggle button {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.lang-toggle button.active {
|
||||
background-color: transparent !important;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.lang-toggle span.slash {
|
||||
padding: 0 0.2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
button[aria-controls='menu'][aria-expanded='true'] {
|
||||
left: 0;
|
||||
|
|
@ -359,7 +285,6 @@ button[aria-controls='menu'][aria-expanded='false']
|
|||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
column-gap: calc(var(--gutter) / 2);
|
||||
}
|
||||
#menu header::before {
|
||||
content: '';
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@
|
|||
modal
|
||||
:draggable="false"
|
||||
:dismissableMask="true"
|
||||
:header="t('dialogs.requestProject')"
|
||||
header="Demander la création d’un projet"
|
||||
class="dialog"
|
||||
:closeOnEscape="true"
|
||||
>
|
||||
<template #header>
|
||||
<h2 class="font-serif text-lg">{{ t('dialogs.requestProject') }}</h2>
|
||||
<h2 class="font-serif text-lg">Demander la création d’un projet</h2>
|
||||
</template>
|
||||
|
||||
<form
|
||||
|
|
@ -18,28 +18,24 @@
|
|||
class="w-full h-full p-16 flex flex-col"
|
||||
style="--row-gap: 1rem"
|
||||
>
|
||||
<label for="project-title" class="sr-only">{{
|
||||
t('forms.projectName')
|
||||
}}</label>
|
||||
<label for="project-title" class="sr-only">Nom du projet</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="title"
|
||||
id="project-title"
|
||||
:placeholder="t('forms.projectName')"
|
||||
placeholder="Nom du projet"
|
||||
class="w-full rounded-md border border-grey-200 px-16 py-12"
|
||||
required
|
||||
/>
|
||||
|
||||
<label for="project-details" class="sr-only">{{
|
||||
t('forms.projectDetails')
|
||||
}}</label>
|
||||
<label for="project-details" class="sr-only">Détails du projet</label>
|
||||
<textarea
|
||||
id="project-details"
|
||||
name="details"
|
||||
v-model="details"
|
||||
cols="30"
|
||||
rows="10"
|
||||
:placeholder="t('forms.projectDetailsPlaceholder')"
|
||||
placeholder="Détails du projet…"
|
||||
class="w-full flex-1 rounded-md border border-grey-200 px-16 py-12"
|
||||
required
|
||||
></textarea>
|
||||
|
|
@ -56,24 +52,26 @@
|
|||
class="flex font-medium mt-4"
|
||||
style="--column-gap: var(--space-4)"
|
||||
>
|
||||
{{ t('dialogs.createWithDTL') }}
|
||||
<span class="flex justify-center text-sm" data-icon="leaf">{{
|
||||
t('dtl.title')
|
||||
}}</span>
|
||||
Créer avec
|
||||
<span class="flex justify-center text-sm" data-icon="leaf"
|
||||
>Design to Light</span
|
||||
>
|
||||
</label>
|
||||
<p class="text-sm mt-8 mb-4">
|
||||
{{ t('dialogs.dtlDescription') }}
|
||||
Découvrez la note environnementale de votre projet et allégez l’impact
|
||||
de votre projet grâce à nos expertises d’optimisation du poids de
|
||||
flacon.
|
||||
</p>
|
||||
<router-link to="/design-to-light" class="text-sm font-medium">{{
|
||||
t('dialogs.learnMore')
|
||||
}}</router-link>
|
||||
<router-link to="/design-to-light" class="text-sm font-medium"
|
||||
>En savoir plus</router-link
|
||||
>
|
||||
</div>
|
||||
|
||||
<footer class="flex-columns w-full mt-16" style="column-gap: 0.5rem">
|
||||
<button class="btn btn--black-10" @click="emits('close')">
|
||||
{{ t('buttons.cancel') }}
|
||||
Annuler
|
||||
</button>
|
||||
<button class="btn" type="submit">{{ t('buttons.submit') }}</button>
|
||||
<button class="btn" type="submit">Soumettre</button>
|
||||
</footer>
|
||||
</form>
|
||||
</Dialog>
|
||||
|
|
@ -83,9 +81,6 @@
|
|||
import Dialog from 'primevue/dialog';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useApiStore } from '../stores/api';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const title = ref('');
|
||||
const details = ref('');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<header class="flex">
|
||||
<h2 id="tabslist" class="sr-only">{{ t('brief.projects') }}</h2>
|
||||
<h2 id="tabslist" class="sr-only">Projets</h2>
|
||||
<Tabs :tabs="tabs" @update:currentTab="changeTab" />
|
||||
</header>
|
||||
<section
|
||||
|
|
@ -11,7 +11,6 @@
|
|||
:aria-label="tabs[0].label"
|
||||
class="flow"
|
||||
:class="{ skeleton: isProjectsLoading }"
|
||||
:data-empty-text="t('projects.none')"
|
||||
>
|
||||
<Project
|
||||
v-for="project in currentProjects"
|
||||
|
|
@ -26,7 +25,6 @@
|
|||
tabindex="0"
|
||||
:aria-label="tabs[1].label"
|
||||
class="flow"
|
||||
:data-empty-text="t('projects.none')"
|
||||
>
|
||||
<Project
|
||||
v-for="project in archivedProjects"
|
||||
|
|
@ -41,9 +39,7 @@ import Project from './project/Project.vue';
|
|||
import { useProjectsStore } from '../stores/projects';
|
||||
import { ref, computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { projects, currentProjects, archivedProjects, isProjectsLoading } =
|
||||
storeToRefs(useProjectsStore());
|
||||
|
||||
|
|
@ -51,13 +47,13 @@ const currentTab = ref('currentProjects');
|
|||
const tabs = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: t('projects.current'),
|
||||
label: 'Projets en cours',
|
||||
id: 'currentProjects',
|
||||
count: currentProjects.value.length,
|
||||
isActive: currentTab.value === 'currentProjects',
|
||||
},
|
||||
{
|
||||
label: t('projects.archived'),
|
||||
label: 'Projets archivés',
|
||||
id: 'archivedProjects',
|
||||
count: archivedProjects.value.length,
|
||||
isActive: currentTab.value === 'archivedProjects',
|
||||
|
|
@ -76,7 +72,7 @@ section {
|
|||
min-height: calc(100vh - 8.5rem);
|
||||
}
|
||||
section:not(.skeleton):empty::after {
|
||||
content: attr(data-empty-text);
|
||||
content: 'Aucun projet pour le moment';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
v-model="currentValue"
|
||||
:options="items"
|
||||
optionLabel="title"
|
||||
:placeholder="t('forms.selectVariation')"
|
||||
:placeholder="'Sélectionnez une déclinaison'"
|
||||
:maxSelectedLabels="3"
|
||||
class="font-serif"
|
||||
:class="{ active: currentValue }"
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
<p v-if="currentValue">
|
||||
{{ currentValue.title }}
|
||||
</p>
|
||||
<p v-else>{{ t('forms.selectVariation') }}</p>
|
||||
<p v-else>Sélectionnez une déclinaison</p>
|
||||
</template>
|
||||
|
||||
<template #option="slotProps">
|
||||
|
|
@ -65,9 +65,6 @@
|
|||
import { onBeforeMount, ref, watch, nextTick } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useDialogStore } from '../stores/dialog';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// Props
|
||||
const { items, label, isCompareModeEnabled, index } = defineProps({
|
||||
|
|
|
|||
|
|
@ -39,7 +39,9 @@
|
|||
|
||||
<footer v-if="!comment.isEditMode" class="comment__replies">
|
||||
<p v-if="comment.replies?.length > 0">
|
||||
{{ comment.replies.length }} {{ comment.replies.length > 1 ? t('comments.replies') : t('comments.reply') }}
|
||||
{{ comment.replies.length }} réponse{{
|
||||
comment.replies.length > 1 ? 's' : ''
|
||||
}}
|
||||
</p>
|
||||
<div
|
||||
v-if="userStore.canEditComment(comment)"
|
||||
|
|
@ -50,14 +52,14 @@
|
|||
data-icon="edit"
|
||||
@click="editComment($event)"
|
||||
>
|
||||
<span class="sr-only">{{ t('comments.edit') }}</span>
|
||||
<span class="sr-only">Éditer</span>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--transparent btn--icon btn--sm"
|
||||
data-icon="delete"
|
||||
@click="deleteComment($event)"
|
||||
>
|
||||
<span class="sr-only">{{ t('buttons.delete') }}</span>
|
||||
<span class="sr-only">Supprimer</span>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
@ -66,11 +68,11 @@
|
|||
<input
|
||||
type="submit"
|
||||
class="btn btn--tranparent"
|
||||
:value="t('buttons.save')"
|
||||
value="Sauvegarder"
|
||||
@click="saveEditedComment($event)"
|
||||
/>
|
||||
<button class="btn btn--white-10" @click="cancelEditComment($event)">
|
||||
{{ t('buttons.cancel') }}
|
||||
Annuler
|
||||
</button>
|
||||
</footer>
|
||||
</article>
|
||||
|
|
@ -86,11 +88,9 @@ import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
|||
import { storeToRefs } from 'pinia';
|
||||
import { usePageStore } from '../../stores/page';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
dayjs.locale('fr');
|
||||
|
||||
const { t } = useI18n();
|
||||
const { comment, commentIndex } = defineProps({
|
||||
comment: Object,
|
||||
commentIndex: Number,
|
||||
|
|
@ -125,11 +125,11 @@ function formatDate() {
|
|||
const dateNumber = parseInt(dayjs(comment.date).format('YYMMD'));
|
||||
|
||||
if (dateNumber === todayNumber) {
|
||||
return t('dates.today');
|
||||
return "Aujourd'hui";
|
||||
}
|
||||
|
||||
if (dateNumber === todayNumber - 1) {
|
||||
return t('dates.yesterday');
|
||||
return 'hier';
|
||||
}
|
||||
|
||||
return dayjs(comment.date).format('D MMM YY');
|
||||
|
|
@ -153,7 +153,7 @@ async function read() {
|
|||
page.value.uri
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(t('errors.readNotificationFailed'), error);
|
||||
console.log('Erreur lors de la lecture de la notification : ', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
<template>
|
||||
<aside id="comments-container" aria-labelledby="comments-label">
|
||||
<h2 id="comments-label" class="sr-only">{{ t('comments.title') }}</h2>
|
||||
<h2 id="comments-label" class="sr-only">Commentaires</h2>
|
||||
<div
|
||||
class="comments | flow"
|
||||
:class="{ empty: !comments || comments.length === 0 }"
|
||||
:data-empty-message="t('comments.emptyMessage')"
|
||||
>
|
||||
<template v-if="comments">
|
||||
<template v-if="!openedComment">
|
||||
|
|
@ -27,7 +26,7 @@
|
|||
isAddOpen = false;
|
||||
"
|
||||
>
|
||||
<span>{{ t('buttons.backToList') }}</span>
|
||||
<span>Retour à la liste</span>
|
||||
</button>
|
||||
<Comment
|
||||
:comment="openedComment"
|
||||
|
|
@ -54,7 +53,7 @@
|
|||
class="btn btn--white-20 | w-full"
|
||||
@click="toggleCommentPositionMode(true)"
|
||||
>
|
||||
{{ t('buttons.addComment') }}
|
||||
Ajouter un commentaire
|
||||
</button>
|
||||
<button
|
||||
v-else-if="openedComment && !isAddOpen"
|
||||
|
|
@ -62,7 +61,7 @@
|
|||
class="btn btn--white-20 | justify-start w-full | text-white-50"
|
||||
@click="isAddOpen = true"
|
||||
>
|
||||
{{ t('buttons.reply') }}
|
||||
Répondre…
|
||||
</button>
|
||||
<!-- TODO: afficher #new-comment une fois le bouton Ajouter un commentaire cliqué -->
|
||||
<div
|
||||
|
|
@ -71,10 +70,11 @@
|
|||
class="bg-primary | text-sm text-white | rounded-lg | p-12"
|
||||
>
|
||||
<p class="flex justify-start | mb-12" data-icon="comment">
|
||||
<strong>{{ t('comments.new') }}</strong>
|
||||
<strong>Nouveau commentaire</strong>
|
||||
</p>
|
||||
<p>
|
||||
{{ t('comments.newInstruction') }}
|
||||
Dans la zone du contenu, cliquez où vous souhaitez positionner le
|
||||
commentaire
|
||||
</p>
|
||||
</div>
|
||||
<form
|
||||
|
|
@ -84,13 +84,13 @@
|
|||
class="flow | p-12 | rounded-xl"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<label class="sr-only" for="comment">{{ t('comments.your') }}</label>
|
||||
<label class="sr-only" for="comment">Votre commentaire</label>
|
||||
<textarea
|
||||
v-model="draftComment.text"
|
||||
:disabled="isSubmitting ? true : undefined"
|
||||
name="comment"
|
||||
id="comment"
|
||||
:placeholder="t('forms.commentPlaceholder')"
|
||||
placeholder="Ajouter un commentaire…"
|
||||
rows="5"
|
||||
class="text-sm | rounded-lg bg-black p-12"
|
||||
></textarea>
|
||||
|
|
@ -99,11 +99,11 @@
|
|||
type="submit"
|
||||
class="btn"
|
||||
:class="{ submitting: isSubmitting }"
|
||||
:value="isSubmitting ? t('comments.inProgress') : undefined"
|
||||
:value="isSubmitting ? 'En cours' : undefined"
|
||||
:disabled="isSubmitting ? true : undefined"
|
||||
/>
|
||||
<button class="btn btn--white-10" @click="isAddOpen = false">
|
||||
{{ t('buttons.cancel') }}
|
||||
Annuler
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
|
|
@ -122,11 +122,9 @@ import { useDialogStore } from '../../stores/dialog';
|
|||
import Comment from './Comment.vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
dayjs.locale('fr');
|
||||
|
||||
const { t } = useI18n();
|
||||
const { user } = useUserStore();
|
||||
const { page } = usePageStore();
|
||||
const dialog = useDialogStore();
|
||||
|
|
|
|||
|
|
@ -1,16 +1,13 @@
|
|||
<template>
|
||||
<button id="dtl-btn" class="bg-black rounded-md p-4" :title="t('dtl.grade', { grade: hasAlternatives ? t('menu.news') : grade })" data-icon="leaf" :data-grade="grade" :data-new="hasAlternatives ? true : undefined">
|
||||
<span lang="en" class="sr-only">{{ t('dtl.title') }}</span>
|
||||
<span v-if="hasAlternatives" lang="en" class="new">{{ t('menu.news') }}</span>
|
||||
<button id="dtl-btn" class="bg-black rounded-md p-4" :title="'Design to Light: ' + (hasAlternatives ? 'New' : grade)" data-icon="leaf" :data-grade="grade" :data-new="hasAlternatives ? true : undefined">
|
||||
<span lang="en" class="sr-only">Design to Light</span>
|
||||
<span v-if="hasAlternatives" lang="en" class="new">New</span>
|
||||
</button>
|
||||
</template>
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { usePageStore } from "../../stores/page";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { page } = storeToRefs(usePageStore());
|
||||
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
proposals.length === 1 && isDialogOpen
|
||||
? activeProposal.title
|
||||
? activeProposal.title
|
||||
: t('dtl.title')
|
||||
: t('dtl.title')
|
||||
: 'Design to light'
|
||||
: 'Design to light'
|
||||
}}
|
||||
</h2>
|
||||
<button
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
class="btn btn--icon btn--transparent | ml-auto"
|
||||
data-icon="close"
|
||||
>
|
||||
<span class="sr-only">{{ t('buttons.close') }}</span>
|
||||
<span class="sr-only">Fermer</span>
|
||||
</button>
|
||||
</header>
|
||||
<nav v-if="!isDialogOpen" class="tabs" role="tablist" tabindex="-1">
|
||||
|
|
@ -42,8 +42,8 @@
|
|||
proposal.title
|
||||
? proposal.title
|
||||
: index === 0
|
||||
? t('dtl.initialProposal')
|
||||
: t('dtl.alternative', { index })
|
||||
? 'Proposition initiale'
|
||||
: 'Alternative ' + index
|
||||
}}
|
||||
</button>
|
||||
</nav>
|
||||
|
|
@ -67,12 +67,12 @@
|
|||
/>
|
||||
</router-link>
|
||||
<p>
|
||||
{{ t('dtl.proposalBasedOn') }} <br />du {{ activeProposal.date }}
|
||||
<br />{{ stepLabel }}
|
||||
Données basées sur la proposition <br />du {{ activeProposal.date }}
|
||||
<br />{{ activeProposal.stepLabel }}
|
||||
</p>
|
||||
</div>
|
||||
<div id="note-globale" class="px-32 py-16 border-b flow">
|
||||
<h3>{{ t('dtl.globalScore') }}</h3>
|
||||
<h3>Note globale</h3>
|
||||
<div class="flex" style="--column-gap: 1rem">
|
||||
<p :data-grade="activeProposal.grades.global.letter">
|
||||
<strong class="sr-only">{{
|
||||
|
|
@ -100,15 +100,15 @@
|
|||
</div>
|
||||
</div>
|
||||
<div id="positionnement" class="px-32 py-16 border-b flow">
|
||||
<h3>{{ t('dtl.positioning') }}</h3>
|
||||
<h3>Positionnement</h3>
|
||||
<dl>
|
||||
<dt id="design">{{ t('dtl.design') }}</dt>
|
||||
<dt id="design">Design</dt>
|
||||
<dd>
|
||||
<span class="sr-only">{{
|
||||
activeProposal.grades.position.complexity
|
||||
}}</span>
|
||||
</dd>
|
||||
<dt id="poids">{{ t('dtl.weight') }}</dt>
|
||||
<dt id="poids">Poids</dt>
|
||||
<dd>
|
||||
<span class="sr-only">{{
|
||||
activeProposal.grades.position.weight
|
||||
|
|
@ -132,7 +132,7 @@
|
|||
</button>
|
||||
</div>
|
||||
<div id="indicateur" class="px-32 py-16 border-b flow">
|
||||
<h3>{{ t('dtl.indicators') }}</h3>
|
||||
<h3>Indicateur des composants impliqués</h3>
|
||||
<div class="grid">
|
||||
<template
|
||||
v-for="indicator in activeProposal.grades.indicators"
|
||||
|
|
@ -170,8 +170,8 @@
|
|||
>
|
||||
{{
|
||||
page.hasOptimizationRequest
|
||||
? t('dtl.requestPending')
|
||||
: t('dtl.requestOptimization')
|
||||
? "Demande d'expertise en cours de traitement…"
|
||||
: 'Demander une expertise d’optimisation'
|
||||
}}
|
||||
</button>
|
||||
</footer>
|
||||
|
|
@ -190,7 +190,6 @@ import { storeToRefs } from 'pinia';
|
|||
import { ref, onBeforeUnmount, computed } from 'vue';
|
||||
import { useDialogStore } from '../../stores/dialog';
|
||||
import { usePageStore } from '../../stores/page';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { proposals } = defineProps({
|
||||
proposals: Array,
|
||||
|
|
@ -198,7 +197,6 @@ const { proposals } = defineProps({
|
|||
|
||||
const { page } = storeToRefs(usePageStore());
|
||||
const { openedFile, activeTracks } = storeToRefs(useDialogStore());
|
||||
const { t } = useI18n();
|
||||
|
||||
const isDialogOpen = computed(() => {
|
||||
if (openedFile.value) {
|
||||
|
|
@ -217,17 +215,6 @@ const emits = defineEmits(['close']);
|
|||
const activeProposal =
|
||||
proposals.length === 1 ? computed(() => proposals[0]) : ref(proposals[0]);
|
||||
|
||||
const stepLabel = computed(() => {
|
||||
const proposal = activeProposal.value || activeProposal;
|
||||
const location = proposal.location;
|
||||
|
||||
if (location.step === 'virtualSample') {
|
||||
return location.type === 'dynamic' ? t('dtl.dynamicTrack') : t('dtl.staticTrack');
|
||||
}
|
||||
|
||||
return t('steps.' + location.step);
|
||||
});
|
||||
|
||||
window.addEventListener('keyup', closeOnEscape);
|
||||
window.addEventListener('click', close);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,15 +5,15 @@
|
|||
modal
|
||||
:draggable="false"
|
||||
:dismissableMask="true"
|
||||
:header="t('dialogs.requestMeeting')"
|
||||
header="Demander un rendez-vous"
|
||||
class="dialog"
|
||||
:closeOnEscape="true"
|
||||
@click="preventClose($event)"
|
||||
>
|
||||
<template #header>
|
||||
<h2 class="font-serif text-lg">{{ t('dialogs.requestMeeting') }}</h2>
|
||||
<h2 class="font-serif text-lg">Demander un rendez-vous</h2>
|
||||
<p class="flex justify-center text-sm" data-icon="leaf">
|
||||
{{ t('dtl.title') }}
|
||||
Design to Light
|
||||
</p>
|
||||
</template>
|
||||
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
class="w-full h-full p-16 flex flex-col"
|
||||
style="--row-gap: 1rem"
|
||||
>
|
||||
<label for="projects" class="sr-only">{{ t('brief.projects') }}</label>
|
||||
<label for="projects" class="sr-only">Projet</label>
|
||||
<select
|
||||
name="projects"
|
||||
id="projects"
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
class="w-full rounded-md border border-grey-200 px-16 py-12"
|
||||
required
|
||||
>
|
||||
<option value="">{{ t('forms.selectVariation') }}</option>
|
||||
<option value="">Sélectionnez le projet</option>
|
||||
<option
|
||||
v-for="project in currentProjects"
|
||||
:key="project.uri"
|
||||
|
|
@ -41,37 +41,35 @@
|
|||
</option>
|
||||
</select>
|
||||
|
||||
<label for="appointment-subject" class="sr-only">{{
|
||||
t('forms.meetingSubject')
|
||||
}}</label>
|
||||
<label for="appointment-subject" class="sr-only"
|
||||
>Objet du rendez-vous</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
v-model="subject"
|
||||
id="appointment-subject"
|
||||
:placeholder="t('forms.meetingSubject')"
|
||||
placeholder="Objet du rendez-vous"
|
||||
class="w-full rounded-md border border-grey-200 px-16 py-12"
|
||||
required
|
||||
/>
|
||||
|
||||
<label for="appointment-details" class="sr-only">{{
|
||||
t('forms.meetingDetails')
|
||||
}}</label>
|
||||
<label for="appointment-details" class="sr-only">Détails du projet</label>
|
||||
<textarea
|
||||
id="appointment-details"
|
||||
name="details"
|
||||
v-model="details"
|
||||
cols="30"
|
||||
rows="10"
|
||||
:placeholder="t('forms.meetingDetailsPlaceholder')"
|
||||
placeholder="Décrivez votre demande…"
|
||||
class="w-full flex-1 rounded-md border border-grey-200 px-16 py-12"
|
||||
required
|
||||
></textarea>
|
||||
|
||||
<footer class="flex-columns w-full mt-16" style="column-gap: 0.5rem">
|
||||
<button class="btn btn--black-10" @click="emits('close')">
|
||||
{{ t('buttons.cancel') }}
|
||||
Annuler
|
||||
</button>
|
||||
<button class="btn" type="submit">{{ t('buttons.submit') }}</button>
|
||||
<button class="btn" type="submit">Soumettre</button>
|
||||
</footer>
|
||||
</form>
|
||||
</Dialog>
|
||||
|
|
@ -84,9 +82,6 @@ import { storeToRefs } from 'pinia';
|
|||
import { useProjectsStore } from '../../stores/projects';
|
||||
import { usePageStore } from '../../stores/page';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { page } = storeToRefs(usePageStore());
|
||||
const { currentProjects } = storeToRefs(useProjectsStore());
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<!-- Favorite button -->
|
||||
<button
|
||||
class="favorite"
|
||||
:aria-label="isFavorite ? t('inspirations.removeFromFavorites') : t('inspirations.addToFavorites')"
|
||||
:aria-label="isFavorite ? 'Retirer des favoris' : 'Ajouter aux favoris'"
|
||||
:aria-pressed="isFavorite"
|
||||
@click="toggleFavorite"
|
||||
>
|
||||
|
|
@ -34,9 +34,6 @@
|
|||
import { computed } from "vue";
|
||||
import { useUserStore } from "../../stores/user";
|
||||
import { useApiStore } from "../../stores/api";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// Props
|
||||
const { item, inspirationUri } = defineProps({
|
||||
|
|
@ -73,7 +70,7 @@ async function toggleFavorite() {
|
|||
// Update item favorite users list based on API response
|
||||
item.favoriteForUsers = newFavoriteUsers;
|
||||
} catch (error) {
|
||||
console.error(t('errors.toggleFavoriteFailed'), error);
|
||||
console.error("Failed to toggle favorite:", error);
|
||||
isFavorite.value = previousState; // Rollback on failure
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@
|
|||
<strong
|
||||
class="notification__type | font-medium text-primary"
|
||||
data-icon="calendar"
|
||||
>{{ t('notifications.meetingRequest') }}</strong
|
||||
>Demande de rendez-vous</strong
|
||||
>
|
||||
<span class="notification__client | text-grey-700"
|
||||
>{{ notification.project.title }}
|
||||
{{
|
||||
notification.project.status === "draft" ? t('notifications.draft') : ""
|
||||
notification.project.status === "draft" ? "(brouillon)" : ""
|
||||
}}</span
|
||||
>
|
||||
<time
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
v-if="notification.type"
|
||||
class="notification__body | text-md font-medium | line-clamp"
|
||||
v-html="
|
||||
t('notifications.author') + ' ' +
|
||||
'Auteur : ' +
|
||||
(notification.author.name
|
||||
? notification.author.name + ' (' + notification.author.email + ')'
|
||||
: notification.author.email) +
|
||||
|
|
@ -44,9 +44,6 @@
|
|||
import { useNotificationsStore } from "../../stores/notifications";
|
||||
import { useUserStore } from "../../stores/user";
|
||||
import { useApiStore } from "../../stores/api";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { notification } = defineProps({ notification: Object });
|
||||
const { formatDate } = useNotificationsStore();
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@
|
|||
class="notification | bg-white rounded-lg | p-16 | flow"
|
||||
data-type="content"
|
||||
@click="readNotification()"
|
||||
:title="t('notifications.goToContent')"
|
||||
title="Aller au contenu"
|
||||
>
|
||||
<header>
|
||||
<p class="flex">
|
||||
<strong
|
||||
class="notification__type | font-medium text-primary"
|
||||
data-icon="content"
|
||||
>{{ t('notifications.content') }}</strong
|
||||
>Contenu</strong
|
||||
>
|
||||
<span class="notification__client | text-grey-700">{{
|
||||
notification.project.title
|
||||
|
|
@ -36,9 +36,6 @@ import { useRouter } from "vue-router";
|
|||
import { useNotificationsStore } from "../../stores/notifications";
|
||||
import { useUserStore } from "../../stores/user";
|
||||
import { useApiStore } from "../../stores/api";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const router = useRouter();
|
||||
const { notification } = defineProps({ notification: Object });
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@
|
|||
<strong
|
||||
class="notification__type | font-medium text-primary"
|
||||
data-icon="document"
|
||||
>{{ t('notifications.projectRequest') }}</strong
|
||||
>Demande de création de projet</strong
|
||||
>
|
||||
<span class="notification__client | text-grey-700"
|
||||
>{{ notification.project.title }}
|
||||
{{
|
||||
notification.project.status === "draft" ? t('notifications.draft') : ""
|
||||
notification.project.status === "draft" ? "(brouillon)" : ""
|
||||
}}</span
|
||||
>
|
||||
<time
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
v-if="notification.type"
|
||||
class="notification__body | text-md font-medium | line-clamp"
|
||||
v-html="
|
||||
t('notifications.from') + ' ' +
|
||||
'De la part de ' +
|
||||
(notification.author.name
|
||||
? notification.author.name + ' (' + notification.author.email + ')'
|
||||
: notification.author.email) +
|
||||
|
|
@ -44,9 +44,6 @@
|
|||
import { useNotificationsStore } from "../../stores/notifications";
|
||||
import { useUserStore } from "../../stores/user";
|
||||
import { useApiStore } from "../../stores/api";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { notification } = defineProps({ notification: Object });
|
||||
const { formatDate } = useNotificationsStore();
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@
|
|||
data-icon="comment"
|
||||
@click="isCommentsOpen = !isCommentsOpen"
|
||||
>
|
||||
<span class="sr-only">{{
|
||||
isCommentsOpen ? t('buttons.hideComments') : t('buttons.showComments')
|
||||
}}</span>
|
||||
<span class="sr-only"
|
||||
>{{ isCommentsOpen ? 'Masquer' : 'Afficher' }} les commentaires</span
|
||||
>
|
||||
</button>
|
||||
<a
|
||||
id="download-pdf"
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
:href="openedFile.url"
|
||||
download
|
||||
>
|
||||
<span class="sr-only">{{ t('buttons.downloadPdf') }}</span>
|
||||
<span class="sr-only">Télécharger le fichier PDF</span>
|
||||
</a>
|
||||
<Comments v-if="isCommentsOpen" />
|
||||
</template>
|
||||
|
|
@ -36,9 +36,6 @@ import { ref, watch, computed, unref } from 'vue';
|
|||
import { useDialogStore } from '../../stores/dialog';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { VPdfViewer, useLicense } from '@vue-pdf-viewer/viewer';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const licenseKey =
|
||||
import.meta.env.VITE_VPV_LICENSE ??
|
||||
|
|
|
|||
|
|
@ -15,13 +15,12 @@
|
|||
</router-link>
|
||||
</h3>
|
||||
<p>
|
||||
{{ t('dates.updatedOn') }}
|
||||
<time :datetime="project.modified">{{ formattedModified }}</time>
|
||||
Dernière mise à jour le
|
||||
<time :datetime="project.modified">{{ frenchFormattedModified }}</time>
|
||||
</p>
|
||||
</hgroup>
|
||||
<img :src="project.logo" alt="Logo" class="project-logo | rounded-sm" />
|
||||
<ol
|
||||
v-if="Array.isArray(project.steps)"
|
||||
class="project-steps"
|
||||
:data-steps="project.steps.length"
|
||||
:style="'--steps:' + project.steps.length"
|
||||
|
|
@ -32,7 +31,7 @@
|
|||
:data-status="setStatus(project.steps, project.currentStep, step)"
|
||||
>
|
||||
<span class="pill" :data-icon="step.id">
|
||||
<span>{{ t('steps.' + step.id) }}</span>
|
||||
<span>{{ step.label }}</span>
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
|
|
@ -42,17 +41,15 @@
|
|||
<script setup>
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/fr';
|
||||
import 'dayjs/locale/en';
|
||||
import { computed } from 'vue';
|
||||
import { useProjectStore } from '../../stores/project';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
dayjs.locale('fr');
|
||||
|
||||
const { project } = defineProps({ project: Object });
|
||||
|
||||
const { stepsLabels, setStatus, isEmptyBrief } = useProjectStore();
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
const formattedModified = computed(() =>
|
||||
dayjs(project.modified).locale(locale.value).format('dddd D MMMM YYYY')
|
||||
const frenchFormattedModified = dayjs(project.modified).format(
|
||||
'dddd D MMMM YYYY'
|
||||
);
|
||||
|
||||
const { stepsLabels, setStatus, isEmptyBrief } = useProjectStore();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
:data-status="setStatus(page.steps, page.content.currentstep, step)"
|
||||
>
|
||||
<h2 :id="step.id">
|
||||
<span :data-icon="step.id">{{ t('steps.' + step.id) }}</span>
|
||||
<span :data-icon="step.id">{{ step.label }}</span>
|
||||
</h2>
|
||||
<div
|
||||
ref="cards-node"
|
||||
|
|
@ -23,22 +23,21 @@ import 'dayjs/locale/fr';
|
|||
import { usePageStore } from '../../stores/page';
|
||||
import { computed, onMounted, useTemplateRef } from 'vue';
|
||||
import { useProjectStore } from '../../stores/project';
|
||||
import Brief from './cards/Brief.vue';
|
||||
import ClientBrief from './cards/ClientBrief.vue';
|
||||
import MultipleDocuments from './cards/MultipleDocuments.vue';
|
||||
import SimpleDocument from './cards/SimpleDocument.vue';
|
||||
import VirtualSample from './cards/VirtualSample.vue';
|
||||
import PhysicalSample from './cards/PhysicalSample.vue';
|
||||
import { useUserStore } from '../../stores/user';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { step } = defineProps({
|
||||
step: Object,
|
||||
});
|
||||
|
||||
const cardsMap = {
|
||||
clientBrief: Brief,
|
||||
clientBrief: ClientBrief,
|
||||
proposal: MultipleDocuments,
|
||||
extendedBrief: Brief,
|
||||
extendedBrief: SimpleDocument,
|
||||
industrialIdeation: SimpleDocument,
|
||||
virtualSample: VirtualSample,
|
||||
physicalSample: PhysicalSample,
|
||||
|
|
@ -50,7 +49,6 @@ const { page } = usePageStore();
|
|||
const { setStatus } = useProjectStore();
|
||||
const cardsNode = useTemplateRef('cards-node');
|
||||
const { user } = useUserStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
// Hooks
|
||||
onMounted(() => {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
modal
|
||||
:draggable="false"
|
||||
:dismissableMask="true"
|
||||
:header="t('dialogs.pdfTitle')"
|
||||
header="Titre du PDF"
|
||||
class="dialog"
|
||||
:class="[
|
||||
{ 'with-comments': isCommentsOpen },
|
||||
|
|
@ -16,13 +16,13 @@
|
|||
<template #header>
|
||||
<button
|
||||
v-if="
|
||||
['clientBrief', 'extendedBrief'].includes(dialog.content.id) &&
|
||||
dialog.content.id === 'clientBrief' &&
|
||||
dialog.content.isValidated !== true
|
||||
"
|
||||
class="btn"
|
||||
@click="validate()"
|
||||
>
|
||||
{{ t('buttons.validate') }}
|
||||
Valider et envoyer le brief
|
||||
</button>
|
||||
<h2
|
||||
v-if="openedFile"
|
||||
|
|
@ -52,9 +52,7 @@ import { useRoute, useRouter } from 'vue-router';
|
|||
import { storeToRefs } from 'pinia';
|
||||
import { useApiStore } from '../../stores/api';
|
||||
import { usePageStore } from '../../stores/page';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { openedFile, isCommentsOpen } = storeToRefs(useDialogStore());
|
||||
|
||||
const router = useRouter();
|
||||
|
|
@ -93,7 +91,7 @@ const correspondingDTLProposal = computed(() => {
|
|||
// Functions
|
||||
async function validate() {
|
||||
const response = await api.validateBrief(
|
||||
route.path + '/' + dialog.content.slug,
|
||||
route.path + '/client-brief',
|
||||
route.fullPath
|
||||
);
|
||||
if (response.success) {
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@
|
|||
<label
|
||||
for="project-description"
|
||||
class="flex | text-sm text-grey-700 | mb-8"
|
||||
>{{ t('forms.description') }}</label
|
||||
>Description du projet</label
|
||||
>
|
||||
<textarea
|
||||
name="project-description"
|
||||
id="project-description"
|
||||
:placeholder="t('forms.descriptionPlaceholder')"
|
||||
placeholder="Ajoutez une description générale de votre projet…"
|
||||
rows="2"
|
||||
class="border border-grey-200 | rounded-xl | p-16 | w-full"
|
||||
v-model="page.content.description"
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
></textarea>
|
||||
</div>
|
||||
<fieldset class="project-details__filters | flex-1">
|
||||
<legend class="text-sm text-grey-700 | mb-8">{{ t('forms.filterByTags') }}</legend>
|
||||
<legend class="text-sm text-grey-700 | mb-8">Filtrer par tags</legend>
|
||||
<div class="flex" style="gap: var(--space-8)">
|
||||
<button
|
||||
class="btn btn--sm btn--grey"
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
role="switch"
|
||||
@click="clearTags()"
|
||||
>
|
||||
{{ t('buttons.seeAll') }}
|
||||
Voir tout
|
||||
</button>
|
||||
<template v-for="tag in page.tags" :key="tag">
|
||||
<input
|
||||
|
|
@ -53,9 +53,6 @@ import { ref, watch } from "vue";
|
|||
import { usePageStore } from "../../../stores/page";
|
||||
import StringUtils from "../../../utils/string";
|
||||
import debounce from "lodash/debounce";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { selectedTags } = defineProps({
|
||||
selectedTags: Array,
|
||||
|
|
@ -96,7 +93,7 @@ const saveDescription = debounce(() => {
|
|||
console.log(json);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(t('errors.saveFailed'), error);
|
||||
console.error("Erreur lors de la sauvegarde :", error);
|
||||
isWaitingForSave.value = false;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
v-model:visible="isOpen"
|
||||
id="image-details"
|
||||
modal
|
||||
:header="t('dialogs.imageDetails')"
|
||||
header="Détails de l’image"
|
||||
class="bg-white | text-grey-800 | rounded-2xl | overflow-hidden"
|
||||
>
|
||||
<picture :style="'--image: url('+image.url+')'">
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
</picture>
|
||||
<div class="flex flex-col | p-32" style="--row-gap: var(--space-32)">
|
||||
<fieldset class="image__tags">
|
||||
<legend class="text-sm text-grey-700 | mb-8">{{ t('forms.tags') }}</legend>
|
||||
<legend class="text-sm text-grey-700 | mb-8">Tags</legend>
|
||||
<div class="flex" style="gap: var(--space-8)">
|
||||
<template v-for="(pageTag, index) in page.tags" :key="index">
|
||||
<input
|
||||
|
|
@ -33,12 +33,12 @@
|
|||
<label
|
||||
for="image-description"
|
||||
class="flex | text-sm text-grey-700 | mb-8"
|
||||
>{{ t('forms.imageDescription') }}</label
|
||||
>Description de l’image</label
|
||||
>
|
||||
<textarea
|
||||
name="image-description"
|
||||
id="image-description"
|
||||
:placeholder="t('forms.imageDescriptionPlaceholder')"
|
||||
placeholder="Ajoutez une description à l’image…"
|
||||
class="border border-grey-200 | rounded-xl | p-16 | w-full"
|
||||
v-model="image.description"
|
||||
@input="saveDescription()"
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
class="btn btn--black-10 | ml-auto mt-auto"
|
||||
@click="remove()"
|
||||
>
|
||||
{{ t('buttons.deleteImage') }}
|
||||
Supprimer cette image
|
||||
</button>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
|
@ -61,9 +61,6 @@ import { usePageStore } from "../../../stores/page";
|
|||
import StringUtils from "../../../utils/string";
|
||||
import Dialog from "primevue/dialog";
|
||||
import debounce from "lodash/debounce";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { imageDetails } = defineProps({
|
||||
imageDetails: Object,
|
||||
|
|
@ -100,7 +97,7 @@ function saveTags() {
|
|||
console.log(json);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(t('errors.saveFailed'), error);
|
||||
console.error("Erreur lors de la sauvegarde :", error);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -125,7 +122,7 @@ const saveDescription = debounce(() => {
|
|||
emit("");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(t('errors.saveFailed'), error);
|
||||
console.error("Erreur lors de la sauvegarde :", error);
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
|
|
@ -145,7 +142,7 @@ function remove() {
|
|||
isOpen.value = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(t('errors.deleteFailed'), error);
|
||||
console.error("Erreur lors de la suppression :", error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
class="flex flex-col | bg-white | border border-grey-200 | text-grey-800 | font-medium | rounded-2xl"
|
||||
@click="isAddImagesModalOpen = true"
|
||||
>
|
||||
{{ t('forms.addImages') }}
|
||||
Ajouter une ou plusieurs images
|
||||
</button>
|
||||
<template v-for="image in page.moodboard" :key="image.uri">
|
||||
<figure
|
||||
|
|
@ -52,13 +52,11 @@
|
|||
import Header from "./Header.vue";
|
||||
import { usePageStore } from "../../../stores/page";
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import ImageDetailsModal from "./ImageDetailsModal.vue";
|
||||
import AddImagesModal from "./add-images-modal/AddImagesModal.vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
const { page } = storeToRefs(usePageStore());
|
||||
const { t } = useI18n();
|
||||
|
||||
const selectedTags = ref([]);
|
||||
const imageDetails = ref(null);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
:draggable="false"
|
||||
:dismissableMask="true"
|
||||
modal
|
||||
:header="t('dialogs.addImages')"
|
||||
header="Ajouter des images"
|
||||
class="bg-white | text-grey-800 | rounded-2xl | overflow-hidden | p-32"
|
||||
>
|
||||
<div class="with-sidebar | h-full">
|
||||
|
|
@ -56,18 +56,19 @@
|
|||
id="delete-image"
|
||||
v-model:visible="deleteIsOpen"
|
||||
modal
|
||||
:header="t('dialogs.deleteConfirm')"
|
||||
header="Êtes-vous sûr de vouloir supprimer cette image ?"
|
||||
class="bg-white | text-grey-800 | rounded-2xl | overflow-hidden | text-center | w-full max-w | p-16 pt-32"
|
||||
style="--row-gap: var(--space-32); --max-w: 40rem"
|
||||
>
|
||||
<p class="text-grey-700 | px-16">
|
||||
{{ t('dialogs.deleteWarning') }}
|
||||
Si vous supprimez cette image, celle-ci disparaîtra de votre brief ainsi
|
||||
que toutes les informations qui lui sont attribuées.
|
||||
</p>
|
||||
<template #footer>
|
||||
<button class="btn btn--secondary | flex-1" @click="deleteIsOpen = false">
|
||||
{{ t('buttons.cancel') }}
|
||||
Annuler
|
||||
</button>
|
||||
<button class="btn | flex-1" @click="">{{ t('buttons.delete') }}</button>
|
||||
<button class="btn | flex-1" @click="">Supprimer</button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
|
@ -78,9 +79,6 @@ import ImagesEditPanel from './ImagesEditPanel.vue';
|
|||
import { ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useAddImagesModalStore } from '../../../../stores/addImagesModal';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { isAddImagesModalOpen } = defineProps({
|
||||
isAddImagesModalOpen: Boolean,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
<textarea
|
||||
name="image-description"
|
||||
id="image-description"
|
||||
:placeholder="t('forms.imageDescriptionPlaceholder')"
|
||||
placeholder="Ajoutez une description à cette image…"
|
||||
rows="3"
|
||||
class="border border-grey-200 | rounded-xl | p-16 | w-full"
|
||||
v-model="image.description"
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
</div>
|
||||
<fieldset class="image-details__filters | flex-1">
|
||||
<legend class="text-sm text-grey-700 | mb-8">
|
||||
{{ t('forms.selectTags') }}
|
||||
Sélectionner un ou plusieurs tags
|
||||
</legend>
|
||||
<div class="flex" style="gap: var(--space-8)">
|
||||
<template v-for="tag in page.tags" :key="tag">
|
||||
|
|
@ -95,7 +95,7 @@
|
|||
</template>
|
||||
</Accordion>
|
||||
<button class="btn | w-full | mt-auto" @click="addImagesToBrief()">
|
||||
{{ t('buttons.addSelectedImages') }}
|
||||
Ajouter les images sélectionnées
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -109,10 +109,8 @@ import StringUtils from "../../../../utils/string";
|
|||
import { storeToRefs } from "pinia";
|
||||
import { useAddImagesModalStore } from "../../../../stores/addImagesModal";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { page } = storeToRefs(usePageStore());
|
||||
const { t } = useI18n();
|
||||
|
||||
const { activeTab } = storeToRefs(useAddImagesModalStore());
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
:multiple="true"
|
||||
accept="image/*"
|
||||
:maxFileSize="1000000"
|
||||
:invalidFileSizeMessage="t('errors.saveFailed')"
|
||||
:chooseLabel="t('forms.addImages')"
|
||||
invalidFileSizeMessage="Fichier trop lourd"
|
||||
chooseLabel="Ajouter une ou plusieurs images"
|
||||
class="flex flex-col justify-center | bg-white | border border-grey-200 | text-grey-800 | font-medium | rounded-xl"
|
||||
ref="uploadBtn"
|
||||
>
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
removeFileCallback,
|
||||
}"
|
||||
>
|
||||
<div v-if="files.length > 0">{{ t('forms.uploadedFiles') }}</div>
|
||||
<div v-if="files.length > 0">Fichiers importés</div>
|
||||
</template>
|
||||
</FileUpload>
|
||||
<figure
|
||||
|
|
@ -71,9 +71,6 @@ import { storeToRefs } from "pinia";
|
|||
import { computed, ref } from "vue";
|
||||
import { useAddImagesModalStore } from "../../../../stores/addImagesModal";
|
||||
import ArrayUtils from "../../../../utils/array";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { page } = storeToRefs(usePageStore());
|
||||
const toast = useToast();
|
||||
|
|
|
|||
|
|
@ -3,20 +3,20 @@
|
|||
v-if="images.length > 0"
|
||||
:step="step"
|
||||
:images="images"
|
||||
:uri="addLocalePrefix(step.uri)"
|
||||
:uri="'/' + step.uri"
|
||||
/>
|
||||
<Document v-if="pdf" :step="step" :pdf="pdf" />
|
||||
|
||||
<button
|
||||
v-if="images.length === 0 && step.id === 'clientBrief'"
|
||||
v-if="images.length === 0"
|
||||
class="btn | w-full"
|
||||
@click="goToImagesBrief()"
|
||||
>
|
||||
{{ t('brief.addPlatform') }}
|
||||
Ajouter un brief via la plateforme
|
||||
</button>
|
||||
<div class="btn | w-full" v-if="!pdf && step.id === 'clientBrief'">
|
||||
<div class="btn | w-full" v-if="!pdf">
|
||||
<label for="upload-pdf">
|
||||
{{ t('brief.addPdf') }}
|
||||
Ajouter un brief PDF
|
||||
<input
|
||||
id="upload-pdf"
|
||||
type="file"
|
||||
|
|
@ -35,13 +35,10 @@ import Images from "./Images.vue";
|
|||
import Document from "./Document.vue";
|
||||
import { useBriefStore } from "../../../stores/brief";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { addLocalePrefix } from "../../../utils/router";
|
||||
|
||||
const { step } = defineProps({ step: Object });
|
||||
const { addPdf } = useBriefStore();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
const images = computed(() => {
|
||||
return step.files.filter((file) => file.type === "image");
|
||||
|
|
@ -52,6 +49,6 @@ const pdf = computed(() => {
|
|||
});
|
||||
|
||||
function goToImagesBrief() {
|
||||
router.push(location.pathname + "/" + step.slug);
|
||||
router.push(location.pathname + "/client-brief");
|
||||
}
|
||||
</script>
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
<time
|
||||
class="card__date | text-grey-700"
|
||||
:datetime="dayjs(date).format('YYYY-M-DD')"
|
||||
>{{ dayjs(date).locale(locale).format("DD MMMM YYYY") }}</time
|
||||
>{{ dayjs(date).format("DD MMMM YYYY") }}</time
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -11,9 +11,6 @@
|
|||
<script setup>
|
||||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/fr";
|
||||
import "dayjs/locale/en";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { date } = defineProps({ date: String });
|
||||
const { locale } = useI18n();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
v-if="pdf.comments?.length > 0"
|
||||
class="order-last | text-sm text-primary font-medium"
|
||||
>
|
||||
<router-link :to="addLocalePrefix(step.uri) + '&comments=true'">
|
||||
<router-link :to="'/' + step.uri + '&comments=true'">
|
||||
{{ pdf.comments.length }} commentaire{{
|
||||
pdf.comments.length > 1 ? "s" : ""
|
||||
}}
|
||||
|
|
@ -39,7 +39,6 @@ import { useRoute } from "vue-router";
|
|||
import DateTime from "./DateTime.vue";
|
||||
import { computed } from "vue";
|
||||
import { useDesignToLightStore } from "../../../stores/designToLight";
|
||||
import { addLocalePrefix } from "../../../utils/router";
|
||||
|
||||
const { step, pdf, index } = defineProps({
|
||||
step: Object,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<article class="card">
|
||||
<hgroup class="order-last">
|
||||
<h3 class="card__title | font-serif | text-lg">
|
||||
<router-link :to="uri" class="link-full">{{ t('steps.' + step.id) }}</router-link>
|
||||
<router-link :to="uri" class="link-full">{{ step.label }}</router-link>
|
||||
</h3>
|
||||
</hgroup>
|
||||
<DateTime :date="step.modified" />
|
||||
|
|
@ -42,7 +42,6 @@ import DateTime from './DateTime.vue';
|
|||
import { useDesignToLightStore } from '../../../stores/designToLight';
|
||||
import { useVirtualSampleStore } from '../../../stores/virtualSample';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { images, step, uri } = defineProps({
|
||||
images: Array,
|
||||
|
|
@ -52,7 +51,6 @@ const { images, step, uri } = defineProps({
|
|||
|
||||
const { isDesignToLightStep } = useDesignToLightStore();
|
||||
const { allVariations } = useVirtualSampleStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const commentsCount = computed(() => {
|
||||
let count = 0;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
:style="'--cover: url(' + step.cover + ')'"
|
||||
>
|
||||
<h3 class="text-lg font-serif">
|
||||
<router-link :to="addLocalePrefix(step.uri)" class="link-full">{{
|
||||
<router-link :to="'/' + step.uri" class="link-full">{{
|
||||
step.title
|
||||
}}</router-link>
|
||||
</h3>
|
||||
|
|
@ -27,7 +27,6 @@
|
|||
<script setup>
|
||||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/fr";
|
||||
import { addLocalePrefix } from "../../../utils/router";
|
||||
|
||||
const { step } = defineProps({ step: Object });
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import Images from './Images.vue';
|
|||
import { computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useVirtualSampleStore } from '../../../stores/virtualSample';
|
||||
import { addLocalePrefix } from '../../../utils/router';
|
||||
|
||||
const { step } = defineProps({ step: Object });
|
||||
|
||||
|
|
@ -25,23 +24,20 @@ const images = computed(() => {
|
|||
},
|
||||
];
|
||||
}
|
||||
return allVariations.value.map((variation) => getFrontView(variation)).filter(Boolean) ?? [];
|
||||
return allVariations.value.map((variation) => getFrontView(variation)) ?? [];
|
||||
});
|
||||
|
||||
const uri = addLocalePrefix(step.uri);
|
||||
const uri = '/' + step.uri;
|
||||
|
||||
function getFrontView(variation) {
|
||||
if (variation.files.length === 1) return variation.files[0];
|
||||
const xMax = parseInt(
|
||||
variation.files[variation.files.length - 1].name.split('_')[1].split('.')[0]
|
||||
);
|
||||
const xFrontView = Math.round((xMax + 1) / 2);
|
||||
const xFrontView = (xMax + 1) / 2;
|
||||
const extension = variation.files[0].name.split('.')[1];
|
||||
const frontViewName = '0_' + xFrontView + '.' + extension;
|
||||
const frontView = variation.files.find((file) => file.name === frontViewName);
|
||||
if (!frontView) {
|
||||
console.warn(`[VirtualSample] Front view "${frontViewName}" not found in variation "${variation.title}", falling back to first file.`);
|
||||
}
|
||||
return frontView ?? variation.files[0];
|
||||
return frontView;
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@
|
|||
>
|
||||
<span>{{
|
||||
isCompareModeEnabled
|
||||
? t('buttons.exitCompare')
|
||||
: t('buttons.compareTracks')
|
||||
? 'Quitter le mode comparer'
|
||||
: 'Comparer les pistes'
|
||||
}}</span>
|
||||
</button>
|
||||
</header>
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
:backgroundColor="activeTrack.backgroundColor"
|
||||
/>
|
||||
<div v-else class="track-empty | bg-white rounded-xl w-full p-32">
|
||||
<p>{{ t('virtualSample.noContent') }}</p>
|
||||
<p>Contenu non disponible pour cette piste</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
v-if="isCompareModeEnabled && activeTracks.length < 2"
|
||||
class="track-empty | bg-white rounded-xl w-full p-32"
|
||||
>
|
||||
<p>{{ t('virtualSample.selectToCompare') }}</p>
|
||||
<p>Sélectionnez sur la piste que vous souhaitez comparer</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -62,14 +62,11 @@ import { usePageStore } from '../../../stores/page';
|
|||
import { useDialogStore } from '../../../stores/dialog';
|
||||
import { useVirtualSampleStore } from '../../../stores/virtualSample';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Interactive360 from './Interactive360.vue';
|
||||
import SingleImage from './SingleImage.vue';
|
||||
import Selector from '../../Selector.vue';
|
||||
import slugify from 'slugify';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
|
|
@ -87,7 +84,7 @@ const tracks = computed(() => {
|
|||
|
||||
for (const key in raw) {
|
||||
list.push({
|
||||
title: key === 'Autres pistes' ? t('virtualSample.otherTracks') : key,
|
||||
title: key,
|
||||
slug: slugify(key),
|
||||
variations: raw[key] || [],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
modal
|
||||
:draggable="false"
|
||||
:dismissableMask="true"
|
||||
:header="t('dialogs.renderTitle')"
|
||||
header="Titre du rendu"
|
||||
class="dialog"
|
||||
:class="[
|
||||
{ 'with-comments': isCommentsOpen },
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
aria-controls="dynamic"
|
||||
@click="activeTab = 'dynamic'"
|
||||
>
|
||||
<span>{{ t('virtualSample.dynamicPresentation') }}</span>
|
||||
<span>Présentation dynamique</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="step.files.static"
|
||||
|
|
@ -38,10 +38,10 @@
|
|||
:aria-pressed="activeTab === 'static' ? true : false"
|
||||
aria-controls="static"
|
||||
>
|
||||
<span>{{ t('virtualSample.staticView') }}</span>
|
||||
<span>Vue statique</span>
|
||||
</button>
|
||||
</div>
|
||||
<h2 class="font-serif text-lg">{{ t('virtualSample.title') }}</h2>
|
||||
<h2 class="font-serif text-lg">Échantillon virtuel</h2>
|
||||
</template>
|
||||
|
||||
<DynamicView id="dynamic" v-if="activeTab === 'dynamic'" />
|
||||
|
|
@ -66,8 +66,8 @@
|
|||
>
|
||||
<span>{{
|
||||
!isLoopAnimationEnabled
|
||||
? t('buttons.loopAnimation')
|
||||
: t('buttons.stopAnimation')
|
||||
? 'Animation en boucle'
|
||||
: 'Arrêter l’animation'
|
||||
}}</span>
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -79,7 +79,7 @@
|
|||
@click="isCommentsOpen = !isCommentsOpen"
|
||||
>
|
||||
<span class="sr-only"
|
||||
>{{ isCommentsOpen ? t('buttons.hideComments') : t('buttons.showComments') }}</span
|
||||
>{{ isCommentsOpen ? 'Masquer' : 'Afficher' }} les commentaires</span
|
||||
>
|
||||
</button>
|
||||
</template>
|
||||
|
|
@ -103,9 +103,7 @@ import { useVirtualSampleStore } from '../../../stores/virtualSample';
|
|||
import { useDialogStore } from '../../../stores/dialog';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { usePageStore } from '../../../stores/page';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { file } = defineProps({
|
||||
file: Object,
|
||||
});
|
||||
|
|
@ -143,12 +141,12 @@ watch(isOpen, (newValue) => {
|
|||
const downloadText = computed(() => {
|
||||
if (activeTab.value === 'dynamic') {
|
||||
if (activeTracks.value.length === 1) {
|
||||
return t('buttons.downloadImage');
|
||||
return "Télécharger l'image";
|
||||
} else {
|
||||
return t('buttons.downloadImages');
|
||||
return 'Télécharger les images';
|
||||
}
|
||||
} else {
|
||||
return t('buttons.downloadPdf');
|
||||
return 'Télécharger le PDF';
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,201 +0,0 @@
|
|||
{
|
||||
"menu": {
|
||||
"home": "Home",
|
||||
"notifications": "Notifications",
|
||||
"meetings": "Meetings",
|
||||
"designToLight": "Design to Light",
|
||||
"inspirations": "Inspirations",
|
||||
"profile": "Profile",
|
||||
"logout": "Logout",
|
||||
"currentProjects": "Current projects",
|
||||
"archivedProjects": "Archived projects",
|
||||
"news": "New",
|
||||
"show": "Show menu",
|
||||
"hide": "Hide menu",
|
||||
"newModifications": "New modifications"
|
||||
},
|
||||
"steps": {
|
||||
"clientBrief": "Client brief",
|
||||
"proposal": "Commercial proposal",
|
||||
"extendedBrief": "Extended brief",
|
||||
"industrialIdeation": "Industrial ideation",
|
||||
"virtualSample": "Virtual sample",
|
||||
"physicalSample": "Physical sample"
|
||||
},
|
||||
"dtl": {
|
||||
"dynamicTrack": "Virtual sample - dynamic track",
|
||||
"staticTrack": "Virtual sample - static track",
|
||||
"title": "Design to Light",
|
||||
"grade": "Design to Light: {grade}",
|
||||
"globalScore": "Global score",
|
||||
"positioning": "Positioning",
|
||||
"design": "Design",
|
||||
"weight": "Weight",
|
||||
"indicators": "Component indicators",
|
||||
"requestOptimization": "Request optimization expertise",
|
||||
"requestPending": "Expertise request being processed…",
|
||||
"initialProposal": "Initial proposal",
|
||||
"alternative": "Alternative {index}",
|
||||
"proposalBasedOn": "Data based on the proposal"
|
||||
},
|
||||
"buttons": {
|
||||
"edit": "Edit",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"delete": "Delete",
|
||||
"submit": "Submit",
|
||||
"retry": "Retry",
|
||||
"add": "Add",
|
||||
"close": "Close",
|
||||
"validate": "Validate and send the brief",
|
||||
"validateMinimum": "Add at least one image",
|
||||
"showPassword": "Show password",
|
||||
"hidePassword": "Hide password",
|
||||
"showProject": "Show project",
|
||||
"hideProject": "Hide project",
|
||||
"showComments": "Show comments",
|
||||
"hideComments": "Hide comments",
|
||||
"backToProject": "Back to project",
|
||||
"backToList": "Back to list",
|
||||
"addComment": "Add a comment",
|
||||
"reply": "Reply…",
|
||||
"seeAll": "See all",
|
||||
"deleteImage": "Delete this image",
|
||||
"downloadImage": "Download image",
|
||||
"downloadImages": "Download images",
|
||||
"downloadPdf": "Download PDF",
|
||||
"update": "Update",
|
||||
"requestMeeting": "Request a meeting",
|
||||
"markAllAsRead": "Mark all as read",
|
||||
"loopAnimation": "Loop animation",
|
||||
"stopAnimation": "Stop animation",
|
||||
"compareTracks": "Compare tracks",
|
||||
"exitCompare": "Exit compare mode",
|
||||
"addSelectedImages": "Add selected images"
|
||||
},
|
||||
"forms": {
|
||||
"email": "Email",
|
||||
"emailPlaceholder": "email{'@'}example.com",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Minimum 8 characters",
|
||||
"newPassword": "New password",
|
||||
"confirmPassword": "Confirm new password…",
|
||||
"projectName": "Project name",
|
||||
"projectDetails": "Project details",
|
||||
"projectDetailsPlaceholder": "Project details…",
|
||||
"meetingSubject": "Meeting subject",
|
||||
"meetingDetails": "Project details",
|
||||
"meetingDetailsPlaceholder": "Describe your request…",
|
||||
"description": "Project description",
|
||||
"descriptionPlaceholder": "Add a general description of your project…",
|
||||
"imageDescription": "Image description",
|
||||
"imageDescriptionPlaceholder": "Add a description to the image…",
|
||||
"commentPlaceholder": "Add a comment…",
|
||||
"filterByTags": "Filter by tags",
|
||||
"tags": "Tags",
|
||||
"selectVariation": "Select a variation",
|
||||
"uploadedFiles": "Uploaded files",
|
||||
"addImages": "Add one or more images",
|
||||
"selectTags": "Select one or more tags"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Login",
|
||||
"passwordShown": "Password shown",
|
||||
"passwordHidden": "Password hidden",
|
||||
"fillFields": "Please fill in the fields.",
|
||||
"inProgress": "In progress…",
|
||||
"updateSuccess": "Update successful",
|
||||
"updateInProgress": "in progress…"
|
||||
},
|
||||
"account": {
|
||||
"comments": "Comments",
|
||||
"noClient": "No associated client",
|
||||
"managedProjects": "Managed projects",
|
||||
"myProjects": "My projects",
|
||||
"currentStep": "Current step:",
|
||||
"projectCount": "Number of projects",
|
||||
"clientLogo": "{clientName} logo"
|
||||
},
|
||||
"brief": {
|
||||
"addPlatform": "Add a brief via the platform",
|
||||
"addPdf": "Add a PDF brief",
|
||||
"projects": "Projects"
|
||||
},
|
||||
"comments": {
|
||||
"title": "Comments",
|
||||
"your": "Your comment",
|
||||
"new": "New comment",
|
||||
"newInstruction": "In the content area, click where you want to position the comment",
|
||||
"reply": "reply",
|
||||
"replies": "replies",
|
||||
"edit": "Edit",
|
||||
"inProgress": "In progress",
|
||||
"emptyMessage": "Share your ideas by adding comments"
|
||||
},
|
||||
"dates": {
|
||||
"today": "Today",
|
||||
"yesterday": "yesterday",
|
||||
"updatedOn": "Last updated on"
|
||||
},
|
||||
"projects": {
|
||||
"none": "No projects at the moment",
|
||||
"current": "Current projects",
|
||||
"archived": "Archived projects"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"none": "You have no new notifications",
|
||||
"noneUnread": "You have no unread notifications",
|
||||
"all": "All",
|
||||
"unread": "Unread",
|
||||
"projectRequest": "Project creation request",
|
||||
"meetingRequest": "Meeting request",
|
||||
"content": "Content",
|
||||
"draft": "(draft)",
|
||||
"from": "From",
|
||||
"author": "Author:",
|
||||
"goToContent": "Go to content"
|
||||
},
|
||||
"meetings": {
|
||||
"none": "You have no scheduled meetings",
|
||||
"upcoming": "Upcoming",
|
||||
"past": "Past"
|
||||
},
|
||||
"inspirations": {
|
||||
"title": "Inspirations",
|
||||
"favorites": "My Favorites",
|
||||
"addToFavorites": "Add to favorites",
|
||||
"removeFromFavorites": "Remove from favorites",
|
||||
"new": "New"
|
||||
},
|
||||
"dialogs": {
|
||||
"requestProject": "Request project creation",
|
||||
"requestMeeting": "Request a meeting",
|
||||
"imageDetails": "Image details",
|
||||
"addImages": "Add images",
|
||||
"deleteConfirm": "Are you sure you want to delete this image?",
|
||||
"deleteWarning": "If you delete this image, it will disappear from your brief along with all the information attributed to it.",
|
||||
"pdfTitle": "PDF title",
|
||||
"renderTitle": "Render title",
|
||||
"createWithDTL": "Create with Design to Light",
|
||||
"learnMore": "Learn more",
|
||||
"dtlDescription": "Discover the environmental score of your project..."
|
||||
},
|
||||
"virtualSample": {
|
||||
"title": "Virtual sample",
|
||||
"dynamicPresentation": "Dynamic presentation",
|
||||
"staticView": "Static view",
|
||||
"noContent": "Content not available for this track",
|
||||
"selectToCompare": "Select the track you want to compare",
|
||||
"otherTracks": "Other tracks"
|
||||
},
|
||||
"errors": {
|
||||
"saveFailed": "Save failed",
|
||||
"deleteFailed": "Delete failed",
|
||||
"toggleProjectFailed": "Failed to show/hide project",
|
||||
"toggleFavoriteFailed": "Failed to toggle favorite",
|
||||
"readNotificationFailed": "Failed to read notification:",
|
||||
"readAllNotificationsFailed": "Could not read all notifications",
|
||||
"markNotificationFailed": "Could not mark notification as read"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
{
|
||||
"menu": {
|
||||
"home": "Home",
|
||||
"notifications": "Notifications",
|
||||
"meetings": "Réunions",
|
||||
"designToLight": "Design to Light",
|
||||
"inspirations": "Inspirations",
|
||||
"profile": "Profil",
|
||||
"logout": "Déconnexion",
|
||||
"currentProjects": "Projets en cours",
|
||||
"archivedProjects": "Projets archivés",
|
||||
"news": "Nouveautés",
|
||||
"show": "Afficher le menu",
|
||||
"hide": "Masquer le menu",
|
||||
"newModifications": "Nouvelles modifications"
|
||||
},
|
||||
"steps": {
|
||||
"clientBrief": "Brief client",
|
||||
"proposal": "Proposition commerciale",
|
||||
"extendedBrief": "Brief enrichi",
|
||||
"industrialIdeation": "Idéation industrielle",
|
||||
"virtualSample": "Échantillon virtuel",
|
||||
"physicalSample": "Échantillon physique"
|
||||
},
|
||||
"dtl": {
|
||||
"dynamicTrack": "Échantillon virtuel - piste dynamique",
|
||||
"staticTrack": "Échantillon virtuel - piste statique",
|
||||
"title": "Design to Light",
|
||||
"grade": "Design to Light: {grade}",
|
||||
"globalScore": "Note globale",
|
||||
"positioning": "Positionnement",
|
||||
"design": "Design",
|
||||
"weight": "Poids",
|
||||
"indicators": "Indicateur des composants impliqués",
|
||||
"requestOptimization": "Demander une expertise d'optimisation",
|
||||
"requestPending": "Demande d'expertise en cours de traitement…",
|
||||
"initialProposal": "Proposition initiale",
|
||||
"alternative": "Alternative {index}",
|
||||
"proposalBasedOn": "Données basées sur la proposition"
|
||||
},
|
||||
"buttons": {
|
||||
"edit": "Modifier",
|
||||
"cancel": "Annuler",
|
||||
"save": "Sauvegarder",
|
||||
"delete": "Supprimer",
|
||||
"submit": "Soumettre",
|
||||
"retry": "Réessayer",
|
||||
"add": "Ajouter",
|
||||
"close": "Fermer",
|
||||
"validate": "Valider et envoyer le brief",
|
||||
"validateMinimum": "Ajoutez au moins une image",
|
||||
"showPassword": "Afficher le mot de passe",
|
||||
"hidePassword": "Masquer le mot de passe",
|
||||
"showProject": "Afficher le projet",
|
||||
"hideProject": "Masquer le projet",
|
||||
"showComments": "Afficher les commentaires",
|
||||
"hideComments": "Masquer les commentaires",
|
||||
"backToProject": "Retour au projet",
|
||||
"backToList": "Retour à la liste",
|
||||
"addComment": "Ajouter un commentaire",
|
||||
"reply": "Répondre…",
|
||||
"seeAll": "Voir tout",
|
||||
"deleteImage": "Supprimer cette image",
|
||||
"downloadImage": "Télécharger l'image",
|
||||
"downloadImages": "Télécharger les images",
|
||||
"downloadPdf": "Télécharger le PDF",
|
||||
"update": "Mettre à jour",
|
||||
"requestMeeting": "Demander un RDV",
|
||||
"markAllAsRead": "Marquer tout comme lu",
|
||||
"loopAnimation": "Animation en boucle",
|
||||
"stopAnimation": "Arrêter l'animation",
|
||||
"compareTracks": "Comparer les pistes",
|
||||
"exitCompare": "Quitter le mode comparer",
|
||||
"addSelectedImages": "Ajouter les images sélectionnées"
|
||||
},
|
||||
"forms": {
|
||||
"email": "Email",
|
||||
"emailPlaceholder": "mail{'@'}exemple.com",
|
||||
"password": "Mot de passe",
|
||||
"passwordPlaceholder": "Minimum 8 caractères",
|
||||
"newPassword": "Nouveau mot de passe",
|
||||
"confirmPassword": "Confirmez le nouveau mot de passe…",
|
||||
"projectName": "Nom du projet",
|
||||
"projectDetails": "Détails du projet",
|
||||
"projectDetailsPlaceholder": "Détails du projet…",
|
||||
"meetingSubject": "Objet du rendez-vous",
|
||||
"meetingDetails": "Détails du projet",
|
||||
"meetingDetailsPlaceholder": "Décrivez votre demande…",
|
||||
"description": "Description du projet",
|
||||
"descriptionPlaceholder": "Ajoutez une description générale de votre projet…",
|
||||
"imageDescription": "Description de l'image",
|
||||
"imageDescriptionPlaceholder": "Ajoutez une description à l'image…",
|
||||
"commentPlaceholder": "Ajouter un commentaire…",
|
||||
"filterByTags": "Filtrer par tags",
|
||||
"tags": "Tags",
|
||||
"selectVariation": "Sélectionnez une déclinaison",
|
||||
"uploadedFiles": "Fichiers importés",
|
||||
"addImages": "Ajouter une ou plusieurs images",
|
||||
"selectTags": "Sélectionner un ou plusieurs tags"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Connexion",
|
||||
"passwordShown": "Mot de passe affiché",
|
||||
"passwordHidden": "Mot de passe masqué",
|
||||
"fillFields": "Veuillez remplir les champs.",
|
||||
"inProgress": "En cours…",
|
||||
"updateSuccess": "Mise à jour réussie",
|
||||
"updateInProgress": "en cours…"
|
||||
},
|
||||
"account": {
|
||||
"comments": "Commentaires",
|
||||
"noClient": "Pas de client associé",
|
||||
"managedProjects": "Projets managés",
|
||||
"myProjects": "Mes projets",
|
||||
"currentStep": "Étape en cours :",
|
||||
"projectCount": "Nombre de projets",
|
||||
"clientLogo": "logo {clientName}"
|
||||
},
|
||||
"brief": {
|
||||
"addPlatform": "Ajouter un brief via la plateforme",
|
||||
"addPdf": "Ajouter un brief PDF",
|
||||
"projects": "Projets"
|
||||
},
|
||||
"comments": {
|
||||
"title": "Commentaires",
|
||||
"your": "Votre commentaire",
|
||||
"new": "Nouveau commentaire",
|
||||
"newInstruction": "Dans la zone du contenu, cliquez où vous souhaitez positionner le commentaire",
|
||||
"reply": "réponse",
|
||||
"replies": "réponses",
|
||||
"edit": "Éditer",
|
||||
"inProgress": "En cours",
|
||||
"emptyMessage": "Partagez vos idées en ajoutant des commentaires"
|
||||
},
|
||||
"dates": {
|
||||
"today": "Aujourd'hui",
|
||||
"yesterday": "hier",
|
||||
"updatedOn": "Dernière mise à jour le"
|
||||
},
|
||||
"projects": {
|
||||
"none": "Aucun projet pour le moment",
|
||||
"current": "Projets en cours",
|
||||
"archived": "Projets archivés"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"none": "Vous n'avez pas de nouvelles notifications",
|
||||
"noneUnread": "Vous n'avez pas de notifications non lues",
|
||||
"all": "Tout",
|
||||
"unread": "Non lu",
|
||||
"projectRequest": "Demande de création de projet",
|
||||
"meetingRequest": "Demande de rendez-vous",
|
||||
"content": "Contenu",
|
||||
"draft": "(brouillon)",
|
||||
"from": "De la part de",
|
||||
"author": "Auteur :",
|
||||
"goToContent": "Aller au contenu"
|
||||
},
|
||||
"meetings": {
|
||||
"none": "Vous n'avez aucune réunion programmée",
|
||||
"upcoming": "À venir",
|
||||
"past": "Passées"
|
||||
},
|
||||
"inspirations": {
|
||||
"title": "Les Inspirations",
|
||||
"favorites": "Mes Favoris",
|
||||
"addToFavorites": "Ajouter aux favoris",
|
||||
"removeFromFavorites": "Retirer des favoris",
|
||||
"new": "Nouveauté"
|
||||
},
|
||||
"dialogs": {
|
||||
"requestProject": "Demander la création d'un projet",
|
||||
"requestMeeting": "Demander un rendez-vous",
|
||||
"imageDetails": "Détails de l'image",
|
||||
"addImages": "Ajouter des images",
|
||||
"deleteConfirm": "Êtes-vous sûr de vouloir supprimer cette image ?",
|
||||
"deleteWarning": "Si vous supprimez cette image, celle-ci disparaîtra de votre brief ainsi que toutes les informations qui lui sont attribuées.",
|
||||
"pdfTitle": "Titre du PDF",
|
||||
"renderTitle": "Titre du rendu",
|
||||
"createWithDTL": "Créer avec Design to Light",
|
||||
"learnMore": "En savoir plus",
|
||||
"dtlDescription": "Découvrez la note environnementale de votre projet..."
|
||||
},
|
||||
"virtualSample": {
|
||||
"title": "Échantillon virtuel",
|
||||
"dynamicPresentation": "Présentation dynamique",
|
||||
"staticView": "Vue statique",
|
||||
"noContent": "Contenu non disponible pour cette piste",
|
||||
"selectToCompare": "Sélectionnez sur la piste que vous souhaitez comparer",
|
||||
"otherTracks": "Autres pistes"
|
||||
},
|
||||
"errors": {
|
||||
"saveFailed": "Erreur lors de la sauvegarde",
|
||||
"deleteFailed": "Erreur lors de la suppression",
|
||||
"toggleProjectFailed": "Erreur lors du masquage/affichage du projet",
|
||||
"toggleFavoriteFailed": "Failed to toggle favorite",
|
||||
"readNotificationFailed": "Erreur lors de la lecture de la notification :",
|
||||
"readAllNotificationsFailed": "Could not read all notifications",
|
||||
"markNotificationFailed": "Could not mark notification as read"
|
||||
}
|
||||
}
|
||||
23
src/main.js
23
src/main.js
|
|
@ -6,30 +6,12 @@ import PrimeVue from 'primevue/config';
|
|||
import ToastService from 'primevue/toastservice';
|
||||
import Select from 'primevue/select';
|
||||
import MultiSelect from 'primevue/multiselect';
|
||||
import { router, setI18nLocale } from './router/router.js';
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import fr from './locales/fr.json';
|
||||
import en from './locales/en.json';
|
||||
import { setI18nInstance } from './stores/locale';
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'fr',
|
||||
fallbackLocale: 'fr',
|
||||
messages: {
|
||||
fr,
|
||||
en,
|
||||
},
|
||||
});
|
||||
|
||||
// Donner l'instance i18n au store locale pour synchronisation immédiate
|
||||
setI18nInstance(i18n);
|
||||
import { router } from './router/router.js';
|
||||
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
|
||||
app.use(pinia);
|
||||
app.use(i18n);
|
||||
app.use(PrimeVue, {
|
||||
unstyled: true,
|
||||
});
|
||||
|
|
@ -37,7 +19,4 @@ app.use(ToastService);
|
|||
app.use(router);
|
||||
app.component('Select', Select);
|
||||
app.component('MultiSelect', MultiSelect);
|
||||
|
||||
setI18nLocale(i18n);
|
||||
|
||||
app.mount('#app');
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ import routes from './routes';
|
|||
import { useApiStore } from '../stores/api';
|
||||
import { usePageStore } from '../stores/page';
|
||||
import { useUserStore } from '../stores/user';
|
||||
import { useLocaleStore } from '../stores/locale';
|
||||
import { useAnalyticsStore } from '../stores/analytics';
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
|
|
@ -14,23 +12,13 @@ const router = createRouter({
|
|||
router.beforeEach(async (to, from, next) => {
|
||||
const pageStore = usePageStore();
|
||||
const userStore = useUserStore();
|
||||
const localeStore = useLocaleStore();
|
||||
|
||||
const urlLocale = to.params.locale === 'en' ? 'en' : 'fr';
|
||||
localeStore.initLocale(urlLocale);
|
||||
|
||||
let apiPath = to.path;
|
||||
if (urlLocale === 'en' && apiPath.startsWith('/en')) {
|
||||
apiPath = apiPath.replace(/^\/en/, '') || '/';
|
||||
}
|
||||
|
||||
const api = useApiStore();
|
||||
try {
|
||||
const res = await api.fetchData(apiPath);
|
||||
const res = await api.fetchData(to.path);
|
||||
|
||||
const loginPath = urlLocale === 'en' ? '/en/login' : '/login';
|
||||
if (to.path === loginPath && res.user) {
|
||||
location.href = urlLocale === 'en' ? '/en' : '/';
|
||||
if (to.path === '/login' && res.user) {
|
||||
location.href = '/';
|
||||
}
|
||||
|
||||
pageStore.page = res.page;
|
||||
|
|
@ -42,72 +30,4 @@ router.beforeEach(async (to, from, next) => {
|
|||
}
|
||||
});
|
||||
|
||||
router.afterEach((to) => {
|
||||
const userStore = useUserStore();
|
||||
const pageStore = usePageStore();
|
||||
|
||||
if (userStore.user) {
|
||||
const analytics = useAnalyticsStore();
|
||||
analytics.initSession();
|
||||
|
||||
const { pageType, pageName } = getPageInfo(to, pageStore.page);
|
||||
analytics.trackVisit(to.path, pageType, pageName);
|
||||
}
|
||||
});
|
||||
|
||||
function getPageInfo(route, page) {
|
||||
const path = route.path;
|
||||
|
||||
if (path === '/' || path === '/en') {
|
||||
return { pageType: 'home', pageName: 'Accueil' };
|
||||
}
|
||||
|
||||
if (path.includes('/login')) {
|
||||
return { pageType: 'login', pageName: 'Connexion' };
|
||||
}
|
||||
|
||||
if (path.includes('/account')) {
|
||||
return { pageType: 'account', pageName: 'Compte' };
|
||||
}
|
||||
|
||||
if (path.includes('/notifications')) {
|
||||
return { pageType: 'notifications', pageName: 'Notifications' };
|
||||
}
|
||||
|
||||
if (path.includes('/reunions')) {
|
||||
return { pageType: 'reunions', pageName: 'Réunions' };
|
||||
}
|
||||
|
||||
if (path.includes('/inspirations')) {
|
||||
return { pageType: 'inspirations', pageName: 'Inspirations' };
|
||||
}
|
||||
|
||||
if (path.includes('/design-to-light')) {
|
||||
return { pageType: 'design-to-light', pageName: 'Design to Light' };
|
||||
}
|
||||
|
||||
if (path.includes('/client-brief')) {
|
||||
return { pageType: 'client-brief', pageName: page?.title || 'Brief Client' };
|
||||
}
|
||||
|
||||
if (path.includes('/extended-brief')) {
|
||||
return { pageType: 'extended-brief', pageName: page?.title || 'Brief Étendu' };
|
||||
}
|
||||
|
||||
if (path.includes('/projects/')) {
|
||||
return { pageType: 'project', pageName: page?.title || 'Projet' };
|
||||
}
|
||||
|
||||
return { pageType: 'unknown', pageName: path };
|
||||
}
|
||||
|
||||
export function setI18nLocale(i18n) {
|
||||
router.afterEach(() => {
|
||||
const localeStore = useLocaleStore();
|
||||
if (i18n.global.locale.value !== localeStore.currentLocale) {
|
||||
i18n.global.locale.value = localeStore.currentLocale;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { router };
|
||||
|
|
|
|||
|
|
@ -10,55 +10,53 @@ import Account from '../views/Account.vue';
|
|||
|
||||
const routes = [
|
||||
{
|
||||
path: '/:locale?',
|
||||
path: '/',
|
||||
component: Home,
|
||||
},
|
||||
{
|
||||
name: 'Login',
|
||||
path: '/:locale?/login',
|
||||
path: '/login',
|
||||
component: Login,
|
||||
},
|
||||
{
|
||||
name: 'Account',
|
||||
path: '/:locale?/account',
|
||||
path: '/account',
|
||||
component: Account,
|
||||
},
|
||||
{
|
||||
path: '/:locale?/notifications',
|
||||
path: '/notifications',
|
||||
component: Notifications,
|
||||
},
|
||||
{
|
||||
path: '/:locale?/reunions',
|
||||
path: '/reunions',
|
||||
component: Reunions,
|
||||
},
|
||||
{
|
||||
path: '/:locale?/inspirations',
|
||||
path: '/inspirations',
|
||||
component: Inspirations,
|
||||
},
|
||||
{
|
||||
path: '/:locale?/design-to-light',
|
||||
path: '/design-to-light',
|
||||
component: DesignToLight,
|
||||
},
|
||||
{
|
||||
path: '/:locale?/projects/:id',
|
||||
path: '/projects/:id',
|
||||
component: Kanban,
|
||||
},
|
||||
{
|
||||
path: '/:locale?/projects/:id/client-brief',
|
||||
path: '/projects/:id/client-brief',
|
||||
component: Brief,
|
||||
},
|
||||
{
|
||||
path: '/:locale?/projects/:id/extended-brief',
|
||||
path: '/projects/:id/extended-brief',
|
||||
component: Brief,
|
||||
},
|
||||
|
||||
// Redirections
|
||||
{
|
||||
path: '/:locale?/projects/:id/industrial-ideation',
|
||||
path: '/projects/:id/industrial-ideation',
|
||||
redirect: (to) => {
|
||||
const prefix = to.params.locale === 'en' ? '/en' : '';
|
||||
return (
|
||||
prefix +
|
||||
'/projects/' +
|
||||
to.params.id +
|
||||
'?dialog=industrial-ideation&comments=true'
|
||||
|
|
@ -66,10 +64,9 @@ const routes = [
|
|||
},
|
||||
},
|
||||
{
|
||||
path: '/:locale?/projects/:id/proposal',
|
||||
path: '/projects/:id/proposal',
|
||||
redirect: (to) => {
|
||||
const prefix = to.params.locale === 'en' ? '/en' : '';
|
||||
return prefix + '/projects/' + to.params.id + '?dialog=proposal&comments=true';
|
||||
return '/projects/' + to.params.id + '?dialog=proposal&comments=true';
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export const useAnalyticsStore = defineStore('analytics', () => {
|
||||
const sessionId = ref(null);
|
||||
|
||||
function initSession() {
|
||||
// Récupérer sessionId depuis sessionStorage ou en créer un nouveau
|
||||
const storedSessionId = sessionStorage.getItem('analyticsSessionId');
|
||||
|
||||
if (storedSessionId) {
|
||||
sessionId.value = storedSessionId;
|
||||
} else {
|
||||
sessionId.value = generateSessionId();
|
||||
sessionStorage.setItem('analyticsSessionId', sessionId.value);
|
||||
}
|
||||
}
|
||||
|
||||
function generateSessionId() {
|
||||
return `session_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
||||
}
|
||||
|
||||
async function trackVisit(pageUrl, pageType, pageName = null) {
|
||||
if (!sessionId.value) {
|
||||
initSession();
|
||||
}
|
||||
|
||||
const data = {
|
||||
sessionId: sessionId.value,
|
||||
pageUrl,
|
||||
pageType,
|
||||
pageName,
|
||||
};
|
||||
|
||||
try {
|
||||
await fetch('/track-visit.json', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
} catch (error) {
|
||||
// Tracking silencieux : ne pas bloquer l'app si ça échoue
|
||||
console.debug('Analytics tracking failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
initSession,
|
||||
trackVisit,
|
||||
};
|
||||
});
|
||||
|
|
@ -289,32 +289,6 @@ export const useApiStore = defineStore("api", () => {
|
|||
}
|
||||
}
|
||||
|
||||
async function toggleHiddenProject(projectUuid) {
|
||||
const headers = {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ projectUuid }),
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch("/toggle-hidden-project.json", headers);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === "success") {
|
||||
userStore.toggleHiddenProject(projectUuid);
|
||||
console.log("Projet masqué/affiché avec succès.");
|
||||
return data;
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur lors du toggle du projet masqué:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
fetchData,
|
||||
fetchRoute,
|
||||
|
|
@ -325,7 +299,6 @@ export const useApiStore = defineStore("api", () => {
|
|||
// Nouvelles fonctions
|
||||
markNotificationRead,
|
||||
markAllNotificationsRead,
|
||||
toggleHiddenProject,
|
||||
// Anciennes fonctions (rétro-compatibilité)
|
||||
readNotification,
|
||||
readAllNotifications,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { defineStore } from 'pinia';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useAnalyticsStore } from './analytics';
|
||||
import { usePageStore } from './page';
|
||||
|
||||
export const useDialogStore = defineStore('dialog', () => {
|
||||
const content = ref(null);
|
||||
|
|
@ -150,22 +148,6 @@ export const useDialogStore = defineStore('dialog', () => {
|
|||
}
|
||||
}
|
||||
|
||||
// Analytics tracking pour ouverture de fichiers
|
||||
watch(openedFile, (newFile) => {
|
||||
if (newFile) {
|
||||
const analytics = useAnalyticsStore();
|
||||
const pageStore = usePageStore();
|
||||
const currentPath = route.path;
|
||||
const projectTitle = pageStore.page?.title || 'Projet';
|
||||
const fileLabel = newFile.label?.length ? newFile.label : (newFile.name || newFile.filename);
|
||||
analytics.trackVisit(
|
||||
`${currentPath}#file-${newFile.uuid}`,
|
||||
'modal-file',
|
||||
`${projectTitle} / ${fileLabel}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
content,
|
||||
activeTracks,
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
let i18nInstance = null;
|
||||
|
||||
export function setI18nInstance(i18n) {
|
||||
i18nInstance = i18n;
|
||||
}
|
||||
|
||||
export const useLocaleStore = defineStore('locale', () => {
|
||||
const currentLocale = ref('fr');
|
||||
|
||||
const isEnglish = computed(() => currentLocale.value === 'en');
|
||||
const isFrench = computed(() => currentLocale.value === 'fr');
|
||||
|
||||
function setLocale(locale) {
|
||||
if (locale !== 'fr' && locale !== 'en') {
|
||||
console.warn(`Invalid locale: ${locale}, defaulting to 'fr'`);
|
||||
locale = 'fr';
|
||||
}
|
||||
currentLocale.value = locale;
|
||||
localStorage.setItem('locale', locale);
|
||||
document.documentElement.lang = locale;
|
||||
|
||||
// Synchroniser i18n immédiatement
|
||||
if (i18nInstance) {
|
||||
i18nInstance.global.locale.value = locale;
|
||||
}
|
||||
}
|
||||
|
||||
function detectUserLocale() {
|
||||
const browserLang = navigator.language.toLowerCase();
|
||||
return browserLang.startsWith('en') ? 'en' : 'fr';
|
||||
}
|
||||
|
||||
function initLocale(urlLocale = null) {
|
||||
let locale = 'fr';
|
||||
|
||||
if (urlLocale && (urlLocale === 'en' || urlLocale === 'fr')) {
|
||||
locale = urlLocale;
|
||||
} else {
|
||||
const storedLocale = localStorage.getItem('locale');
|
||||
if (storedLocale === 'en' || storedLocale === 'fr') {
|
||||
locale = storedLocale;
|
||||
} else {
|
||||
locale = detectUserLocale();
|
||||
}
|
||||
}
|
||||
|
||||
setLocale(locale);
|
||||
}
|
||||
|
||||
return {
|
||||
currentLocale,
|
||||
isEnglish,
|
||||
isFrench,
|
||||
setLocale,
|
||||
detectUserLocale,
|
||||
initLocale,
|
||||
};
|
||||
});
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { defineStore } from 'pinia';
|
||||
import { useApiStore } from './api.js';
|
||||
import { useUserStore } from './user.js';
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
export const useProjectsStore = defineStore('projects', () => {
|
||||
|
|
@ -8,40 +7,25 @@ export const useProjectsStore = defineStore('projects', () => {
|
|||
const projects = ref(null);
|
||||
|
||||
const currentProjects = computed(() => {
|
||||
const userStore = useUserStore();
|
||||
const hiddenProjectUuids = userStore.hiddenProjects || [];
|
||||
return (
|
||||
projects.value
|
||||
?.filter((project) =>
|
||||
project.status === 'listed' &&
|
||||
!hiddenProjectUuids.includes(project.uuid)
|
||||
)
|
||||
?.filter((project) => project.status === 'listed')
|
||||
.sort((a, b) => new Date(b.modified) - new Date(a.modified)) ?? []
|
||||
);
|
||||
});
|
||||
|
||||
const draftProjects = computed(() => {
|
||||
const userStore = useUserStore();
|
||||
const hiddenProjectUuids = userStore.hiddenProjects || [];
|
||||
return (
|
||||
projects.value
|
||||
?.filter((project) =>
|
||||
project.status === 'draft' &&
|
||||
!hiddenProjectUuids.includes(project.uuid)
|
||||
)
|
||||
?.filter((project) => project.status === 'draft')
|
||||
.sort((a, b) => new Date(b.modified) - new Date(a.modified)) ?? []
|
||||
);
|
||||
});
|
||||
|
||||
const archivedProjects = computed(() => {
|
||||
const userStore = useUserStore();
|
||||
const hiddenProjectUuids = userStore.hiddenProjects || [];
|
||||
return (
|
||||
projects.value
|
||||
?.filter((project) =>
|
||||
project.status === 'unlisted' &&
|
||||
!hiddenProjectUuids.includes(project.uuid)
|
||||
)
|
||||
?.filter((project) => project.status === 'unlisted')
|
||||
.sort((a, b) => new Date(b.modified) - new Date(a.modified)) ?? []
|
||||
);
|
||||
});
|
||||
|
|
@ -52,8 +36,6 @@ export const useProjectsStore = defineStore('projects', () => {
|
|||
isProjectsLoading.value = false;
|
||||
projects.value = json.page.children;
|
||||
// }, 3000);
|
||||
}).catch(() => {
|
||||
isProjectsLoading.value = false;
|
||||
});
|
||||
|
||||
// Functions
|
||||
|
|
|
|||
|
|
@ -19,10 +19,7 @@ export const useUserStore = defineStore('user', () => {
|
|||
if (!projects.value || !user.value) return [];
|
||||
|
||||
return projects.value.flatMap((project) => {
|
||||
if (!Array.isArray(project.notifications)) {
|
||||
if (project.notifications) console.error(`[Notifications] project.notifications n'est pas un tableau pour "${project.slug}"`, project.notifications);
|
||||
return [];
|
||||
}
|
||||
if (!project.notifications) return [];
|
||||
|
||||
return project.notifications.map((notification) => ({
|
||||
...notification,
|
||||
|
|
@ -90,40 +87,13 @@ export const useUserStore = defineStore('user', () => {
|
|||
return user.value.uuid === comment.author.uuid;
|
||||
}
|
||||
|
||||
const hiddenProjects = computed(() => {
|
||||
return user.value?.hiddenProjects || [];
|
||||
});
|
||||
|
||||
const visibleProjects = computed(() => {
|
||||
if (!user.value?.projects) return [];
|
||||
const projectsArray = Array.isArray(user.value.projects)
|
||||
? user.value.projects
|
||||
: Object.values(user.value.projects);
|
||||
return projectsArray.filter(
|
||||
(project) => !hiddenProjects.value.includes(project.uuid)
|
||||
);
|
||||
});
|
||||
|
||||
function toggleHiddenProject(projectUuid) {
|
||||
if (!user.value) return;
|
||||
const index = user.value.hiddenProjects.indexOf(projectUuid);
|
||||
if (index > -1) {
|
||||
user.value.hiddenProjects.splice(index, 1);
|
||||
} else {
|
||||
user.value.hiddenProjects.push(projectUuid);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
isLogged,
|
||||
notifications,
|
||||
hiddenProjects,
|
||||
visibleProjects,
|
||||
// Nouvelles fonctions
|
||||
markNotificationRead,
|
||||
markAllNotificationsRead,
|
||||
toggleHiddenProject,
|
||||
// Anciennes fonctions (rétro-compatibilité)
|
||||
readNotification,
|
||||
readAllNotifications,
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
import { useLocaleStore } from '../stores/locale';
|
||||
|
||||
export function localeRoute(path) {
|
||||
const localeStore = useLocaleStore();
|
||||
const locale = localeStore.currentLocale;
|
||||
|
||||
if (locale === 'en') {
|
||||
return path.startsWith('/en') ? path : `/en${path}`;
|
||||
}
|
||||
|
||||
return path.startsWith('/en') ? path.replace(/^\/en/, '') || '/' : path;
|
||||
}
|
||||
|
||||
export function addLocalePrefix(path) {
|
||||
const localeStore = useLocaleStore();
|
||||
const locale = localeStore.currentLocale;
|
||||
|
||||
// S'assurer que le path commence par /
|
||||
if (!path.startsWith('/')) {
|
||||
path = '/' + path;
|
||||
}
|
||||
|
||||
// Ajouter /en si locale anglaise et pas déjà présent
|
||||
if (locale === 'en' && !path.startsWith('/en')) {
|
||||
return '/en' + path;
|
||||
}
|
||||
|
||||
// Enlever /en si locale française
|
||||
if (locale === 'fr' && path.startsWith('/en')) {
|
||||
return path.replace(/^\/en/, '') || '/';
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
export function removeLocalePrefix(path) {
|
||||
return path.replace(/^\/(en|fr)/, '') || '/';
|
||||
}
|
||||
|
||||
export function getLocaleFromPath(path) {
|
||||
const match = path.match(/^\/(en|fr)/);
|
||||
return match ? match[1] : 'fr';
|
||||
}
|
||||
|
|
@ -20,13 +20,13 @@
|
|||
role="group"
|
||||
aria-labelledby="username"
|
||||
>
|
||||
<label for="username" class="text-grey-700">{{ t('forms.email') }}</label>
|
||||
<label for="username" class="text-grey-700">Email</label>
|
||||
<input
|
||||
v-if="isEditingEmail"
|
||||
type="email"
|
||||
v-model="email"
|
||||
id="username"
|
||||
:placeholder="t('forms.emailPlaceholder')"
|
||||
placeholder="mail@exemple.com"
|
||||
autocomplete="username"
|
||||
class="w-full rounded-md border border-grey-200 px-16 py-12 mt-8"
|
||||
:class="{ invalid: !isEmailValid }"
|
||||
|
|
@ -55,14 +55,14 @@
|
|||
@click="isEditingEmail = true"
|
||||
class="btn | w-full text-md"
|
||||
>
|
||||
{{ t('buttons.edit') }}
|
||||
Modifier
|
||||
</button>
|
||||
<button
|
||||
v-if="isEditingEmail"
|
||||
class="btn btn--secondary | w-full text-md"
|
||||
@click="isEditingEmail = false"
|
||||
>
|
||||
{{ t('buttons.cancel') }}
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -72,15 +72,17 @@
|
|||
class="bg-white rounded-2xl px-16 py-24"
|
||||
aria-labelledby="password-label"
|
||||
>
|
||||
<h2 class="sr-only" id="password-label">{{ t('forms.password') }}</h2>
|
||||
<h2 class="sr-only" id="password-label">Mot de passe</h2>
|
||||
<div class="flow">
|
||||
<div class="field | w-full" role="group" aria-labelledby="password">
|
||||
<label for="password" class="text-grey-700">{{ t('forms.newPassword') }}</label>
|
||||
<label for="password" class="text-grey-700"
|
||||
>Nouveau mot de passe</label
|
||||
>
|
||||
<input
|
||||
:type="isPasswordVisible ? 'text' : 'password'"
|
||||
v-model="password"
|
||||
id="password"
|
||||
:placeholder="t('forms.passwordPlaceholder')"
|
||||
placeholder="Minimum 8 caractères"
|
||||
autocomplete="current-password"
|
||||
autocapitalize="none"
|
||||
autocorrect="off"
|
||||
|
|
@ -95,7 +97,11 @@
|
|||
:aria-pressed="isPasswordVisible ? 'true' : 'false'"
|
||||
aria-controls="password"
|
||||
@click="isPasswordVisible = !isPasswordVisible"
|
||||
:title="isPasswordVisible ? t('buttons.hidePassword') : t('buttons.showPassword')"
|
||||
:title="
|
||||
isPasswordVisible
|
||||
? 'Masquer le mot de passe'
|
||||
: 'Afficher le mot de passe'
|
||||
"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
|
|
@ -115,7 +121,11 @@
|
|||
d="M17.8827 19.2968C16.1814 20.3755 14.1638 21.0002 12.0003 21.0002C6.60812 21.0002 2.12215 17.1204 1.18164 12.0002C1.61832 9.62282 2.81932 7.5129 4.52047 5.93457L1.39366 2.80777L2.80788 1.39355L22.6069 21.1925L21.1927 22.6068L17.8827 19.2968ZM5.9356 7.3497C4.60673 8.56015 3.6378 10.1672 3.22278 12.0002C4.14022 16.0521 7.7646 19.0002 12.0003 19.0002C13.5997 19.0002 15.112 18.5798 16.4243 17.8384L14.396 15.8101C13.7023 16.2472 12.8808 16.5002 12.0003 16.5002C9.51498 16.5002 7.50026 14.4854 7.50026 12.0002C7.50026 11.1196 7.75317 10.2981 8.19031 9.60442L5.9356 7.3497ZM12.9139 14.328L9.67246 11.0866C9.5613 11.3696 9.50026 11.6777 9.50026 12.0002C9.50026 13.3809 10.6196 14.5002 12.0003 14.5002C12.3227 14.5002 12.6309 14.4391 12.9139 14.328ZM20.8068 16.5925L19.376 15.1617C20.0319 14.2268 20.5154 13.1586 20.7777 12.0002C19.8603 7.94818 16.2359 5.00016 12.0003 5.00016C11.1544 5.00016 10.3329 5.11773 9.55249 5.33818L7.97446 3.76015C9.22127 3.26959 10.5793 3.00016 12.0003 3.00016C17.3924 3.00016 21.8784 6.87992 22.8189 12.0002C22.5067 13.6998 21.8038 15.2628 20.8068 16.5925ZM11.7229 7.50857C11.8146 7.50299 11.9071 7.50016 12.0003 7.50016C14.4855 7.50016 16.5003 9.51488 16.5003 12.0002C16.5003 12.0933 16.4974 12.1858 16.4919 12.2775L11.7229 7.50857Z"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="sr-only">{{ isPasswordVisible ? t('buttons.hidePassword') : t('buttons.showPassword') }}</span>
|
||||
<span class="sr-only">{{
|
||||
isPasswordVisible
|
||||
? 'Masquer le mot de passe'
|
||||
: 'Afficher le mot de passe'
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -123,12 +133,14 @@
|
|||
role="group"
|
||||
aria-labelledby="password-confirm"
|
||||
>
|
||||
<label for="passwordConfirm" class="text-grey-700">{{ t('forms.confirmPassword') }}</label>
|
||||
<label for="passwordConfirm" class="text-grey-700"
|
||||
>Confirmez le nouveau mot de passe…</label
|
||||
>
|
||||
<input
|
||||
:type="isPasswordVisible ? 'text' : 'password'"
|
||||
v-model="passwordConfirm"
|
||||
id="password-confirm"
|
||||
:placeholder="t('forms.passwordPlaceholder')"
|
||||
placeholder="Minimum 8 caractères"
|
||||
autocomplete="current-password"
|
||||
autocapitalize="none"
|
||||
autocorrect="off"
|
||||
|
|
@ -143,7 +155,11 @@
|
|||
:aria-pressed="isPasswordVisible ? 'true' : 'false'"
|
||||
aria-controls="password-confirm"
|
||||
@click="isPasswordVisible = !isPasswordVisible"
|
||||
:title="isPasswordVisible ? t('buttons.hidePassword') : t('buttons.showPassword')"
|
||||
:title="
|
||||
isPasswordVisible
|
||||
? 'Masquer le mot de passe'
|
||||
: 'Afficher le mot de passe'
|
||||
"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
|
|
@ -163,7 +179,11 @@
|
|||
d="M17.8827 19.2968C16.1814 20.3755 14.1638 21.0002 12.0003 21.0002C6.60812 21.0002 2.12215 17.1204 1.18164 12.0002C1.61832 9.62282 2.81932 7.5129 4.52047 5.93457L1.39366 2.80777L2.80788 1.39355L22.6069 21.1925L21.1927 22.6068L17.8827 19.2968ZM5.9356 7.3497C4.60673 8.56015 3.6378 10.1672 3.22278 12.0002C4.14022 16.0521 7.7646 19.0002 12.0003 19.0002C13.5997 19.0002 15.112 18.5798 16.4243 17.8384L14.396 15.8101C13.7023 16.2472 12.8808 16.5002 12.0003 16.5002C9.51498 16.5002 7.50026 14.4854 7.50026 12.0002C7.50026 11.1196 7.75317 10.2981 8.19031 9.60442L5.9356 7.3497ZM12.9139 14.328L9.67246 11.0866C9.5613 11.3696 9.50026 11.6777 9.50026 12.0002C9.50026 13.3809 10.6196 14.5002 12.0003 14.5002C12.3227 14.5002 12.6309 14.4391 12.9139 14.328ZM20.8068 16.5925L19.376 15.1617C20.0319 14.2268 20.5154 13.1586 20.7777 12.0002C19.8603 7.94818 16.2359 5.00016 12.0003 5.00016C11.1544 5.00016 10.3329 5.11773 9.55249 5.33818L7.97446 3.76015C9.22127 3.26959 10.5793 3.00016 12.0003 3.00016C17.3924 3.00016 21.8784 6.87992 22.8189 12.0002C22.5067 13.6998 21.8038 15.2628 20.8068 16.5925ZM11.7229 7.50857C11.8146 7.50299 11.9071 7.50016 12.0003 7.50016C14.4855 7.50016 16.5003 9.51488 16.5003 12.0002C16.5003 12.0933 16.4974 12.1858 16.4919 12.2775L11.7229 7.50857Z"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="sr-only">{{ isPasswordVisible ? t('buttons.hidePassword') : t('buttons.showPassword') }}</span>
|
||||
<span class="sr-only">{{
|
||||
isPasswordVisible
|
||||
? 'Masquer le mot de passe'
|
||||
: 'Afficher le mot de passe'
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -178,18 +198,17 @@
|
|||
</section>
|
||||
|
||||
<section
|
||||
v-if="user.role !== 'pochet'"
|
||||
class="bg-white rounded-2xl px-16 py-24"
|
||||
aria-labelledby="client-label"
|
||||
>
|
||||
<h2 id="client-label" class="text-grey-700 mb-16">
|
||||
{{ user.client ? 'Client' : t('account.noClient') }}
|
||||
{{ user.client ? 'Client' : 'Pas de client associé' }}
|
||||
</h2>
|
||||
<div class="flex" style="--column-gap: 2rem">
|
||||
<template v-if="user.client">
|
||||
<img
|
||||
:src="user.client.logo"
|
||||
:alt="t('account.clientLogo', { clientName: user.client.name })"
|
||||
:alt="'logo' + user.client.name"
|
||||
class="rounded-md"
|
||||
width="72"
|
||||
height="72"
|
||||
|
|
@ -200,99 +219,12 @@
|
|||
</section>
|
||||
|
||||
<section
|
||||
v-if="
|
||||
user.hasOwnProperty('projects') &&
|
||||
(user.role === 'pochet' || user.role === 'client')
|
||||
"
|
||||
v-if="user.hasOwnProperty('projects')"
|
||||
class="bg-white rounded-2xl px-16 py-24"
|
||||
aria-labelledby="projects-label"
|
||||
>
|
||||
<h2 id="projects-label" class="text-grey-700 mb-24">
|
||||
{{ user.role === 'pochet' ? t('account.managedProjects') : t('account.myProjects') }}
|
||||
</h2>
|
||||
|
||||
<div class="projects-list flow" style="--flow-space: 1rem">
|
||||
<div
|
||||
v-for="project in user.role === 'pochet'
|
||||
? visibleProjects
|
||||
: allProjectsList"
|
||||
:key="project.uuid"
|
||||
class="project-card | flex items-center | bg-grey-800 text-white rounded-lg px-16 py-12"
|
||||
style="--column-gap: 1rem"
|
||||
>
|
||||
<button
|
||||
v-if="user.role === 'pochet'"
|
||||
@click="toggleProject(project.uuid)"
|
||||
class="btn btn--sm btn--primary"
|
||||
:aria-pressed="true"
|
||||
:title="t('buttons.hideProject')"
|
||||
:aria-label="t('buttons.hideProject')"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M12.0003 3C17.3924 3 21.8784 6.87976 22.8189 12C21.8784 17.1202 17.3924 21 12.0003 21C6.60812 21 2.12215 17.1202 1.18164 12C2.12215 6.87976 6.60812 3 12.0003 3ZM12.0003 19C16.2359 19 19.8603 16.052 20.7777 12C19.8603 7.94803 16.2359 5 12.0003 5C7.7646 5 4.14022 7.94803 3.22278 12C4.14022 16.052 7.7646 19 12.0003 19ZM12.0003 16.5C9.51498 16.5 7.50026 14.4853 7.50026 12C7.50026 9.51472 9.51498 7.5 12.0003 7.5C14.4855 7.5 16.5003 9.51472 16.5003 12C16.5003 14.4853 14.4855 16.5 12.0003 16.5ZM12.0003 14.5C13.381 14.5 14.5003 13.3807 14.5003 12C14.5003 10.6193 13.381 9.5 12.0003 9.5C10.6196 9.5 9.50026 10.6193 9.50026 12C9.50026 13.3807 10.6196 14.5 12.0003 14.5Z"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div>
|
||||
<p class="font-medium">{{ project.title }}</p>
|
||||
<p class="text-sm text-grey-600">
|
||||
{{ t('account.currentStep') }} {{ t('steps.' + project.step) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template
|
||||
v-if="user.role === 'pochet' && hiddenProjectsList.length > 0"
|
||||
>
|
||||
<div
|
||||
v-for="project in hiddenProjectsList"
|
||||
:key="project.uuid"
|
||||
class="project-card | flex items-center | bg-grey-100 rounded-lg px-16 py-12"
|
||||
style="--column-gap: 1rem; opacity: 0.5"
|
||||
>
|
||||
<button
|
||||
@click="toggleProject(project.uuid)"
|
||||
class="btn btn--sm btn--primary"
|
||||
:title="t('buttons.showProject')"
|
||||
:aria-label="t('buttons.showProject')"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M17.8827 19.2968C16.1814 20.3755 14.1638 21.0002 12.0003 21.0002C6.60812 21.0002 2.12215 17.1204 1.18164 12.0002C1.61832 9.62282 2.81932 7.5129 4.52047 5.93457L1.39366 2.80777L2.80788 1.39355L22.6069 21.1925L21.1927 22.6068L17.8827 19.2968ZM5.9356 7.3497C4.60673 8.56015 3.6378 10.1672 3.22278 12.0002C4.14022 16.0521 7.7646 19.0002 12.0003 19.0002C13.5997 19.0002 15.112 18.5798 16.4243 17.8384L14.396 15.8101C13.7023 16.2472 12.8808 16.5002 12.0003 16.5002C9.51498 16.5002 7.50026 14.4854 7.50026 12.0002C7.50026 11.1196 7.75317 10.2981 8.19031 9.60442L5.9356 7.3497ZM12.9139 14.328L9.67246 11.0866C9.5613 11.3696 9.50026 11.6777 9.50026 12.0002C9.50026 13.3809 10.6196 14.5002 12.0003 14.5002C12.3227 14.5002 12.6309 14.4391 12.9139 14.328ZM20.8068 16.5925L19.376 15.1617C20.0319 14.2268 20.5154 13.1586 20.7777 12.0002C19.8603 7.94818 16.2359 5.00016 12.0003 5.00016C11.1544 5.00016 10.3329 5.11773 9.55249 5.33818L7.97446 3.76015C9.22127 3.26959 10.5793 3.00016 12.0003 3.00016C17.3924 3.00016 21.8784 6.87992 22.8189 12.0002C22.5067 13.6998 21.8038 15.2628 20.8068 16.5925ZM11.7229 7.50857C11.8146 7.50299 11.9071 7.50016 12.0003 7.50016C14.4855 7.50016 16.5003 9.51488 16.5003 12.0002C16.5003 12.0933 16.4974 12.1858 16.4919 12.2775L11.7229 7.50857Z"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div>
|
||||
<p class="font-medium">{{ project.title }}</p>
|
||||
<p class="text-sm text-grey-600">
|
||||
{{ t('account.currentStep') }} {{ t('steps.' + project.step) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-else-if="user.hasOwnProperty('projects')"
|
||||
class="bg-white rounded-2xl px-16 py-24"
|
||||
aria-labelledby="projects-label"
|
||||
>
|
||||
<h2 class="sr-only" id="projects-label">{{ t('brief.projects') }}</h2>
|
||||
<p class="text-grey-700 mb-16">{{ t('account.projectCount') }}</p>
|
||||
<h2 class="sr-only" id="projects-label">Projets</h2>
|
||||
<p class="text-grey-700 mb-16">Nombre de projets</p>
|
||||
<p class="text-xl">{{ Object.values(user.projects).length }}</p>
|
||||
</section>
|
||||
</div>
|
||||
|
|
@ -301,44 +233,14 @@
|
|||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useUserStore } from '../stores/user';
|
||||
import { useApiStore } from '../stores/api';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { user, visibleProjects } = storeToRefs(userStore);
|
||||
const api = useApiStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const hiddenProjectsList = computed(() => {
|
||||
if (!user.value?.projects || !user.value?.hiddenProjects) return [];
|
||||
const projectsArray = Array.isArray(user.value.projects)
|
||||
? user.value.projects
|
||||
: Object.values(user.value.projects);
|
||||
return projectsArray.filter((project) =>
|
||||
user.value.hiddenProjects.includes(project.uuid)
|
||||
);
|
||||
});
|
||||
|
||||
const allProjectsList = computed(() => {
|
||||
if (!user.value?.projects) return [];
|
||||
return Array.isArray(user.value.projects)
|
||||
? user.value.projects
|
||||
: Object.values(user.value.projects);
|
||||
});
|
||||
|
||||
async function toggleProject(projectUuid) {
|
||||
try {
|
||||
await api.toggleHiddenProject(projectUuid);
|
||||
} catch (error) {
|
||||
console.error(t('errors.toggleProjectFailed'), error);
|
||||
}
|
||||
}
|
||||
const { user } = storeToRefs(useUserStore());
|
||||
|
||||
// Email
|
||||
const email = ref('');
|
||||
const emailBtn = ref({
|
||||
text: t('buttons.update'),
|
||||
text: 'Mettre à jour',
|
||||
status: 'ready',
|
||||
});
|
||||
const isEditingEmail = ref(false);
|
||||
|
|
@ -347,7 +249,7 @@ const isEmailValid = computed(() => {
|
|||
return emailRegex.test(email.value);
|
||||
});
|
||||
async function updateEmail() {
|
||||
emailBtn.value.text = t('auth.inProgress');
|
||||
emailBtn.value.text = 'En cours…';
|
||||
emailBtn.value.status = 'pending';
|
||||
|
||||
const headers = {
|
||||
|
|
@ -360,11 +262,11 @@ async function updateEmail() {
|
|||
const json = await response.json();
|
||||
|
||||
if (json.status === 'success') {
|
||||
emailBtn.value.text = t('auth.updateSuccess');
|
||||
emailBtn.value.text = 'Mise à jour réussie';
|
||||
emailBtn.value.status = 'succeed';
|
||||
|
||||
setTimeout(() => {
|
||||
emailBtn.value.text = t('buttons.update');
|
||||
emailBtn.value.text = 'Mettre à jour';
|
||||
emailBtn.value.status = 'ready';
|
||||
isEditingEmail.value = false;
|
||||
}, 1500);
|
||||
|
|
@ -376,7 +278,7 @@ async function updateEmail() {
|
|||
// Password
|
||||
const password = ref('');
|
||||
const passwordBtn = ref({
|
||||
text: t('buttons.update'),
|
||||
text: 'Mettre à jour',
|
||||
status: 'ready',
|
||||
});
|
||||
const passwordConfirm = ref('');
|
||||
|
|
@ -387,7 +289,7 @@ const isPasswordConfirmed = computed(() => {
|
|||
);
|
||||
});
|
||||
async function updatePassword() {
|
||||
passwordBtn.value.text = t('auth.updateInProgress');
|
||||
passwordBtn.value.text = 'en cours…';
|
||||
passwordBtn.value.status = 'pending';
|
||||
|
||||
const headers = {
|
||||
|
|
@ -402,11 +304,11 @@ async function updatePassword() {
|
|||
if (json.status === 'success') {
|
||||
password.value = '';
|
||||
passwordConfirm.value = '';
|
||||
passwordBtn.value.text = t('auth.updateSuccess');
|
||||
passwordBtn.value.text = 'mise à jour réussie';
|
||||
passwordBtn.value.status = 'succeed';
|
||||
|
||||
setTimeout(() => {
|
||||
passwordBtn.value.text = t('buttons.update');
|
||||
passwordBtn.value.text = 'Mettre à jour';
|
||||
passwordBtn.value.status = 'ready';
|
||||
}, 1500);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
data-icon="arrow-left"
|
||||
aria-labelledby="back-to-project"
|
||||
>
|
||||
<span id="back-to-project">{{ t('buttons.backToProject') }}</span>
|
||||
<span id="back-to-project">Retour au projet</span>
|
||||
</router-link>
|
||||
<button
|
||||
class="btn | ml-auto"
|
||||
|
|
@ -18,11 +18,11 @@
|
|||
"
|
||||
:title="
|
||||
!page.hasOwnProperty('moodboard') || page.moodboard.length === 0
|
||||
? t('buttons.validateMinimum')
|
||||
? 'Ajoutez au moins une image'
|
||||
: undefined
|
||||
"
|
||||
>
|
||||
{{ t('buttons.validate') }}
|
||||
Valider et envoyer le brief
|
||||
</button>
|
||||
</header>
|
||||
<Images />
|
||||
|
|
@ -33,9 +33,7 @@ import Images from '../components/project/brief/Images.vue';
|
|||
import { usePageStore } from '../stores/page';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useApiStore } from '../stores/api';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { page } = storeToRefs(usePageStore());
|
||||
const api = useApiStore();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<main v-if="page.inspirations" class="flex flex-col">
|
||||
<header class="flex">
|
||||
<h2 id="tabslist" class="sr-only">{{ t('inspirations.title') }}</h2>
|
||||
<h2 id="tabslist" class="sr-only">Inspirations</h2>
|
||||
<Tabs :tabs="tabs" @update:currentTab="changeTab" />
|
||||
<Selector
|
||||
v-if="page.inspirations.length > 1"
|
||||
|
|
@ -36,9 +36,6 @@ import { useUserStore } from "../stores/user";
|
|||
import { ref, computed } from "vue";
|
||||
import { usePageStore } from "../stores/page";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// Stores
|
||||
const { page } = storeToRefs(usePageStore());
|
||||
|
|
@ -63,14 +60,14 @@ const favoriteImages = computed(() =>
|
|||
|
||||
const tabs = computed(() => [
|
||||
{
|
||||
label: t('inspirations.title'),
|
||||
label: "Les Inspirations",
|
||||
id: "all",
|
||||
icon: null,
|
||||
count: currentInspiration.value.media.length,
|
||||
isActive: currentTab.value === "all",
|
||||
},
|
||||
{
|
||||
label: t('inspirations.favorites'),
|
||||
label: "Mes Favoris",
|
||||
id: "favorites",
|
||||
icon: "favorite",
|
||||
count: favoriteImages.value.length,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
style="--row-gap: 1rem; max-width: 24em"
|
||||
>
|
||||
<div class="field | w-full" role="group" aria-labelledby="username">
|
||||
<label for="username">{{ t('forms.email') }}</label>
|
||||
<label for="username">Email</label>
|
||||
<input
|
||||
@input="updateEmail"
|
||||
type="email"
|
||||
|
|
@ -17,13 +17,13 @@
|
|||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
class="w-full rounded-md border border-grey-200 px-16 py-12 mt-8"
|
||||
:placeholder="t('forms.emailPlaceholder')"
|
||||
placeholder="mail@exemple.com"
|
||||
:aria-invalid="{ true: !isValidEmail }"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="field | w-full" role="group" aria-labelledby="password">
|
||||
<label for="password">{{ t('forms.password') }}</label>
|
||||
<label for="password">Mot de passe</label>
|
||||
<input
|
||||
:type="isPasswordVisible ? 'text' : 'password'"
|
||||
v-model="password"
|
||||
|
|
@ -41,18 +41,18 @@
|
|||
:aria-pressed="isPasswordVisible ? 'true' : 'false'"
|
||||
aria-controls="password"
|
||||
@click="isPasswordVisible = !isPasswordVisible"
|
||||
:title="isPasswordVisible ? t('buttons.hidePassword') : t('buttons.showPassword')"
|
||||
:title="isPasswordVisible ? 'Masquer le mot de passe' : 'Afficher le mot de passe'"
|
||||
>
|
||||
<svg aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
||||
<path v-if="isPasswordVisible" d="M12.0003 3C17.3924 3 21.8784 6.87976 22.8189 12C21.8784 17.1202 17.3924 21 12.0003 21C6.60812 21 2.12215 17.1202 1.18164 12C2.12215 6.87976 6.60812 3 12.0003 3ZM12.0003 19C16.2359 19 19.8603 16.052 20.7777 12C19.8603 7.94803 16.2359 5 12.0003 5C7.7646 5 4.14022 7.94803 3.22278 12C4.14022 16.052 7.7646 19 12.0003 19ZM12.0003 16.5C9.51498 16.5 7.50026 14.4853 7.50026 12C7.50026 9.51472 9.51498 7.5 12.0003 7.5C14.4855 7.5 16.5003 9.51472 16.5003 12C16.5003 14.4853 14.4855 16.5 12.0003 16.5ZM12.0003 14.5C13.381 14.5 14.5003 13.3807 14.5003 12C14.5003 10.6193 13.381 9.5 12.0003 9.5C10.6196 9.5 9.50026 10.6193 9.50026 12C9.50026 13.3807 10.6196 14.5 12.0003 14.5Z"></path>
|
||||
<path v-else d="M17.8827 19.2968C16.1814 20.3755 14.1638 21.0002 12.0003 21.0002C6.60812 21.0002 2.12215 17.1204 1.18164 12.0002C1.61832 9.62282 2.81932 7.5129 4.52047 5.93457L1.39366 2.80777L2.80788 1.39355L22.6069 21.1925L21.1927 22.6068L17.8827 19.2968ZM5.9356 7.3497C4.60673 8.56015 3.6378 10.1672 3.22278 12.0002C4.14022 16.0521 7.7646 19.0002 12.0003 19.0002C13.5997 19.0002 15.112 18.5798 16.4243 17.8384L14.396 15.8101C13.7023 16.2472 12.8808 16.5002 12.0003 16.5002C9.51498 16.5002 7.50026 14.4854 7.50026 12.0002C7.50026 11.1196 7.75317 10.2981 8.19031 9.60442L5.9356 7.3497ZM12.9139 14.328L9.67246 11.0866C9.5613 11.3696 9.50026 11.6777 9.50026 12.0002C9.50026 13.3809 10.6196 14.5002 12.0003 14.5002C12.3227 14.5002 12.6309 14.4391 12.9139 14.328ZM20.8068 16.5925L19.376 15.1617C20.0319 14.2268 20.5154 13.1586 20.7777 12.0002C19.8603 7.94818 16.2359 5.00016 12.0003 5.00016C11.1544 5.00016 10.3329 5.11773 9.55249 5.33818L7.97446 3.76015C9.22127 3.26959 10.5793 3.00016 12.0003 3.00016C17.3924 3.00016 21.8784 6.87992 22.8189 12.0002C22.5067 13.6998 21.8038 15.2628 20.8068 16.5925ZM11.7229 7.50857C11.8146 7.50299 11.9071 7.50016 12.0003 7.50016C14.4855 7.50016 16.5003 9.51488 16.5003 12.0002C16.5003 12.0933 16.4974 12.1858 16.4919 12.2775L11.7229 7.50857Z"></path>
|
||||
</svg>
|
||||
<span class="sr-only">{{ isPasswordVisible ? t('buttons.hidePassword') : t('buttons.showPassword') }}</span>
|
||||
<span class="sr-only">{{ isPasswordVisible ? 'Masquer le mot de passe' : 'Afficher le mot de passe' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="error" v-if="errorMessage" v-html="errorMessage"></p>
|
||||
<div class="sr-only" id="announce" aria-live="assertive">
|
||||
{{ isPasswordVisible ? t('auth.passwordShown') : t('auth.passwordHidden') }}
|
||||
{{ isPasswordVisible ? 'Mot de passe affiché' : 'Mot de passe masqué' }}
|
||||
</div>
|
||||
<button
|
||||
@click="login"
|
||||
|
|
@ -68,9 +68,7 @@
|
|||
</template>
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
const email = ref('');
|
||||
const isValidEmail = ref(false);
|
||||
const password = ref('');
|
||||
|
|
@ -78,7 +76,7 @@ const isPasswordVisible = ref(false);
|
|||
const errorMessage = ref(null);
|
||||
|
||||
const submitBtn = ref({
|
||||
message: t('auth.login'),
|
||||
message: 'Connexion',
|
||||
state: 'ready',
|
||||
});
|
||||
|
||||
|
|
@ -92,10 +90,10 @@ function updateEmail(event) {
|
|||
|
||||
async function login() {
|
||||
if (email.value.length === 0 || password.value.length === 0) {
|
||||
errorMessage.value = t('auth.fillFields');
|
||||
errorMessage.value = 'Veuillez remplir les champs.';
|
||||
}
|
||||
|
||||
submitBtn.value.message = t('auth.inProgress');
|
||||
submitBtn.value.message = 'En cours…';
|
||||
submitBtn.value.state = 'pending';
|
||||
|
||||
const headers = {
|
||||
|
|
@ -116,7 +114,7 @@ async function login() {
|
|||
}
|
||||
} else {
|
||||
errorMessage.value = json.message;
|
||||
submitBtn.value.message = t('buttons.retry');
|
||||
submitBtn.value.message = 'Réessayer';
|
||||
submitBtn.value.state = 'ready';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
:disabled="!sortedNotifications.length"
|
||||
@click="readAll()"
|
||||
>
|
||||
{{ t('buttons.markAllAsRead') }}
|
||||
Marquer tout come lu
|
||||
</button>
|
||||
</header>
|
||||
<div
|
||||
|
|
@ -32,9 +32,9 @@
|
|||
/>
|
||||
</svg>
|
||||
<p v-if="currentTab === 'all'">
|
||||
{{ t('notifications.none') }}
|
||||
Vous n’avez pas de nouvelles notifications
|
||||
</p>
|
||||
<p v-else>{{ t('notifications.noneUnread') }}</p>
|
||||
<p v-else>Vous n’avez pas de notifications non lues</p>
|
||||
</div>
|
||||
<section v-else class="notifications | flow">
|
||||
<template
|
||||
|
|
@ -67,11 +67,9 @@ import Content from '../components/notifications/Content.vue';
|
|||
import { useRouter } from 'vue-router';
|
||||
import ProjectRequest from '../components/notifications/ProjectRequest.vue';
|
||||
import AppointmentRequest from '../components/notifications/AppointmentRequest.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
dayjs.locale('fr');
|
||||
|
||||
const { t } = useI18n();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
const { notifications } = storeToRefs(useUserStore());
|
||||
|
|
@ -80,14 +78,14 @@ const currentTab = ref('all');
|
|||
const tabs = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: t('notifications.all'),
|
||||
label: 'Tout',
|
||||
id: 'all',
|
||||
icon: null,
|
||||
count: null,
|
||||
isActive: currentTab.value === 'all',
|
||||
},
|
||||
{
|
||||
label: t('notifications.unread'),
|
||||
label: 'Non lu',
|
||||
id: 'unread',
|
||||
icon: null,
|
||||
count: null,
|
||||
|
|
@ -123,7 +121,7 @@ function readAll() {
|
|||
try {
|
||||
api.markAllNotificationsRead();
|
||||
} catch (error) {
|
||||
console.log(t('errors.readAllNotificationsFailed'), error);
|
||||
console.log('Could not read all notifications : ', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -134,7 +132,7 @@ async function handleNotificationClick(notification) {
|
|||
try {
|
||||
await api.markNotificationRead(notification);
|
||||
} catch (error) {
|
||||
console.log(t('errors.markNotificationFailed'), error);
|
||||
console.log('Could not mark notification as read:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
<svg aria-hidden="true" width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.25 8.75C5.58696 8.75 4.95107 9.01339 4.48223 9.48223C4.01339 9.95107 3.75 10.587 3.75 11.25V33.75C3.75 34.413 4.01339 35.0489 4.48223 35.5178C4.95107 35.9866 5.58696 36.25 6.25 36.25H33.75C34.413 36.25 35.0489 35.9866 35.5178 35.5178C35.9866 35.0489 36.25 34.413 36.25 33.75V11.25C36.25 10.587 35.9866 9.95107 35.5178 9.48223C35.0489 9.01339 34.413 8.75 33.75 8.75H28.75M3.75 18.75H36.25M11.25 3.75V13.75M28.75 3.75V13.75M11.25 8.75H23.75" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<p class="mb-32">{{ t('meetings.none') }}</p>
|
||||
<button class="btn">{{ t('buttons.requestMeeting') }}</button>
|
||||
<p class="mb-32">Vous n’avez aucune réunion programmée</p>
|
||||
<button class="btn">Demander un RDV</button>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
|
@ -21,23 +21,21 @@ import { usePageStore } from "../stores/page"
|
|||
import { useUserStore } from "../stores/user";
|
||||
import { ref, computed } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { page } = storeToRefs(usePageStore());
|
||||
const user = useUserStore().user;
|
||||
const currentTab = ref("future");
|
||||
const tabs = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: t('meetings.upcoming'),
|
||||
label: "À venir",
|
||||
id: "future",
|
||||
icon: null,
|
||||
count: null,
|
||||
isActive: currentTab.value === "future",
|
||||
},
|
||||
{
|
||||
label: t('meetings.past'),
|
||||
label: "Passées",
|
||||
id: "past",
|
||||
icon: null,
|
||||
count: null,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export default defineConfig(({ mode }) => {
|
|||
},
|
||||
},
|
||||
sourcemap: mode === 'staging',
|
||||
minify: mode === 'production',
|
||||
minify: mode === 'production' ? 'esbuild' : false,
|
||||
},
|
||||
server: {
|
||||
cors: true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue