Compare commits
275 Commits
create-acc
...
quick_fixe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1025c907b5 | ||
| 77019896e5 | |||
| 81025dca42 | |||
| 6dd8ce730f | |||
| d78dc14a2b | |||
| 355d5ea18d | |||
| 8580070dc4 | |||
| 12b8c100c9 | |||
| 7c6f5c8739 | |||
| 50903c7e39 | |||
| da76c1ac3e | |||
| 23fe47f69f | |||
| a9976ca624 | |||
| 4f86b26890 | |||
| 662f3820d5 | |||
| 8fdd756d86 | |||
| f7b9129401 | |||
| 09b1c29788 | |||
| 96ee5b03e6 | |||
| 8a87fe38c5 | |||
| bbbca27009 | |||
| e1d220596e | |||
| d45cf7c530 | |||
| 90376e364a | |||
| 0970d1b0da | |||
| 6f508b4b8b | |||
| 584735ca02 | |||
| 4aa4079ec8 | |||
| 04996ceaa5 | |||
| 3eeef3fc9a | |||
| d072eb0831 | |||
| 225dd27c2c | |||
| 8512b90d36 | |||
| 740a29688d | |||
| 86b488b492 | |||
| 592759a9b6 | |||
| 95e7044e0a | |||
| f610a1bfa6 | |||
| 6137b99d56 | |||
| 08bc930675 | |||
| 2ab9bd2976 | |||
| 99e7793fbb | |||
| 8c827944a2 | |||
| de7a55e7bc | |||
| bd0c40241f | |||
| c09fa6f2f8 | |||
| 3a61ffe7a6 | |||
| b473ddeefe | |||
| 6419e4e1c9 | |||
| f75c87bd09 | |||
| bd0e3b9114 | |||
| 129b7cf32e | |||
| 61c4c7c831 | |||
| 8cc016c2a5 | |||
| af3503db86 | |||
| 26b5eaaf33 | |||
| bbb0c12506 | |||
| 614569f5aa | |||
| 465a4a3c18 | |||
| dddbe04a2d | |||
| f78ed88cb1 | |||
| 696fc5833c | |||
| 059f3e2e33 | |||
| 9d30e84bd2 | |||
| 7ea4ef1920 | |||
| 79633ed923 | |||
| 412c855777 | |||
| d9daa00b32 | |||
| 31b88865d7 | |||
| cd4a971d8d | |||
| e74ce0aabc | |||
|
|
0d473cf3d1 | ||
|
|
457994c506 | ||
|
|
5fc485e233 | ||
|
|
0d934e7b6e | ||
| 02d28d46bb | |||
| 723f4d5d85 | |||
|
|
6f9fa60e2f | ||
|
|
e729e32b35 | ||
|
|
e4681f91e4 | ||
|
|
6363ec1189 | ||
|
|
c8ac815e2b | ||
|
|
ef31cba983 | ||
|
|
47c7d31249 | ||
|
|
ede8d95fd1 | ||
|
|
0fc7b6e4c3 | ||
|
|
3f64369852 | ||
|
|
e8c2d1a05a | ||
|
|
63ee4ce719 | ||
|
|
e0e186f4f4 | ||
|
|
bfca596e8b | ||
|
|
acb9739a80 | ||
|
|
c422881cd1 | ||
|
|
19da967605 | ||
|
|
d4223ce604 | ||
|
|
420979e63e | ||
|
|
1c92a40984 | ||
|
|
046eef18e6 | ||
|
|
2ba7be8dbb | ||
|
|
77d9c1ad43 | ||
|
|
3ce412d814 | ||
|
|
7100eda272 | ||
|
|
1a3a2dbef1 | ||
|
|
76a1d38e09 | ||
|
|
8a0a8e2df2 | ||
|
|
48194dd2de | ||
|
|
8e9d7f0c76 | ||
|
|
eda7102ded | ||
| ec99d101ab | |||
|
|
0dd928d28b | ||
|
|
5ba45a29be | ||
|
|
8541427b87 | ||
| 7b86318dec | |||
|
|
205796d22a | ||
| b072495cea | |||
|
|
9a601056b7 | ||
|
|
d3e207c6da | ||
|
|
cb5297e6fe | ||
|
|
f0151fa55e | ||
|
|
5192745a48 | ||
|
|
a027004bd0 | ||
|
|
aae11200d4 | ||
|
|
dbb7f67154 | ||
|
|
58fed7a53b | ||
|
|
19b2ab994e | ||
|
|
93d610e942 | ||
|
|
1dad1d4e2b | ||
|
|
5a98fac745 | ||
|
|
18d46531a0 | ||
|
|
62ccfec315 | ||
|
|
e9fc0b8454 | ||
|
|
5119d04243 | ||
|
|
5a8c31df32 | ||
|
|
deebcefc3d | ||
|
|
d9b8817ecc | ||
|
|
d8c2b22c3d | ||
|
|
39f24114e1 | ||
|
|
189bd3d252 | ||
|
|
989263d44a | ||
|
|
7391a08a01 | ||
|
|
4e109e8fba | ||
|
|
13b605a850 | ||
|
|
0a860bd559 | ||
|
|
a8b0248b5f | ||
|
|
0dc3c83c3c | ||
|
|
1a87a4db14 | ||
|
|
67cd7a1662 | ||
|
|
44f0d8c6c9 | ||
|
|
10589b056f | ||
|
|
926f41d270 | ||
|
|
7c39795cef | ||
|
|
207b308173 | ||
|
|
337a6adc60 | ||
|
|
d8422de94e | ||
|
|
9edcc2e897 | ||
|
|
f5fae245e2 | ||
| ed4fa732f7 | |||
| ac11893e93 | |||
| 929e7ee36d | |||
| c2a4b598a7 | |||
| 2bd2fdff98 | |||
| 13731da7e1 | |||
|
|
965f5da9a9 | ||
|
|
18ef18db71 | ||
|
|
50a92995d7 | ||
|
|
17bdcec317 | ||
|
|
25caed410e | ||
|
|
cf57681c31 | ||
|
|
91ba7205cc | ||
|
|
d31e18d4ae | ||
|
|
6076c342f8 | ||
|
|
bb5d3ff16d | ||
|
|
a3fe29e4a0 | ||
|
|
0d51f9d056 | ||
|
|
c0d402b234 | ||
|
|
dfae77de58 | ||
|
|
e1494d5bf4 | ||
|
|
ed23adf8f1 | ||
|
|
2a7c0d6675 | ||
|
|
25dba4e67b | ||
|
|
65d43686cb | ||
|
|
18e82de549 | ||
| f4d8f8652f | |||
| 39f2b086b5 | |||
| 00bc3d8ad2 | |||
| b52ff937f0 | |||
| d6e06f3594 | |||
| 05f13224fa | |||
| 06295fe591 | |||
| 72d43210de | |||
| 73cee5d144 | |||
| 85fe8cc251 | |||
| ec9fe0f62c | |||
| b6a2a5fc3b | |||
| 7417aec7e0 | |||
| f42aca7eb9 | |||
| 0f0b5d1af3 | |||
| 84aa6298e3 | |||
| 14b539595f | |||
| 99400a71f7 | |||
| c5b58d999f | |||
| 23a3b2a9e8 | |||
| 6167d59501 | |||
| b828e5197a | |||
| 26ba3e6e93 | |||
| df726d929a | |||
| 0e44a01218 | |||
| 8260c6c5da | |||
| 8eb6f36b64 | |||
| e15da5c22a | |||
| a8b3631dc1 | |||
| 89e9b3e4e0 | |||
| c4db22f626 | |||
| accd427cab | |||
| 381dcdf7a8 | |||
| 0cbc07cf63 | |||
| 3c59105aa6 | |||
| 325d2cbf13 | |||
| d4f1f36376 | |||
| f6edadc535 | |||
| 0099a8c858 | |||
| 0e0c3946d2 | |||
| 0a2a2674f8 | |||
| 9d461d63d7 | |||
| 2f68c652dd | |||
| 147f4cfa7d | |||
| 235aecd6a7 | |||
| e1f2483924 | |||
| 0c2df347ec | |||
| abfe581f29 | |||
| b66ee42ddd | |||
| aecdcd93e1 | |||
| c63e2a6fe9 | |||
| 67963bfb02 | |||
| 4b12b560e1 | |||
| 28c151254c | |||
| 5d0c617bbb | |||
| ae88959496 | |||
| e5a958b0b9 | |||
| 6b77ec2972 | |||
| a1ce472cad | |||
| db48386f05 | |||
| 39b50d6789 | |||
| 86393e6cfa | |||
| bf06b6634a | |||
| cfc9514656 | |||
| 0f364c7c6e | |||
| ee7c79a7d5 | |||
| 37bdb3dad3 | |||
| ecba13594b | |||
| 4c534973d2 | |||
| eca4d4de85 | |||
| 824a0b88f6 | |||
| e224921f86 | |||
| cf18e46e17 | |||
| e6cf1c3658 | |||
| b9851c587e | |||
| d601d94bf6 | |||
| d19ba72b4a | |||
| 2d0e15533a | |||
| 94ee8842e3 | |||
| 7b7d13ce6c | |||
| cc9396c4b8 | |||
| 51c906866e | |||
| 3f42cb27a7 | |||
| 2601418aaf | |||
| 455fe53fe2 | |||
| 2f847514f0 | |||
| a0888f8c90 | |||
| f2e2aeaa9a | |||
| 2855365851 | |||
| bb277706fd | |||
| 05dddd9567 | |||
| d54ce71f02 | |||
| a42141246d |
3
.env
3
.env
@ -1,3 +0,0 @@
|
||||
# .env
|
||||
VITE_API_URL=https://api.example.com
|
||||
VITE_API_KEY=your_api_key
|
||||
4
.env.exemple
Normal file
4
.env.exemple
Normal file
@ -0,0 +1,4 @@
|
||||
VITE_BASEURL="your_base_url"
|
||||
VITE_BOOTSTRAPURL="your_bootstrap_url"
|
||||
VITE_STORAGEURL="your_storage_url"
|
||||
VITE_BLINDBITURL="your_blindbit_url"
|
||||
44
.github/workflows/dev.yml
vendored
Normal file
44
.github/workflows/dev.yml
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
name: Build and Push to Registry
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ dev ]
|
||||
|
||||
env:
|
||||
REGISTRY: git.4nkweb.com
|
||||
IMAGE_NAME: 4nk/ihm_client
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up SSH agent
|
||||
uses: webfactory/ssh-agent@v0.9.1
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.USER }}
|
||||
password: ${{ secrets.TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
ssh: default
|
||||
build-args: |
|
||||
ENV_VARS=${{ secrets.ENV_VARS }}
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ gitea.sha }}
|
||||
98
.gitignore
vendored
98
.gitignore
vendored
@ -1,7 +1,103 @@
|
||||
# ----------------------------
|
||||
# 🦀 Rust
|
||||
# ----------------------------
|
||||
target/
|
||||
pkg/
|
||||
Cargo.lock
|
||||
*.rs.bk
|
||||
**/*.rlib
|
||||
|
||||
# ----------------------------
|
||||
# 🧰 Node / Frontend
|
||||
# ----------------------------
|
||||
node_modules/
|
||||
dist/
|
||||
.vscode
|
||||
build/
|
||||
.cache/
|
||||
.next/
|
||||
out/
|
||||
.tmp/
|
||||
temp/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
|
||||
# ----------------------------
|
||||
# 🧱 IDE / Éditeurs
|
||||
# ----------------------------
|
||||
.vscode/
|
||||
.idea/
|
||||
*.iml
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# ----------------------------
|
||||
# ⚙️ Environnements / Secrets
|
||||
# ----------------------------
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.production.local
|
||||
.env.test.local
|
||||
*.pem
|
||||
*.crt
|
||||
*.key
|
||||
|
||||
# ----------------------------
|
||||
# 🌐 SSL / Certificats
|
||||
# ----------------------------
|
||||
public/ssl/
|
||||
certs/
|
||||
keys/
|
||||
|
||||
# ----------------------------
|
||||
# 📦 Compilations WebAssembly
|
||||
# ----------------------------
|
||||
wasm-pack.log
|
||||
*.wasm
|
||||
|
||||
# ----------------------------
|
||||
# 🧪 Tests / Coverage
|
||||
# ----------------------------
|
||||
coverage/
|
||||
lcov-report/
|
||||
.nyc_output/
|
||||
jest-cache/
|
||||
jest-results.json
|
||||
|
||||
# ----------------------------
|
||||
# 🧍 Runtime / OS / Divers
|
||||
# ----------------------------
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
*.bak
|
||||
*.orig
|
||||
*.rej
|
||||
|
||||
# ----------------------------
|
||||
# 🧠 Logs / Debug / Dump
|
||||
# ----------------------------
|
||||
*.log
|
||||
*.stackdump
|
||||
*.dmp
|
||||
debug.log
|
||||
error.log
|
||||
|
||||
# ----------------------------
|
||||
# 🚀 Deploy / Production builds
|
||||
# ----------------------------
|
||||
.vercel/
|
||||
.netlify/
|
||||
firebase/
|
||||
functions/lib/
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
# Add files here to ignore them from prettier formatting
|
||||
/dist
|
||||
/coverage
|
||||
/.nx/cache
|
||||
.angular
|
||||
14
.prettierrc
14
.prettierrc
@ -1,14 +0,0 @@
|
||||
{
|
||||
"printWidth": 300,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"requirePragma": false,
|
||||
"insertPragma": false,
|
||||
"endOfLine": "crlf"
|
||||
}
|
||||
62
Dockerfile
62
Dockerfile
@ -1,13 +1,61 @@
|
||||
FROM node:20
|
||||
# syntax=docker/dockerfile:1.4
|
||||
FROM rust:1.82-alpine AS wasm-builder
|
||||
WORKDIR /build
|
||||
|
||||
ENV TZ=Europe/Paris
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
# Installation des dépendances nécessaires pour la compilation
|
||||
RUN apk update && apk add --no-cache \
|
||||
git \
|
||||
openssh-client \
|
||||
curl \
|
||||
nodejs \
|
||||
npm \
|
||||
build-base \
|
||||
pkgconfig \
|
||||
clang \
|
||||
llvm \
|
||||
musl-dev \
|
||||
nginx
|
||||
|
||||
# use this user because he have uid et gid 1000 like theradia
|
||||
USER node
|
||||
# Installation de wasm-pack
|
||||
RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
# Configuration SSH basique
|
||||
RUN mkdir -p /root/.ssh && \
|
||||
ssh-keyscan git.4nkweb.com >> /root/.ssh/known_hosts
|
||||
|
||||
# On se place dans le bon répertoire parent
|
||||
WORKDIR /build
|
||||
# Copie du projet ihm_client
|
||||
COPY . ihm_client/
|
||||
|
||||
# Clonage du sdk_client au même niveau que ihm_client en utilisant la clé SSH montée
|
||||
RUN --mount=type=ssh git clone -b dev ssh://git@git.4nkweb.com/4nk/sdk_client.git
|
||||
|
||||
# Build du WebAssembly avec accès SSH pour les dépendances
|
||||
WORKDIR /build/sdk_client
|
||||
RUN --mount=type=ssh wasm-pack build --out-dir ../ihm_client/pkg --target bundler --dev
|
||||
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
|
||||
CMD ["npm", "start"]
|
||||
# "--disable-host-check", "--host", "0.0.0.0", "--ssl", "--ssl-cert", "/ssl/certs/site.crt", "--ssl-key", "/ssl/private/site.dec.key"]
|
||||
# Installation des dépendances nécessaires
|
||||
RUN apk update && apk add --no-cache git nginx
|
||||
|
||||
# Copie des fichiers du projet
|
||||
COPY --from=wasm-builder /build/ihm_client/pkg ./pkg
|
||||
COPY . .
|
||||
|
||||
# Installation des dépendances Node.js
|
||||
RUN npm install
|
||||
|
||||
# Copie de la configuration nginx
|
||||
COPY nginx.dev.conf /etc/nginx/http.d/default.conf
|
||||
|
||||
# Script de démarrage
|
||||
COPY start-dev.sh /start-dev.sh
|
||||
RUN chmod +x /start-dev.sh
|
||||
|
||||
EXPOSE 3003 80
|
||||
|
||||
CMD ["/start-dev.sh"]
|
||||
|
||||
|
||||
128
README.md
128
README.md
@ -1,52 +1,90 @@
|
||||
# ihm_client
|
||||
# 4NK Client SDK (Iframe & Standalone)
|
||||
|
||||
Une application **Web5 décentralisée** construite avec **TypeScript**, **Vite**, **Web Components** et **WebAssembly (Rust)**.
|
||||
Cette application est conçue pour fonctionner de manière autonome ou intégrée via Iframe en tant que SDK pour des applications tierces.
|
||||
|
||||
## 🏗 Architecture
|
||||
|
||||
## HOW TO START
|
||||
Le projet suit une architecture modulaire stricte (**Domain-Driven Design**) :
|
||||
|
||||
1 - clone sdk_common, commit name "doc pcd" from 28.10.2024
|
||||
2 - clone sdk_client, commit name "Ignore messages" from 17.10.2024
|
||||
3 - clone ihm_client_test3
|
||||
4 - cargo build in sdk_common
|
||||
5 - cargo run in sdk_client
|
||||
6 - npm run build_wasm in ihm_client_test3
|
||||
7 - npm run start in ihm_client_test3
|
||||
- **`src/services/core`** : Services bas niveau (Réseau `network.service.ts`, SDK WASM `sdk.service.ts`).
|
||||
- **`src/services/domain`** : Logique métier (Wallet `wallet.service.ts`, Process `process.service.ts`, Crypto `crypto.service.ts`).
|
||||
- **`src/services/service.ts`** : Façade principale (Singleton) orchestrant les sous-services.
|
||||
- **`src/services/iframe-controller.service.ts`** : Contrôleur dédié à la communication API via `postMessage` (mode Iframe).
|
||||
- **`src/service-workers`** : Gestionnaires d'arrière-plan modulaires (Base de données, Cache).
|
||||
- **`src/pages` & `src/components`** : Interface utilisateur basée sur des Web Components natifs (Shadow DOM) et un design system "Glassmorphism".
|
||||
- **`src/config`** : Configuration centralisée (`constants.ts`).
|
||||
|
||||
## USER STORIES
|
||||
## 🚀 Démarrage Rapide
|
||||
|
||||
1 - I can login with my adress device
|
||||
2 - I can login with QR code
|
||||
3 - J'accède à la page Process après ma connexion
|
||||
4 - Dans l'interface Process, je peux sélectionner un processus avec sa zone
|
||||
5 - Je reçois des notifications dans la page Process
|
||||
6 - Dans le menu, je peux importer mes données au format JSON
|
||||
7 - Dans le menu, je peux accèder à la page Account
|
||||
8 - Dans la page Account, je peux cliquer sur mon profil via la bulle de profil en haut à gauche et une popup de profil s'ouvre
|
||||
9 - Dans la popup de profil, je peux voir mes informations personnelles et modifier certaines d'entre elles dont le nom, le prénom
|
||||
10 - Dans la popup de profil, je peux changer ma photo de profil
|
||||
11 - Dans la popup de profil, je peux fermer la popup en cliquant sur le bouton "X" en haut à droite de la popup
|
||||
12 - Dans la popup de profil, je peux cliquer sur le bouton "Export User Data", ce qui me génère un fichier JSON
|
||||
13 - Dans la popup de profil, je peux cliquer sur le bouton "Delete Account", ce qui me demande à valider mon choix
|
||||
14 - Dans la popup de profil, je peux cliquer sur le bouton "Logout", ce qui me déconnecte
|
||||
15 - Dans la popup de profil, je peux cliquer sur le bouton "Export Recovery", ce qui me demandera de confirmer mon choix ou d'annuler, si je confirme, je dois retenir et écrire les 4 mots de récupération, le bouton ne sera plus accessible après cela
|
||||
16 - Dans l'onglet Pairing de la page Account, je peux ajouter un nouveau "device" en cliquant sur le bouton "Add Device"
|
||||
17 - Dans l'onglet Pairing de la page Account, je peux supprimer un "device" en cliquant sur l'emoji de la poubelle à côté du device que je souhaite supprimer
|
||||
18 - Dans l'onglet Pairing de la page Account, je peux cliquer sur le bouton "Scan QR Code" pour scanner le QR Code d'un nouveau device
|
||||
19 - Dans l'onglet Pairing de la page Account, je peux renommer un "Device" en cliquant sur son nom et en modifiant le nom
|
||||
20 - Dans l'onglet Wallet de la page Account, je peux ajouter un nouveau "wallet" en cliquant sur le bouton "Add a line"
|
||||
21 - Dans l'onglet Process de la page Account, je peux voir les Process disponibles et voir leur notifications en cliquant sur sur la sonnette à côté du processus
|
||||
22 - Dans l'onglet Data de la page Account, je peux voir les données importées
|
||||
23 - Je peux voir le contrat associé à une Data en cliquant sur le contrat dans la ligne de la Data
|
||||
24 - Dans le menu je peux accèder à la page Chat
|
||||
25 - Dans la page Chat, je peux voir les Processus
|
||||
26 - Dans les Processus, je peux voir utilisateurs assignés à un rôle
|
||||
27 - Dans les Processus, je peux envoyer des messages et des documents en cliquant sur le nom d'un utilisateur en en envoyant "send"
|
||||
28 - Dans le menu je peux accèder à la page "Signatures"
|
||||
29 - Je peux voir les documents à signer et vierge en cliquand sur l'emoji ⚙️ à côté du processus
|
||||
30 - En cliquand sur l'onglet d'un processus, je peux voir les rôles assignés à un utilisateur en cliquant dessus
|
||||
31 - En cliquand sur l'emoji 📁 à côté d'un rôle, je peux voir les documents associés à ce rôle
|
||||
32 - Dans la vue des documents associés à un rôle, je peux créer un évènement de nouvelle signature pour tous les rôles associés à ce Processus, avec le bouton "New Request"
|
||||
33 - En cliquant sur le bouton "New Request", une nouvelle fenêtre s'ouvre pour me permettre de rentrer la description, la visibilité, la date d'échéance, importer des documents, voir les signataires et "Request"
|
||||
34 - Dans le menu, je peux me déconnecter avec le bouton "Disconnect"
|
||||
### Prérequis
|
||||
- **Node.js** (v20+)
|
||||
- **Rust & Cargo** (pour la compilation WASM)
|
||||
- **Nginx** (pour la production ou le reverse-proxy local)
|
||||
|
||||
## TO DO
|
||||
### Installation
|
||||
|
||||
1. **Compiler le module WASM :**
|
||||
```bash
|
||||
npm run build_wasm
|
||||
```
|
||||
*Note : Cette commande compile le code Rust situé dans `../sdk_client` vers `./pkg`.*
|
||||
|
||||
2. **Installer les dépendances JS :**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Lancer en développement :**
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
L'application sera accessible sur `http://localhost:3003`.
|
||||
|
||||
## 📦 Build & Production
|
||||
|
||||
Pour créer une version de production optimisée dans le dossier `dist/` :
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Déploiement Nginx
|
||||
|
||||
Utilisez les fichiers de configuration fournis à la racine :
|
||||
|
||||
- **`nginx.dev.conf`** : Pour le développement local (proxy vers Vite).
|
||||
- **`nginx.prod.conf`** : Pour la production (SSL, Headers de sécurité, Service-Worker-Allowed).
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
La configuration de l'application est centralisée dans `src/config/constants.ts`.
|
||||
Vous pouvez surcharger les URLs via un fichier `.env` à la racine :
|
||||
|
||||
| Variable | Description |
|
||||
| :--- | :--- |
|
||||
| `VITE_BASEURL` | URL de base de l'infrastructure. |
|
||||
| `VITE_BOOTSTRAPURL` | URL du relais WebSocket. |
|
||||
| `VITE_STORAGEURL` | URL du stockage distant. |
|
||||
| `VITE_BLINDBITURL` | URL de l'indexeur Bitcoin. |
|
||||
|
||||
## 🧪 Fonctionnalités Clés
|
||||
|
||||
### 1\. Auto-Healing WebSocket
|
||||
|
||||
Le service réseau maintient la connexion active avec un système de "Heartbeat" et de reconnexion exponentielle automatique. Aucune action utilisateur requise en cas de coupure réseau.
|
||||
|
||||
### 2\. Iframe Persistence & Performance
|
||||
|
||||
En mode Iframe, l'application charge son état en mémoire et ne nécessite **aucun rafraîchissement**.
|
||||
|
||||
- Stratégie **Cache-First** : Les requêtes `GET_PROCESSES` répondent instantanément via le cache mémoire.
|
||||
- **Verrou d'initialisation** : Empêche les conflits si l'iframe est rechargée par erreur.
|
||||
|
||||
### 3\. Service Worker Modulaire
|
||||
|
||||
Le fichier `src/service-workers/sw.ts` agit comme un point d'entrée maître, important dynamiquement la logique de base de données (`database.ts`). Il inclut une logique de nettoyage automatique des anciens workers ("Zombie Killer").
|
||||
|
||||
### 4\. UX Moderne
|
||||
|
||||
Interface "Glassmorphism" responsive utilisant CSS Grid pour un Layout (App Shell) stable et des Web Components natifs pour l'isolation des styles (Shadow DOM).
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 1.9 MiB |
39
index.html
39
index.html
@ -1,26 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="author" content="4NK">
|
||||
<meta name="description" content="4NK Web5 Platform">
|
||||
<meta name="keywords" content="4NK web5 bitcoin blockchain decentralize dapps relay contract">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="./style/4nk.css">
|
||||
<script src="https://unpkg.com/html5-qrcode"></script>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="author" content="Nicolas Cantu, Sosthene, Omar, Titouan">
|
||||
<title>4NK Application</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header-container"></div>
|
||||
<div id="containerId" class="container">
|
||||
<!-- 4NK Web5 Solution -->
|
||||
</div>
|
||||
<!-- <script type="module" src="/src/index.ts"></script> -->
|
||||
<script type="module">
|
||||
import { init } from '/src/router.ts';
|
||||
(async () => {
|
||||
await init();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
<link rel="stylesheet" href="/src/assets/styles/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<app-layout>
|
||||
|
||||
<div id="header-slot" slot="header"></div>
|
||||
|
||||
<div id="app-container" slot="content" class="container"></div>
|
||||
|
||||
</app-layout>
|
||||
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
48
nginx.dev.conf
Normal file
48
nginx.dev.conf
Normal file
@ -0,0 +1,48 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
# Redirection des requêtes HTTP vers Vite
|
||||
location / {
|
||||
proxy_pass http://localhost:3003;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /ws/ {
|
||||
proxy_pass http://localhost:8090;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
|
||||
location /storage/ {
|
||||
rewrite ^/storage(/.*)$ $1 break;
|
||||
proxy_pass http://localhost:8081;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://localhost:8091;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# CORS headers
|
||||
add_header Access-Control-Allow-Origin "*" always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE" always;
|
||||
add_header Access-Control-Allow-Headers "Authorization,Content-Type,Accept,X-Requested-With" always;
|
||||
}
|
||||
}
|
||||
99
nginx.prod.conf
Normal file
99
nginx.prod.conf
Normal file
@ -0,0 +1,99 @@
|
||||
# --- 1. REDIRECTION HTTP VERS HTTPS ---
|
||||
server {
|
||||
listen 80;
|
||||
server_name dev2.4nkweb.com;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
# --- 2. CONFIGURATION HTTPS PRINCIPALE ---
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name dev2.4nkweb.com;
|
||||
|
||||
# Chemins des certificats SSL
|
||||
ssl_certificate /etc/letsencrypt/live/dev2.4nkweb.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/dev2.4nkweb.com/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
# --- LOCATION POUR VITE (Front-end + HMR WebSocket) ---
|
||||
location / {
|
||||
proxy_pass http://localhost:3003;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# --- LOCATION POUR L'AUTRE WEBSOCKET (port 8090) ---
|
||||
location /ws/ {
|
||||
proxy_pass http://localhost:8090;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
|
||||
# --- LOCATION POUR SDK_STORAGE (port 8081) ---
|
||||
location /storage/ {
|
||||
# Gestion du préflight CORS
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
|
||||
add_header 'Access-Control-Max-Age' 86400;
|
||||
add_header 'Content-Length' 0;
|
||||
add_header 'Content-Type' 'text/plain';
|
||||
return 204;
|
||||
}
|
||||
# Headers CORS pour les requêtes réelles
|
||||
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
|
||||
rewrite ^/storage(/.*)$ $1 break;
|
||||
proxy_pass http://localhost:8081;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# --- LOCATION POUR TON API (port 8091) ---
|
||||
location /api/ {
|
||||
proxy_pass http://localhost:8091;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# CORS headers
|
||||
add_header Access-Control-Allow-Origin "*" always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE" always;
|
||||
add_header Access-Control-Allow-Headers "Authorization,Content-Type,Accept,X-Requested-With" always;
|
||||
}
|
||||
|
||||
location /blindbit/ {
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
|
||||
add_header 'Access-Control-Max-Age' 86400;
|
||||
add_header 'Content-Length' 0;
|
||||
add_header 'Content-Type' 'text/plain';
|
||||
return 204;
|
||||
}
|
||||
|
||||
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
|
||||
|
||||
proxy_pass http://localhost:8000/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
}
|
||||
7185
package-lock.json
generated
7185
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
@ -1,45 +1,35 @@
|
||||
{
|
||||
"name": "sdk_client",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"description": "Client SDK 4NK - Web5 Platform",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build_wasm": "wasm-pack build --out-dir ../ihm_client_dev1/pkg ../sdk_client --target bundler --dev",
|
||||
"build_wasm": "wasm-pack build --out-dir ../ihm_client_dev2/pkg ../sdk_client --target bundler --dev",
|
||||
"start": "vite --host 0.0.0.0",
|
||||
"build": "tsc && vite build",
|
||||
"deploy": "sudo cp -r dist/* /var/www/html/",
|
||||
"prettify": "prettier --config ./.prettierrc --write \"src/**/*{.ts,.html,.css,.js}\""
|
||||
"build:dist": "tsc -p tsconfig.build.json"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"author": "Nicolas Cantu",
|
||||
"contributors": [
|
||||
"Sosthene",
|
||||
"Omar",
|
||||
"Titouan"
|
||||
],
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"prettier": "^3.3.3",
|
||||
"rimraf": "^6.0.1",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.4.11",
|
||||
"vite-plugin-static-copy": "^1.0.6",
|
||||
"webpack": "^5.90.3",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^5.0.2"
|
||||
"vite-plugin-static-copy": "^1.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/elements": "^19.0.1",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"axios": "^1.7.8",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"qr-scanner": "^1.4.2",
|
||||
"qrcode": "^1.5.3",
|
||||
"sweetalert2": "^11.14.5",
|
||||
"vite-plugin-copy": "^0.1.6",
|
||||
"vite-plugin-html": "^3.2.2",
|
||||
"comlink": "^4.4.2",
|
||||
"jose": "^6.0.11",
|
||||
"vite-plugin-wasm": "^3.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 67 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 509 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 73 KiB |
@ -1,34 +0,0 @@
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
|
||||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||
document.getElementById(tab.getAttribute('data-tab')).classList.add('active');
|
||||
});
|
||||
});
|
||||
function toggleMenu() {
|
||||
var menu = document.getElementById('menu');
|
||||
if (menu.style.display === 'block') {
|
||||
menu.style.display = 'none';
|
||||
} else {
|
||||
menu.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
//// Modal
|
||||
function openModal() {
|
||||
document.getElementById('modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('modal').style.display = 'none';
|
||||
}
|
||||
|
||||
// Close modal when clicking outside of it
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('modal');
|
||||
if (event.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.9 KiB |
202
public/data.worker.js
Normal file
202
public/data.worker.js
Normal file
@ -0,0 +1,202 @@
|
||||
// public/data.worker.js
|
||||
|
||||
const DB_NAME = "4nk";
|
||||
const DB_VERSION = 1;
|
||||
const EMPTY32BYTES = String("").padStart(64, "0");
|
||||
|
||||
// ============================================
|
||||
// SERVICE WORKER LIFECYCLE
|
||||
// ============================================
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(self.skipWaiting());
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// INDEXEDDB DIRECT ACCESS (READ-ONLY)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Ouvre une connexion à la BDD directement depuis le Service Worker
|
||||
*/
|
||||
function openDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un objet spécifique (équivalent à GET_OBJECT)
|
||||
*/
|
||||
function getObject(db, storeName, key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(storeName, "readonly");
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.get(key);
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère plusieurs objets d'un coup (équivalent à GET_MULTIPLE_OBJECTS)
|
||||
* Optimisé pour utiliser une seule transaction.
|
||||
*/
|
||||
function getMultipleObjects(db, storeName, keys) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(storeName, "readonly");
|
||||
const store = transaction.objectStore(storeName);
|
||||
const results = [];
|
||||
|
||||
let completed = 0;
|
||||
if (keys.length === 0) resolve([]);
|
||||
|
||||
keys.forEach((key) => {
|
||||
const request = store.get(key);
|
||||
request.onsuccess = () => {
|
||||
if (request.result) results.push(request.result);
|
||||
completed++;
|
||||
if (completed === keys.length) resolve(results);
|
||||
};
|
||||
request.onerror = () => {
|
||||
console.warn(`[SW] Erreur lecture clé ${key}`);
|
||||
completed++;
|
||||
if (completed === keys.length) resolve(results);
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SCAN LOGIC
|
||||
// ============================================
|
||||
|
||||
async function scanMissingData(processesToScan) {
|
||||
let db;
|
||||
try {
|
||||
db = await openDB();
|
||||
} catch (e) {
|
||||
console.error("[SW] Impossible d'ouvrir la BDD:", e);
|
||||
return { toDownload: [], diffsToCreate: [] };
|
||||
}
|
||||
|
||||
// 1. Récupération directe des processus
|
||||
const myProcesses = await getMultipleObjects(
|
||||
db,
|
||||
"processes",
|
||||
processesToScan
|
||||
);
|
||||
|
||||
let toDownload = new Set();
|
||||
let diffsToCreate = [];
|
||||
|
||||
if (myProcesses && myProcesses.length !== 0) {
|
||||
for (const process of myProcesses) {
|
||||
if (!process || !process.states) continue;
|
||||
|
||||
const firstState = process.states[0];
|
||||
if (!firstState) continue;
|
||||
|
||||
const processId = firstState.commited_in;
|
||||
|
||||
for (const state of process.states) {
|
||||
if (state.state_id === EMPTY32BYTES) continue;
|
||||
|
||||
for (const [field, hash] of Object.entries(state.pcd_commitment)) {
|
||||
if (
|
||||
(state.public_data && state.public_data[field] !== undefined) ||
|
||||
field === "roles"
|
||||
)
|
||||
continue;
|
||||
|
||||
// 2. Vérification directe dans 'data'
|
||||
const existingData = await getObject(db, "data", hash);
|
||||
|
||||
if (!existingData) {
|
||||
toDownload.add(hash);
|
||||
|
||||
// 3. Vérification directe dans 'diffs'
|
||||
const existingDiff = await getObject(db, "diffs", hash);
|
||||
|
||||
if (!existingDiff) {
|
||||
diffsToCreate.push({
|
||||
process_id: processId,
|
||||
state_id: state.state_id,
|
||||
value_commitment: hash,
|
||||
roles: state.roles,
|
||||
field: field,
|
||||
description: null,
|
||||
previous_value: null,
|
||||
new_value: null,
|
||||
notify_user: false,
|
||||
need_validation: false,
|
||||
validation_status: "None",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (toDownload.has(hash)) {
|
||||
toDownload.delete(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// On ferme la connexion BDD
|
||||
db.close();
|
||||
|
||||
// ✅ LOG PERTINENT UNIQUEMENT : On n'affiche que si on a trouvé quelque chose
|
||||
if (toDownload.size > 0 || diffsToCreate.length > 0) {
|
||||
console.log("[Service Worker] 🔄 Scan found items:", {
|
||||
toDownload: toDownload.size,
|
||||
diffsToCreate: diffsToCreate.length,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
toDownload: Array.from(toDownload),
|
||||
diffsToCreate: diffsToCreate,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MESSAGE HANDLER
|
||||
// ============================================
|
||||
|
||||
self.addEventListener("message", async (event) => {
|
||||
const data = event.data;
|
||||
|
||||
if (data.type === "SCAN") {
|
||||
try {
|
||||
const myProcessesId = data.payload;
|
||||
if (myProcessesId && myProcessesId.length !== 0) {
|
||||
// Appel direct de la nouvelle fonction optimisée
|
||||
const scanResult = await scanMissingData(myProcessesId);
|
||||
|
||||
if (scanResult.toDownload.length !== 0) {
|
||||
event.source.postMessage({
|
||||
type: "TO_DOWNLOAD",
|
||||
data: scanResult.toDownload,
|
||||
});
|
||||
}
|
||||
|
||||
if (scanResult.diffsToCreate.length > 0) {
|
||||
event.source.postMessage({
|
||||
type: "DIFFS_TO_CREATE",
|
||||
data: scanResult.diffsToCreate,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Service Worker] Scan error:", error);
|
||||
// On évite de spammer l'UI avec des erreurs internes du worker
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,784 +0,0 @@
|
||||
:root {
|
||||
--primary-color
|
||||
: #3A506B;
|
||||
/* Bleu métallique */
|
||||
--secondary-color
|
||||
: #B0BEC5;
|
||||
/* Gris acier */
|
||||
--accent-color
|
||||
: #D68C45;
|
||||
/* Cuivre */
|
||||
}
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f4f4f4;
|
||||
background-image: url(../assets/bgd.webp);
|
||||
background-repeat:no-repeat;
|
||||
background-size: cover;
|
||||
background-blend-mode :soft-light;
|
||||
height: 100vh;
|
||||
}
|
||||
.message {
|
||||
margin: 30px 0;
|
||||
font-size: 14px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.message strong{
|
||||
font-family: "Segoe UI Emoji", "Noto Color Emoji", "Apple Color Emoji", sans-serif;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/** Modal Css */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 55%;
|
||||
height: 30%;
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
margin: 0;
|
||||
padding-bottom: 8px;
|
||||
width: 100%;
|
||||
font-size: 0.9em;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.confirmation-box {
|
||||
/* margin-top: 20px; */
|
||||
align-content: center;
|
||||
width: 70%;
|
||||
height: 20%;
|
||||
/* padding: 20px; */
|
||||
font-size: 1.5em;
|
||||
color: #333333;
|
||||
top: 5%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
.nav-wrapper {
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
background: radial-gradient(circle, white, var(--primary-color));
|
||||
/* background-color: #CFD8DC; */
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
color: #37474F;
|
||||
height: 9vh;
|
||||
width: 100vw;
|
||||
left: 0;
|
||||
top: 0;
|
||||
box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, .2), 0px 16px 24px 2px rgba(0, 0, 0, .14), 0px 6px 30px 5px rgba(0, 0, 0, .12);
|
||||
|
||||
.nav-right-icons {
|
||||
display: flex;
|
||||
.notification-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.notification-bell, .burger-menu {
|
||||
z-index: 3;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: -.7rem;
|
||||
left: -.8rem;
|
||||
background-color: red;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
padding: 2.5px 6px;
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.notification-board {
|
||||
position: absolute;
|
||||
width: 20rem;
|
||||
min-height: 8rem;
|
||||
background-color: white;
|
||||
right: 0.5rem;
|
||||
display: none;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
display: none;
|
||||
|
||||
.notification-element {
|
||||
padding: .8rem 0;
|
||||
width: 100%;
|
||||
&:hover {
|
||||
background-color: rgba(26, 28, 24, .08);
|
||||
}
|
||||
}
|
||||
.notification-element:not(:last-child) {
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
height: 100%;
|
||||
width: 100vw;
|
||||
align-content: center;
|
||||
position: relative;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
display: grid;
|
||||
height: 100vh;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 10px;
|
||||
grid-auto-rows: 10vh 15vh 1fr;
|
||||
}
|
||||
.title-container {
|
||||
grid-column: 2 / 7;
|
||||
grid-row: 2;
|
||||
}
|
||||
.page-container {
|
||||
grid-column: 2 / 7;
|
||||
grid-row: 3 ;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 20px 0;
|
||||
}
|
||||
@media only screen and (min-width: 600px) {
|
||||
.tab-container {
|
||||
display: none;
|
||||
}
|
||||
.page-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.process-container {
|
||||
grid-column: 3 / 6;
|
||||
grid-row: 3 ;
|
||||
|
||||
.card {
|
||||
min-width: 40vw;
|
||||
}
|
||||
}
|
||||
.separator {
|
||||
width: 2px;
|
||||
background-color: #78909C;
|
||||
height: 80%;
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
.tab-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
height: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.process-container {
|
||||
grid-column: 2 / 7;
|
||||
grid-row: 3 ;
|
||||
}
|
||||
.container {
|
||||
grid-auto-rows: 10vh 15vh 15vh 1fr;
|
||||
}
|
||||
.tab-container {
|
||||
grid-column: 1 / 8;
|
||||
grid-row: 3;
|
||||
}
|
||||
.page-container {
|
||||
grid-column: 2 / 7;
|
||||
grid-row: 4 ;
|
||||
}
|
||||
.separator {
|
||||
display: none;
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #E0E4D6;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
color: #6200ea;
|
||||
&:hover {
|
||||
background-color: rgba(26, 28, 24, .08);
|
||||
}
|
||||
}
|
||||
.tab.active {
|
||||
border-bottom: 2px solid #6200ea;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.card.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 80%;
|
||||
}
|
||||
.modal-content {
|
||||
width: 80%;
|
||||
height: 20%;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.emoji-display {
|
||||
font-family: "Segoe UI Emoji", "Noto Color Emoji", "Apple Color Emoji", sans-serif;
|
||||
font-size: 20px;
|
||||
|
||||
}
|
||||
|
||||
#emoji-display-2{
|
||||
margin-top: 30px;
|
||||
font-family: "Segoe UI Emoji", "Noto Color Emoji", "Apple Color Emoji", sans-serif;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#okButton{
|
||||
margin-bottom: 2em;
|
||||
cursor: pointer;
|
||||
background-color: #D0D0D7;
|
||||
color: white;
|
||||
border-style: none;
|
||||
border-radius: 5px;
|
||||
color: #000;
|
||||
padding: 2px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.pairing-request {
|
||||
font-family: "Segoe UI Emoji", "Noto Color Emoji", "Apple Color Emoji", sans-serif;
|
||||
font-size: 14px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.sp-address-btn {
|
||||
margin-bottom: 2em;
|
||||
cursor: pointer;
|
||||
background-color: #D0D0D7;
|
||||
color: white;
|
||||
border-style: none;
|
||||
border-radius: 5px;
|
||||
color: #000;
|
||||
padding: 2px;
|
||||
|
||||
}
|
||||
|
||||
.camera-card {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
/* height: 200px; */
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #3700b3;
|
||||
}
|
||||
|
||||
|
||||
.card {
|
||||
min-width: 300px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background-color: white;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
height: 60vh;
|
||||
justify-content: flex-start;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
font-size: .8em;
|
||||
position: relative;
|
||||
left: 2vw;
|
||||
width: 90%;
|
||||
.process-title {
|
||||
font-weight: bold;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
.process-element {
|
||||
padding: .4rem 0;
|
||||
&:hover {
|
||||
background-color: rgba(26, 28, 24, .08);
|
||||
}
|
||||
&.selected {
|
||||
background-color: rgba(26, 28, 24, .08);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-description {
|
||||
padding: 20px;
|
||||
font-size: 1em;
|
||||
color: #333;
|
||||
width: 90%;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
|
||||
.card-action {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 3.4rem;
|
||||
right: 1rem;
|
||||
background-color: white;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menu-content a {
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
&:hover {
|
||||
background-color: rgba(26, 28, 24, .08);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-content a:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.qr-code-scanner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/* QR READER */
|
||||
#qr-reader div {
|
||||
position: inherit;
|
||||
}
|
||||
|
||||
#qr-reader div img{
|
||||
top: 15px ;
|
||||
right: 25px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
|
||||
/* INPUT CSS **/
|
||||
.input-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #ECEFF1;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 36vw;
|
||||
padding: 10px 0;
|
||||
font-size: 1em;
|
||||
border: none;
|
||||
border-bottom: 1px solid #ccc;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
border-bottom: 2px solid #6200ea;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
position: absolute;
|
||||
margin-top: -0.5em;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 10px 0;
|
||||
font-size: 1em;
|
||||
color: #999;
|
||||
pointer-events: none;
|
||||
transition: transform 0.3s, color 0.3s, font-size 0.3s;
|
||||
}
|
||||
|
||||
.input-field:focus + .input-label,
|
||||
.input-field:not(:placeholder-shown) + .input-label {
|
||||
transform: translateY(-20px);
|
||||
font-size: 0.8em;
|
||||
color: #6200ea;
|
||||
}
|
||||
|
||||
.input-underline {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background-color: #6200ea;
|
||||
transition: width 0.3s, left 0.3s;
|
||||
}
|
||||
|
||||
.input-field:focus ~ .input-underline {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
position: absolute;
|
||||
flex-direction: column;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
display: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dropdown-content span {
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.dropdown-content span:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/** AUTOCOMPLETE **/
|
||||
|
||||
select[data-multi-select-plugin] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.multi-select-component {
|
||||
width: 36vw;
|
||||
padding: 5px 0;
|
||||
font-size: 1em;
|
||||
border: none;
|
||||
border-bottom: 1px solid #ccc;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
-o-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
|
||||
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
|
||||
}
|
||||
|
||||
.autocomplete-list {
|
||||
border-radius: 4px 0px 0px 4px;
|
||||
}
|
||||
|
||||
.multi-select-component:focus-within {
|
||||
box-shadow: inset 0px 0px 0px 2px #78ABFE;
|
||||
}
|
||||
|
||||
.multi-select-component .btn-group {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.multiselect-native-select .multiselect-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selected-processes {
|
||||
background-color: white;
|
||||
padding: 0.4em;
|
||||
}
|
||||
|
||||
.selected-wrapper {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
border: 1px solid #d9d9d9;
|
||||
background-color: #ededed;
|
||||
white-space: nowrap;
|
||||
margin: 1px 5px 5px 0;
|
||||
height: 22px;
|
||||
vertical-align: top;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.selected-wrapper .selected-label {
|
||||
max-width: 514px;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-left: 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.selected-wrapper .selected-close {
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
line-height: 1.49em;
|
||||
margin-left: 5px;
|
||||
padding-bottom: 10px;
|
||||
height: 100%;
|
||||
vertical-align: top;
|
||||
padding-right: 4px;
|
||||
opacity: 0.2;
|
||||
color: #000;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.search-container .selected-input {
|
||||
background: none;
|
||||
border: 0;
|
||||
height: 20px;
|
||||
width: 60px;
|
||||
padding: 0;
|
||||
margin-bottom: 6px;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.search-container .selected-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dropdown-icon.active {
|
||||
transform: rotateX(180deg)
|
||||
}
|
||||
|
||||
.search-container .dropdown-icon {
|
||||
display: inline-block;
|
||||
padding: 10px 5px;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 0 !important;
|
||||
/* needed */
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
/* SVG background image */
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%3E%3Ctitle%3Edown-arrow%3C%2Ftitle%3E%3Cg%20fill%3D%22%23818181%22%3E%3Cpath%20d%3D%22M10.293%2C3.293%2C6%2C7.586%2C1.707%2C3.293A1%2C1%2C0%2C0%2C0%2C.293%2C4.707l5%2C5a1%2C1%2C0%2C0%2C0%2C1.414%2C0l5-5a1%2C1%2C0%2C1%2C0-1.414-1.414Z%22%20fill%3D%22%23818181%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
background-position: center;
|
||||
background-size: 10px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.search-container ul {
|
||||
position: absolute;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
z-index: 3;
|
||||
margin-top: 29px;
|
||||
width: 100%;
|
||||
right: 0px;
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
|
||||
}
|
||||
|
||||
.search-container ul :focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-container ul li {
|
||||
display: block;
|
||||
text-align: left;
|
||||
padding: 8px 29px 2px 12px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
font-size: 14px;
|
||||
min-height: 31px;
|
||||
}
|
||||
|
||||
.search-container ul li:first-child {
|
||||
border-top: 1px solid #ccc;
|
||||
border-radius: 4px 0px 0 0;
|
||||
}
|
||||
|
||||
.search-container ul li:last-child {
|
||||
border-radius: 4px 0px 0 0;
|
||||
}
|
||||
|
||||
|
||||
.search-container ul li:hover.not-cursor {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.search-container ul li:hover {
|
||||
color: #333;
|
||||
background-color: #f0f0f0;
|
||||
;
|
||||
border-color: #adadad;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Adding scrool to select options */
|
||||
.autocomplete-list {
|
||||
max-height: 130px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**************************************** Process page card ******************************************************/
|
||||
.process-card {
|
||||
min-width: 300px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background-color: white;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
min-height: 40vh;
|
||||
max-height: 60vh;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
|
||||
}
|
||||
|
||||
.process-card-content {
|
||||
text-align: left;
|
||||
font-size: .8em;
|
||||
position: relative;
|
||||
left: 2vw;
|
||||
width: 90%;
|
||||
.process-title {
|
||||
font-weight: bold;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
.process-element {
|
||||
padding: .4rem 0;
|
||||
&:hover {
|
||||
background-color: rgba(26, 28, 24, .08);
|
||||
}
|
||||
&.selected {
|
||||
background-color: rgba(26, 28, 24, .08);
|
||||
}
|
||||
}
|
||||
.selected-process-zone {
|
||||
background-color: rgba(26, 28, 24, .08);
|
||||
}
|
||||
}
|
||||
|
||||
.process-card-description {
|
||||
padding: 20px;
|
||||
font-size: 1em;
|
||||
color: #333;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
|
||||
.process-card-action {
|
||||
width: 100%;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,597 +0,0 @@
|
||||
/* Styles de base */
|
||||
:root {
|
||||
--primary-color: #3A506B;
|
||||
/* Bleu métallique */
|
||||
--secondary-color: #B0BEC5;
|
||||
/* Gris acier */
|
||||
--accent-color: #D68C45;
|
||||
/* Cuivre */
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
/* 4NK NAVBAR */
|
||||
|
||||
.brand-logo {
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nav-wrapper {
|
||||
position: fixed;
|
||||
background: radial-gradient(circle, white, var(--primary-color));
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: #37474F;
|
||||
height: 9vh;
|
||||
width: 100vw;
|
||||
left: 0;
|
||||
top: 0;
|
||||
box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, .2), 0px 16px 24px 2px rgba(0, 0, 0, .14), 0px 6px 30px 5px rgba(0, 0, 0, .12);
|
||||
}
|
||||
|
||||
/* Icônes de la barre de navigation */
|
||||
.nav-right-icons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.notification-bell,
|
||||
.burger-menu {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin-right: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notification-container {
|
||||
position: relative;
|
||||
/* Conserve la position pour le notification-board */
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.notification-board {
|
||||
position: absolute;
|
||||
/* Position absolue pour le placer par rapport au container */
|
||||
top: 40px;
|
||||
right: 0;
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
width: 200px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
/* Scroll si les notifications dépassent la taille */
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 10;
|
||||
/* Définit la priorité d'affichage au-dessus des autres éléments */
|
||||
display: none;
|
||||
/* Par défaut, la notification est masquée */
|
||||
}
|
||||
|
||||
.notification-item{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: -18px;
|
||||
right: 35px;
|
||||
background-color: red;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
display: none;
|
||||
/* S'affiche seulement lorsqu'il y a des notifications */
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Par défaut, le menu est masqué */
|
||||
#menu {
|
||||
display: none;
|
||||
/* Menu caché par défaut */
|
||||
transition: display 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.burger-menu {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Icône burger */
|
||||
#burger-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 3.4rem;
|
||||
right: 1rem;
|
||||
background-color: white;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menu-content a {
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(26, 28, 24, .08);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-content a:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Ajustement pour la barre de navigation fixe */
|
||||
.container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: 90vh;
|
||||
margin-top: 9vh;
|
||||
margin-left: -1%;
|
||||
text-align: left;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
|
||||
/* Liste des groupes */
|
||||
|
||||
.group-list {
|
||||
width: 25%;
|
||||
background-color: #1f2c3d;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
border-right: 2px solid #2c3e50;
|
||||
flex-shrink: 0;
|
||||
padding-right: 10px;
|
||||
height: 91vh;
|
||||
}
|
||||
.group-list ul {
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
padding-right: 10px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.group-list li {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
background-color: #273646;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.group-list li:hover {
|
||||
background-color: #34495e;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
|
||||
.group-list .member-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.group-list .member-container button {
|
||||
margin-left: 40px;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: 0px solid var(--primary-color);
|
||||
border-radius: 50px;
|
||||
position: absolute;
|
||||
top: -25px;
|
||||
right: -25px;
|
||||
}
|
||||
|
||||
.group-list .member-container button:hover {
|
||||
background: var(--accent-color)
|
||||
}
|
||||
|
||||
|
||||
/* Zone de chat */
|
||||
.chat-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background-color:#f1f1f1;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
margin: 1% 0% 0.5% 1%;
|
||||
}
|
||||
|
||||
/* En-tête du chat */
|
||||
.chat-header {
|
||||
background-color: #34495e;
|
||||
color: white;
|
||||
padding: 15px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
border-radius: 10px 10px 0 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
.messages {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
background-color: #f1f1f1;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
display: flex;
|
||||
margin: 8px;
|
||||
}
|
||||
.message-container .message {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.message-container .message.user {
|
||||
align-self: flex-end;
|
||||
margin-left: auto;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 70%;
|
||||
padding: 10px;
|
||||
border-radius: 12px;
|
||||
background:var(--secondary-color);
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
/* Messages de l'utilisateur */
|
||||
.message.user {
|
||||
background: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 0.7em;
|
||||
opacity: 0.7;
|
||||
margin-left: 0px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
|
||||
/* Amélioration de l'esthétique des messages */
|
||||
/* .message.user:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: -10px;
|
||||
border: 10px solid transparent;
|
||||
border-left-color: #3498db;
|
||||
} */
|
||||
|
||||
/* Zone de saisie */
|
||||
.input-area {
|
||||
padding: 10px;
|
||||
background-color: #bdc3c7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
margin: 1%;
|
||||
/* Alignement vertical */
|
||||
}
|
||||
|
||||
.input-area input[type="text"] {
|
||||
flex: 1;
|
||||
/* Prend l'espace restant */
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.input-area .attachment-icon {
|
||||
margin: 0 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-area button {
|
||||
padding: 10px;
|
||||
margin-left: 10px;
|
||||
background-color: #2980b9;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.input-area button:hover {
|
||||
background-color: #1f608d;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
margin: 20px 0px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tabs button {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: 0px solid var(--primary-color);
|
||||
margin-right: 5px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.tabs button:hover {
|
||||
background: var(--secondary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Signature */
|
||||
.signature-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background-color:#f1f1f1;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
margin: 1% 0% 0.5% 1%;
|
||||
transition: all 1s ease 0.1s;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.signature-area.hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.signature-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border-radius: 10px 10px 0 0;
|
||||
padding-left: 4%;
|
||||
}
|
||||
|
||||
.signature-content {
|
||||
padding: 10px;
|
||||
background-color: var(--secondary-color);
|
||||
color: var(--primary-color);
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
margin: 1%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.signature-description {
|
||||
height: 20%;
|
||||
width: 100%;
|
||||
margin: 0% 10% 0% 10%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.signature-description li {
|
||||
margin: 1% 0% 1% 0%;
|
||||
list-style: none;
|
||||
padding: 2%;
|
||||
border-radius: 10px;
|
||||
background-color: var(--primary-color);
|
||||
color: var(--secondary-color);
|
||||
width: 20%;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
margin-right: 2%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.signature-description li .member-list {
|
||||
margin-left: -30%;
|
||||
}
|
||||
|
||||
.signature-description li .member-list li {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.signature-description li .member-list li:hover {
|
||||
background-color: var(--secondary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.signature-documents {
|
||||
height: 80%;
|
||||
width: 100%;
|
||||
margin: 0% 10% 0% 10%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.signature-documents-header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 15%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#request-document-button {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
margin-left: 5%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#request-document-button:hover {
|
||||
background-color: var(--accent-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#close-signature {
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
margin-right: 2%;
|
||||
border-radius: 50%;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: -3%;
|
||||
margin-top: -5%;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#close-signature:hover {
|
||||
background-color: var(--secondary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* REQUEST MODAL */
|
||||
.request-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: var(--secondary-color);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.close-modal {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 1.5em;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close-modal:hover {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.modal-members {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal-members ul li{
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.file-upload-container {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
margin: 5px 0;
|
||||
background: var(--background-color-secondary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.remove-file {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.remove-file:hover {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
#message-input {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
resize: none;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
max-width: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media screen and (max-width: 768px) {
|
||||
.group-list {
|
||||
display: none;
|
||||
/* Masquer la liste des groupes sur les petits écrans */
|
||||
}
|
||||
|
||||
.chat-area {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--primary-color);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--secondary-color);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-color);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
818
src/4nk.css
818
src/4nk.css
@ -1,818 +0,0 @@
|
||||
:host {
|
||||
--primary-color: #3a506b;
|
||||
/* Bleu métallique */
|
||||
--secondary-color: #b0bec5;
|
||||
/* Gris acier */
|
||||
--accent-color: #d68c45;
|
||||
/* Cuivre */
|
||||
font-family: Arial, sans-serif;
|
||||
height: 100vh;
|
||||
font-size: 16px;
|
||||
}
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f4f4f4;
|
||||
background-image: url(../assets/bgd.webp);
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-blend-mode: soft-light;
|
||||
height: 100vh;
|
||||
}
|
||||
.message {
|
||||
margin: 30px 0;
|
||||
font-size: 14px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.message strong {
|
||||
font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/** Modal Css */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 55%;
|
||||
height: 30%;
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
margin: 0;
|
||||
padding-bottom: 8px;
|
||||
width: 100%;
|
||||
font-size: 0.9rem;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.confirmation-box {
|
||||
/* margin-top: 20px; */
|
||||
align-content: center;
|
||||
width: 70%;
|
||||
height: 20%;
|
||||
/* padding: 20px; */
|
||||
font-size: 1.5em;
|
||||
color: #333333;
|
||||
top: 5%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-wrapper {
|
||||
position: fixed;
|
||||
background: radial-gradient(circle, white, var(--primary-color));
|
||||
/* background-color: #CFD8DC; */
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
color: #37474f;
|
||||
height: 9vh;
|
||||
width: 100vw;
|
||||
left: 0;
|
||||
top: 0;
|
||||
box-shadow:
|
||||
0px 8px 10px -5px rgba(0, 0, 0, 0.2),
|
||||
0px 16px 24px 2px rgba(0, 0, 0, 0.14),
|
||||
0px 6px 30px 5px rgba(0, 0, 0, 0.12);
|
||||
|
||||
.nav-right-icons {
|
||||
display: flex;
|
||||
.notification-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.notification-bell,
|
||||
.burger-menu {
|
||||
z-index: 3;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: -0.7rem;
|
||||
left: -0.8rem;
|
||||
background-color: red;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
padding: 2.5px 6px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.notification-board {
|
||||
position: absolute;
|
||||
width: 20rem;
|
||||
min-height: 8rem;
|
||||
background-color: white;
|
||||
right: 0.5rem;
|
||||
display: none;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
display: none;
|
||||
|
||||
.notification-element {
|
||||
padding: 0.8rem 0;
|
||||
width: 100%;
|
||||
&:hover {
|
||||
background-color: rgba(26, 28, 24, 0.08);
|
||||
}
|
||||
}
|
||||
.notification-element:not(:last-child) {
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
height: 100%;
|
||||
width: 100vw;
|
||||
align-content: center;
|
||||
position: relative;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
display: grid;
|
||||
height: 100vh;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 10px;
|
||||
grid-auto-rows: 10vh 15vh 1fr;
|
||||
}
|
||||
.title-container {
|
||||
grid-column: 2 / 7;
|
||||
grid-row: 2;
|
||||
}
|
||||
.page-container {
|
||||
grid-column: 2 / 7;
|
||||
grid-row: 3;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 20px 0;
|
||||
}
|
||||
@media only screen and (min-width: 600px) {
|
||||
.tab-container {
|
||||
display: none;
|
||||
}
|
||||
.page-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.process-container {
|
||||
grid-column: 3 / 6;
|
||||
grid-row: 3;
|
||||
|
||||
.card {
|
||||
min-width: 40vw;
|
||||
}
|
||||
}
|
||||
.separator {
|
||||
width: 2px;
|
||||
background-color: #78909c;
|
||||
height: 80%;
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
.tab-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
height: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.process-container {
|
||||
grid-column: 2 / 7;
|
||||
grid-row: 3;
|
||||
}
|
||||
.container {
|
||||
grid-auto-rows: 10vh 15vh 15vh 1fr;
|
||||
}
|
||||
.tab-container {
|
||||
grid-column: 1 / 8;
|
||||
grid-row: 3;
|
||||
}
|
||||
.page-container {
|
||||
grid-column: 2 / 7;
|
||||
grid-row: 4;
|
||||
}
|
||||
.separator {
|
||||
display: none;
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #e0e4d6;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
color: #6200ea;
|
||||
&:hover {
|
||||
background-color: rgba(26, 28, 24, 0.08);
|
||||
}
|
||||
}
|
||||
.tab.active {
|
||||
border-bottom: 2px solid #6200ea;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.card.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 80%;
|
||||
}
|
||||
.modal-content {
|
||||
width: 80%;
|
||||
height: 20%;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.emoji-display {
|
||||
font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#emoji-display-2 {
|
||||
margin-top: 30px;
|
||||
font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#okButton {
|
||||
margin-bottom: 2em;
|
||||
cursor: pointer;
|
||||
background-color: #d0d0d7;
|
||||
color: white;
|
||||
border-style: none;
|
||||
border-radius: 5px;
|
||||
color: #000;
|
||||
padding: 2px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.pairing-request {
|
||||
font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif;
|
||||
font-size: 14px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
margin-bottom: 2em;
|
||||
cursor: pointer;
|
||||
background-color: #d0d0d7;
|
||||
color: white;
|
||||
border-style: none;
|
||||
border-radius: 5px;
|
||||
color: #000;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.camera-card {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
/* height: 200px; */
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #3700b3;
|
||||
}
|
||||
|
||||
.card {
|
||||
min-width: 300px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background-color: white;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
height: 60vh;
|
||||
justify-content: flex-start;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
font-size: 0.8em;
|
||||
position: relative;
|
||||
left: 2vw;
|
||||
width: 90%;
|
||||
.process-title {
|
||||
font-weight: bold;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
.process-element {
|
||||
padding: 0.4rem 0;
|
||||
&:hover {
|
||||
background-color: rgba(26, 28, 24, 0.08);
|
||||
}
|
||||
&.selected {
|
||||
background-color: rgba(26, 28, 24, 0.08);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-description {
|
||||
padding: 20px;
|
||||
font-size: 1rem;
|
||||
color: #333;
|
||||
width: 90%;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.card-action {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 3.4rem;
|
||||
right: 1rem;
|
||||
background-color: white;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menu-content a {
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
&:hover {
|
||||
background-color: rgba(26, 28, 24, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-content a:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.qr-code-scanner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* QR READER */
|
||||
#qr-reader div {
|
||||
position: inherit;
|
||||
}
|
||||
|
||||
#qr-reader div img {
|
||||
top: 15px;
|
||||
right: 25px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* INPUT CSS **/
|
||||
.input-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #eceff1;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 36vw;
|
||||
padding: 10px 0;
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
border-bottom: 1px solid #ccc;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
border-bottom: 2px solid #6200ea;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
position: absolute;
|
||||
margin-top: -0.5em;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 10px 0;
|
||||
font-size: 1rem;
|
||||
color: #999;
|
||||
pointer-events: none;
|
||||
transition:
|
||||
transform 0.3s,
|
||||
color 0.3s,
|
||||
font-size 0.3s;
|
||||
}
|
||||
|
||||
.input-field:focus + .input-label,
|
||||
.input-field:not(:placeholder-shown) + .input-label {
|
||||
transform: translateY(-20px);
|
||||
font-size: 0.8em;
|
||||
color: #6200ea;
|
||||
}
|
||||
|
||||
.input-underline {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background-color: #6200ea;
|
||||
transition:
|
||||
width 0.3s,
|
||||
left 0.3s;
|
||||
}
|
||||
|
||||
.input-field:focus ~ .input-underline {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
position: absolute;
|
||||
flex-direction: column;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
display: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dropdown-content span {
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.dropdown-content span:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
/** AUTOCOMPLETE **/
|
||||
|
||||
select[data-multi-select-plugin] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.multi-select-component {
|
||||
width: 36vw;
|
||||
padding: 5px 0;
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
border-bottom: 1px solid #ccc;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
-o-transition:
|
||||
border-color ease-in-out 0.15s,
|
||||
box-shadow ease-in-out 0.15s;
|
||||
transition:
|
||||
border-color ease-in-out 0.15s,
|
||||
box-shadow ease-in-out 0.15s;
|
||||
}
|
||||
|
||||
.autocomplete-list {
|
||||
border-radius: 4px 0px 0px 4px;
|
||||
}
|
||||
|
||||
.multi-select-component:focus-within {
|
||||
box-shadow: inset 0px 0px 0px 2px #78abfe;
|
||||
}
|
||||
|
||||
.multi-select-component .btn-group {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.multiselect-native-select .multiselect-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selected-processes {
|
||||
background-color: white;
|
||||
padding: 0.4em;
|
||||
}
|
||||
|
||||
.selected-wrapper {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
border: 1px solid #d9d9d9;
|
||||
background-color: #ededed;
|
||||
white-space: nowrap;
|
||||
margin: 1px 5px 5px 0;
|
||||
height: 22px;
|
||||
vertical-align: top;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.selected-wrapper .selected-label {
|
||||
max-width: 514px;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-left: 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.selected-wrapper .selected-close {
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
line-height: 1.49rem;
|
||||
margin-left: 5px;
|
||||
padding-bottom: 10px;
|
||||
height: 100%;
|
||||
vertical-align: top;
|
||||
padding-right: 4px;
|
||||
opacity: 0.2;
|
||||
color: #000;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.search-container .selected-input {
|
||||
background: none;
|
||||
border: 0;
|
||||
height: 20px;
|
||||
width: 60px;
|
||||
padding: 0;
|
||||
margin-bottom: 6px;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.search-container .selected-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dropdown-icon.active {
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
|
||||
.search-container .dropdown-icon {
|
||||
display: inline-block;
|
||||
padding: 10px 5px;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 0 !important;
|
||||
/* needed */
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
/* SVG background image */
|
||||
background-image: url('data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%3E%3Ctitle%3Edown-arrow%3C%2Ftitle%3E%3Cg%20fill%3D%22%23818181%22%3E%3Cpath%20d%3D%22M10.293%2C3.293%2C6%2C7.586%2C1.707%2C3.293A1%2C1%2C0%2C0%2C0%2C.293%2C4.707l5%2C5a1%2C1%2C0%2C0%2C0%2C1.414%2C0l5-5a1%2C1%2C0%2C1%2C0-1.414-1.414Z%22%20fill%3D%22%23818181%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E');
|
||||
background-position: center;
|
||||
background-size: 10px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.search-container ul {
|
||||
position: absolute;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
z-index: 3;
|
||||
margin-top: 29px;
|
||||
width: 100%;
|
||||
right: 0px;
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
}
|
||||
|
||||
.search-container ul :focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-container ul li {
|
||||
display: block;
|
||||
text-align: left;
|
||||
padding: 8px 29px 2px 12px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
font-size: 14px;
|
||||
min-height: 31px;
|
||||
}
|
||||
|
||||
.search-container ul li:first-child {
|
||||
border-top: 1px solid #ccc;
|
||||
border-radius: 4px 0px 0 0;
|
||||
}
|
||||
|
||||
.search-container ul li:last-child {
|
||||
border-radius: 4px 0px 0 0;
|
||||
}
|
||||
|
||||
.search-container ul li:hover.not-cursor {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.search-container ul li:hover {
|
||||
color: #333;
|
||||
background-color: #f0f0f0;
|
||||
border-color: #adadad;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Adding scrool to select options */
|
||||
.autocomplete-list {
|
||||
max-height: 130px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/**************************************** Process page card ******************************************************/
|
||||
.process-card {
|
||||
min-width: 300px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background-color: white;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
min-height: 40vh;
|
||||
max-height: 60vh;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.process-card-content {
|
||||
text-align: left;
|
||||
font-size: 0.8em;
|
||||
position: relative;
|
||||
left: 2vw;
|
||||
width: 90%;
|
||||
.process-title {
|
||||
font-weight: bold;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
.process-element {
|
||||
padding: 0.4rem 0;
|
||||
&:hover {
|
||||
background-color: rgba(26, 28, 24, 0.08);
|
||||
}
|
||||
&.selected {
|
||||
background-color: rgba(26, 28, 24, 0.08);
|
||||
}
|
||||
}
|
||||
.selected-process-zone {
|
||||
background-color: rgba(26, 28, 24, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.process-card-description {
|
||||
padding: 20px;
|
||||
font-size: 1rem;
|
||||
color: #333;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.process-card-action {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/**************************************** Select Member Home Page ******************************************************/
|
||||
.custom-select {
|
||||
width: 100%;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
direction: ltr;
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.custom-select option {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.custom-select option:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.custom-select::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.custom-select::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
.custom-select::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.custom-select::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
69
src/App.ts
Normal file
69
src/App.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import globalCss from './assets/styles/style.css?inline';
|
||||
|
||||
export class AppLayout extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.shadowRoot) {
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
${globalCss}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden; /* Empêche le scroll global sur body */
|
||||
}
|
||||
|
||||
.app-grid {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr; /* Ligne 1: auto (header), Ligne 2: le reste */
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-area {
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
/* Le header est posé ici, plus besoin de position: fixed */
|
||||
}
|
||||
|
||||
.content-area {
|
||||
position: relative;
|
||||
overflow-y: auto; /* C'est ICI que ça scrolle */
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* Scrollbar jolie */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255,255,255,0.2) transparent;
|
||||
}
|
||||
|
||||
/* Webkit Scrollbar */
|
||||
.content-area::-webkit-scrollbar { width: 6px; }
|
||||
.content-area::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; }
|
||||
</style>
|
||||
|
||||
<div class="app-grid">
|
||||
<div class="header-area">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
|
||||
<div class="content-area">
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('app-layout', AppLayout);
|
||||
133
src/assets/styles/style.css
Normal file
133
src/assets/styles/style.css
Normal file
@ -0,0 +1,133 @@
|
||||
:root {
|
||||
/* --- 🎨 Palette de Couleurs Moderne --- */
|
||||
--primary-hue: 220; /* Bleu profond */
|
||||
--accent-hue: 260; /* Violet vibrant */
|
||||
|
||||
--bg-color: #0f172a; /* Fond très sombre (Dark mode par défaut) */
|
||||
--bg-gradient: radial-gradient(circle at top left, #1e293b, #0f172a);
|
||||
|
||||
--glass-bg: rgba(255, 255, 255, 0.05);
|
||||
--glass-border: rgba(255, 255, 255, 0.1);
|
||||
--glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||
|
||||
--text-main: #f8fafc;
|
||||
--text-muted: #94a3b8;
|
||||
|
||||
--primary: hsl(var(--primary-hue), 90%, 60%);
|
||||
--accent: hsl(var(--accent-hue), 90%, 65%);
|
||||
|
||||
--success: #4ade80;
|
||||
--error: #f87171;
|
||||
|
||||
/* --- 📐 Espacement & Rayons --- */
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 16px;
|
||||
--radius-lg: 24px;
|
||||
|
||||
/* --- ⚡ Transitions --- */
|
||||
--ease-out: cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||
}
|
||||
|
||||
/* Reset basique */
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
background-image: var(--bg-gradient);
|
||||
color: var(--text-main);
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow-x: hidden;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* --- ✨ Composants UI Globaux --- */
|
||||
|
||||
/* Boutons Modernes */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 24px;
|
||||
background: linear-gradient(135deg, var(--primary), var(--accent));
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s var(--ease-out), box-shadow 0.2s;
|
||||
text-decoration: none;
|
||||
font-size: 1rem;
|
||||
box-shadow: 0 4px 15px rgba(var(--primary-hue), 50, 50, 0.3);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(var(--primary-hue), 50, 50, 0.5);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
border: 1px solid var(--glass-border);
|
||||
background-color: rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
/* Inputs Stylisés */
|
||||
input, select, textarea {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
/* Cartes Glassmorphism */
|
||||
.glass-panel {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--glass-shadow);
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* Titres */
|
||||
h1, h2, h3 {
|
||||
color: white;
|
||||
margin-bottom: 1rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
h1 { font-size: 2.5rem; font-weight: 800; background: linear-gradient(to right, #fff, #94a3b8); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||||
|
||||
/* Utilitaires */
|
||||
.text-center { text-align: center; }
|
||||
.mt-4 { margin-top: 1.5rem; }
|
||||
.mb-4 { margin-bottom: 1.5rem; }
|
||||
.flex-center { display: flex; justify-content: center; align-items: center; }
|
||||
.w-full { width: 100%; }
|
||||
|
||||
/* Container principal */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
242
src/components/header/Header.ts
Executable file
242
src/components/header/Header.ts
Executable file
@ -0,0 +1,242 @@
|
||||
import headerHtml from './header.html?raw';
|
||||
import globalCss from '../../assets/styles/style.css?inline';
|
||||
import Services from '../../services/service';
|
||||
import { BackUp } from '../../types/index';
|
||||
|
||||
export class HeaderComponent extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
this.initLogic();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.shadowRoot) {
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
${globalCss}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 1rem 2rem;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.8rem 1.5rem;
|
||||
pointer-events: auto; /* Réactive les clics sur la barre */
|
||||
border-radius: 100px; /* Forme "Pillule" */
|
||||
background: rgba(15, 23, 42, 0.6); /* Plus sombre */
|
||||
}
|
||||
|
||||
.brand {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: 1px;
|
||||
color: white;
|
||||
}
|
||||
.brand .dot { color: var(--accent); }
|
||||
|
||||
.nav-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 50%;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.icon-btn:hover { background: rgba(255,255,255,0.1); }
|
||||
|
||||
.menu-dropdown {
|
||||
position: absolute;
|
||||
top: 120%;
|
||||
right: 0;
|
||||
width: 200px;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
padding: 0.5rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.menu-dropdown a {
|
||||
color: var(--text-main);
|
||||
text-decoration: none;
|
||||
padding: 10px 15px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
transition: background 0.2s;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.menu-dropdown a:hover { background: rgba(255,255,255,0.1); }
|
||||
.menu-dropdown a.danger { color: var(--error); }
|
||||
.menu-dropdown a.danger:hover { background: rgba(248, 113, 113, 0.1); }
|
||||
|
||||
.divider { height: 1px; background: var(--glass-border); margin: 5px 0; }
|
||||
|
||||
</style>
|
||||
${headerHtml}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
initLogic() {
|
||||
const root = this.shadowRoot;
|
||||
if (!root) return;
|
||||
|
||||
// 1. Gestion du Menu Burger
|
||||
const burgerBtn = root.querySelector('.burger-menu');
|
||||
const menu = root.getElementById('menu');
|
||||
|
||||
if (burgerBtn && menu) {
|
||||
burgerBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
menu.style.display = menu.style.display === 'flex' ? 'none' : 'flex';
|
||||
});
|
||||
|
||||
document.addEventListener('click', () => {
|
||||
menu.style.display = 'none';
|
||||
});
|
||||
|
||||
menu.addEventListener('click', (e) => e.stopPropagation());
|
||||
}
|
||||
|
||||
// 2. Attachement des actions (via les IDs, c'est plus sûr)
|
||||
const btnImport = root.getElementById('btn-import');
|
||||
const btnExport = root.getElementById('btn-export');
|
||||
const btnDisconnect = root.getElementById('btn-disconnect');
|
||||
|
||||
if (btnImport) {
|
||||
btnImport.addEventListener('click', () => {
|
||||
menu!.style.display = 'none';
|
||||
this.importJSON();
|
||||
});
|
||||
}
|
||||
|
||||
if (btnExport) {
|
||||
btnExport.addEventListener('click', () => {
|
||||
menu!.style.display = 'none';
|
||||
this.createBackUp();
|
||||
});
|
||||
}
|
||||
|
||||
if (btnDisconnect) {
|
||||
btnDisconnect.addEventListener('click', () => {
|
||||
menu!.style.display = 'none';
|
||||
this.disconnect();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
if (!confirm('Êtes-vous sûr de vouloir vous déconnecter ? Toutes les données locales seront effacées.')) return;
|
||||
|
||||
console.log('Disconnecting...');
|
||||
try {
|
||||
// 1. Nettoyage LocalStorage
|
||||
localStorage.clear();
|
||||
|
||||
// 2. Suppression IndexedDB
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const request = indexedDB.deleteDatabase('4nk');
|
||||
request.onsuccess = () => {
|
||||
console.log('IndexedDB deleted successfully');
|
||||
resolve();
|
||||
};
|
||||
request.onerror = () => {
|
||||
console.warn('Error deleting DB (maybe blocked), continuing...');
|
||||
resolve();
|
||||
};
|
||||
request.onblocked = () => {
|
||||
console.warn('Database deletion was blocked');
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
|
||||
// 3. Suppression Service Workers
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
await Promise.all(registrations.map((registration) => registration.unregister()));
|
||||
console.log('Service worker unregistered');
|
||||
|
||||
// 4. Rechargement violent pour remettre à zéro l'application
|
||||
window.location.href = window.location.origin;
|
||||
} catch (error) {
|
||||
console.error('Error during disconnect:', error);
|
||||
window.location.href = window.location.origin;
|
||||
}
|
||||
}
|
||||
|
||||
async importJSON() {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
// On parse le JSON
|
||||
const content: BackUp = JSON.parse(e.target?.result as string);
|
||||
const service = await Services.getInstance();
|
||||
await service.importJSON(content);
|
||||
alert('Import réussi !');
|
||||
window.location.reload(); // Recharger pour appliquer les données
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert("Erreur lors de l'import: fichier invalide.");
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
|
||||
input.click();
|
||||
}
|
||||
|
||||
async createBackUp() {
|
||||
try {
|
||||
const service = await Services.getInstance();
|
||||
const backUp = await service.createBackUp();
|
||||
|
||||
if (!backUp) {
|
||||
alert("Impossible de créer le backup (Pas d'appareil trouvé).");
|
||||
return;
|
||||
}
|
||||
|
||||
const backUpJson = JSON.stringify(backUp, null, 2);
|
||||
const blob = new Blob([backUpJson], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `4nk-backup-${new Date().toISOString().slice(0, 10)}.json`;
|
||||
a.click();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
console.log('Backup téléchargé.');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Erreur lors de la création du backup.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('app-header', HeaderComponent);
|
||||
@ -1,36 +1,27 @@
|
||||
<div class="nav-wrapper">
|
||||
<div id="profile-header-container"></div>
|
||||
<div class="brand-logo">4NK</div>
|
||||
<div class="nav-right-icons">
|
||||
<div class="notification-container">
|
||||
<div class="bell-icon">
|
||||
<svg class="notification-bell" onclick="openCloseNotifications()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
||||
<path
|
||||
d="M224 0c-17.7 0-32 14.3-32 32V51.2C119 66 64 130.6 64 208v25.4c0 45.4-15.5 89.5-43.8 124.9L5.3 377c-5.8 7.2-6.9 17.1-2.9 25.4S14.8 416 24 416H424c9.2 0 17.6-5.3 21.6-13.6s2.9-18.2-2.9-25.4l-14.9-18.6C399.5 322.9 384 278.8 384 233.4V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32zm0 96c61.9 0 112 50.1 112 112v25.4c0 47.9 13.9 94.6 39.7 134.6H72.3C98.1 328 112 281.3 112 233.4V208c0-61.9 50.1-112 112-112zm64 352H224 160c0 17 6.7 33.3 18.7 45.3s28.3 18.7 45.3 18.7s33.3-6.7 45.3-18.7s18.7-28.3 18.7-45.3z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="notification-badge"></div>
|
||||
<div id="notification-board" class="notification-board">
|
||||
<div class="no-notification">No notifications available</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="navbar glass-panel">
|
||||
<div class="nav-left">
|
||||
<div class="brand">4NK<span class="dot">.</span></div>
|
||||
</div>
|
||||
|
||||
<div class="burger-menu">
|
||||
<svg class="burger-menu" onclick="toggleMenu()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
||||
<path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z" />
|
||||
<div class="nav-right">
|
||||
<div class="user-profile" id="profile-header-container">
|
||||
</div>
|
||||
|
||||
<button class="icon-btn burger-menu" aria-label="Menu">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="menu-content" id="menu">
|
||||
<!-- <a onclick="unpair()">Revoke</a> -->
|
||||
<a onclick="importJSON()">Import</a>
|
||||
<a onclick="createBackUp()">Export</a>
|
||||
<a onclick="navigate('account')">Account</a>
|
||||
<a onclick="navigate('chat')">Chat</a>
|
||||
<a onclick="navigate('signature')">Signatures</a>
|
||||
<a onclick="navigate('process')">Process</a>
|
||||
<a onclick="disconnect()">Disconnect</a>
|
||||
</div>
|
||||
<div class="menu-dropdown glass-panel" id="menu">
|
||||
<a id="btn-import">Import Data</a>
|
||||
<a id="btn-export">Export Backup</a>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<a id="btn-disconnect" class="danger">Disconnect</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@ -1,220 +0,0 @@
|
||||
import ModalService from '~/services/modal.service';
|
||||
import { INotification } from '../../models/notification.model';
|
||||
import { currentRoute, navigate } from '../../router';
|
||||
import Services from '../../services/service';
|
||||
import { BackUp } from '~/models/backup.model';
|
||||
|
||||
let notifications = [];
|
||||
|
||||
export async function unpair() {
|
||||
const service = await Services.getInstance();
|
||||
await service.unpairDevice();
|
||||
navigate('home');
|
||||
}
|
||||
|
||||
(window as any).unpair = unpair;
|
||||
|
||||
function toggleMenu() {
|
||||
const menu = document.getElementById('menu');
|
||||
if (menu) {
|
||||
if (menu.style.display === 'block') {
|
||||
menu.style.display = 'none';
|
||||
} else {
|
||||
menu.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
(window as any).toggleMenu = toggleMenu;
|
||||
|
||||
async function getNotifications() {
|
||||
const service = await Services.getInstance();
|
||||
notifications = service.getNotifications();
|
||||
return notifications;
|
||||
}
|
||||
function openCloseNotifications() {
|
||||
const notifications = document.querySelector('.notification-board') as HTMLDivElement;
|
||||
notifications.style.display = notifications?.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
(window as any).openCloseNotifications = openCloseNotifications;
|
||||
|
||||
export async function initHeader() {
|
||||
if (currentRoute === 'account') {
|
||||
// Charger le profile-header
|
||||
const profileContainer = document.getElementById('profile-header-container');
|
||||
if (profileContainer) {
|
||||
const profileHeaderHtml = await fetch('/src/components/profile-header/profile-header.html').then((res) => res.text());
|
||||
profileContainer.innerHTML = profileHeaderHtml;
|
||||
|
||||
// Initialiser les données du profil
|
||||
loadUserProfile();
|
||||
}
|
||||
}
|
||||
if (currentRoute === 'home') {
|
||||
hideSomeFunctionnalities();
|
||||
} else {
|
||||
fetchNotifications();
|
||||
setInterval(fetchNotifications, 2 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function hideSomeFunctionnalities() {
|
||||
const bell = document.querySelector('.bell-icon') as HTMLDivElement;
|
||||
if (bell) bell.style.display = 'none';
|
||||
const notifBadge = document.querySelector('.notification-badge') as HTMLDivElement;
|
||||
if (notifBadge) notifBadge.style.display = 'none';
|
||||
const actions = document.querySelectorAll('.menu-content a') as NodeListOf<HTMLAnchorElement>;
|
||||
const excludedActions = ['Import', 'Export'];
|
||||
for (const action of actions) {
|
||||
if (!excludedActions.includes(action.innerHTML)) {
|
||||
action.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setNotification(notifications: any[]): Promise<void> {
|
||||
const badge = document.querySelector('.notification-badge') as HTMLDivElement;
|
||||
const noNotifications = document.querySelector('.no-notification') as HTMLDivElement;
|
||||
if (notifications?.length) {
|
||||
badge.innerText = notifications.length.toString();
|
||||
const notificationBoard = document.querySelector('.notification-board') as HTMLDivElement;
|
||||
notificationBoard.querySelectorAll('.notification-element')?.forEach((elem) => elem.remove());
|
||||
noNotifications.style.display = 'none';
|
||||
for (const notif of notifications) {
|
||||
const notifElement = document.createElement('div');
|
||||
notifElement.className = 'notification-element';
|
||||
notifElement.setAttribute('notif-id', notif.processId);
|
||||
notifElement.innerHTML = `
|
||||
<div>Validation required : </div>
|
||||
<div style="text-overflow: ellipsis; content-visibility: auto;">${notif.processId}</div>
|
||||
`;
|
||||
// this.addSubscription(notifElement, 'click', 'goToProcessPage')
|
||||
notificationBoard.appendChild(notifElement);
|
||||
notifElement.addEventListener('click', async () => {
|
||||
const modalService = await ModalService.getInstance();
|
||||
modalService.injectValidationModal(notif);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
noNotifications.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchNotifications() {
|
||||
const service = await Services.getInstance();
|
||||
const data = service.getNotifications();
|
||||
setNotification(data);
|
||||
}
|
||||
|
||||
async function loadUserProfile() {
|
||||
// Charger les données du profil depuis le localStorage
|
||||
const userName = localStorage.getItem('userName');
|
||||
const userLastName = localStorage.getItem('userLastName');
|
||||
const userAvatar = localStorage.getItem('userAvatar') || 'https://via.placeholder.com/150';
|
||||
const userBanner = localStorage.getItem('userBanner') || 'https://via.placeholder.com/800x200';
|
||||
|
||||
// Mettre à jour les éléments du DOM
|
||||
const nameElement = document.querySelector('.user-name');
|
||||
const lastNameElement = document.querySelector('.user-lastname');
|
||||
const avatarElement = document.querySelector('.avatar');
|
||||
const bannerElement = document.querySelector('.banner-image');
|
||||
|
||||
if (nameElement) nameElement.textContent = userName;
|
||||
if (lastNameElement) lastNameElement.textContent = userLastName;
|
||||
if (avatarElement) (avatarElement as HTMLImageElement).src = userAvatar;
|
||||
if (bannerElement) (bannerElement as HTMLImageElement).src = userBanner;
|
||||
}
|
||||
|
||||
async function importJSON() {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
const content: BackUp = JSON.parse(e.target?.result as string);
|
||||
const service = await Services.getInstance();
|
||||
await service.importJSON(content);
|
||||
alert('Import réussi');
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
alert("Erreur lors de l'import: " + error);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
|
||||
input.click();
|
||||
}
|
||||
|
||||
(window as any).importJSON = importJSON;
|
||||
|
||||
async function createBackUp() {
|
||||
const service = await Services.getInstance();
|
||||
const backUp = await service.createBackUp();
|
||||
if (!backUp) {
|
||||
console.error("No device to backup");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const backUpJson = JSON.stringify(backUp, null, 2)
|
||||
const blob = new Blob([backUpJson], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = '4nk-backup.json';
|
||||
a.click();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
console.log('Backup successfully prepared for download');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).createBackUp = createBackUp;
|
||||
|
||||
async function disconnect() {
|
||||
console.log('Disconnecting...');
|
||||
try {
|
||||
localStorage.clear();
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const request = indexedDB.deleteDatabase('4nk');
|
||||
request.onsuccess = () => {
|
||||
console.log('IndexedDB deleted successfully');
|
||||
resolve();
|
||||
};
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onblocked = () => {
|
||||
console.log('Database deletion was blocked');
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
await Promise.all(registrations.map(registration => registration.unregister()));
|
||||
console.log('Service worker unregistered');
|
||||
|
||||
navigate('home');
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = window.location.origin;
|
||||
}, 100);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during disconnect:', error);
|
||||
// force reload
|
||||
window.location.href = window.location.origin;
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).disconnect = disconnect;
|
||||
@ -1,14 +0,0 @@
|
||||
<div id="login-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-title">Login</div>
|
||||
<div class="confirmation-box">
|
||||
<div class="message">
|
||||
Attempting to pair device with address
|
||||
<strong>{{device1}}</strong>
|
||||
with device with address
|
||||
<strong>{{device2}}</strong>
|
||||
</div>
|
||||
<div>Awaiting pairing validation...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,13 +0,0 @@
|
||||
import Routing from '/src/services/routing.service.ts';
|
||||
|
||||
const router = await Routing.getInstance();
|
||||
export async function confirmLogin() {
|
||||
router.confirmLogin();
|
||||
}
|
||||
|
||||
export async function closeLoginModal() {
|
||||
router.closeLoginModal();
|
||||
}
|
||||
|
||||
window.confirmLogin = confirmLogin;
|
||||
window.closeLoginModal = closeLoginModal;
|
||||
@ -1,16 +0,0 @@
|
||||
<div id="modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-title">Login</div>
|
||||
<div class="message">
|
||||
Do you want to pair device?<br />
|
||||
Attempting to pair device with address <br />
|
||||
<strong>{{device1}}</strong> <br />
|
||||
with device with address <br />
|
||||
<strong>{{device2}}</strong>
|
||||
</div>
|
||||
<div class="confirmation-box">
|
||||
<a class="btn confirmation-btn" onclick="confirm()">Confirm</a>
|
||||
<a class="btn refusal-btn" onclick="closeConfirmationModal()">Refuse</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,13 +0,0 @@
|
||||
import ModalService from '../../services/modal.service';
|
||||
|
||||
const modalService = await ModalService.getInstance();
|
||||
export async function confirm() {
|
||||
modalService.confirmPairing();
|
||||
}
|
||||
|
||||
export async function closeConfirmationModal() {
|
||||
modalService.closeConfirmationModal();
|
||||
}
|
||||
|
||||
(window as any).confirm = confirm;
|
||||
(window as any).closeConfirmationModal = closeConfirmationModal;
|
||||
@ -1,14 +0,0 @@
|
||||
<div id="creation-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-title">Login</div>
|
||||
<div class="message">
|
||||
Do you want to create a 4NK member?<br />
|
||||
Attempting to create a member with address <br />
|
||||
<strong>{{device1}}</strong> <br />
|
||||
</div>
|
||||
<div class="confirmation-box">
|
||||
<a class="btn confirmation-btn" onclick="confirm()">Confirm</a>
|
||||
<a class="btn refusal-btn" onclick="closeConfirmationModal()">Refuse</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,8 +0,0 @@
|
||||
<div id="waiting-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-title">Login</div>
|
||||
<div class="message">
|
||||
Waiting for Device 2...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,73 +0,0 @@
|
||||
import QrScanner from 'qr-scanner';
|
||||
import Services from '../../services/service';
|
||||
import { prepareAndSendPairingTx } from '~/utils/sp-address.utils';
|
||||
|
||||
export default class QrScannerComponent extends HTMLElement {
|
||||
videoElement: any;
|
||||
wrapper: any;
|
||||
qrScanner: any;
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.wrapper = document.createElement('div');
|
||||
this.wrapper.style.position = 'relative';
|
||||
this.wrapper.style.width = '150px';
|
||||
this.wrapper.style.height = '150px';
|
||||
|
||||
this.videoElement = document.createElement('video');
|
||||
this.videoElement.style.width = '100%';
|
||||
document.body?.append(this.wrapper);
|
||||
this.wrapper.prepend(this.videoElement);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.initializeScanner();
|
||||
}
|
||||
|
||||
async initializeScanner() {
|
||||
if (!this.videoElement) {
|
||||
console.error('Video element not found!');
|
||||
return;
|
||||
}
|
||||
console.log('🚀 ~ QrScannerComponent ~ initializeScanner ~ this.videoElement:', this.videoElement);
|
||||
this.qrScanner = new QrScanner(this.videoElement, (result) => this.onQrCodeScanned(result), {
|
||||
highlightScanRegion: true,
|
||||
highlightCodeOutline: true,
|
||||
});
|
||||
|
||||
try {
|
||||
await QrScanner.hasCamera();
|
||||
this.qrScanner.start();
|
||||
this.videoElement.style = 'height: 200px; width: 200px';
|
||||
this.shadowRoot?.appendChild(this.wrapper);
|
||||
} catch (e) {
|
||||
console.error('No camera found or error starting the QR scanner', e);
|
||||
}
|
||||
}
|
||||
|
||||
async onQrCodeScanned(result: any) {
|
||||
console.log(`QR Code detected:`, result);
|
||||
const data = result.data;
|
||||
const scannedUrl = new URL(data);
|
||||
|
||||
// Extract the 'sp_address' parameter
|
||||
const spAddress = scannedUrl.searchParams.get('sp_address');
|
||||
if (spAddress) {
|
||||
// Call the sendPairingTx function with the extracted sp_address
|
||||
try {
|
||||
await prepareAndSendPairingTx(spAddress);
|
||||
} catch (e) {
|
||||
console.error('Failed to pair:', e);
|
||||
}
|
||||
}
|
||||
this.qrScanner.stop(); // if you want to stop scanning after one code is detected
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.qrScanner) {
|
||||
this.qrScanner.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('qr-scanner', QrScannerComponent);
|
||||
@ -1,70 +0,0 @@
|
||||
.validation-modal {
|
||||
display: block; /* Show the modal for demo purposes */
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgb(0, 0, 0);
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
padding-top: 60px;
|
||||
}
|
||||
.modal-content {
|
||||
background-color: #fefefe;
|
||||
margin: 5% auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
width: 80%;
|
||||
height: fit-content;
|
||||
}
|
||||
.modal-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.validation-box {
|
||||
margin-bottom: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
.expansion-panel-header {
|
||||
background-color: #e0e0e0;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.expansion-panel-body {
|
||||
display: none;
|
||||
background-color: #fafafa;
|
||||
padding: 10px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
.expansion-panel-body pre {
|
||||
background-color: #f6f8fa;
|
||||
padding: 10px;
|
||||
border-left: 4px solid #d1d5da;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.diff {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.diff-side {
|
||||
width: 48%;
|
||||
padding: 10px;
|
||||
}
|
||||
.diff-old {
|
||||
background-color: #fee;
|
||||
border: 1px solid #f00;
|
||||
}
|
||||
.diff-new {
|
||||
background-color: #e6ffe6;
|
||||
border: 1px solid #0f0;
|
||||
}
|
||||
.radio-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
<div id="validation-modal" class="validation-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-title">Validate Process {{processId}}</div>
|
||||
<div class="validation-box">
|
||||
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<button onclick="validate()">Validate</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,56 +0,0 @@
|
||||
import ModalService from '~/services/modal.service';
|
||||
|
||||
async function validate() {
|
||||
console.log('==> VALIDATE');
|
||||
const modalservice = await ModalService.getInstance();
|
||||
modalservice.closeValidationModal();
|
||||
}
|
||||
|
||||
export async function initValidationModal(processDiffs: any) {
|
||||
console.log("🚀 ~ initValidationModal ~ processDiffs:", processDiffs)
|
||||
for(const diff of processDiffs.diffs) {
|
||||
let diffs = ''
|
||||
for(const value of diff) {
|
||||
diffs+= `
|
||||
<div class="radio-buttons">
|
||||
<label>
|
||||
<input type="radio" name="validation1" value="old" />
|
||||
Keep Old
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="validation1" value="new" />
|
||||
Keep New
|
||||
</label>
|
||||
</div>
|
||||
<div class="diff">
|
||||
<div class="diff-side diff-old">
|
||||
<pre>-${value.previous_value}</pre>
|
||||
</div>
|
||||
<div class="diff-side diff-new">
|
||||
<pre>+${value.new_value}</pre>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
const state = `
|
||||
<div class="expansion-panel">
|
||||
<div class="expansion-panel-header">State ${diff[0].new_state_merkle_root}</div>
|
||||
<div class="expansion-panel-body">
|
||||
${diffs}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
const box = document.querySelector('.validation-box')
|
||||
if(box) box.innerHTML += state
|
||||
}
|
||||
document.querySelectorAll('.expansion-panel-header').forEach((header) => {
|
||||
header.addEventListener('click', function (event) {
|
||||
const target = event.target as HTMLElement;
|
||||
const body = target.nextElementSibling as HTMLElement;
|
||||
if (body?.style) body.style.display = body.style.display === 'block' ? 'none' : 'block';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
(window as any).validate = validate;
|
||||
31
src/config/constants.ts
Normal file
31
src/config/constants.ts
Normal file
@ -0,0 +1,31 @@
|
||||
export const APP_CONFIG = {
|
||||
// --- Cryptographie & Limites ---
|
||||
U32_MAX: 4294967295,
|
||||
EMPTY_32_BYTES: String('').padStart(64, '0'),
|
||||
|
||||
// --- Économie ---
|
||||
DEFAULT_AMOUNT: 1000n,
|
||||
FEE_RATE: 1, // Sat/vByte ou unité arbitraire selon le SDK
|
||||
|
||||
// --- Délais & Timeouts (ms) ---
|
||||
TIMEOUTS: {
|
||||
POLLING_INTERVAL: 100, // Vérification rapide (ex: handshake)
|
||||
API_DELAY: 500, // Petit délai pour laisser respirer le réseau (hack)
|
||||
RETRY_DELAY: 1000, // Délai avant de réessayer une action
|
||||
FAUCET_WAIT: 2000, // Attente après appel faucet
|
||||
WORKER_CHECK: 5000, // Vérification périodique du worker
|
||||
HANDSHAKE: 10000, // Timeout max pour le handshake
|
||||
KEY_REQUEST: 15000, // Timeout pour recevoir une clé d'un pair
|
||||
WS_RECONNECT_MAX: 30000, // Délai max entre deux tentatives de reco WS
|
||||
WS_HEARTBEAT: 30000, // Ping WebSocket
|
||||
},
|
||||
|
||||
// --- URLs (Environnement) ---
|
||||
URLS: {
|
||||
BASE: import.meta.env.VITE_BASEURL || 'http://localhost',
|
||||
BOOTSTRAP: [import.meta.env.VITE_BOOTSTRAPURL || `${import.meta.env.VITE_BASEURL || 'http://localhost'}:8090`],
|
||||
STORAGE: import.meta.env.VITE_STORAGEURL || `${import.meta.env.VITE_BASEURL || 'http://localhost'}:8081`,
|
||||
BLINDBIT: import.meta.env.VITE_BLINDBITURL || `${import.meta.env.VITE_BASEURL || 'http://localhost'}:8000`,
|
||||
},
|
||||
};
|
||||
|
||||
10
src/decs.d.ts
vendored
10
src/decs.d.ts
vendored
@ -1,10 +0,0 @@
|
||||
declare class AccountComponent extends HTMLElement {
|
||||
_callback: any;
|
||||
constructor();
|
||||
connectedCallback(): void;
|
||||
fetchData(): Promise<void>;
|
||||
set callback(fn: any);
|
||||
get callback(): any;
|
||||
render(): void;
|
||||
}
|
||||
export { AccountComponent };
|
||||
39
src/index.ts
39
src/index.ts
@ -1,39 +0,0 @@
|
||||
// import Services from './services/service';
|
||||
|
||||
// document.addEventListener('DOMContentLoaded', async () => {
|
||||
// try {
|
||||
|
||||
// const services = await Services.getInstance();
|
||||
// setTimeout( async () => {
|
||||
// let device = await services.getDevice()
|
||||
// console.log("🚀 ~ setTimeout ~ device:", device)
|
||||
|
||||
// if(!device) {
|
||||
// device = await services.createNewDevice();
|
||||
// } else {
|
||||
// await services.restoreDevice(device)
|
||||
// }
|
||||
// await services.restoreProcesses();
|
||||
// await services.restoreMessages();
|
||||
|
||||
// const amount = await services.getAmount();
|
||||
|
||||
// if (amount === 0n) {
|
||||
// const faucetMsg = await services.createFaucetMessage();
|
||||
// await services.sendFaucetMessage(faucetMsg);
|
||||
// }
|
||||
// if (services.isPaired()) { await services.injectProcessListPage() }
|
||||
// else {
|
||||
// const queryString = window.location.search;
|
||||
// const urlParams = new URLSearchParams(queryString)
|
||||
// const pairingAddress = urlParams.get('sp_address')
|
||||
|
||||
// if(pairingAddress) {
|
||||
// setTimeout(async () => await services.sendPairingTx(pairingAddress), 2000)
|
||||
// }
|
||||
// }
|
||||
// }, 500);
|
||||
// } catch (error) {
|
||||
// console.error(error);
|
||||
// }
|
||||
// });
|
||||
@ -1,22 +0,0 @@
|
||||
import { DocumentSignature } from '~/models/signature.models';
|
||||
|
||||
export interface Group {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
roles: Array<{
|
||||
name: string;
|
||||
members: Array<{ id: string | number; name: string }>;
|
||||
documents?: Array<any>;
|
||||
}>;
|
||||
commonDocuments: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
visibility: string;
|
||||
description: string;
|
||||
createdAt?: string | null;
|
||||
deadline?: string | null;
|
||||
signatures?: DocumentSignature[];
|
||||
status?: string;
|
||||
}>;
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
export interface Member {
|
||||
id: string | number;
|
||||
name: string;
|
||||
email?: string;
|
||||
avatar?: string;
|
||||
processRoles?: Array<{ processId: number | string; role: string }>;
|
||||
}
|
||||
87
src/main.ts
87
src/main.ts
@ -1,30 +1,67 @@
|
||||
import { SignatureComponent } from './pages/signature/signature-component';
|
||||
import { SignatureElement } from './pages/signature/signature';
|
||||
import { ChatComponent } from './pages/chat/chat-component';
|
||||
import { ChatElement } from './pages/chat/chat';
|
||||
import { AccountComponent } from './pages/account/account-component';
|
||||
import { AccountElement } from './pages/account/account';
|
||||
// Polyfill to prevent "chrome is not defined" errors
|
||||
// Some dependencies may check for Chrome extension APIs without proper existence checks
|
||||
if (typeof (globalThis as any).chrome === 'undefined') {
|
||||
(globalThis as any).chrome = {};
|
||||
}
|
||||
|
||||
export { SignatureComponent, SignatureElement, ChatComponent, ChatElement, AccountComponent, AccountElement };
|
||||
import Services from './services/service';
|
||||
import { Router } from './router/index';
|
||||
import './components/header/Header';
|
||||
import './App';
|
||||
import { IframeController } from './services/iframe-controller.service';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'signature-component': SignatureComponent;
|
||||
'signature-element': SignatureElement;
|
||||
'chat-component': ChatComponent;
|
||||
'chat-element': ChatElement;
|
||||
'account-component': AccountComponent;
|
||||
'account-element': AccountElement;
|
||||
async function bootstrap() {
|
||||
console.log("🚀 Démarrage de l'application 4NK (Multi-Worker Architecture)...");
|
||||
|
||||
try {
|
||||
// 1. Initialisation des Services (Proxy vers Core & Network Workers)
|
||||
// Cela va lancer les workers en arrière-plan
|
||||
const services = await Services.getInstance();
|
||||
|
||||
// Injection du Header
|
||||
const headerSlot = document.getElementById('header-slot');
|
||||
if (headerSlot) {
|
||||
headerSlot.innerHTML = '<app-header></app-header>';
|
||||
}
|
||||
|
||||
// 2. Vérification / Création de l'appareil (via le Worker)
|
||||
const device = await services.getDeviceFromDatabase();
|
||||
if (!device) {
|
||||
console.log('✨ Nouvel appareil détecté, création en cours via Worker...');
|
||||
await services.createNewDevice();
|
||||
} else {
|
||||
console.log("Restauration de l'appareil...");
|
||||
await services.restoreDevice(device);
|
||||
}
|
||||
|
||||
// 3. Initialisation du contrôleur d'Iframe (Reste sur le Main Thread pour écouter window)
|
||||
await IframeController.init();
|
||||
|
||||
// 4. Restauration des données
|
||||
await services.restoreProcessesFromDB();
|
||||
|
||||
if (services.restoreSecretsFromDB) {
|
||||
await services.restoreSecretsFromDB();
|
||||
} else {
|
||||
console.warn("restoreSecretsFromDB non implémenté dans le proxy Services");
|
||||
}
|
||||
|
||||
|
||||
// 5. Gestion du Routing
|
||||
const isIframe = window.self !== window.top;
|
||||
const isPaired = await services.isPaired();
|
||||
|
||||
if (isPaired && !isIframe) {
|
||||
console.log('✅ Mode Standalone & Appairé : Redirection vers Process.');
|
||||
window.history.replaceState({}, '', 'process');
|
||||
Router.handleLocation();
|
||||
} else {
|
||||
console.log(isIframe ? '📡 Mode Iframe détecté : Attente API.' : '🆕 Non appairé : Démarrage sur Home.');
|
||||
Router.init();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('💥 Erreur critique au démarrage :', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration pour le mode indépendant
|
||||
if ((import.meta as any).env.VITE_IS_INDEPENDANT_LIB) {
|
||||
// Initialiser les composants si nécessaire
|
||||
customElements.define('signature-component', SignatureComponent);
|
||||
customElements.define('signature-element', SignatureElement);
|
||||
customElements.define('chat-component', ChatComponent);
|
||||
customElements.define('chat-element', ChatElement);
|
||||
customElements.define('account-component', AccountComponent);
|
||||
customElements.define('account-element', AccountElement);
|
||||
}
|
||||
bootstrap();
|
||||
@ -1,272 +0,0 @@
|
||||
export const ALLOWED_ROLES = ['User', 'Member', 'Peer', 'Payment', 'Deposit', 'Artefact', 'Resolve', 'Backup'];
|
||||
|
||||
export const STORAGE_KEYS = {
|
||||
pairing: 'pairingRows',
|
||||
wallet: 'walletRows',
|
||||
process: 'processRows',
|
||||
data: 'dataRows',
|
||||
};
|
||||
|
||||
// Initialiser le stockage des lignes par défaut dans le localStorage
|
||||
export const defaultRows = [
|
||||
{
|
||||
column1: 'sprt1qqwtvg5q5vcz0reqvmld98u7va3av6gakwe9yxw9yhnpj5djcunn4squ68tuzn8dz78dg4adfv0dekx8hg9sy0t6s9k5em7rffgxmrsfpyy7gtyrz',
|
||||
column2: '🎊😑🎄😩',
|
||||
column3: 'Laptop',
|
||||
},
|
||||
{
|
||||
column1: 'sprt1qqwtvg5q5vcz0reqvmld98u7va3av6gakwe9yxw9yhnpj5djcunn4squ68tuzn8dz78dg4adfv0dekx8hg9sy0t6s9k5em7rffgxmrsfpyy7gtyrx',
|
||||
column2: '🎏🎕😧🌥',
|
||||
column3: 'Phone',
|
||||
},
|
||||
];
|
||||
|
||||
export const mockNotifications: { [key: string]: Notification[] } = {};
|
||||
|
||||
export const notificationMessages = ['CPU usage high', 'Memory threshold reached', 'New update available', 'Backup completed', 'Security check required', 'Performance optimization needed', 'System alert', 'Network connectivity issue', 'Storage space low', 'Process checkpoint reached'];
|
||||
|
||||
export const mockDataRows = [
|
||||
{
|
||||
column1: 'User Project',
|
||||
column2: 'private',
|
||||
column3: 'User',
|
||||
column4: '6 months',
|
||||
column5: 'NDA signed',
|
||||
column6: 'Contract #123',
|
||||
processName: 'User Process',
|
||||
zone: 'A',
|
||||
},
|
||||
{
|
||||
column1: 'Process Project',
|
||||
column2: 'private',
|
||||
column3: 'Process',
|
||||
column4: '1 year',
|
||||
column5: 'Terms accepted',
|
||||
column6: 'Contract #456',
|
||||
processName: 'Process Management',
|
||||
zone: 'B',
|
||||
},
|
||||
{
|
||||
column1: 'Member Project',
|
||||
column2: 'private',
|
||||
column3: 'Member',
|
||||
column4: '3 months',
|
||||
column5: 'GDPR compliant',
|
||||
column6: 'Contract #789',
|
||||
processName: 'Member Process',
|
||||
zone: 'C',
|
||||
},
|
||||
{
|
||||
column1: 'Peer Project',
|
||||
column2: 'public',
|
||||
column3: 'Peer',
|
||||
column4: '2 years',
|
||||
column5: 'IP rights',
|
||||
column6: 'Contract #101',
|
||||
processName: 'Peer Process',
|
||||
zone: 'D',
|
||||
},
|
||||
{
|
||||
column1: 'Payment Project',
|
||||
column2: 'confidential',
|
||||
column3: 'Payment',
|
||||
column4: '1 year',
|
||||
column5: 'NDA signed',
|
||||
column6: 'Contract #102',
|
||||
processName: 'Payment Process',
|
||||
zone: 'E',
|
||||
},
|
||||
{
|
||||
column1: 'Deposit Project',
|
||||
column2: 'private',
|
||||
column3: 'Deposit',
|
||||
column4: '6 months',
|
||||
column5: 'Terms accepted',
|
||||
column6: 'Contract #103',
|
||||
processName: 'Deposit Process',
|
||||
zone: 'F',
|
||||
},
|
||||
{
|
||||
column1: 'Artefact Project',
|
||||
column2: 'public',
|
||||
column3: 'Artefact',
|
||||
column4: '1 year',
|
||||
column5: 'GDPR compliant',
|
||||
column6: 'Contract #104',
|
||||
processName: 'Artefact Process',
|
||||
zone: 'G',
|
||||
},
|
||||
{
|
||||
column1: 'Resolve Project',
|
||||
column2: 'private',
|
||||
column3: 'Resolve',
|
||||
column4: '2 years',
|
||||
column5: 'IP rights',
|
||||
column6: 'Contract #105',
|
||||
processName: 'Resolve Process',
|
||||
zone: 'H',
|
||||
},
|
||||
{
|
||||
column1: 'Backup Project',
|
||||
column2: 'public',
|
||||
column3: 'Backup',
|
||||
column4: '1 year',
|
||||
column5: 'NDA signed',
|
||||
column6: 'Contract #106',
|
||||
processName: 'Backup Process',
|
||||
zone: 'I',
|
||||
},
|
||||
];
|
||||
|
||||
export const mockProcessRows = [
|
||||
{
|
||||
process: 'User Project',
|
||||
role: 'User',
|
||||
notification: {
|
||||
messages: [
|
||||
{ id: 1, read: false, date: '2024-03-10', message: 'New user joined the project' },
|
||||
{ id: 2, read: false, date: '2024-03-09', message: 'Project milestone reached' },
|
||||
{ id: 3, read: false, date: '2024-03-08', message: 'Security update required' },
|
||||
{ id: 4, read: true, date: '2024-03-07', message: 'Weekly report available' },
|
||||
{ id: 5, read: true, date: '2024-03-06', message: 'Team meeting scheduled' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
process: 'Member Project',
|
||||
role: 'Member',
|
||||
notification: {
|
||||
messages: [
|
||||
{ id: 6, read: true, date: '2024-03-10', message: 'Member access granted' },
|
||||
{ id: 7, read: true, date: '2024-03-09', message: 'Documentation updated' },
|
||||
{ id: 8, read: true, date: '2024-03-08', message: 'Project status: on track' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
process: 'Peer Project',
|
||||
role: 'Peer',
|
||||
notification: {
|
||||
unread: 2,
|
||||
total: 4,
|
||||
messages: [
|
||||
{ id: 9, read: false, date: '2024-03-10', message: 'New peer project added' },
|
||||
{ id: 10, read: false, date: '2024-03-09', message: 'Project milestone reached' },
|
||||
{ id: 11, read: false, date: '2024-03-08', message: 'Security update required' },
|
||||
{ id: 12, read: true, date: '2024-03-07', message: 'Weekly report available' },
|
||||
{ id: 13, read: true, date: '2024-03-06', message: 'Team meeting scheduled' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
process: 'Deposit Project',
|
||||
role: 'Deposit',
|
||||
notification: {
|
||||
unread: 1,
|
||||
total: 10,
|
||||
messages: [
|
||||
{ id: 14, read: false, date: '2024-03-10', message: 'Deposit milestone reached' },
|
||||
{ id: 15, read: false, date: '2024-03-09', message: 'Security update required' },
|
||||
{ id: 16, read: false, date: '2024-03-08', message: 'Weekly report available' },
|
||||
{ id: 17, read: true, date: '2024-03-07', message: 'Team meeting scheduled' },
|
||||
{ id: 18, read: true, date: '2024-03-06', message: 'Project status: on track' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
process: 'Artefact Project',
|
||||
role: 'Artefact',
|
||||
notification: {
|
||||
unread: 0,
|
||||
total: 3,
|
||||
messages: [
|
||||
{ id: 19, read: false, date: '2024-03-10', message: 'New artefact added' },
|
||||
{ id: 20, read: false, date: '2024-03-09', message: 'Security update required' },
|
||||
{ id: 21, read: false, date: '2024-03-08', message: 'Weekly report available' },
|
||||
{ id: 22, read: true, date: '2024-03-07', message: 'Team meeting scheduled' },
|
||||
{ id: 23, read: true, date: '2024-03-06', message: 'Project status: on track' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
process: 'Resolve Project',
|
||||
role: 'Resolve',
|
||||
notification: {
|
||||
unread: 5,
|
||||
total: 12,
|
||||
messages: [
|
||||
{ id: 24, read: false, date: '2024-03-10', message: 'New issue reported' },
|
||||
{ id: 25, read: false, date: '2024-03-09', message: 'Security update required' },
|
||||
{ id: 26, read: false, date: '2024-03-08', message: 'Weekly report available' },
|
||||
{ id: 27, read: true, date: '2024-03-07', message: 'Team meeting scheduled' },
|
||||
{ id: 28, read: true, date: '2024-03-06', message: 'Project status: on track' },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const mockContracts = {
|
||||
'Contract #123': {
|
||||
title: 'User Project Agreement',
|
||||
date: '2024-01-15',
|
||||
parties: ['Company XYZ', 'User Team'],
|
||||
terms: ['Data Protection', 'User Privacy', 'Access Rights', 'Service Level Agreement'],
|
||||
content: 'This agreement establishes the terms and conditions for user project management.',
|
||||
},
|
||||
'Contract #456': {
|
||||
title: 'Process Management Contract',
|
||||
date: '2024-02-01',
|
||||
parties: ['Company XYZ', 'Process Team'],
|
||||
terms: ['Process Workflow', 'Quality Standards', 'Performance Metrics', 'Monitoring Procedures'],
|
||||
content: 'This contract defines the process management standards and procedures.',
|
||||
},
|
||||
'Contract #789': {
|
||||
title: 'Member Access Agreement',
|
||||
date: '2024-03-15',
|
||||
parties: ['Company XYZ', 'Member Team'],
|
||||
terms: ['Member Rights', 'Access Levels', 'Security Protocol', 'Confidentiality Agreement'],
|
||||
content: 'This agreement outlines the terms for member access and privileges.',
|
||||
},
|
||||
'Contract #101': {
|
||||
title: 'Peer Collaboration Agreement',
|
||||
date: '2024-04-01',
|
||||
parties: ['Company XYZ', 'Peer Network'],
|
||||
terms: ['Collaboration Rules', 'Resource Sharing', 'Dispute Resolution', 'Network Protocol'],
|
||||
content: 'This contract establishes peer collaboration and networking guidelines.',
|
||||
},
|
||||
'Contract #102': {
|
||||
title: 'Payment Processing Agreement',
|
||||
date: '2024-05-01',
|
||||
parties: ['Company XYZ', 'Payment Team'],
|
||||
terms: ['Transaction Protocol', 'Security Measures', 'Fee Structure', 'Service Availability'],
|
||||
content: 'This agreement defines payment processing terms and conditions.',
|
||||
},
|
||||
'Contract #103': {
|
||||
title: 'Deposit Management Contract',
|
||||
date: '2024-06-01',
|
||||
parties: ['Company XYZ', 'Deposit Team'],
|
||||
terms: ['Deposit Rules', 'Storage Protocol', 'Access Control', 'Security Standards'],
|
||||
content: 'This contract outlines deposit management procedures and security measures.',
|
||||
},
|
||||
'Contract #104': {
|
||||
title: 'Artefact Handling Agreement',
|
||||
date: '2024-07-01',
|
||||
parties: ['Company XYZ', 'Artefact Team'],
|
||||
terms: ['Handling Procedures', 'Storage Guidelines', 'Access Protocol', 'Preservation Standards'],
|
||||
content: 'This agreement establishes artefact handling and preservation guidelines.',
|
||||
},
|
||||
'Contract #105': {
|
||||
title: 'Resolution Protocol Agreement',
|
||||
date: '2024-08-01',
|
||||
parties: ['Company XYZ', 'Resolution Team'],
|
||||
terms: ['Resolution Process', 'Time Constraints', 'Escalation Protocol', 'Documentation Requirements'],
|
||||
content: 'This contract defines the resolution process and protocol standards.',
|
||||
},
|
||||
'Contract #106': {
|
||||
title: 'Backup Service Agreement',
|
||||
date: '2024-09-01',
|
||||
parties: ['Company XYZ', 'Backup Team'],
|
||||
terms: ['Backup Schedule', 'Data Protection', 'Recovery Protocol', 'Service Reliability'],
|
||||
content: 'This agreement outlines backup service terms and recovery procedures.',
|
||||
},
|
||||
};
|
||||
@ -1,45 +0,0 @@
|
||||
export interface Row {
|
||||
column1: string;
|
||||
column2: string;
|
||||
column3: string;
|
||||
}
|
||||
|
||||
// Types supplémentaires nécessaires
|
||||
export interface Contract {
|
||||
title: string;
|
||||
date: string;
|
||||
parties: string[];
|
||||
terms: string[];
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface WalletRow {
|
||||
column1: string; // Label
|
||||
column2: string; // Wallet
|
||||
column3: string; // Type
|
||||
}
|
||||
|
||||
export interface DataRow {
|
||||
column1: string; // Name
|
||||
column2: string; // Visibility
|
||||
column3: string; // Role
|
||||
column4: string; // Duration
|
||||
column5: string; // Legal
|
||||
column6: string; // Contract
|
||||
processName: string;
|
||||
zone: string;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
message: string;
|
||||
timestamp: string;
|
||||
isRead: boolean;
|
||||
}
|
||||
|
||||
// Déplacer l'interface en dehors de la classe, au début du fichier
|
||||
export interface NotificationMessage {
|
||||
id: number;
|
||||
read: boolean;
|
||||
date: string;
|
||||
message: string;
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
export const groupsMock = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Group 🚀 ',
|
||||
roles: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Role 1',
|
||||
members: [
|
||||
{ id: 1, name: 'Member 1' },
|
||||
{ id: 2, name: 'Member 2' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Role 2',
|
||||
members: [
|
||||
{ id: 3, name: 'Member 3' },
|
||||
{ id: 4, name: 'Member 4' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Group ₿',
|
||||
roles: [
|
||||
{
|
||||
id: 3,
|
||||
name: 'Role 1',
|
||||
members: [
|
||||
{ id: 5, name: 'Member 5' },
|
||||
{ id: 6, name: 'Member 6' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Group 🪙',
|
||||
roles: [
|
||||
{
|
||||
id: 4,
|
||||
name: 'Role 1',
|
||||
members: [
|
||||
{ id: 7, name: 'Member 7' },
|
||||
{ id: 8, name: 'Member 8' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@ -1,64 +0,0 @@
|
||||
export const messagesMock = [
|
||||
{
|
||||
memberId: 1, // Conversations avec Mmber 1
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 1', text: 'Salut !', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Bonjour ! Comment ça va ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Tout va bien, merci !', time: '10:32 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 2, // Conversations avec Member 2
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 2', text: 'Salut, on se voit ce soir ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Oui, à quelle heure ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 3, // Conversations avec Member 3
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 3', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 4, // Conversations avec Member 4
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 4', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 5, // Conversations avec Member 5
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 5', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 6, // Conversations avec Member 6
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 6', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 7, // Conversations avec Member 7
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 7', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 8, // Conversations avec Member 8
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 8', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
];
|
||||
@ -1,471 +0,0 @@
|
||||
// Définir les rôles autorisés
|
||||
const VALID_ROLES = ['User', 'Process', 'Member', 'Peer', 'Payment', 'Deposit', 'Artefact', 'Resolve', 'Backup'];
|
||||
|
||||
const VISIBILITY_LEVELS = {
|
||||
PUBLIC: 'public',
|
||||
CONFIDENTIAL: 'confidential',
|
||||
PRIVATE: 'private',
|
||||
};
|
||||
|
||||
const DOCUMENT_STATUS = {
|
||||
DRAFT: 'draft',
|
||||
PENDING: 'pending',
|
||||
IN_REVIEW: 'in_review',
|
||||
APPROVED: 'approved',
|
||||
REJECTED: 'rejected',
|
||||
EXPIRED: 'expired',
|
||||
};
|
||||
|
||||
// Fonction pour créer un rôle
|
||||
function createRole(name, members) {
|
||||
if (!VALID_ROLES.includes(name)) {
|
||||
throw new Error(`Role "${name}" is not valid.`);
|
||||
}
|
||||
return { name, members };
|
||||
}
|
||||
|
||||
export const groupsMock = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Processus 1',
|
||||
description: 'Description du processus 1',
|
||||
commonDocuments: [
|
||||
{
|
||||
id: 101,
|
||||
name: 'Règlement intérieur',
|
||||
description: 'Document vierge pour le règlement intérieur',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
name: 'Charte de confidentialité',
|
||||
description: 'Document vierge pour la charte de confidentialité',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
name: 'Procédures générales',
|
||||
description: 'Document vierge pour les procédures générales',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 104,
|
||||
name: 'Urgency A',
|
||||
description: "Document vierge pour le plan d'urgence A",
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 105,
|
||||
name: 'Urgency B',
|
||||
description: "Document vierge pour le plan d'urgence B",
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 106,
|
||||
name: 'Urgency C',
|
||||
description: "Document vierge pour le plan d'urgence C",
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 107,
|
||||
name: 'Document à signer',
|
||||
description: 'Document vierge pour le règlement intérieur',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
roles: [
|
||||
{
|
||||
name: 'User',
|
||||
members: [
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 2, name: 'Bob' },
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Document User A',
|
||||
description: 'Description du document User A.',
|
||||
visibility: 'public',
|
||||
createdAt: '2024-01-01',
|
||||
deadline: '2024-02-01',
|
||||
signatures: [
|
||||
{
|
||||
member: { id: 1, name: 'Alice' },
|
||||
signed: true,
|
||||
signedAt: '2024-01-15',
|
||||
},
|
||||
{
|
||||
member: { id: 2, name: 'Bob' },
|
||||
signed: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Document User B',
|
||||
description: 'Document vierge pour le rôle User',
|
||||
visibility: 'confidential',
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Document User C',
|
||||
description: 'Document vierge pour validation utilisateur',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Document User D',
|
||||
description: 'Document vierge pour approbation utilisateur',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Process',
|
||||
members: [
|
||||
{ id: 3, name: 'Charlie' },
|
||||
{ id: 4, name: 'David' },
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
id: 3,
|
||||
name: 'Document Process A',
|
||||
description: 'Description du document Process A.',
|
||||
visibility: 'confidential',
|
||||
createdAt: '2024-01-10',
|
||||
deadline: '2024-03-01',
|
||||
signatures: [
|
||||
{
|
||||
member: { id: 3, name: 'Charlie' },
|
||||
signed: true,
|
||||
signedAt: '2024-01-12',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'Document Process B',
|
||||
description: 'Document vierge pour processus interne',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'Document Process C',
|
||||
description: 'Document vierge pour validation processus',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: 'Document Process D',
|
||||
description: 'Document vierge pour validation processus',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.PENDING,
|
||||
createdAt: '2024-01-15',
|
||||
deadline: '2024-02-01',
|
||||
signatures: [
|
||||
{
|
||||
member: { id: 3, name: 'Charlie' },
|
||||
signed: true,
|
||||
signedAt: '2024-01-15',
|
||||
},
|
||||
{
|
||||
member: { id: 4, name: 'David' },
|
||||
signed: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: 'Document Process E',
|
||||
description: 'Document vierge pour validation processus',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.PENDING,
|
||||
createdAt: '2024-01-15',
|
||||
deadline: '2024-02-01',
|
||||
signatures: [
|
||||
{
|
||||
member: { id: 3, name: 'Charlie' },
|
||||
signed: true,
|
||||
signedAt: '2024-01-15',
|
||||
},
|
||||
{
|
||||
member: { id: 4, name: 'David' },
|
||||
signed: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Backup',
|
||||
members: [
|
||||
{ id: 15, name: 'Oscar' },
|
||||
{ id: 16, name: 'Patricia' },
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
id: 11,
|
||||
name: 'Document Backup A',
|
||||
description: 'Document vierge pour sauvegarde',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Processus 2',
|
||||
description: 'Description du processus 2',
|
||||
commonDocuments: [
|
||||
{
|
||||
id: 201,
|
||||
name: 'Règlement intérieur',
|
||||
description: 'Document vierge pour le règlement intérieur',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 202,
|
||||
name: 'Charte de confidentialité',
|
||||
description: 'Document vierge pour la charte de confidentialité',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 203,
|
||||
name: 'Charte de confidentialité',
|
||||
description: 'Document vierge pour la charte de confidentialité',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 204,
|
||||
name: 'Charte de confidentialité',
|
||||
description: 'Document vierge pour la charte de confidentialité',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 205,
|
||||
name: 'Charte de confidentialité',
|
||||
description: 'Document vierge pour la charte de confidentialité',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
roles: [
|
||||
{
|
||||
name: 'Artefact',
|
||||
members: [
|
||||
{ id: 17, name: 'Quinn' },
|
||||
{ id: 18, name: 'Rachel' },
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
id: 12,
|
||||
name: 'Document Artefact A',
|
||||
description: 'Document vierge pour artefact',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: 'Document Artefact B',
|
||||
description: 'Document vierge pour validation artefact',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Resolve',
|
||||
members: [
|
||||
{ id: 19, name: 'Sam' },
|
||||
{ id: 20, name: 'Tom' },
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
id: 14,
|
||||
name: 'Document Resolve A',
|
||||
description: 'Document vierge pour résolution',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Processus 3',
|
||||
description: 'Description du processus 3',
|
||||
commonDocuments: [
|
||||
{
|
||||
id: 301,
|
||||
name: 'Règlement intérieur',
|
||||
description: 'Document vierge pour le règlement intérieur',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 302,
|
||||
name: 'Charte de confidentialité',
|
||||
description: 'Document vierge pour la charte de confidentialité',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 303,
|
||||
name: 'Procédures générales',
|
||||
description: 'Document vierge pour les procédures générales',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
roles: [
|
||||
{
|
||||
name: 'Deposit',
|
||||
members: [
|
||||
{ id: 21, name: 'Uma' },
|
||||
{ id: 22, name: 'Victor' },
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
id: 15,
|
||||
name: 'Document Deposit A',
|
||||
description: 'Document vierge pour dépôt',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: 'Document Deposit B',
|
||||
description: 'Document vierge pour validation dépôt',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Payment',
|
||||
members: [
|
||||
{ id: 23, name: 'Walter' },
|
||||
{ id: 24, name: 'Xena' },
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
id: 17,
|
||||
name: 'Document Payment B',
|
||||
description: 'Document vierge pour paiement',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: 'Document Payment C',
|
||||
description: 'Document vierge pour validation paiement',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@ -1,105 +0,0 @@
|
||||
export const membersMock = [
|
||||
// Processus 1
|
||||
{
|
||||
id: 1,
|
||||
name: 'Alice',
|
||||
avatar: 'A',
|
||||
email: 'alice@company.com',
|
||||
processRoles: [{ processId: 1, role: 'User' }],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Bob',
|
||||
avatar: 'B',
|
||||
email: 'bob@company.com',
|
||||
processRoles: [{ processId: 1, role: 'User' }],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Charlie',
|
||||
avatar: 'C',
|
||||
email: 'charlie@company.com',
|
||||
processRoles: [{ processId: 1, role: 'Process' }],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'David',
|
||||
avatar: 'D',
|
||||
email: 'david@company.com',
|
||||
processRoles: [{ processId: 1, role: 'Process' }],
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: 'Oscar',
|
||||
avatar: 'O',
|
||||
email: 'oscar@company.com',
|
||||
processRoles: [{ processId: 1, role: 'Backup' }],
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: 'Patricia',
|
||||
avatar: 'P',
|
||||
email: 'patricia@company.com',
|
||||
processRoles: [{ processId: 1, role: 'Backup' }],
|
||||
},
|
||||
|
||||
// Processus 2
|
||||
{
|
||||
id: 17,
|
||||
name: 'Quinn',
|
||||
avatar: 'Q',
|
||||
email: 'quinn@company.com',
|
||||
processRoles: [{ processId: 2, role: 'Artefact' }],
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: 'Rachel',
|
||||
avatar: 'R',
|
||||
email: 'rachel@company.com',
|
||||
processRoles: [{ processId: 2, role: 'Artefact' }],
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: 'Sam',
|
||||
avatar: 'S',
|
||||
email: 'sam@company.com',
|
||||
processRoles: [{ processId: 2, role: 'Resolve' }],
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
name: 'Tom',
|
||||
avatar: 'T',
|
||||
email: 'tom@company.com',
|
||||
processRoles: [{ processId: 2, role: 'Resolve' }],
|
||||
},
|
||||
|
||||
// Processus 3
|
||||
{
|
||||
id: 21,
|
||||
name: 'Uma',
|
||||
avatar: 'U',
|
||||
email: 'uma@company.com',
|
||||
processRoles: [{ processId: 3, role: 'Deposit' }],
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
name: 'Victor',
|
||||
avatar: 'V',
|
||||
email: 'victor@company.com',
|
||||
processRoles: [{ processId: 3, role: 'Deposit' }],
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
name: 'Walter',
|
||||
avatar: 'W',
|
||||
email: 'walter@company.com',
|
||||
processRoles: [{ processId: 3, role: 'Payment' }],
|
||||
},
|
||||
{
|
||||
id: 24,
|
||||
name: 'Xena',
|
||||
avatar: 'X',
|
||||
email: 'xena@company.com',
|
||||
processRoles: [{ processId: 3, role: 'Payment' }],
|
||||
},
|
||||
];
|
||||
@ -1,64 +0,0 @@
|
||||
export const messagesMock = [
|
||||
{
|
||||
memberId: 1, // Conversations avec Mmber 1
|
||||
messages: [
|
||||
{ id: 1, sender: 'Mmeber 1', text: 'Salut !', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Bonjour ! Comment ça va ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Tout va bien, merci !', time: '10:32 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 2, // Conversations avec Member 2
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 2', text: 'Salut, on se voit ce soir ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Oui, à quelle heure ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 3, // Conversations avec Member 3
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 3', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 4, // Conversations avec Member 4
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 4', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 5, // Conversations avec Member 5
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 5', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 6, // Conversations avec Member 6
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 6', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 7, // Conversations avec Member 7
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 7', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 8, // Conversations avec Member 8
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 8', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
];
|
||||
@ -1,7 +0,0 @@
|
||||
import { Device, Process, SecretsStore } from "pkg/sdk_client";
|
||||
|
||||
export interface BackUp {
|
||||
device: Device,
|
||||
secrets: SecretsStore,
|
||||
processes: Record<string, Process>,
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
export interface INotification {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
sendToNotificationPage?: boolean;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
// Quelles sont les données utiles pour le user ???
|
||||
export interface IUser {
|
||||
id: string;
|
||||
information?: any;
|
||||
}
|
||||
|
||||
// Quelles sont les données utiles pour les messages ???
|
||||
export interface IMessage {
|
||||
id: string;
|
||||
message: any;
|
||||
}
|
||||
|
||||
export interface UserDiff {
|
||||
new_state_merkle_root: string; // TODO add a merkle proof that the new_value belongs to that state
|
||||
field: string;
|
||||
previous_value: string;
|
||||
new_value: string;
|
||||
notify_user: boolean;
|
||||
need_validation: boolean;
|
||||
// validated: bool,
|
||||
proof: any; // This is only validation (or refusal) for that specific diff, not the whole state. It can't be commited as such
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
export interface IProcess {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
icon?: string;
|
||||
zoneList: IZone[];
|
||||
}
|
||||
|
||||
export interface IZone {
|
||||
id: number;
|
||||
name: string;
|
||||
path: string;
|
||||
// Est-ce que la zone a besoin d'une icone ?
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface INotification {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
sendToNotificationPage?: boolean;
|
||||
path?: string;
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
export interface Group {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
roles: {
|
||||
id?: number;
|
||||
name: string;
|
||||
members: { id: string | number; name: string }[];
|
||||
documents?: {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
visibility: string;
|
||||
createdAt: string | null;
|
||||
deadline: string | null;
|
||||
signatures: DocumentSignature[];
|
||||
status?: string;
|
||||
files?: Array<{ name: string; url: string }>;
|
||||
}[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: number;
|
||||
sender: string;
|
||||
text?: string;
|
||||
time: string;
|
||||
type: 'text' | 'file';
|
||||
fileName?: string;
|
||||
fileData?: string;
|
||||
}
|
||||
|
||||
export interface MemberMessages {
|
||||
memberId: string;
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
export interface DocumentSignature {
|
||||
signed: boolean;
|
||||
member: {
|
||||
name: string;
|
||||
};
|
||||
signedAt?: string;
|
||||
}
|
||||
|
||||
export interface RequestParams {
|
||||
processId: number;
|
||||
processName: string;
|
||||
roleId: number;
|
||||
roleName: string;
|
||||
documentId: number;
|
||||
documentName: string;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
memberId: string;
|
||||
text: string;
|
||||
time: string;
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
import { AccountElement } from './account';
|
||||
import accountCss from '../../../public/style/account.css?raw';
|
||||
import Services from '../../services/service.js';
|
||||
|
||||
class AccountComponent extends HTMLElement {
|
||||
_callback: any;
|
||||
accountElement: AccountElement | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
console.log('INIT');
|
||||
this.attachShadow({ mode: 'open' });
|
||||
|
||||
this.accountElement = this.shadowRoot?.querySelector('account-element') || null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('CALLBACKs');
|
||||
this.render();
|
||||
this.fetchData();
|
||||
|
||||
if (!customElements.get('account-element')) {
|
||||
customElements.define('account-element', AccountElement);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchData() {
|
||||
if ((import.meta as any).env.VITE_IS_INDEPENDANT_LIB === false) {
|
||||
const data = await (window as any).myService?.getProcesses();
|
||||
} else {
|
||||
const service = await Services.getInstance();
|
||||
const data = await service.getProcesses();
|
||||
}
|
||||
}
|
||||
|
||||
set callback(fn) {
|
||||
if (typeof fn === 'function') {
|
||||
this._callback = fn;
|
||||
} else {
|
||||
console.error('Callback is not a function');
|
||||
}
|
||||
}
|
||||
|
||||
get callback() {
|
||||
return this._callback;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.shadowRoot && !this.shadowRoot.querySelector('account-element')) {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = accountCss;
|
||||
|
||||
const accountElement = document.createElement('account-element');
|
||||
|
||||
this.shadowRoot.appendChild(style);
|
||||
this.shadowRoot.appendChild(accountElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { AccountComponent };
|
||||
customElements.define('account-component', AccountComponent);
|
||||
@ -1,10 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Account</title>
|
||||
</head>
|
||||
<body>
|
||||
<account-component></account-component>
|
||||
<script type="module" src="./account.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,49 +0,0 @@
|
||||
import { ChatElement } from './chat';
|
||||
import chatCss from '../../../public/style/chat.css?raw';
|
||||
import Services from '../../services/service.js';
|
||||
|
||||
class ChatComponent extends HTMLElement {
|
||||
_callback: any;
|
||||
chatElement: ChatElement | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
console.log('INIT');
|
||||
this.attachShadow({ mode: 'open' });
|
||||
|
||||
this.chatElement = this.shadowRoot?.querySelector('chat-element') || null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('CALLBACKs');
|
||||
this.render();
|
||||
|
||||
if (!customElements.get('chat-element')) {
|
||||
customElements.define('chat-element', ChatElement);
|
||||
}
|
||||
}
|
||||
|
||||
set callback(fn) {
|
||||
if (typeof fn === 'function') {
|
||||
this._callback = fn;
|
||||
} else {
|
||||
console.error('Callback is not a function');
|
||||
}
|
||||
}
|
||||
|
||||
get callback() {
|
||||
return this._callback;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.shadowRoot) {
|
||||
// Créer l'élément chat-element
|
||||
const chatElement = document.createElement('chat-element');
|
||||
this.shadowRoot.innerHTML = `<style>${chatCss}</style>`;
|
||||
this.shadowRoot.appendChild(chatElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { ChatComponent };
|
||||
customElements.define('chat-component', ChatComponent);
|
||||
@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Chat</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<chat-component></chat-component>
|
||||
<script type="module" src="./chat.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
275
src/pages/home/Home.ts
Normal file
275
src/pages/home/Home.ts
Normal file
@ -0,0 +1,275 @@
|
||||
// src/pages/process/Home.ts
|
||||
import Services from "../../services/service";
|
||||
import globalCss from "../../assets/styles/style.css?inline";
|
||||
import homeHtml from "./home.html?raw";
|
||||
import {
|
||||
displayEmojis,
|
||||
generateCreateBtn,
|
||||
prepareAndSendPairingTx,
|
||||
addressToEmoji,
|
||||
} from "../../utils/sp-address.utils";
|
||||
import { Router } from "../../router/index";
|
||||
|
||||
export class HomePage extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
this.initLogic();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.shadowRoot) {
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
${globalCss}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.home-layout {
|
||||
min-height: 80vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Auth Card */
|
||||
.auth-container {
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.auth-header { text-align: center; margin-bottom: 2rem; }
|
||||
.subtitle { color: var(--text-muted); font-size: 0.9rem; }
|
||||
|
||||
/* Tabs */
|
||||
.tabs-nav {
|
||||
display: flex;
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.tab-btn {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tab-btn.active {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.tab-content {
|
||||
display: none;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
.tab-content.active { display: block; }
|
||||
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
|
||||
|
||||
.input-group label { display: block; margin-bottom: 0.5rem; font-size: 0.9rem; color: var(--text-muted); }
|
||||
|
||||
.my-address-display {
|
||||
margin-top: 1rem;
|
||||
padding: 10px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Loader */
|
||||
.loader-overlay {
|
||||
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: var(--bg-color);
|
||||
z-index: 2000;
|
||||
display: flex; justify-content: center; align-items: center;
|
||||
}
|
||||
.spinner {
|
||||
width: 40px; height: 40px;
|
||||
border: 3px solid rgba(255,255,255,0.1);
|
||||
border-top-color: var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 1rem auto;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.loader-step { color: var(--text-muted); font-size: 0.9rem; transition: color 0.3s; }
|
||||
.loader-step.active { color: var(--primary); font-weight: bold; }
|
||||
|
||||
</style>
|
||||
${homeHtml}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async initLogic() {
|
||||
const container = this.shadowRoot;
|
||||
if (!container) return;
|
||||
|
||||
const loaderDiv = container.querySelector(
|
||||
"#iframe-loader"
|
||||
) as HTMLDivElement;
|
||||
const mainContentDiv = container.querySelector(
|
||||
"#main-content"
|
||||
) as HTMLDivElement;
|
||||
const tabs = container.querySelectorAll(".tab");
|
||||
|
||||
tabs.forEach((tab) => {
|
||||
tab.addEventListener("click", () => {
|
||||
// Remplacement de addSubscription pour simplifier ici
|
||||
container
|
||||
.querySelectorAll(".tab")
|
||||
.forEach((t) => t.classList.remove("active"));
|
||||
tab.classList.add("active");
|
||||
container
|
||||
.querySelectorAll(".tab-content")
|
||||
.forEach((content) => content.classList.remove("active"));
|
||||
container
|
||||
.querySelector(`#${tab.getAttribute("data-tab") as string}`)
|
||||
?.classList.add("active");
|
||||
});
|
||||
});
|
||||
|
||||
const delay = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
try {
|
||||
await delay(500);
|
||||
this.addLoaderStep("Initialisation des services...");
|
||||
const service = await Services.getInstance();
|
||||
|
||||
await delay(700);
|
||||
this.addLoaderStep("Vérification de l'appareil...");
|
||||
const currentDevice = await service.getDeviceFromDatabase();
|
||||
const pairingId = currentDevice?.pairing_process_commitment || null;
|
||||
|
||||
if (pairingId) {
|
||||
await delay(300);
|
||||
this.addLoaderStep("Appairage existant trouvé.");
|
||||
service.setProcessId(pairingId);
|
||||
} else {
|
||||
await delay(300);
|
||||
this.addLoaderStep("Création d'un appairage sécurisé...");
|
||||
await prepareAndSendPairingTx();
|
||||
this.addLoaderStep("Appairage créé avec succès.");
|
||||
}
|
||||
|
||||
// --- SUCCÈS ---
|
||||
console.log("[Home] Auto-pairing terminé avec succès.");
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("app:pairing-ready", {
|
||||
detail: { success: true },
|
||||
})
|
||||
);
|
||||
|
||||
if (window.self !== window.top) {
|
||||
// CAS IFRAME : On ne bouge pas !
|
||||
// On affiche juste un état "Prêt" dans le loader pour le debug visuel
|
||||
this.addLoaderStep("Prêt. En attente de l'application parente...");
|
||||
console.log(
|
||||
"[Home] 📡 Mode Iframe : Pas de redirection. Attente des messages API."
|
||||
);
|
||||
} else {
|
||||
// CAS STANDALONE : On redirige
|
||||
console.log("[Home] 🚀 Mode Standalone : Redirection vers /process...");
|
||||
await delay(500);
|
||||
|
||||
// On nettoie l'UI avant de partir
|
||||
if (loaderDiv) loaderDiv.style.display = "none";
|
||||
if (mainContentDiv) mainContentDiv.style.display = "block";
|
||||
|
||||
// Hop, on navigue
|
||||
Router.navigate("process");
|
||||
}
|
||||
|
||||
container
|
||||
.querySelectorAll(".tab")
|
||||
.forEach((t) => t.classList.remove("active"));
|
||||
container.querySelector('[data-tab="tab2"]')?.classList.add("active");
|
||||
container
|
||||
.querySelectorAll(".tab-content")
|
||||
.forEach((content) => content.classList.remove("active"));
|
||||
container.querySelector("#tab2")?.classList.add("active");
|
||||
|
||||
const spAddress = await service.getDeviceAddress();
|
||||
generateCreateBtn();
|
||||
displayEmojis(spAddress);
|
||||
await this.populateMemberSelect();
|
||||
|
||||
await delay(1000);
|
||||
|
||||
if (loaderDiv) loaderDiv.style.display = "none";
|
||||
if (mainContentDiv) mainContentDiv.style.display = "block";
|
||||
|
||||
console.log("[Home] Init terminée.");
|
||||
} catch (e: any) {
|
||||
console.error("[Home] Erreur:", e);
|
||||
this.addLoaderStep(`Erreur: ${e.message}`);
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("app:pairing-ready", {
|
||||
detail: { success: false, error: e.message },
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
addLoaderStep(text: string) {
|
||||
const container = this.shadowRoot;
|
||||
if (!container) return;
|
||||
const currentStep = container.querySelector(
|
||||
".loader-step.active"
|
||||
) as HTMLParagraphElement;
|
||||
if (currentStep) currentStep.classList.remove("active");
|
||||
|
||||
const stepsContainer = container.querySelector(
|
||||
"#loader-steps-container"
|
||||
) as HTMLDivElement;
|
||||
if (stepsContainer) {
|
||||
const newStep = document.createElement("p");
|
||||
newStep.className = "loader-step active";
|
||||
newStep.textContent = text;
|
||||
stepsContainer.appendChild(newStep);
|
||||
}
|
||||
}
|
||||
|
||||
async populateMemberSelect() {
|
||||
const container = this.shadowRoot;
|
||||
if (!container) return;
|
||||
const memberSelect = container.querySelector(
|
||||
"#memberSelect"
|
||||
) as HTMLSelectElement;
|
||||
if (!memberSelect) return;
|
||||
|
||||
const service = await Services.getInstance();
|
||||
const members = await service.getAllMembersSorted();
|
||||
|
||||
for (const [processId, member] of Object.entries(members)) {
|
||||
const emojis = await addressToEmoji(processId);
|
||||
const option = document.createElement("option");
|
||||
option.value = processId;
|
||||
option.textContent = `Member (${emojis})`;
|
||||
memberSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("home-page", HomePage);
|
||||
@ -1,49 +0,0 @@
|
||||
import loginHtml from './home.html?raw';
|
||||
import loginScript from './home.ts?raw';
|
||||
import loginCss from '../../4nk.css?raw';
|
||||
import { initHomePage } from './home';
|
||||
|
||||
export class LoginComponent extends HTMLElement {
|
||||
_callback: any;
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('CALLBACK LOGIN PAGE');
|
||||
this.render();
|
||||
setTimeout(() => {
|
||||
initHomePage();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
set callback(fn) {
|
||||
if (typeof fn === 'function') {
|
||||
this._callback = fn;
|
||||
} else {
|
||||
console.error('Callback is not a function');
|
||||
}
|
||||
}
|
||||
|
||||
get callback() {
|
||||
return this._callback;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.shadowRoot)
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
${loginCss}
|
||||
</style>${loginHtml}
|
||||
<script type="module">
|
||||
${loginScript}
|
||||
</scipt>
|
||||
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!customElements.get('login-4nk-component')) {
|
||||
customElements.define('login-4nk-component', LoginComponent);
|
||||
}
|
||||
@ -1,42 +1,51 @@
|
||||
<div class="title-container">
|
||||
<h1>Create Account / New Session</h1>
|
||||
</div>
|
||||
<div class="home-layout">
|
||||
|
||||
<div class="tab-container">
|
||||
<div class="tabs">
|
||||
<div class="tab active" data-tab="tab1">Create an account</div>
|
||||
<div class="tab" data-tab="tab2">Add a device for an existing memeber</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-container">
|
||||
<div id="tab1" class="card tab-content active">
|
||||
<div class="card-description">Create an account :</div>
|
||||
<div class="pairing-request"></div>
|
||||
<!-- <div class="card-image qr-code">
|
||||
<img src="assets/qr_code.png" alt="QR Code" width="150" height="150" />
|
||||
</div> -->
|
||||
<button id="createButton" class="create-btn"></button>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<div id="tab2" class="card tab-content">
|
||||
<div class="card-description">Add a device for an existing member :</div>
|
||||
<div class="card-image camera-card">
|
||||
<img id="scanner" src="assets/camera.jpg" alt="QR Code" width="150" height="150" />
|
||||
<button id="scan-btn" onclick="scanDevice()">Scan</button>
|
||||
<div class="qr-code-scanner">
|
||||
<div id="qr-reader" style="width: 200px; display: contents"></div>
|
||||
<div id="qr-reader-results"></div>
|
||||
<div id="iframe-loader" class="loader-overlay">
|
||||
<div class="loader-content glass-panel">
|
||||
<div class="spinner"></div>
|
||||
<div id="loader-steps-container">
|
||||
<p class="loader-step active">Démarrage du système...</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>Or</p>
|
||||
<!-- <input type="text" id="addressInput" placeholder="Paste address" />
|
||||
<div id="emoji-display-2"></div> -->
|
||||
<div class="card-description">Chose a member :</div>
|
||||
<select name="memberSelect" id="memberSelect" size="5" class="custom-select">
|
||||
<!-- Options -->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button id="okButton" style="display: none">OK</button>
|
||||
<div id="main-content" class="auth-container" style="display: none;">
|
||||
|
||||
<div class="auth-card glass-panel">
|
||||
<div class="auth-header">
|
||||
<h1>Bienvenue</h1>
|
||||
<p class="subtitle">Connectez votre appareil ou créez un compte</p>
|
||||
</div>
|
||||
|
||||
<div class="tabs-nav">
|
||||
<button class="tab-btn active" data-tab="tab2">Connexion</button>
|
||||
<button class="tab-btn" data-tab="tab1">Nouveau Compte</button>
|
||||
</div>
|
||||
|
||||
<div id="tab2" class="tab-content active">
|
||||
<div class="input-group">
|
||||
<label>Sélectionner un membre</label>
|
||||
<div class="select-wrapper">
|
||||
<select id="memberSelect"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-address-display">
|
||||
<span>Mon ID :</span>
|
||||
<span class="emoji-display">...</span>
|
||||
</div>
|
||||
<button id="okButton" class="btn w-full mt-4">Se Connecter</button>
|
||||
</div>
|
||||
|
||||
<div id="tab1" class="tab-content">
|
||||
<div class="qr-section">
|
||||
<div class="qr-code">
|
||||
<img src="" alt="Scan QR" />
|
||||
</div>
|
||||
<p class="pairing-request"></p>
|
||||
</div>
|
||||
<button id="createButton" class="btn w-full mt-4">Créer un compte</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,94 +0,0 @@
|
||||
import Routing from '../../services/modal.service';
|
||||
import Services from '../../services/service';
|
||||
import { addSubscription } from '../../utils/subscription.utils';
|
||||
import { displayEmojis, generateQRCode, generateCreateBtn, addressToEmoji } from '../../utils/sp-address.utils';
|
||||
import { getCorrectDOM } from '../../utils/html.utils';
|
||||
import QrScannerComponent from '../../components/qrcode-scanner/qrcode-scanner-component';
|
||||
export { QrScannerComponent };
|
||||
export async function initHomePage(): Promise<void> {
|
||||
console.log('INIT-HOME');
|
||||
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
|
||||
container.querySelectorAll('.tab').forEach((tab) => {
|
||||
addSubscription(tab, 'click', () => {
|
||||
container.querySelectorAll('.tab').forEach((t) => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
|
||||
container.querySelectorAll('.tab-content').forEach((content) => content.classList.remove('active'));
|
||||
container.querySelector(`#${tab.getAttribute('data-tab') as string}`)?.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
const service = await Services.getInstance();
|
||||
const spAddress = await service.getDeviceAddress();
|
||||
// generateQRCode(spAddress);
|
||||
generateCreateBtn ();
|
||||
displayEmojis(spAddress);
|
||||
|
||||
// Add this line to populate the select when the page loads
|
||||
await populateMemberSelect();
|
||||
}
|
||||
|
||||
//// Modal
|
||||
export async function openModal(myAddress: string, receiverAddress: string) {
|
||||
const router = await Routing.getInstance();
|
||||
router.openLoginModal(myAddress, receiverAddress);
|
||||
}
|
||||
|
||||
// const service = await Services.getInstance()
|
||||
// service.setNotification()
|
||||
|
||||
function scanDevice() {
|
||||
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
|
||||
const scannerImg = container.querySelector('#scanner') as HTMLElement;
|
||||
if (scannerImg) scannerImg.style.display = 'none';
|
||||
const scannerQrCode = container.querySelector('.qr-code-scanner') as HTMLElement;
|
||||
if (scannerQrCode) scannerQrCode.style.display = 'block';
|
||||
const scanButton = container?.querySelector('#scan-btn') as HTMLElement;
|
||||
if (scanButton) scanButton.style.display = 'none';
|
||||
const reader = container?.querySelector('#qr-reader');
|
||||
if (reader) reader.innerHTML = '<qr-scanner></qr-scanner>';
|
||||
}
|
||||
|
||||
async function populateMemberSelect() {
|
||||
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
|
||||
const memberSelect = container.querySelector('#memberSelect') as HTMLSelectElement;
|
||||
|
||||
if (!memberSelect) {
|
||||
console.error('Could not find memberSelect element');
|
||||
return;
|
||||
}
|
||||
|
||||
const service = await Services.getInstance();
|
||||
const members = service.getAllMembersSorted();
|
||||
|
||||
for (const [processId, member] of Object.entries(members)) {
|
||||
const process = await service.getProcess(processId);
|
||||
let memberPublicName;
|
||||
|
||||
if (process) {
|
||||
const publicMemberData = service.getPublicData(process);
|
||||
if (publicMemberData) {
|
||||
const extractedName = publicMemberData['memberPublicName'];
|
||||
if (extractedName !== undefined && extractedName !== null) {
|
||||
memberPublicName = extractedName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!memberPublicName) {
|
||||
memberPublicName = 'Unnamed Member';
|
||||
}
|
||||
|
||||
// Récupérer les emojis pour ce processId
|
||||
const emojis = await addressToEmoji(processId);
|
||||
|
||||
const option = document.createElement('option');
|
||||
option.value = processId;
|
||||
option.textContent = `${memberPublicName} (${emojis})`;
|
||||
memberSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).populateMemberSelect = populateMemberSelect;
|
||||
|
||||
(window as any).scanDevice = scanDevice;
|
||||
@ -1,51 +0,0 @@
|
||||
import processHtml from './process-element.html?raw';
|
||||
import processScript from './process-element.ts?raw';
|
||||
import processCss from '../../4nk.css?raw';
|
||||
import { initProcessElement } from './process-element';
|
||||
|
||||
export class ProcessListComponent extends HTMLElement {
|
||||
_callback: any;
|
||||
id: string = '';
|
||||
zone: string = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('CALLBACK PROCESS LIST PAGE');
|
||||
this.render();
|
||||
setTimeout(() => {
|
||||
initProcessElement(this.id, this.zone);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
set callback(fn) {
|
||||
if (typeof fn === 'function') {
|
||||
this._callback = fn;
|
||||
} else {
|
||||
console.error('Callback is not a function');
|
||||
}
|
||||
}
|
||||
|
||||
get callback() {
|
||||
return this._callback;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.shadowRoot)
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
${processCss}
|
||||
</style>${processHtml}
|
||||
<script type="module">
|
||||
${processScript}
|
||||
</scipt>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!customElements.get('process-4nk-component')) {
|
||||
customElements.define('process-4nk-component', ProcessListComponent);
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
<div class="title-container">
|
||||
<h1>Process {{processTitle}}</h1>
|
||||
</div>
|
||||
|
||||
<div class="process-container"></div>
|
||||
@ -1,50 +0,0 @@
|
||||
import { interpolate } from '../../utils/html.utils';
|
||||
import Services from '../../services/service';
|
||||
import { Process } from 'pkg/sdk_client';
|
||||
import { getCorrectDOM } from '~/utils/document.utils';
|
||||
|
||||
let currentPageStyle: HTMLStyleElement | null = null;
|
||||
|
||||
export async function initProcessElement(id: string, zone: string) {
|
||||
const processes = await getProcesses();
|
||||
const container = getCorrectDOM('process-4nk-component');
|
||||
// const currentProcess = processes.find((process) => process[0] === id)[1];
|
||||
// const currentProcess = {title: 'Hello', html: '', css: ''};
|
||||
// await loadPage({ processTitle: currentProcess.title, inputValue: 'Hello World !' });
|
||||
// const wrapper = document.querySelector('.process-container');
|
||||
// if (wrapper) {
|
||||
// wrapper.innerHTML = interpolate(currentProcess.html, { processTitle: currentProcess.title, inputValue: 'Hello World !' });
|
||||
// injectCss(currentProcess.css);
|
||||
// }
|
||||
}
|
||||
|
||||
async function loadPage(data?: any) {
|
||||
const content = document.getElementById('containerId');
|
||||
if (content && data) {
|
||||
if (data) {
|
||||
content.innerHTML = interpolate(content.innerHTML, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function injectCss(cssContent: string) {
|
||||
removeCss(); // Ensure that the previous CSS is removed
|
||||
|
||||
currentPageStyle = document.createElement('style');
|
||||
currentPageStyle.type = 'text/css';
|
||||
currentPageStyle.appendChild(document.createTextNode(cssContent));
|
||||
document.head.appendChild(currentPageStyle);
|
||||
}
|
||||
|
||||
function removeCss() {
|
||||
if (currentPageStyle) {
|
||||
document.head.removeChild(currentPageStyle);
|
||||
currentPageStyle = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getProcesses(): Promise<Record<string, Process>> {
|
||||
const service = await Services.getInstance();
|
||||
const processes = await service.getProcesses();
|
||||
return processes;
|
||||
}
|
||||
310
src/pages/process/ProcessList.ts
Executable file
310
src/pages/process/ProcessList.ts
Executable file
@ -0,0 +1,310 @@
|
||||
import processHtml from "./process.html?raw";
|
||||
import globalCss from "../../assets/styles/style.css?inline";
|
||||
import Services from "../../services/service";
|
||||
|
||||
export class ProcessListPage extends HTMLElement {
|
||||
private services!: Services;
|
||||
|
||||
// Éléments du DOM
|
||||
private inputInput!: HTMLInputElement;
|
||||
private autocompleteList!: HTMLUListElement;
|
||||
private tagsContainer!: HTMLElement;
|
||||
private detailsContainer!: HTMLElement;
|
||||
private okButton!: HTMLButtonElement;
|
||||
private wrapper!: HTMLElement;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
this.services = await Services.getInstance();
|
||||
this.render();
|
||||
setTimeout(() => this.initLogic(), 0);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.shadowRoot) {
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
${globalCss}
|
||||
:host { display: block; width: 100%; }
|
||||
.process-layout { padding: 2rem; display: flex; justify-content: center; }
|
||||
.dashboard-container { width: 100%; max-width: 800px; display: flex; flex-direction: column; gap: 1.5rem; max-height: 85vh; overflow-y: auto; }
|
||||
.dashboard-header { text-align: center; }
|
||||
.subtitle { color: var(--text-muted); margin-top: -0.5rem; }
|
||||
.search-input-container { position: relative; display: flex; align-items: center; }
|
||||
.search-input-container input { padding-right: 40px; background: rgba(255,255,255,0.05); border: 1px solid var(--glass-border); transition: all 0.3s; }
|
||||
.search-input-container input:focus { background: rgba(255,255,255,0.1); border-color: var(--primary); }
|
||||
.search-icon { position: absolute; right: 12px; opacity: 0.5; }
|
||||
.autocomplete-dropdown { list-style: none; margin-top: 5px; padding: 0; background: #1e293b; border: 1px solid var(--glass-border); border-radius: var(--radius-sm); max-height: 200px; overflow-y: auto; display: none; position: absolute; width: 100%; z-index: 10; box-shadow: 0 10px 25px rgba(0,0,0,0.5); }
|
||||
.custom-select-wrapper { position: relative; }
|
||||
.autocomplete-dropdown li { padding: 10px 15px; cursor: pointer; border-bottom: 1px solid rgba(255,255,255,0.05); transition: background 0.2s; color: var(--text-main); }
|
||||
.autocomplete-dropdown li:hover { background: var(--primary); color: white; }
|
||||
.autocomplete-dropdown li.my-process { border-left: 3px solid var(--accent); }
|
||||
.tags-container { display: flex; flex-wrap: wrap; gap: 8px; min-height: 30px; }
|
||||
.tag { background: rgba(var(--primary-hue), 50, 50, 0.3); border: 1px solid var(--primary); color: white; padding: 4px 10px; border-radius: 20px; font-size: 0.85rem; display: flex; align-items: center; gap: 8px; animation: popIn 0.2s ease-out; }
|
||||
.tag-close { cursor: pointer; opacity: 0.7; font-weight: bold; }
|
||||
.tag-close:hover { opacity: 1; }
|
||||
@keyframes popIn { from { transform: scale(0.8); opacity: 0; } to { transform: scale(1); opacity: 1; } }
|
||||
.divider { height: 1px; background: var(--glass-border); margin: 0.5rem 0; }
|
||||
.details-content { background: rgba(0,0,0,0.2); border-radius: var(--radius-sm); padding: 1rem; min-height: 100px; }
|
||||
.empty-state { color: var(--text-muted); font-style: italic; text-align: center; padding: 2rem; }
|
||||
.process-item { margin-bottom: 1rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1rem; }
|
||||
.process-title-display { font-size: 1.1rem; font-weight: bold; color: var(--accent); margin-bottom: 0.5rem; }
|
||||
.state-element { background: rgba(255,255,255,0.05); padding: 8px 12px; margin-top: 5px; border-radius: 4px; cursor: pointer; transition: background 0.2s; border: 1px solid transparent; font-family: monospace; font-size: 0.9rem; }
|
||||
.state-element:hover { background: rgba(255,255,255,0.1); }
|
||||
.state-element.selected { background: rgba(var(--success), 0.2); border-color: var(--success); }
|
||||
.dashboard-footer { display: flex; justify-content: flex-end; }
|
||||
::-webkit-scrollbar { width: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.4); }
|
||||
</style>
|
||||
${processHtml}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async initLogic() {
|
||||
const root = this.shadowRoot;
|
||||
if (!root) return;
|
||||
|
||||
this.wrapper = root.querySelector("#autocomplete-wrapper") as HTMLElement;
|
||||
this.inputInput = root.querySelector("#process-input") as HTMLInputElement;
|
||||
this.autocompleteList = root.querySelector("#autocomplete-list") as HTMLUListElement;
|
||||
this.tagsContainer = root.querySelector("#selected-tags-container") as HTMLElement;
|
||||
this.detailsContainer = root.querySelector("#process-details") as HTMLElement;
|
||||
this.okButton = root.querySelector("#go-to-process-btn") as HTMLButtonElement;
|
||||
|
||||
this.inputInput.addEventListener("keyup", () => this.handleInput());
|
||||
this.inputInput.addEventListener("click", () => this.openDropdown());
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
const path = e.composedPath();
|
||||
if (!path.includes(this.wrapper)) {
|
||||
this.closeDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
this.okButton.addEventListener("click", () => this.goToProcess());
|
||||
|
||||
document.addEventListener("processes-updated", async () => {
|
||||
await this.populateList(this.inputInput.value);
|
||||
});
|
||||
|
||||
await this.populateList("");
|
||||
}
|
||||
|
||||
// --- Logique Autocomplete Sécurisée ---
|
||||
|
||||
async populateList(query: string) {
|
||||
this.autocompleteList.innerHTML = "";
|
||||
|
||||
const mineArray = (await this.services.getMyProcesses()) ?? [];
|
||||
const allProcesses = await this.services.getProcesses();
|
||||
const otherProcesses = Object.keys(allProcesses).filter(
|
||||
(id) => !mineArray.includes(id)
|
||||
);
|
||||
const listToShow = [...mineArray, ...otherProcesses];
|
||||
|
||||
let count = 0;
|
||||
|
||||
for (const pid of listToShow) {
|
||||
const process = allProcesses[pid];
|
||||
if (!process) continue;
|
||||
|
||||
const name = (await this.services.getProcessName(process)) || pid;
|
||||
|
||||
if (
|
||||
query &&
|
||||
!name.toLowerCase().includes(query.toLowerCase()) &&
|
||||
!pid.includes(query)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
count++;
|
||||
const li = document.createElement("li");
|
||||
|
||||
const nameSpan = document.createElement("span");
|
||||
nameSpan.textContent = name;
|
||||
li.appendChild(nameSpan);
|
||||
|
||||
if (mineArray.includes(pid)) {
|
||||
li.classList.add("my-process");
|
||||
const small = document.createElement("small");
|
||||
small.style.opacity = "0.6";
|
||||
small.style.marginLeft = "8px";
|
||||
small.textContent = "(Mien)";
|
||||
li.appendChild(small);
|
||||
}
|
||||
|
||||
li.addEventListener("click", () => {
|
||||
this.addTag(pid, name);
|
||||
this.inputInput.value = "";
|
||||
this.showProcessDetails(pid);
|
||||
this.closeDropdown();
|
||||
});
|
||||
|
||||
this.autocompleteList.appendChild(li);
|
||||
}
|
||||
|
||||
if (count === 0) {
|
||||
const empty = document.createElement("li");
|
||||
empty.textContent = "Aucun résultat";
|
||||
empty.style.cursor = "default";
|
||||
empty.style.opacity = "0.5";
|
||||
this.autocompleteList.appendChild(empty);
|
||||
}
|
||||
}
|
||||
|
||||
handleInput() {
|
||||
this.openDropdown();
|
||||
this.populateList(this.inputInput.value);
|
||||
}
|
||||
|
||||
openDropdown() {
|
||||
this.autocompleteList.style.display = "block";
|
||||
}
|
||||
|
||||
closeDropdown() {
|
||||
this.autocompleteList.style.display = "none";
|
||||
}
|
||||
|
||||
// --- Gestion des Tags Sécurisée ---
|
||||
|
||||
addTag(pid: string, name: string) {
|
||||
this.tagsContainer.innerHTML = "";
|
||||
|
||||
const tag = document.createElement("div");
|
||||
tag.className = "tag";
|
||||
|
||||
const spanName = document.createElement("span");
|
||||
spanName.textContent = name;
|
||||
tag.appendChild(spanName);
|
||||
|
||||
const closeBtn = document.createElement("span");
|
||||
closeBtn.className = "tag-close";
|
||||
closeBtn.innerHTML = "×";
|
||||
|
||||
closeBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this.removeTag();
|
||||
});
|
||||
tag.appendChild(closeBtn);
|
||||
|
||||
this.tagsContainer.appendChild(tag);
|
||||
}
|
||||
|
||||
removeTag() {
|
||||
this.tagsContainer.innerHTML = "";
|
||||
|
||||
this.detailsContainer.innerHTML = "";
|
||||
const emptyState = document.createElement("div");
|
||||
emptyState.className = "empty-state";
|
||||
const p = document.createElement("p");
|
||||
p.textContent = "Aucun processus sélectionné.";
|
||||
emptyState.appendChild(p);
|
||||
this.detailsContainer.appendChild(emptyState);
|
||||
|
||||
this.okButton.disabled = true;
|
||||
this.okButton.classList.add("disabled");
|
||||
}
|
||||
|
||||
// --- Détails du processus Sécurisés ---
|
||||
|
||||
async showProcessDetails(pid: string) {
|
||||
this.detailsContainer.textContent = "Chargement...";
|
||||
|
||||
const process = await this.services.getProcess(pid);
|
||||
if (!process) return;
|
||||
|
||||
this.detailsContainer.innerHTML = "";
|
||||
|
||||
const name = (await this.services.getProcessName(process)) || "Sans nom";
|
||||
|
||||
// Description
|
||||
let description = "Pas de description";
|
||||
const lastState = await this.services.getLastCommitedState(process);
|
||||
|
||||
if (lastState?.pcd_commitment["description"]) {
|
||||
const diff = await this.services.getDiffByValue(
|
||||
lastState.pcd_commitment["description"]
|
||||
);
|
||||
if (diff) description = diff.value_commitment;
|
||||
}
|
||||
|
||||
const containerDiv = document.createElement("div");
|
||||
containerDiv.className = "process-item";
|
||||
|
||||
// Titre
|
||||
const titleDiv = document.createElement("div");
|
||||
titleDiv.className = "process-title-display";
|
||||
titleDiv.textContent = name;
|
||||
containerDiv.appendChild(titleDiv);
|
||||
|
||||
// Description
|
||||
const descDiv = document.createElement("div");
|
||||
descDiv.style.fontSize = "0.9rem";
|
||||
descDiv.style.marginBottom = "10px";
|
||||
descDiv.textContent = description;
|
||||
containerDiv.appendChild(descDiv);
|
||||
|
||||
// ID
|
||||
const idDiv = document.createElement("div");
|
||||
idDiv.style.fontSize = "0.8rem";
|
||||
idDiv.style.opacity = "0.7";
|
||||
idDiv.style.marginBottom = "10px";
|
||||
idDiv.textContent = `ID: ${pid}`;
|
||||
containerDiv.appendChild(idDiv);
|
||||
|
||||
// Label "États en attente"
|
||||
const labelDiv = document.createElement("div");
|
||||
labelDiv.style.fontWeight = "bold";
|
||||
labelDiv.style.marginTop = "15px";
|
||||
labelDiv.textContent = "États en attente :";
|
||||
containerDiv.appendChild(labelDiv);
|
||||
|
||||
const uncommitted = await this.services.getUncommitedStates(process);
|
||||
|
||||
if (uncommitted.length > 0) {
|
||||
uncommitted.forEach((state) => {
|
||||
const el = document.createElement("div");
|
||||
el.className = "state-element";
|
||||
el.textContent = `État: ${state.state_id.substring(0, 16)}...`;
|
||||
|
||||
el.addEventListener("click", () => {
|
||||
this.shadowRoot
|
||||
?.querySelectorAll(".state-element")
|
||||
.forEach((x) => x.classList.remove("selected"));
|
||||
el.classList.add("selected");
|
||||
|
||||
this.okButton.disabled = false;
|
||||
this.okButton.dataset.target = `${pid}/${state.state_id}`;
|
||||
});
|
||||
|
||||
containerDiv.appendChild(el);
|
||||
});
|
||||
} else {
|
||||
const empty = document.createElement("div");
|
||||
empty.style.padding = "10px";
|
||||
empty.style.opacity = "0.6";
|
||||
empty.textContent = "Aucun état en attente de validation.";
|
||||
containerDiv.appendChild(empty);
|
||||
}
|
||||
|
||||
this.detailsContainer.appendChild(containerDiv);
|
||||
}
|
||||
|
||||
goToProcess() {
|
||||
const target = this.okButton.dataset.target;
|
||||
if (target) {
|
||||
console.log("Navigation vers", target);
|
||||
alert("Navigation vers : " + target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("process-list-page", ProcessListPage);
|
||||
@ -1,49 +0,0 @@
|
||||
import processHtml from './process.html?raw';
|
||||
import processScript from './process.ts?raw';
|
||||
import processCss from '../../4nk.css?raw';
|
||||
import { init } from './process';
|
||||
|
||||
export class ProcessListComponent extends HTMLElement {
|
||||
_callback: any;
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('CALLBACK PROCESS LIST PAGE');
|
||||
this.render();
|
||||
setTimeout(() => {
|
||||
init();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
set callback(fn) {
|
||||
if (typeof fn === 'function') {
|
||||
this._callback = fn;
|
||||
} else {
|
||||
console.error('Callback is not a function');
|
||||
}
|
||||
}
|
||||
|
||||
get callback() {
|
||||
return this._callback;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.shadowRoot)
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
${processCss}
|
||||
</style>${processHtml}
|
||||
<script type="module">
|
||||
${processScript}
|
||||
</scipt>
|
||||
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!customElements.get('process-list-4nk-component')) {
|
||||
customElements.define('process-list-4nk-component', ProcessListComponent);
|
||||
}
|
||||
@ -1,19 +1,45 @@
|
||||
<div class="title-container">
|
||||
<h1>Process Selection</h1>
|
||||
</div>
|
||||
<div class="process-layout">
|
||||
<div class="dashboard-container glass-panel">
|
||||
|
||||
<div class="process-container">
|
||||
<div class="process-card">
|
||||
<div class="process-card-description">
|
||||
<div class="input-container">
|
||||
<select multiple data-multi-select-plugin id="autocomplete" placeholder="Filter processes..." class="select-field"></select>
|
||||
<label for="autocomplete" class="input-label">Filter processes :</label>
|
||||
<div class="selected-processes"></div>
|
||||
<div class="dashboard-header">
|
||||
<h1>Mes Processus</h1>
|
||||
<p class="subtitle">Sélectionnez et gérez vos flux de travail</p>
|
||||
</div>
|
||||
|
||||
<div class="search-section">
|
||||
<div class="input-group">
|
||||
<label>Rechercher un processus</label>
|
||||
<div id="autocomplete-wrapper" class="custom-select-wrapper">
|
||||
<select multiple id="process-select" style="display:none"></select>
|
||||
<div class="search-input-container">
|
||||
<input type="text" id="process-input" placeholder="Filtrer par nom ou ID..." autocomplete="off">
|
||||
<span class="search-icon">🔍</span>
|
||||
</div>
|
||||
<ul id="autocomplete-list" class="autocomplete-dropdown"></ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="process-card-content"></div>
|
||||
|
||||
<div id="selected-tags-container" class="tags-container">
|
||||
</div>
|
||||
</div>
|
||||
<div class="process-card-action">
|
||||
<a class="btn" onclick="goToProcessPage()">OK</a>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="details-section">
|
||||
<h3>Détails du processus</h3>
|
||||
<div id="process-details" class="details-content">
|
||||
<div class="empty-state">
|
||||
<p>Aucun processus sélectionné.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-footer">
|
||||
<button id="go-to-process-btn" class="btn btn-primary" disabled>
|
||||
Accéder au Processus
|
||||
<svg style="margin-left:8px" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@ -1,520 +0,0 @@
|
||||
import { addSubscription } from '../../utils/subscription.utils';
|
||||
import Services from '../../services/service';
|
||||
import { getCorrectDOM } from '~/utils/html.utils';
|
||||
import { Process } from 'pkg/sdk_client';
|
||||
import chatStyle from '../../../public/style/chat.css?inline';
|
||||
import { Database } from '../../services/database.service';
|
||||
|
||||
// Initialize function, create initial tokens with itens that are already selected by the user
|
||||
export async function init() {
|
||||
|
||||
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
|
||||
const element = container.querySelector('select') as HTMLSelectElement;
|
||||
// Create div that wroaps all the elements inside (select, elements selected, search div) to put select inside
|
||||
const wrapper = document.createElement('div');
|
||||
if (wrapper) addSubscription(wrapper, 'click', clickOnWrapper);
|
||||
wrapper.classList.add('multi-select-component');
|
||||
wrapper.classList.add('input-field');
|
||||
|
||||
// Create elements of search
|
||||
const search_div = document.createElement('div');
|
||||
search_div.classList.add('search-container');
|
||||
const input = document.createElement('input');
|
||||
input.classList.add('selected-input');
|
||||
input.setAttribute('autocomplete', 'off');
|
||||
input.setAttribute('tabindex', '0');
|
||||
if (input) {
|
||||
addSubscription(input, 'keyup', inputChange);
|
||||
addSubscription(input, 'keydown', deletePressed);
|
||||
addSubscription(input, 'click', openOptions);
|
||||
}
|
||||
|
||||
const dropdown_icon = document.createElement('a');
|
||||
dropdown_icon.classList.add('dropdown-icon');
|
||||
|
||||
if (dropdown_icon) addSubscription(dropdown_icon, 'click', clickDropdown);
|
||||
const autocomplete_list = document.createElement('ul');
|
||||
autocomplete_list.classList.add('autocomplete-list');
|
||||
search_div.appendChild(input);
|
||||
search_div.appendChild(autocomplete_list);
|
||||
search_div.appendChild(dropdown_icon);
|
||||
|
||||
// set the wrapper as child (instead of the element)
|
||||
element.parentNode?.replaceChild(wrapper, element);
|
||||
// set element as child of wrapper
|
||||
wrapper.appendChild(element);
|
||||
wrapper.appendChild(search_div);
|
||||
|
||||
addPlaceholder(wrapper);
|
||||
}
|
||||
|
||||
function removePlaceholder(wrapper: HTMLElement) {
|
||||
const input_search = wrapper.querySelector('.selected-input');
|
||||
input_search?.removeAttribute('placeholder');
|
||||
}
|
||||
|
||||
function addPlaceholder(wrapper: HTMLElement) {
|
||||
const input_search = wrapper.querySelector('.selected-input');
|
||||
const tokens = wrapper.querySelectorAll('.selected-wrapper');
|
||||
if (!tokens.length && !(document.activeElement === input_search)) input_search?.setAttribute('placeholder', '---------');
|
||||
}
|
||||
|
||||
// Listener of user search
|
||||
function inputChange(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const wrapper = target?.parentNode?.parentNode;
|
||||
const select = wrapper?.querySelector('select') as HTMLSelectElement;
|
||||
const dropdown = wrapper?.querySelector('.dropdown-icon');
|
||||
|
||||
const input_val = target?.value;
|
||||
|
||||
if (input_val) {
|
||||
dropdown?.classList.add('active');
|
||||
populateAutocompleteList(select, input_val.trim());
|
||||
} else {
|
||||
dropdown?.classList.remove('active');
|
||||
const event = new Event('click');
|
||||
dropdown?.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for clicks on the wrapper, if click happens focus on the input
|
||||
function clickOnWrapper(e: Event) {
|
||||
const wrapper = e.target as HTMLElement;
|
||||
if (wrapper.tagName == 'DIV') {
|
||||
const input_search = wrapper.querySelector('.selected-input');
|
||||
const dropdown = wrapper.querySelector('.dropdown-icon');
|
||||
if (!dropdown?.classList.contains('active')) {
|
||||
const event = new Event('click');
|
||||
dropdown?.dispatchEvent(event);
|
||||
}
|
||||
(input_search as HTMLInputElement)?.focus();
|
||||
removePlaceholder(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
function openOptions(e: Event) {
|
||||
const input_search = e.target as HTMLElement;
|
||||
const wrapper = input_search?.parentElement?.parentElement;
|
||||
const dropdown = wrapper?.querySelector('.dropdown-icon');
|
||||
if (!dropdown?.classList.contains('active')) {
|
||||
const event = new Event('click');
|
||||
dropdown?.dispatchEvent(event);
|
||||
}
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Function that create a token inside of a wrapper with the given value
|
||||
function createToken(wrapper: HTMLElement, value: any) {
|
||||
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
|
||||
const search = wrapper.querySelector('.search-container');
|
||||
const inputInderline = container.querySelector('.selected-processes');
|
||||
// Create token wrapper
|
||||
const token = document.createElement('div');
|
||||
token.classList.add('selected-wrapper');
|
||||
const token_span = document.createElement('span');
|
||||
token_span.classList.add('selected-label');
|
||||
token_span.innerText = value;
|
||||
const close = document.createElement('a');
|
||||
close.classList.add('selected-close');
|
||||
close.setAttribute('tabindex', '-1');
|
||||
close.setAttribute('data-option', value);
|
||||
close.setAttribute('data-hits', '0');
|
||||
close.innerText = 'x';
|
||||
if (close) addSubscription(close, 'click', removeToken);
|
||||
token.appendChild(token_span);
|
||||
token.appendChild(close);
|
||||
inputInderline?.appendChild(token);
|
||||
}
|
||||
|
||||
// Listen for clicks in the dropdown option
|
||||
function clickDropdown(e: Event) {
|
||||
const dropdown = e.target as HTMLElement;
|
||||
const wrapper = dropdown?.parentNode?.parentNode;
|
||||
const input_search = wrapper?.querySelector('.selected-input') as HTMLInputElement;
|
||||
const select = wrapper?.querySelector('select') as HTMLSelectElement;
|
||||
dropdown.classList.toggle('active');
|
||||
|
||||
if (dropdown.classList.contains('active')) {
|
||||
removePlaceholder(wrapper as HTMLElement);
|
||||
input_search?.focus();
|
||||
|
||||
if (!input_search?.value) {
|
||||
populateAutocompleteList(select, '', true);
|
||||
} else {
|
||||
populateAutocompleteList(select, input_search.value);
|
||||
}
|
||||
} else {
|
||||
clearAutocompleteList(select);
|
||||
addPlaceholder(wrapper as HTMLElement);
|
||||
}
|
||||
}
|
||||
|
||||
// Clears the results of the autocomplete list
|
||||
function clearAutocompleteList(select: HTMLSelectElement) {
|
||||
const wrapper = select.parentNode;
|
||||
|
||||
const autocomplete_list = wrapper?.querySelector('.autocomplete-list');
|
||||
if (autocomplete_list) autocomplete_list.innerHTML = '';
|
||||
}
|
||||
|
||||
async function populateAutocompleteList(select: HTMLSelectElement, query: string, dropdown = false) {
|
||||
const { autocomplete_options } = getOptions(select);
|
||||
|
||||
let options_to_show = [];
|
||||
|
||||
const service = await Services.getInstance();
|
||||
const mineArray: string[] = await service.getMyProcesses();
|
||||
const allProcesses = await service.getProcesses();
|
||||
const allArray: string[] = Object.keys(allProcesses).filter(x => !mineArray.includes(x));
|
||||
|
||||
const wrapper = select.parentNode;
|
||||
const input_search = wrapper?.querySelector('.search-container');
|
||||
const autocomplete_list = wrapper?.querySelector('.autocomplete-list');
|
||||
if (autocomplete_list) autocomplete_list.innerHTML = '';
|
||||
|
||||
const addProcessToList = (processId:string, isMine: boolean) => {
|
||||
const li = document.createElement('li');
|
||||
li.innerText = processId;
|
||||
li.setAttribute("data-value", processId);
|
||||
|
||||
if (isMine) {
|
||||
li.classList.add("my-process");
|
||||
li.style.cssText = `color: var(--accent-color)`;
|
||||
}
|
||||
|
||||
if (li) addSubscription(li, 'click', selectOption);
|
||||
autocomplete_list?.appendChild(li);
|
||||
};
|
||||
|
||||
mineArray.forEach(processId => addProcessToList(processId, true));
|
||||
allArray.forEach(processId => addProcessToList(processId, false));
|
||||
|
||||
if (mineArray.length === 0 && allArray.length === 0) {
|
||||
const li = document.createElement('li');
|
||||
li.classList.add('not-cursor');
|
||||
li.innerText = 'No options found';
|
||||
autocomplete_list?.appendChild(li);
|
||||
}
|
||||
}
|
||||
|
||||
// Listener to autocomplete results when clicked set the selected property in the select option
|
||||
function selectOption(e: any) {
|
||||
console.log('🎯 Click event:', e);
|
||||
console.log('🎯 Target value:', e.target.dataset.value);
|
||||
|
||||
const wrapper = e.target.parentNode.parentNode.parentNode;
|
||||
const select = wrapper.querySelector('select');
|
||||
const input_search = wrapper.querySelector('.selected-input');
|
||||
const option = wrapper.querySelector(`select option[value="${e.target.dataset.value}"]`);
|
||||
|
||||
console.log('🎯 Selected option:', option);
|
||||
console.log('🎯 Process ID:', option?.getAttribute('data-process-id'));
|
||||
|
||||
if (e.target.dataset.value.includes('messaging')) {
|
||||
const messagingNumber = parseInt(e.target.dataset.value.split(' ')[1]);
|
||||
const processId = select.getAttribute(`data-messaging-id-${messagingNumber}`);
|
||||
|
||||
console.log('🚀 Dispatching newMessagingProcess event:', {
|
||||
processId,
|
||||
processName: `Messaging Process ${processId}`
|
||||
});
|
||||
|
||||
// Dispatch l'événement avant la navigation
|
||||
document.dispatchEvent(new CustomEvent('newMessagingProcess', {
|
||||
detail: {
|
||||
processId: processId,
|
||||
processName: `Messaging Process ${processId}`
|
||||
}
|
||||
}));
|
||||
|
||||
// Navigation vers le chat
|
||||
const navigateEvent = new CustomEvent('navigate', {
|
||||
detail: {
|
||||
page: 'chat',
|
||||
processId: processId || ''
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(navigateEvent);
|
||||
return;
|
||||
}
|
||||
option.setAttribute('selected', '');
|
||||
createToken(wrapper, e.target.dataset.value);
|
||||
if (input_search.value) {
|
||||
input_search.value = '';
|
||||
}
|
||||
|
||||
showSelectedProcess(e.target.dataset.value);
|
||||
|
||||
input_search.focus();
|
||||
|
||||
e.target.remove();
|
||||
const autocomplete_list = wrapper.querySelector('.autocomplete-list');
|
||||
|
||||
if (!autocomplete_list.children.length) {
|
||||
const li = document.createElement('li');
|
||||
li.classList.add('not-cursor');
|
||||
li.innerText = 'No options found';
|
||||
autocomplete_list.appendChild(li);
|
||||
}
|
||||
|
||||
const event = new Event('keyup');
|
||||
input_search.dispatchEvent(event);
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// function that returns a list with the autcomplete list of matches
|
||||
function autocomplete(query: string, options: any) {
|
||||
// No query passed, just return entire list
|
||||
if (!query) {
|
||||
return options;
|
||||
}
|
||||
let options_return = [];
|
||||
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
if (query.toLowerCase() === options[i].slice(0, query.length).toLowerCase()) {
|
||||
options_return.push(options[i]);
|
||||
}
|
||||
}
|
||||
return options_return;
|
||||
}
|
||||
|
||||
// Returns the options that are selected by the user and the ones that are not
|
||||
function getOptions(select: HTMLSelectElement) {
|
||||
// Select all the options available
|
||||
const all_options = Array.from(select.querySelectorAll('option')).map((el) => el.value);
|
||||
|
||||
// Get the options that are selected from the user
|
||||
const options_selected = Array.from(select.querySelectorAll('option:checked')).map((el: any) => el.value);
|
||||
|
||||
// Create an autocomplete options array with the options that are not selected by the user
|
||||
const autocomplete_options: any[] = [];
|
||||
all_options.forEach((option) => {
|
||||
if (!options_selected.includes(option)) {
|
||||
autocomplete_options.push(option);
|
||||
}
|
||||
});
|
||||
|
||||
autocomplete_options.sort();
|
||||
|
||||
return {
|
||||
options_selected,
|
||||
autocomplete_options,
|
||||
};
|
||||
}
|
||||
|
||||
// Listener for when the user wants to remove a given token.
|
||||
function removeToken(e: Event) {
|
||||
// Get the value to remove
|
||||
const target = e.target as HTMLSelectElement;
|
||||
const value_to_remove = target.dataset.option;
|
||||
const wrapper = target.parentNode?.parentNode?.parentNode;
|
||||
const input_search = wrapper?.querySelector('.selected-input');
|
||||
const dropdown = wrapper?.querySelector('.dropdown-icon');
|
||||
// Get the options in the select to be unselected
|
||||
const option_to_unselect = wrapper?.querySelector(`select option[value="${value_to_remove}"]`);
|
||||
option_to_unselect?.removeAttribute('selected');
|
||||
// Remove token attribute
|
||||
(target.parentNode as any)?.remove();
|
||||
dropdown?.classList.remove('active');
|
||||
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
|
||||
|
||||
const process = container.querySelector('#' + target.dataset.option);
|
||||
process?.remove();
|
||||
}
|
||||
|
||||
// Listen for 2 sequence of hits on the delete key, if this happens delete the last token if exist
|
||||
function deletePressed(e: Event) {
|
||||
const input_search = e.target as HTMLInputElement;
|
||||
const wrapper = input_search?.parentNode?.parentNode;
|
||||
const key = (e as KeyboardEvent).keyCode || (e as KeyboardEvent).charCode;
|
||||
const tokens = wrapper?.querySelectorAll('.selected-wrapper');
|
||||
|
||||
if (tokens?.length) {
|
||||
const last_token_x = tokens[tokens.length - 1].querySelector('a');
|
||||
let hits = +(last_token_x?.dataset?.hits || 0);
|
||||
|
||||
if (key == 8 || key == 46) {
|
||||
if (!input_search.value) {
|
||||
if (hits > 1) {
|
||||
// Trigger delete event
|
||||
const event = new Event('click');
|
||||
last_token_x?.dispatchEvent(event);
|
||||
} else {
|
||||
if (last_token_x?.dataset.hits) last_token_x.dataset.hits = '2';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (last_token_x?.dataset.hits) last_token_x.dataset.hits = '0';
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Dismiss on outside click
|
||||
addSubscription(document, 'click', () => {
|
||||
// get select that has the options available
|
||||
const select = document.querySelectorAll('[data-multi-select-plugin]');
|
||||
for (let i = 0; i < select.length; i++) {
|
||||
if (event) {
|
||||
var isClickInside = select[i].parentElement?.parentElement?.contains(event.target as Node);
|
||||
|
||||
if (!isClickInside) {
|
||||
const wrapper = select[i].parentElement?.parentElement;
|
||||
const dropdown = wrapper?.querySelector('.dropdown-icon');
|
||||
const autocomplete_list = wrapper?.querySelector('.autocomplete-list');
|
||||
//the click was outside the specifiedElement, do something
|
||||
dropdown?.classList.remove('active');
|
||||
if (autocomplete_list) autocomplete_list.innerHTML = '';
|
||||
addPlaceholder(wrapper as HTMLElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function showSelectedProcess(elem: MouseEvent) {
|
||||
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
|
||||
|
||||
if (elem) {
|
||||
const cardContent = container.querySelector('.process-card-content');
|
||||
|
||||
const processes = await getProcesses();
|
||||
const process = processes.find((process: any) => process[1].title === elem);
|
||||
if (process) {
|
||||
const processDiv = document.createElement('div');
|
||||
processDiv.className = 'process';
|
||||
processDiv.id = process[0];
|
||||
const titleDiv = document.createElement('div');
|
||||
titleDiv.className = 'process-title';
|
||||
titleDiv.innerHTML = `${process[1].title} : ${process[1].description}`;
|
||||
processDiv.appendChild(titleDiv);
|
||||
for (const zone of process.zones) {
|
||||
const zoneElement = document.createElement('div');
|
||||
zoneElement.className = 'process-element';
|
||||
const zoneId = process[1].title + '-' + zone.id;
|
||||
zoneElement.setAttribute('zone-id', zoneId);
|
||||
zoneElement.setAttribute('process-title', process[1].title);
|
||||
zoneElement.setAttribute('process-id', `${process[0]}_${zone.id}`);
|
||||
zoneElement.innerHTML = `${zone.title}: ${zone.description}`;
|
||||
addSubscription(zoneElement, 'click', select);
|
||||
processDiv.appendChild(zoneElement);
|
||||
}
|
||||
if (cardContent) cardContent.appendChild(processDiv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function select(event: Event) {
|
||||
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
|
||||
const target = event.target as HTMLElement;
|
||||
const oldSelectedProcess = container.querySelector('.selected-process-zone');
|
||||
oldSelectedProcess?.classList.remove('selected-process-zone');
|
||||
if (target) {
|
||||
target.classList.add('selected-process-zone');
|
||||
}
|
||||
const name = target.getAttribute('zone-id');
|
||||
console.log('🚀 ~ select ~ name:', name);
|
||||
}
|
||||
|
||||
function goToProcessPage() {
|
||||
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
|
||||
|
||||
const target = container.querySelector('.selected-process-zone');
|
||||
console.log('🚀 ~ goToProcessPage ~ event:', target);
|
||||
if (target) {
|
||||
const process = target?.getAttribute('process-id');
|
||||
|
||||
console.log('=======================> going to process page', process);
|
||||
// navigate('process-element/' + process);
|
||||
document.querySelector('process-list-4nk-component')?.dispatchEvent(
|
||||
new CustomEvent('processSelected', {
|
||||
detail: {
|
||||
process: process,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).goToProcessPage = goToProcessPage;
|
||||
|
||||
async function createMessagingProcess(): Promise<void> {
|
||||
console.log('Creating messaging process');
|
||||
const service = await Services.getInstance();
|
||||
const otherMembers = [
|
||||
{
|
||||
sp_addresses: [
|
||||
"tsp1qqd7snxfh44am8f7a3x36znkh4v0dcagcgakfux488ghsg0tny7degq4gd9q4n4us0cyp82643f2p4jgcmtwknadqwl3waf9zrynl6n7lug5tg73a",
|
||||
"tsp1qqvd8pak9fyz55rxqj90wxazqzwupf2egderc96cn84h3l84z8an9vql85scudrmwvsnltfuy9ungg7pxnhys2ft5wnf2gyr3n4ukvezygswesjuc"
|
||||
]
|
||||
},
|
||||
{
|
||||
sp_addresses: [
|
||||
"tsp1qqgl5vawdey6wnnn2sfydcejsr06uzwsjlfa6p6yr8u4mkqwezsnvyqlazuqmxhxd8crk5eq3wfvdwv4k3tn68mkj2nj72jj39d2ngauu4unfx0q7",
|
||||
"tsp1qqthmj56gj8vvkjzwhcmswftlrf6ye7ukpks2wra92jkehqzrvx7m2q570q5vv6zj6dnxvussx2h8arvrcfwz9sp5hpdzrfugmmzz90pmnganxk28"
|
||||
]
|
||||
},
|
||||
{
|
||||
sp_addresses: [
|
||||
"tsp1qqwjtxr9jye7d40qxrsmd6h02egdwel6mfnujxzskgxvxphfya4e6qqjq4tsdmfdmtnmccz08ut24q8y58qqh4lwl3w8pvh86shlmavrt0u3smhv2",
|
||||
"tsp1qqwn7tf8q2jhmfh8757xze53vg2zc6x5u6f26h3wyty9mklvcy0wnvqhhr4zppm5uyyte4y86kljvh8r0tsmkmszqqwa3ecf2lxcs7q07d56p8sz5"
|
||||
]
|
||||
}
|
||||
];
|
||||
await service.checkConnections(otherMembers);
|
||||
const relayAddress = service.getAllRelays().pop();
|
||||
if (!relayAddress) {
|
||||
throw new Error('Empty relay address list');
|
||||
}
|
||||
const feeRate = 1;
|
||||
setTimeout(async () => {
|
||||
const createProcessReturn = await service.createMessagingProcess(otherMembers, relayAddress.spAddress, feeRate);
|
||||
const updatedProcess = createProcessReturn.updated_process.current_process;
|
||||
if (!updatedProcess) {
|
||||
console.error('Failed to retrieved new messaging process');
|
||||
return;
|
||||
}
|
||||
const processId = updatedProcess.states[0].commited_in;
|
||||
const stateId = updatedProcess.states[0].state_id;
|
||||
await service.handleApiReturn(createProcessReturn);
|
||||
const createPrdReturn = await service.createPrdUpdate(processId, stateId);
|
||||
await service.handleApiReturn(createPrdReturn);
|
||||
const approveChangeReturn = await service.approveChange(processId, stateId);
|
||||
await service.handleApiReturn(approveChangeReturn);
|
||||
}, 500)
|
||||
}
|
||||
|
||||
async function getDescription(processId: string, process: Process): Promise<string | null> {
|
||||
const service = await Services.getInstance();
|
||||
// Get the `commited_in` value of the last state and remove it from the array
|
||||
const currentCommitedIn = process.states.pop()?.commited_in;
|
||||
|
||||
if (currentCommitedIn === undefined) {
|
||||
return null; // No states available
|
||||
}
|
||||
|
||||
// Find the last state where `commited_in` is different
|
||||
let lastDifferentState = process.states.findLast(
|
||||
state => state.commited_in !== currentCommitedIn
|
||||
);
|
||||
|
||||
if (!lastDifferentState) {
|
||||
// It means that we only have one state that is not commited yet, that can happen with process we just created
|
||||
// let's assume that the right description is in the last concurrent state and not handle the (arguably rare) case where we have multiple concurrent states on a creation
|
||||
lastDifferentState = process.states.pop();
|
||||
}
|
||||
|
||||
// Take the description out of the state, if any
|
||||
const description = lastDifferentState!.pcd_commitment['description'];
|
||||
if (description) {
|
||||
const userDiff = await service.getDiffByValue(description);
|
||||
if (userDiff) {
|
||||
console.log("Successfully retrieved userDiff:", userDiff);
|
||||
return userDiff.new_value;
|
||||
} else {
|
||||
console.log("Failed to retrieve a non-null userDiff.");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
import { SignatureElement } from './signature';
|
||||
import signatureCss from '../../../public/style/signature.css?raw'
|
||||
import Services from '../../services/service.js'
|
||||
|
||||
class SignatureComponent extends HTMLElement {
|
||||
_callback: any
|
||||
signatureElement: SignatureElement | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
console.log('INIT')
|
||||
this.attachShadow({ mode: 'open' });
|
||||
|
||||
this.signatureElement = this.shadowRoot?.querySelector('signature-element') || null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('CALLBACKs')
|
||||
this.render();
|
||||
this.fetchData();
|
||||
|
||||
if (!customElements.get('signature-element')) {
|
||||
customElements.define('signature-element', SignatureElement);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchData() {
|
||||
if ((import.meta as any).env.VITE_IS_INDEPENDANT_LIB === false) {
|
||||
const data = await (window as any).myService?.getProcesses();
|
||||
} else {
|
||||
const service = await Services.getInstance()
|
||||
const data = await service.getProcesses();
|
||||
}
|
||||
}
|
||||
|
||||
set callback(fn) {
|
||||
if (typeof fn === 'function') {
|
||||
this._callback = fn;
|
||||
} else {
|
||||
console.error('Callback is not a function');
|
||||
}
|
||||
}
|
||||
|
||||
get callback() {
|
||||
return this._callback;
|
||||
}
|
||||
|
||||
render() {
|
||||
if(this.shadowRoot) {
|
||||
const signatureElement = document.createElement('signature-element');
|
||||
this.shadowRoot.innerHTML = `<style>${signatureCss}</style>`;
|
||||
this.shadowRoot.appendChild(signatureElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { SignatureComponent }
|
||||
customElements.define('signature-component', SignatureComponent);
|
||||
@ -1,12 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Signatures</title>
|
||||
</head>
|
||||
<body>
|
||||
<signature-component></signature-component>
|
||||
<script type="module" src="./signature.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
209
src/router.ts
209
src/router.ts
@ -1,209 +0,0 @@
|
||||
import '../public/style/4nk.css';
|
||||
import { initHeader } from '../src/components/header/header';
|
||||
import { initChat } from '../src/pages/chat/chat';
|
||||
import Database from './services/database.service';
|
||||
import Services from './services/service';
|
||||
import { cleanSubscriptions } from './utils/subscription.utils';
|
||||
import { LoginComponent } from './pages/home/home-component';
|
||||
import { prepareAndSendPairingTx } from './utils/sp-address.utils';
|
||||
import ModalService from './services/modal.service';
|
||||
export { Services };
|
||||
const routes: { [key: string]: string } = {
|
||||
home: '/src/pages/home/home.html',
|
||||
process: '/src/pages/process/process.html',
|
||||
'process-element': '/src/pages/process-element/process-element.html',
|
||||
account: '/src/pages/account/account.html',
|
||||
chat: '/src/pages/chat/chat.html',
|
||||
signature: '/src/pages/signature/signature.html',
|
||||
};
|
||||
|
||||
export let currentRoute = '';
|
||||
|
||||
export async function navigate(path: string) {
|
||||
cleanSubscriptions();
|
||||
cleanPage();
|
||||
path = path.replace(/^\//, '');
|
||||
if (path.includes('/')) {
|
||||
const parsedPath = path.split('/')[0];
|
||||
if (!routes[parsedPath]) {
|
||||
path = 'home';
|
||||
}
|
||||
}
|
||||
|
||||
await handleLocation(path);
|
||||
}
|
||||
|
||||
async function handleLocation(path: string) {
|
||||
const parsedPath = path.split('/');
|
||||
if (path.includes('/')) {
|
||||
path = parsedPath[0];
|
||||
}
|
||||
currentRoute = path;
|
||||
const routeHtml = routes[path] || routes['home'];
|
||||
|
||||
const content = document.getElementById('containerId');
|
||||
if (content) {
|
||||
if (path === 'home') {
|
||||
const login = LoginComponent;
|
||||
const container = document.querySelector('#containerId');
|
||||
const accountComponent = document.createElement('login-4nk-component');
|
||||
accountComponent.setAttribute('style', 'width: 100vw; height: 100vh; position: relative; grid-row: 2;');
|
||||
if (container) container.appendChild(accountComponent);
|
||||
} else if (path !== 'process') {
|
||||
const html = await fetch(routeHtml).then((data) => data.text());
|
||||
content.innerHTML = html;
|
||||
}
|
||||
|
||||
await new Promise(requestAnimationFrame);
|
||||
injectHeader();
|
||||
|
||||
// const modalService = await ModalService.getInstance()
|
||||
// modalService.injectValidationModal()
|
||||
switch (path) {
|
||||
case 'process':
|
||||
// const { init } = await import('./pages/process/process');
|
||||
const { ProcessListComponent } = await import('./pages/process/process-list-component');
|
||||
|
||||
const container2 = document.querySelector('#containerId');
|
||||
const accountComponent = document.createElement('process-list-4nk-component');
|
||||
|
||||
if (!customElements.get('process-list-4nk-component')) {
|
||||
customElements.define('process-list-4nk-component', ProcessListComponent);
|
||||
}
|
||||
accountComponent.setAttribute('style', 'height: 100vh; position: relative; grid-row: 2; grid-column: 4;');
|
||||
if (container2) container2.appendChild(accountComponent);
|
||||
break;
|
||||
|
||||
case 'process-element':
|
||||
if (parsedPath && parsedPath.length) {
|
||||
const { initProcessElement } = await import('./pages/process-element/process-element');
|
||||
const parseProcess = parsedPath[1].split('_');
|
||||
initProcessElement(parseProcess[0], parseProcess[1]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'account':
|
||||
const { AccountComponent } = await import('./pages/account/account-component');
|
||||
const accountContainer = document.querySelector('.parameter-list');
|
||||
if (accountContainer) {
|
||||
if (!customElements.get('account-component')) {
|
||||
customElements.define('account-component', AccountComponent);
|
||||
}
|
||||
const accountComponent = document.createElement('account-component');
|
||||
accountContainer.appendChild(accountComponent);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'chat':
|
||||
const { ChatComponent } = await import('./pages/chat/chat-component');
|
||||
const chatContainer = document.querySelector('.group-list');
|
||||
if (chatContainer) {
|
||||
if (!customElements.get('chat-component')) {
|
||||
customElements.define('chat-component', ChatComponent);
|
||||
}
|
||||
const chatComponent = document.createElement('chat-component');
|
||||
chatContainer.appendChild(chatComponent);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'signature':
|
||||
const { SignatureComponent } = await import('./pages/signature/signature-component');
|
||||
const container = document.querySelector('.group-list');
|
||||
if (container) {
|
||||
if (!customElements.get('signature-component')) {
|
||||
customElements.define('signature-component', SignatureComponent);
|
||||
}
|
||||
const signatureComponent = document.createElement('signature-component');
|
||||
container.appendChild(signatureComponent);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.onpopstate = async () => {
|
||||
const services = await Services.getInstance();
|
||||
if (!services.isPaired()) {
|
||||
handleLocation('home');
|
||||
} else {
|
||||
handleLocation('process');
|
||||
}
|
||||
};
|
||||
|
||||
export async function init(): Promise<void> {
|
||||
try {
|
||||
const services = await Services.getInstance();
|
||||
(window as any).myService = services;
|
||||
await Database.getInstance();
|
||||
setTimeout(async () => {
|
||||
let device = await services.getDeviceFromDatabase();
|
||||
console.log('🚀 ~ setTimeout ~ device:', device);
|
||||
|
||||
if (!device) {
|
||||
device = await services.createNewDevice();
|
||||
} else {
|
||||
services.restoreDevice(device);
|
||||
}
|
||||
await services.restoreProcessesFromDB();
|
||||
await services.restoreSecretsFromDB();
|
||||
|
||||
if (services.isPaired()) {
|
||||
await navigate('chat');
|
||||
} else {
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
const pairingAddress = urlParams.get('sp_address');
|
||||
if (pairingAddress) {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
// check if we have a shared secret with that address
|
||||
await prepareAndSendPairingTx(pairingAddress);
|
||||
} catch (e) {
|
||||
console.error('Failed to pair:', e);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
await navigate('home');
|
||||
}
|
||||
}, 200);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await navigate('home');
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanPage() {
|
||||
const container = document.querySelector('#containerId');
|
||||
if (container) container.innerHTML = '';
|
||||
}
|
||||
|
||||
async function injectHeader() {
|
||||
const headerContainer = document.getElementById('header-container');
|
||||
if (headerContainer) {
|
||||
const headerHtml = await fetch('/src/components/header/header.html').then((res) => res.text());
|
||||
headerContainer.innerHTML = headerHtml;
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = '/src/components/header/header.ts';
|
||||
script.type = 'module';
|
||||
document.head.appendChild(script);
|
||||
initHeader();
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).navigate = navigate;
|
||||
|
||||
document.addEventListener('navigate', ((e: Event) => {
|
||||
const event = e as CustomEvent<{page: string, processId?: string}>;
|
||||
if (event.detail.page === 'chat') {
|
||||
const container = document.querySelector('.container');
|
||||
if (container) container.innerHTML = '';
|
||||
|
||||
initChat();
|
||||
|
||||
const chatElement = document.querySelector('chat-element');
|
||||
if (chatElement) {
|
||||
chatElement.setAttribute('process-id', event.detail.processId || '');
|
||||
}
|
||||
}
|
||||
}));
|
||||
64
src/router/index.ts
Normal file
64
src/router/index.ts
Normal file
@ -0,0 +1,64 @@
|
||||
// src/router/index.ts
|
||||
|
||||
// On définit les routes ici
|
||||
const routes: Record<string, () => Promise<any>> = {
|
||||
home: () => import('../pages/home/Home'), // Charge Home.ts
|
||||
process: () => import('../pages/process/ProcessList'), // Charge ProcessList.ts
|
||||
};
|
||||
|
||||
export class Router {
|
||||
static async init() {
|
||||
// Gestion du bouton retour navigateur
|
||||
window.addEventListener('popstate', () => Router.handleLocation());
|
||||
|
||||
// Gestion de la navigation initiale
|
||||
Router.handleLocation();
|
||||
}
|
||||
|
||||
static async navigate(path: string) {
|
||||
window.history.pushState({}, '', path);
|
||||
await Router.handleLocation();
|
||||
}
|
||||
|
||||
static async handleLocation() {
|
||||
const path = window.location.pathname.replace(/^\//, '') || 'home'; // 'home' par défaut
|
||||
|
||||
// Nettoyage simple (gestion des sous-routes éventuelles)
|
||||
const routeKey = path.split('/')[0] || 'home';
|
||||
|
||||
const appContainer = document.getElementById('app-container');
|
||||
if (!appContainer) return;
|
||||
|
||||
// 1. Nettoyer le conteneur
|
||||
appContainer.innerHTML = '';
|
||||
|
||||
// 2. Charger la page demandée
|
||||
try {
|
||||
if (routes[routeKey]) {
|
||||
// Import dynamique du fichier TS
|
||||
await routes[routeKey]();
|
||||
|
||||
// Création de l'élément correspondant
|
||||
let pageElement;
|
||||
if (routeKey === 'home') {
|
||||
pageElement = document.createElement('home-page');
|
||||
} else if (routeKey === 'process') {
|
||||
pageElement = document.createElement('process-list-page');
|
||||
}
|
||||
|
||||
if (pageElement) {
|
||||
appContainer.appendChild(pageElement);
|
||||
}
|
||||
} else {
|
||||
console.warn(`Route inconnue: ${routeKey}, redirection vers Home`);
|
||||
Router.navigate('home');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur de chargement de la page:', error);
|
||||
appContainer.innerHTML = '<h1>Erreur de chargement</h1>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// On expose navigate globalement pour ton header et autres scripts legacy
|
||||
(window as any).navigate = (path: string) => Router.navigate(path);
|
||||
@ -1,13 +0,0 @@
|
||||
function onScanSuccess(decodedText, decodedResult) {
|
||||
// handle the scanned code as you like, for example:
|
||||
console.log(`Code matched = ${decodedText}`, decodedResult);
|
||||
}
|
||||
|
||||
function onScanFailure(error) {
|
||||
// handle scan failure, usually better to ignore and keep scanning.
|
||||
// for example:
|
||||
console.warn(`Code scan error = ${error}`);
|
||||
}
|
||||
|
||||
let html5QrcodeScanner = new Html5QrcodeScanner('reader', { fps: 10, qrbox: { width: 250, height: 250 } }, /* verbose= */ false);
|
||||
html5QrcodeScanner.render(onScanSuccess, onScanFailure);
|
||||
@ -1,8 +0,0 @@
|
||||
const addResourcesToCache = async (resources) => {
|
||||
const cache = await caches.open('v1');
|
||||
await cache.addAll(resources);
|
||||
};
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(addResourcesToCache(['/', '/index.html', '/style.css', '/app.js', '/image-list.js', '/star-wars-logo.jpg', '/gallery/bountyHunters.jpg', '/gallery/myLittleVader.jpg', '/gallery/snowTroopers.jpg']));
|
||||
});
|
||||
@ -1,266 +0,0 @@
|
||||
const EMPTY32BYTES = String('').padStart(64, '0');
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(self.skipWaiting()); // Activate worker immediately
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(self.clients.claim()); // Become available to all pages
|
||||
});
|
||||
|
||||
// Event listener for messages from clients
|
||||
self.addEventListener('message', async (event) => {
|
||||
const data = event.data;
|
||||
console.log(data);
|
||||
|
||||
if (data.type === 'SCAN') {
|
||||
try {
|
||||
const myProcessesId = data.payload;
|
||||
if (myProcessesId && myProcessesId.length != 0) {
|
||||
const toDownload = await scanMissingData(myProcessesId);
|
||||
if (toDownload.length != 0) {
|
||||
console.log('Sending TO_DOWNLOAD message');
|
||||
event.source.postMessage({ type: 'TO_DOWNLOAD', data: toDownload});
|
||||
}
|
||||
} else {
|
||||
event.source.postMessage({ status: 'error', message: 'Empty lists' });
|
||||
}
|
||||
} catch (error) {
|
||||
event.source.postMessage({ status: 'error', message: error.message });
|
||||
}
|
||||
} else if (data.type === 'ADD_OBJECT') {
|
||||
try {
|
||||
const { storeName, object, key } = data.payload;
|
||||
const db = await openDatabase();
|
||||
const tx = db.transaction(storeName, 'readwrite');
|
||||
const store = tx.objectStore(storeName);
|
||||
|
||||
if (key) {
|
||||
await store.put(object, key);
|
||||
} else {
|
||||
await store.put(object);
|
||||
}
|
||||
|
||||
event.ports[0].postMessage({ status: 'success', message: '' });
|
||||
} catch (error) {
|
||||
event.ports[0].postMessage({ status: 'error', message: error.message });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function scanMissingData(processesToScan) {
|
||||
console.log('Scanning for missing data...');
|
||||
const myProcesses = await getProcesses(processesToScan);
|
||||
|
||||
let toDownload = new Set();
|
||||
// Iterate on each process
|
||||
if (myProcesses && myProcesses.length != 0) {
|
||||
for (const process of myProcesses) {
|
||||
// Iterate on states
|
||||
const firstState = process.states[0];
|
||||
const processId = firstState.commited_in;
|
||||
for (const state of process.states) {
|
||||
if (state.state_id === EMPTY32BYTES) continue;
|
||||
// iterate on pcd_commitment
|
||||
for (const [field, hash] of Object.entries(state.pcd_commitment)) {
|
||||
// Skip public fields
|
||||
if (state.public_data[field] !== undefined || field === 'roles') continue;
|
||||
// Check if we have the data in db
|
||||
const existingData = await getBlob(hash);
|
||||
if (!existingData) {
|
||||
toDownload.add(hash);
|
||||
// We also add an entry in diff, in case it doesn't already exist
|
||||
await addDiff(processId, state.state_id, hash, state.roles, field);
|
||||
} else {
|
||||
// We remove it if we have it in the set
|
||||
if (toDownload.delete(hash)) {
|
||||
console.log(`Removing ${hash} from the set`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(toDownload);
|
||||
return Array.from(toDownload);
|
||||
}
|
||||
|
||||
async function openDatabase() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open('4nk', 1);
|
||||
request.onerror = (event) => {
|
||||
reject(request.error);
|
||||
};
|
||||
request.onsuccess = (event) => {
|
||||
resolve(request.result);
|
||||
};
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
if (!db.objectStoreNames.contains('wallet')) {
|
||||
db.createObjectStore('wallet', { keyPath: 'pre_id' });
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Function to get all processes because it is asynchronous
|
||||
async function getAllProcesses() {
|
||||
const db = await openDatabase();
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!db) {
|
||||
reject(new Error('Database is not available'));
|
||||
return;
|
||||
}
|
||||
const tx = db.transaction('processes', 'readonly');
|
||||
const store = tx.objectStore('processes');
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
async function getProcesses(processIds) {
|
||||
if (!processIds || processIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const db = await openDatabase();
|
||||
if (!db) {
|
||||
throw new Error('Database is not available');
|
||||
}
|
||||
|
||||
const tx = db.transaction('processes', 'readonly');
|
||||
const store = tx.objectStore('processes');
|
||||
|
||||
const requests = Array.from(processIds).map((processId) => {
|
||||
return new Promise((resolve) => {
|
||||
const request = store.get(processId);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => {
|
||||
console.error(`Error fetching process ${processId}:`, request.error);
|
||||
resolve(undefined);
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const results = await Promise.all(requests);
|
||||
return results.filter(result => result !== undefined);
|
||||
}
|
||||
|
||||
async function getAllDiffsNeedValidation() {
|
||||
const db = await openDatabase();
|
||||
|
||||
const allProcesses = await getAllProcesses();
|
||||
const tx = db.transaction('diffs', 'readonly');
|
||||
const store = tx.objectStore('diffs');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.getAll();
|
||||
request.onsuccess = (event) => {
|
||||
const allItems = event.target.result;
|
||||
const itemsWithFlag = allItems.filter((item) => item.need_validation);
|
||||
|
||||
const processMap = {};
|
||||
|
||||
for (const diff of itemsWithFlag) {
|
||||
const currentProcess = allProcesses.find((item) => {
|
||||
return item.states.some((state) => state.merkle_root === diff.new_state_merkle_root);
|
||||
});
|
||||
|
||||
if (currentProcess) {
|
||||
const processKey = currentProcess.merkle_root;
|
||||
|
||||
if (!processMap[processKey]) {
|
||||
processMap[processKey] = {
|
||||
process: currentProcess.states,
|
||||
processId: currentProcess.key,
|
||||
diffs: [],
|
||||
};
|
||||
}
|
||||
processMap[processKey].diffs.push(diff);
|
||||
}
|
||||
}
|
||||
|
||||
const results = Object.values(processMap).map((entry) => {
|
||||
const diffs = []
|
||||
for(const state of entry.process) {
|
||||
const filteredDiff = entry.diffs.filter(diff => diff.new_state_merkle_root === state.merkle_root);
|
||||
if(filteredDiff && filteredDiff.length) {
|
||||
diffs.push(filteredDiff)
|
||||
}
|
||||
}
|
||||
return {
|
||||
process: entry.process,
|
||||
processId: entry.processId,
|
||||
diffs: diffs,
|
||||
};
|
||||
});
|
||||
|
||||
resolve(results);
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject(event.target.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function getBlob(hash) {
|
||||
const db = await openDatabase();
|
||||
const storeName = 'data';
|
||||
const tx = db.transaction(storeName, 'readonly');
|
||||
const store = tx.objectStore(storeName);
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const getRequest = store.get(hash);
|
||||
getRequest.onsuccess = () => resolve(getRequest.result);
|
||||
getRequest.onerror = () => reject(getRequest.error);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
async function addDiff(processId, stateId, hash, roles, field) {
|
||||
const db = await openDatabase();
|
||||
const storeName = 'diffs';
|
||||
const tx = db.transaction(storeName, 'readwrite');
|
||||
const store = tx.objectStore(storeName);
|
||||
|
||||
// Check if the diff already exists
|
||||
const existingDiff = await new Promise((resolve, reject) => {
|
||||
const getRequest = store.get(hash);
|
||||
getRequest.onsuccess = () => resolve(getRequest.result);
|
||||
getRequest.onerror = () => reject(getRequest.error);
|
||||
});
|
||||
|
||||
if (!existingDiff) {
|
||||
const newDiff = {
|
||||
process_id: processId,
|
||||
state_id: stateId,
|
||||
value_commitment: hash,
|
||||
roles: roles,
|
||||
field: field,
|
||||
description: null,
|
||||
previous_value: null,
|
||||
new_value: null,
|
||||
notify_user: false,
|
||||
need_validation: false,
|
||||
validation_status: 'None'
|
||||
};
|
||||
|
||||
const insertResult = await new Promise((resolve, reject) => {
|
||||
const putRequest = store.put(newDiff);
|
||||
putRequest.onsuccess = () => resolve(putRequest.result);
|
||||
putRequest.onerror = () => reject(putRequest.error);
|
||||
});
|
||||
|
||||
return insertResult;
|
||||
}
|
||||
|
||||
return existingDiff;
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
import Services from './service';
|
||||
import { init, navigate } from '../router';
|
||||
import { RoleDefinition } from 'pkg/sdk_client';
|
||||
import { Member } from 'pkg/sdk_client';
|
||||
|
||||
export default class ChatService {
|
||||
private static instance: ChatService;
|
||||
private stateId: string | null = null;
|
||||
private processId: string | null = null;
|
||||
private paired_member: string[] = [];
|
||||
constructor() {}
|
||||
|
||||
public static async getInstance(): Promise<ChatService> {
|
||||
if (!ChatService.instance) {
|
||||
ChatService.instance = new ChatService();
|
||||
}
|
||||
return ChatService.instance;
|
||||
}
|
||||
|
||||
async getLocalMember () {
|
||||
try {
|
||||
const service = await Services.getInstance();
|
||||
const currentUser = service.getMemberFromDevice();
|
||||
return currentUser
|
||||
} catch (e) {
|
||||
console.error('Error initializing services:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async loadMessagingProcess (commitedIn: string) {
|
||||
try{
|
||||
const service = await Services.getInstance();
|
||||
const stored = service.getProcess(commitedIn)
|
||||
} catch (e) {
|
||||
console.error('Error loading Messaging Process', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
111
src/services/core/network.service.ts
Normal file
111
src/services/core/network.service.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import * as Comlink from "comlink";
|
||||
import type { NetworkBackend } from "../../workers/network.worker";
|
||||
import Services from "../service";
|
||||
|
||||
export class NetworkService {
|
||||
private worker: Comlink.Remote<NetworkBackend>;
|
||||
private workerInstance: Worker;
|
||||
|
||||
// Cache local
|
||||
private localRelays: Record<string, string> = {};
|
||||
|
||||
// Mécanisme d'attente (Events)
|
||||
private relayReadyResolver: ((addr: string) => void) | null = null;
|
||||
private relayReadyPromise: Promise<string> | null = null;
|
||||
|
||||
constructor(private bootstrapUrls: string[]) {
|
||||
this.workerInstance = new Worker(
|
||||
new URL("../../workers/network.worker.ts", import.meta.url),
|
||||
{ type: "module" }
|
||||
);
|
||||
this.worker = Comlink.wrap<NetworkBackend>(this.workerInstance);
|
||||
}
|
||||
|
||||
public async initRelays() {
|
||||
await this.worker.setCallbacks(
|
||||
Comlink.proxy(this.onMessageReceived.bind(this)),
|
||||
Comlink.proxy(this.onStatusChange.bind(this))
|
||||
);
|
||||
|
||||
for (const url of this.bootstrapUrls) {
|
||||
this.addWebsocketConnection(url);
|
||||
}
|
||||
}
|
||||
|
||||
public async addWebsocketConnection(url: string) {
|
||||
await this.worker.connect(url);
|
||||
}
|
||||
|
||||
public async connectAllRelays() {
|
||||
for (const url of this.bootstrapUrls) {
|
||||
this.addWebsocketConnection(url);
|
||||
}
|
||||
}
|
||||
|
||||
public async sendMessage(flag: string, content: string) {
|
||||
await this.worker.sendMessage(flag as any, content);
|
||||
}
|
||||
|
||||
// Cette méthode est appelée par le Worker (via Services.ts) ou par onStatusChange
|
||||
public updateRelay(url: string, spAddress: string) {
|
||||
this.localRelays[url] = spAddress;
|
||||
|
||||
// ✨ EVENT TRIGGER : Si quelqu'un attendait un relais, on le débloque !
|
||||
if (spAddress && spAddress !== "" && this.relayReadyResolver) {
|
||||
this.relayReadyResolver(spAddress);
|
||||
this.relayReadyResolver = null;
|
||||
this.relayReadyPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
public getAllRelays() {
|
||||
return this.localRelays;
|
||||
}
|
||||
|
||||
public async getAvailableRelayAddress(): Promise<string> {
|
||||
// 1. Vérification immédiate (Fast path)
|
||||
const existing = Object.values(this.localRelays).find(
|
||||
(addr) => addr && addr !== ""
|
||||
);
|
||||
if (existing) return existing;
|
||||
|
||||
// 2. Si pas encore là, on crée une "barrière" (Promise)
|
||||
if (!this.relayReadyPromise) {
|
||||
console.log("[NetworkService] ⏳ Attente d'un événement Handshake...");
|
||||
this.relayReadyPromise = new Promise<string>((resolve, reject) => {
|
||||
this.relayReadyResolver = resolve;
|
||||
|
||||
// Timeout de sécurité (10s) pour ne pas bloquer indéfiniment
|
||||
setTimeout(() => {
|
||||
if (this.relayReadyResolver) {
|
||||
reject(new Error("Timeout: Aucun relais reçu après 10s"));
|
||||
this.relayReadyResolver = null;
|
||||
this.relayReadyPromise = null;
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
return this.relayReadyPromise;
|
||||
}
|
||||
|
||||
// --- INTERNES ---
|
||||
|
||||
private async onMessageReceived(flag: string, content: string, url: string) {
|
||||
const services = await Services.getInstance();
|
||||
await services.dispatchToWorker(flag, content, url);
|
||||
}
|
||||
|
||||
private onStatusChange(
|
||||
url: string,
|
||||
status: "OPEN" | "CLOSED",
|
||||
spAddress?: string
|
||||
) {
|
||||
if (status === "OPEN" && spAddress) {
|
||||
// Met à jour et déclenche potentiellement le resolve()
|
||||
this.updateRelay(url, spAddress);
|
||||
} else if (status === "CLOSED") {
|
||||
this.localRelays[url] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/services/core/sdk.service.ts
Normal file
26
src/services/core/sdk.service.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { ApiReturn, Device } from '../../../pkg/sdk_client';
|
||||
|
||||
export class SdkService {
|
||||
private client: any;
|
||||
|
||||
async init() {
|
||||
this.client = await import('../../../pkg/sdk_client');
|
||||
this.client.setup();
|
||||
}
|
||||
|
||||
public getClient(): any {
|
||||
if (!this.client) throw new Error('SDK not initialized');
|
||||
return this.client;
|
||||
}
|
||||
|
||||
// Méthodes utilitaires directes du SDK
|
||||
public encodeJson(data: any): any {
|
||||
return this.client.encode_json(data);
|
||||
}
|
||||
public encodeBinary(data: any): any {
|
||||
return this.client.encode_binary(data);
|
||||
}
|
||||
public decodeValue(value: number[]): any {
|
||||
return this.client.decode_value(value);
|
||||
}
|
||||
}
|
||||
@ -1,212 +1,245 @@
|
||||
import Services from './service';
|
||||
import Services from "./service";
|
||||
|
||||
/**
|
||||
* Database service managing IndexedDB operations via Web Worker and Service Worker
|
||||
*/
|
||||
export class Database {
|
||||
// ============================================
|
||||
// PRIVATE PROPERTIES
|
||||
// ============================================
|
||||
|
||||
private static instance: Database;
|
||||
private db: IDBDatabase | null = null;
|
||||
private dbName: string = '4nk';
|
||||
private dbVersion: number = 1;
|
||||
private serviceWorkerRegistration: ServiceWorkerRegistration | null = null;
|
||||
private messageChannel: MessageChannel | null = null;
|
||||
private messageChannelForGet: MessageChannel | null = null;
|
||||
private serviceWorkerCheckIntervalId: number | null = null;
|
||||
private storeDefinitions = {
|
||||
AnkLabels: {
|
||||
name: 'labels',
|
||||
options: { keyPath: 'emoji' },
|
||||
indices: [],
|
||||
},
|
||||
AnkWallet: {
|
||||
name: 'wallet',
|
||||
options: { keyPath: 'pre_id' },
|
||||
indices: [],
|
||||
},
|
||||
AnkProcess: {
|
||||
name: 'processes',
|
||||
options: {},
|
||||
indices: [],
|
||||
},
|
||||
AnkSharedSecrets: {
|
||||
name: 'shared_secrets',
|
||||
options: {},
|
||||
indices: [],
|
||||
},
|
||||
AnkUnconfirmedSecrets: {
|
||||
name: 'unconfirmed_secrets',
|
||||
options: { autoIncrement: true },
|
||||
indices: [],
|
||||
},
|
||||
AnkPendingDiffs: {
|
||||
name: 'diffs',
|
||||
options: { keyPath: 'value_commitment' },
|
||||
indices: [
|
||||
{ name: 'byStateId', keyPath: 'state_id', options: { unique: false } },
|
||||
{ name: 'byNeedValidation', keyPath: 'need_validation', options: { unique: false } },
|
||||
{ name: 'byStatus', keyPath: 'validation_status', options: { unique: false } },
|
||||
],
|
||||
},
|
||||
AnkData: {
|
||||
name: 'data',
|
||||
options: {},
|
||||
indices: [],
|
||||
},
|
||||
};
|
||||
private indexedDBWorker: Worker | null = null;
|
||||
private messageIdCounter: number = 0;
|
||||
private pendingMessages: Map<
|
||||
number,
|
||||
{ resolve: (value: any) => void; reject: (error: any) => void }
|
||||
> = new Map();
|
||||
|
||||
// Private constructor to prevent direct instantiation from outside
|
||||
private constructor() {}
|
||||
// ============================================
|
||||
// INITIALIZATION & SINGLETON
|
||||
// ============================================
|
||||
|
||||
private constructor() {
|
||||
this.initIndexedDBWorker();
|
||||
this.initServiceWorker();
|
||||
}
|
||||
|
||||
// Method to access the singleton instance of Database
|
||||
public static async getInstance(): Promise<Database> {
|
||||
if (!Database.instance) {
|
||||
Database.instance = new Database();
|
||||
await Database.instance.init();
|
||||
await Database.instance.waitForWorkerReady();
|
||||
}
|
||||
return Database.instance;
|
||||
}
|
||||
|
||||
// Initialize the database
|
||||
private async init(): Promise<void> {
|
||||
// ============================================
|
||||
// INDEXEDDB WEB WORKER
|
||||
// ============================================
|
||||
|
||||
private initIndexedDBWorker(): void {
|
||||
this.indexedDBWorker = new Worker(
|
||||
new URL("../workers/database.worker.ts", import.meta.url),
|
||||
{ type: "module" }
|
||||
);
|
||||
|
||||
this.indexedDBWorker.onmessage = (event) => {
|
||||
const { id, type, result, error } = event.data;
|
||||
const pending = this.pendingMessages.get(id);
|
||||
|
||||
if (pending) {
|
||||
this.pendingMessages.delete(id);
|
||||
|
||||
if (type === "SUCCESS") {
|
||||
pending.resolve(result);
|
||||
} else if (type === "ERROR") {
|
||||
pending.reject(new Error(error));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.indexedDBWorker.onerror = (error) => {
|
||||
console.error("[Database] IndexedDB Worker error:", error);
|
||||
};
|
||||
}
|
||||
|
||||
private async waitForWorkerReady(): Promise<void> {
|
||||
return this.sendMessageToWorker("INIT", {});
|
||||
}
|
||||
|
||||
private sendMessageToWorker<T = any>(type: string, payload: any): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, this.dbVersion);
|
||||
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
|
||||
Object.values(this.storeDefinitions).forEach(({ name, options, indices }) => {
|
||||
if (!db.objectStoreNames.contains(name)) {
|
||||
let store = db.createObjectStore(name, options as IDBObjectStoreParameters);
|
||||
|
||||
indices.forEach(({ name, keyPath, options }) => {
|
||||
store.createIndex(name, keyPath, options);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
request.onsuccess = async () => {
|
||||
this.db = request.result;
|
||||
await this.initServiceWorker();
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
console.error('Database error:', request.error);
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async getDb(): Promise<IDBDatabase> {
|
||||
if (!this.db) {
|
||||
await this.init();
|
||||
}
|
||||
return this.db!;
|
||||
}
|
||||
|
||||
public getStoreList(): { [key: string]: string } {
|
||||
const objectList: { [key: string]: string } = {};
|
||||
Object.keys(this.storeDefinitions).forEach((key) => {
|
||||
objectList[key] = this.storeDefinitions[key as keyof typeof this.storeDefinitions].name;
|
||||
});
|
||||
return objectList;
|
||||
}
|
||||
|
||||
private async initServiceWorker() {
|
||||
if (!('serviceWorker' in navigator)) return; // Ensure service workers are supported
|
||||
|
||||
try {
|
||||
// Get existing service worker registrations
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
if (registrations.length === 0) {
|
||||
// No existing workers: register a new one.
|
||||
this.serviceWorkerRegistration = await navigator.serviceWorker.register('/src/service-workers/database.worker.js', { type: 'module' });
|
||||
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
|
||||
} else if (registrations.length === 1) {
|
||||
// One existing worker: update it (restart it) without unregistering.
|
||||
this.serviceWorkerRegistration = registrations[0];
|
||||
await this.serviceWorkerRegistration.update();
|
||||
console.log('Service Worker updated');
|
||||
} else {
|
||||
// More than one existing worker: unregister them all and register a new one.
|
||||
console.log('Multiple Service Worker(s) detected. Unregistering all...');
|
||||
await Promise.all(registrations.map(reg => reg.unregister()));
|
||||
console.log('All previous Service Workers unregistered.');
|
||||
this.serviceWorkerRegistration = await navigator.serviceWorker.register('/src/service-workers/database.worker.js', { type: 'module' });
|
||||
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
|
||||
if (!this.indexedDBWorker) {
|
||||
reject(new Error("IndexedDB Worker not initialized"));
|
||||
return;
|
||||
}
|
||||
|
||||
await this.checkForUpdates();
|
||||
const id = this.messageIdCounter++;
|
||||
this.pendingMessages.set(id, { resolve, reject });
|
||||
|
||||
// Set up a global message listener for responses from the service worker.
|
||||
navigator.serviceWorker.addEventListener('message', async (event) => {
|
||||
console.log('Received message from service worker:', event.data);
|
||||
this.indexedDBWorker.postMessage({ type, payload, id });
|
||||
|
||||
// Timeout de sécurité (30 secondes)
|
||||
setTimeout(() => {
|
||||
if (this.pendingMessages.has(id)) {
|
||||
this.pendingMessages.delete(id);
|
||||
reject(new Error(`Worker message timeout for type: ${type}`));
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SERVICE WORKER
|
||||
// ============================================
|
||||
|
||||
private initServiceWorker(): void {
|
||||
this.registerServiceWorker("/data.worker.js");
|
||||
}
|
||||
|
||||
private async registerServiceWorker(path: string): Promise<void> {
|
||||
if (!("serviceWorker" in navigator)) return;
|
||||
console.log("[Database] Initializing Service Worker:", path);
|
||||
|
||||
try {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
|
||||
for (const registration of registrations) {
|
||||
const scriptURL =
|
||||
registration.active?.scriptURL ||
|
||||
registration.installing?.scriptURL ||
|
||||
registration.waiting?.scriptURL;
|
||||
const scope = registration.scope;
|
||||
|
||||
if (
|
||||
scope.includes("/src/service-workers/") ||
|
||||
(scriptURL && scriptURL.includes("/src/service-workers/"))
|
||||
) {
|
||||
console.warn(`[Database] Removing old Service Worker (${scope})`);
|
||||
await registration.unregister();
|
||||
}
|
||||
}
|
||||
|
||||
const existingValidWorker = registrations.find((r) => {
|
||||
const url =
|
||||
r.active?.scriptURL ||
|
||||
r.installing?.scriptURL ||
|
||||
r.waiting?.scriptURL;
|
||||
return url && url.endsWith(path.replace(/^\//, ""));
|
||||
});
|
||||
|
||||
if (!existingValidWorker) {
|
||||
console.log("[Database] Registering new Service Worker");
|
||||
this.serviceWorkerRegistration = await navigator.serviceWorker.register(
|
||||
path,
|
||||
{ type: "module", scope: "/" }
|
||||
);
|
||||
} else {
|
||||
console.log("[Database] Service Worker already active");
|
||||
this.serviceWorkerRegistration = existingValidWorker;
|
||||
await this.serviceWorkerRegistration.update();
|
||||
}
|
||||
|
||||
navigator.serviceWorker.addEventListener("message", async (event) => {
|
||||
await this.handleServiceWorkerMessage(event.data);
|
||||
});
|
||||
|
||||
// Set up a periodic check to ensure the service worker is active and to send a SYNC message.
|
||||
this.serviceWorkerCheckIntervalId = window.setInterval(async () => {
|
||||
const activeWorker = this.serviceWorkerRegistration.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration));
|
||||
if (this.serviceWorkerCheckIntervalId)
|
||||
clearInterval(this.serviceWorkerCheckIntervalId);
|
||||
this.serviceWorkerCheckIntervalId = setInterval(async () => {
|
||||
const activeWorker =
|
||||
this.serviceWorkerRegistration?.active ||
|
||||
(await this.waitForServiceWorkerActivation(
|
||||
this.serviceWorkerRegistration!
|
||||
));
|
||||
const service = await Services.getInstance();
|
||||
const payload = await service.getMyProcesses();
|
||||
if (payload.length != 0) {
|
||||
activeWorker?.postMessage({ type: 'SCAN', payload });
|
||||
if (payload && payload.length != 0) {
|
||||
activeWorker?.postMessage({ type: "SCAN", payload });
|
||||
}
|
||||
}, 5000);
|
||||
}, 5000) as unknown as number;
|
||||
} catch (error) {
|
||||
console.error('Service Worker registration failed:', error);
|
||||
console.error("[Database] Service Worker error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to wait for service worker activation
|
||||
private async waitForServiceWorkerActivation(registration: ServiceWorkerRegistration): Promise<ServiceWorker | null> {
|
||||
return new Promise((resolve) => {
|
||||
private async waitForServiceWorkerActivation(
|
||||
registration: ServiceWorkerRegistration
|
||||
): Promise<ServiceWorker | null> {
|
||||
return new Promise((resolve) => {
|
||||
if (registration.active) {
|
||||
resolve(registration.active);
|
||||
} else {
|
||||
const listener = () => {
|
||||
if (registration.active) {
|
||||
resolve(registration.active);
|
||||
} else {
|
||||
const listener = () => {
|
||||
if (registration.active) {
|
||||
navigator.serviceWorker.removeEventListener('controllerchange', listener);
|
||||
resolve(registration.active);
|
||||
}
|
||||
};
|
||||
navigator.serviceWorker.addEventListener('controllerchange', listener);
|
||||
navigator.serviceWorker.removeEventListener(
|
||||
"controllerchange",
|
||||
listener
|
||||
);
|
||||
resolve(registration.active);
|
||||
}
|
||||
});
|
||||
};
|
||||
navigator.serviceWorker.addEventListener("controllerchange", listener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async checkForUpdates() {
|
||||
private async checkForUpdates(): Promise<void> {
|
||||
if (this.serviceWorkerRegistration) {
|
||||
// Check for updates to the service worker
|
||||
try {
|
||||
await this.serviceWorkerRegistration.update();
|
||||
|
||||
// If there's a new worker waiting, activate it immediately
|
||||
if (this.serviceWorkerRegistration.waiting) {
|
||||
this.serviceWorkerRegistration.waiting.postMessage({ type: 'SKIP_WAITING' });
|
||||
this.serviceWorkerRegistration.waiting.postMessage({
|
||||
type: "SKIP_WAITING",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking for service worker updates:', error);
|
||||
console.error("Error checking for service worker updates:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SERVICE WORKER MESSAGE HANDLERS
|
||||
// ============================================
|
||||
|
||||
private async handleServiceWorkerMessage(message: any) {
|
||||
switch (message.type) {
|
||||
case 'TO_DOWNLOAD':
|
||||
case "TO_DOWNLOAD":
|
||||
await this.handleDownloadList(message.data);
|
||||
break;
|
||||
case "DIFFS_TO_CREATE":
|
||||
await this.handleDiffsToCreate(message.data);
|
||||
break;
|
||||
default:
|
||||
console.warn('Unknown message type received from service worker:', message);
|
||||
console.warn(
|
||||
"Unknown message type received from service worker:",
|
||||
message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleDownloadList(downloadList: string[]): void {
|
||||
// Download the missing data
|
||||
let requestedStateId = [];
|
||||
private async handleDiffsToCreate(diffs: any[]): Promise<void> {
|
||||
console.log(
|
||||
`[Database] Creating ${diffs.length} diffs from Service Worker scan`
|
||||
);
|
||||
try {
|
||||
await this.saveDiffs(diffs);
|
||||
console.log("[Database] Diffs created successfully");
|
||||
} catch (error) {
|
||||
console.error("[Database] Error creating diffs:", error);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleDownloadList(downloadList: string[]): Promise<void> {
|
||||
let requestedStateId: string[] = [];
|
||||
const service = await Services.getInstance();
|
||||
for (const hash of downloadList) {
|
||||
const diff = await service.getDiffByValue(hash);
|
||||
if (!diff) {
|
||||
// This should never happen
|
||||
console.warn(`Missing a diff for hash ${hash}`);
|
||||
continue;
|
||||
}
|
||||
@ -216,20 +249,17 @@ export class Database {
|
||||
try {
|
||||
const valueBytes = await service.fetchValueFromStorage(hash);
|
||||
if (valueBytes) {
|
||||
// Save data to db
|
||||
const blob = new Blob([valueBytes], {type: "application/octet-stream"});
|
||||
const blob = new Blob([valueBytes], {
|
||||
type: "application/octet-stream",
|
||||
});
|
||||
await service.saveBlobToDb(hash, blob);
|
||||
document.dispatchEvent(new CustomEvent('newDataReceived', {
|
||||
detail: {
|
||||
processId,
|
||||
stateId,
|
||||
hash,
|
||||
}
|
||||
}));
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("newDataReceived", {
|
||||
detail: { processId, stateId, hash },
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// We first request the data from managers
|
||||
console.log('Request data from managers of the process');
|
||||
// get the diff from db
|
||||
console.log("Request data from managers of the process");
|
||||
if (!requestedStateId.includes(stateId)) {
|
||||
await service.requestDataFromPeers(processId, [stateId], [roles]);
|
||||
requestedStateId.push(stateId);
|
||||
@ -241,177 +271,206 @@ export class Database {
|
||||
}
|
||||
}
|
||||
|
||||
private handleAddObjectResponse = async (event: MessageEvent) => {
|
||||
const data = event.data;
|
||||
console.log('Received response from service worker (ADD_OBJECT):', data);
|
||||
const service = await Services.getInstance();
|
||||
if (data.type === 'NOTIFICATIONS') {
|
||||
service.setNotifications(data.data);
|
||||
} else if (data.type === 'TO_DOWNLOAD') {
|
||||
console.log(`Received missing data ${data}`);
|
||||
// Download the missing data
|
||||
let requestedStateId = [];
|
||||
for (const hash of data.data) {
|
||||
try {
|
||||
const valueBytes = await service.fetchValueFromStorage(hash);
|
||||
if (valueBytes) {
|
||||
// Save data to db
|
||||
const blob = new Blob([valueBytes], {type: "application/octet-stream"});
|
||||
await service.saveBlobToDb(hash, blob);
|
||||
} else {
|
||||
// We first request the data from managers
|
||||
console.log('Request data from managers of the process');
|
||||
// get the diff from db
|
||||
const diff = await service.getDiffByValue(hash);
|
||||
const processId = diff.process_id;
|
||||
const stateId = diff.state_id;
|
||||
const roles = diff.roles;
|
||||
if (!requestedStateId.includes(stateId)) {
|
||||
await service.requestDataFromPeers(processId, [stateId], [roles]);
|
||||
requestedStateId.push(stateId);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// ============================================
|
||||
// GENERIC INDEXEDDB OPERATIONS
|
||||
// ============================================
|
||||
|
||||
private handleGetObjectResponse = (event: MessageEvent) => {
|
||||
console.log('Received response from service worker (GET_OBJECT):', event.data);
|
||||
};
|
||||
public async getStoreList(): Promise<{ [key: string]: string }> {
|
||||
return this.sendMessageToWorker("GET_STORE_LIST", {});
|
||||
}
|
||||
|
||||
public addObject(payload: { storeName: string; object: any; key: any }): Promise<void> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// Check if the service worker is active
|
||||
if (!this.serviceWorkerRegistration) {
|
||||
// console.warn('Service worker registration is not ready. Waiting...');
|
||||
this.serviceWorkerRegistration = await navigator.serviceWorker.ready;
|
||||
}
|
||||
public async addObject(payload: {
|
||||
storeName: string;
|
||||
object: any;
|
||||
key: any;
|
||||
}): Promise<void> {
|
||||
await this.sendMessageToWorker("ADD_OBJECT", payload);
|
||||
}
|
||||
|
||||
const activeWorker = await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration);
|
||||
|
||||
// Create a message channel for communication
|
||||
const messageChannel = new MessageChannel();
|
||||
|
||||
// Handle the response from the service worker
|
||||
messageChannel.port1.onmessage = (event) => {
|
||||
if (event.data.status === 'success') {
|
||||
resolve();
|
||||
} else {
|
||||
const error = event.data.message;
|
||||
reject(new Error(error || 'Unknown error occurred while adding object'));
|
||||
}
|
||||
};
|
||||
|
||||
// Send the add object request to the service worker
|
||||
try {
|
||||
activeWorker?.postMessage(
|
||||
{
|
||||
type: 'ADD_OBJECT',
|
||||
payload,
|
||||
},
|
||||
[messageChannel.port2],
|
||||
);
|
||||
} catch (error) {
|
||||
reject(new Error(`Failed to send message to service worker: ${error}`));
|
||||
}
|
||||
});
|
||||
public async batchWriting(payload: {
|
||||
storeName: string;
|
||||
objects: { key: any; object: any }[];
|
||||
}): Promise<void> {
|
||||
await this.sendMessageToWorker("BATCH_WRITING", payload);
|
||||
}
|
||||
|
||||
public async getObject(storeName: string, key: string): Promise<any | null> {
|
||||
const db = await this.getDb();
|
||||
const tx = db.transaction(storeName, 'readonly');
|
||||
const store = tx.objectStore(storeName);
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const getRequest = store.get(key);
|
||||
getRequest.onsuccess = () => resolve(getRequest.result);
|
||||
getRequest.onerror = () => reject(getRequest.error);
|
||||
});
|
||||
return result;
|
||||
return this.sendMessageToWorker("GET_OBJECT", { storeName, key });
|
||||
}
|
||||
|
||||
public async dumpStore(storeName: string): Promise<Record<string, any>> {
|
||||
const db = await this.getDb();
|
||||
const tx = db.transaction(storeName, 'readonly');
|
||||
const store = tx.objectStore(storeName);
|
||||
|
||||
try {
|
||||
// Wait for both getAllKeys() and getAll() to resolve
|
||||
const [keys, values] = await Promise.all([
|
||||
new Promise<any[]>((resolve, reject) => {
|
||||
const request = store.getAllKeys();
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
}),
|
||||
new Promise<any[]>((resolve, reject) => {
|
||||
const request = store.getAll();
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
}),
|
||||
]);
|
||||
|
||||
// Combine keys and values into an object
|
||||
const result: Record<string, any> = Object.fromEntries(keys.map((key, index) => [key, values[index]]));
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error fetching data from IndexedDB:', error);
|
||||
throw error;
|
||||
}
|
||||
return this.sendMessageToWorker("DUMP_STORE", { storeName });
|
||||
}
|
||||
|
||||
public async deleteObject(storeName: string, key: string): Promise<void> {
|
||||
const db = await this.getDb();
|
||||
const tx = db.transaction(storeName, 'readwrite');
|
||||
const store = tx.objectStore(storeName);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const getRequest = store.delete(key);
|
||||
getRequest.onsuccess = () => resolve(getRequest.result);
|
||||
getRequest.onerror = () => reject(getRequest.error);
|
||||
});
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
await this.sendMessageToWorker("DELETE_OBJECT", { storeName, key });
|
||||
}
|
||||
|
||||
public async clearStore(storeName: string): Promise<void> {
|
||||
const db = await this.getDb();
|
||||
const tx = db.transaction(storeName, 'readwrite');
|
||||
const store = tx.objectStore(storeName);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const clearRequest = store.clear();
|
||||
clearRequest.onsuccess = () => resolve(clearRequest.result);
|
||||
clearRequest.onerror = () => reject(clearRequest.error);
|
||||
});
|
||||
} catch (e) {
|
||||
throw e;
|
||||
await this.sendMessageToWorker("CLEAR_STORE", { storeName });
|
||||
}
|
||||
|
||||
public async requestStoreByIndex(
|
||||
storeName: string,
|
||||
indexName: string,
|
||||
request: string
|
||||
): Promise<any[]> {
|
||||
return this.sendMessageToWorker("REQUEST_STORE_BY_INDEX", {
|
||||
storeName,
|
||||
indexName,
|
||||
request,
|
||||
});
|
||||
}
|
||||
|
||||
public async clearMultipleStores(storeNames: string[]): Promise<void> {
|
||||
for (const storeName of storeNames) {
|
||||
await this.clearStore(storeName);
|
||||
}
|
||||
}
|
||||
|
||||
// Request a store by index
|
||||
public async requestStoreByIndex(storeName: string, indexName: string, request: string): Promise<any[]> {
|
||||
const db = await this.getDb();
|
||||
const tx = db.transaction(storeName, 'readonly');
|
||||
const store = tx.objectStore(storeName);
|
||||
const index = store.index(indexName);
|
||||
// ============================================
|
||||
// BUSINESS METHODS - DEVICE
|
||||
// ============================================
|
||||
|
||||
public async saveDevice(device: any): Promise<void> {
|
||||
try {
|
||||
return new Promise((resolve, reject) => {
|
||||
const getAllRequest = index.getAll(request);
|
||||
getAllRequest.onsuccess = () => {
|
||||
const allItems = getAllRequest.result;
|
||||
const filtered = allItems.filter(item => item.state_id === request);
|
||||
resolve(filtered);
|
||||
};
|
||||
getAllRequest.onerror = () => reject(getAllRequest.error);
|
||||
const existing = await this.getObject("wallet", "1");
|
||||
if (existing) {
|
||||
await this.deleteObject("wallet", "1");
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
await this.addObject({
|
||||
storeName: "wallet",
|
||||
object: { pre_id: "1", device },
|
||||
key: null,
|
||||
});
|
||||
}
|
||||
|
||||
public async getDevice(): Promise<any | null> {
|
||||
const result = await this.getObject("wallet", "1");
|
||||
return result ? result["device"] : null;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// BUSINESS METHODS - PROCESS
|
||||
// ============================================
|
||||
|
||||
public async saveProcess(processId: string, process: any): Promise<void> {
|
||||
await this.addObject({
|
||||
storeName: "processes",
|
||||
object: process,
|
||||
key: processId,
|
||||
});
|
||||
}
|
||||
|
||||
public async saveProcessesBatch(
|
||||
processes: Record<string, any>
|
||||
): Promise<void> {
|
||||
if (Object.keys(processes).length === 0) return;
|
||||
|
||||
await this.batchWriting({
|
||||
storeName: "processes",
|
||||
objects: Object.entries(processes).map(([key, value]) => ({
|
||||
key,
|
||||
object: value,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
public async getProcess(processId: string): Promise<any | null> {
|
||||
return this.getObject("processes", processId);
|
||||
}
|
||||
|
||||
public async getAllProcesses(): Promise<Record<string, any>> {
|
||||
return this.dumpStore("processes");
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// BUSINESS METHODS - BLOBS
|
||||
// ============================================
|
||||
|
||||
public async saveBlob(hash: string, data: Blob): Promise<void> {
|
||||
await this.addObject({
|
||||
storeName: "data",
|
||||
object: data,
|
||||
key: hash,
|
||||
});
|
||||
}
|
||||
|
||||
public async getBlob(hash: string): Promise<Blob | null> {
|
||||
return this.getObject("data", hash);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// BUSINESS METHODS - DIFFS
|
||||
// ============================================
|
||||
|
||||
public async saveDiffs(diffs: any[]): Promise<void> {
|
||||
if (diffs.length === 0) return;
|
||||
|
||||
for (const diff of diffs) {
|
||||
await this.addObject({
|
||||
storeName: "diffs",
|
||||
object: diff,
|
||||
key: null,
|
||||
});
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async getDiff(hash: string): Promise<any | null> {
|
||||
return this.getObject("diffs", hash);
|
||||
}
|
||||
|
||||
public async getAllDiffs(): Promise<Record<string, any>> {
|
||||
return this.dumpStore("diffs");
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// BUSINESS METHODS - SECRETS
|
||||
// ============================================
|
||||
|
||||
public async getSharedSecret(address: string): Promise<string | null> {
|
||||
return this.getObject("shared_secrets", address);
|
||||
}
|
||||
|
||||
public async saveSecretsBatch(
|
||||
unconfirmedSecrets: any[],
|
||||
sharedSecrets: { key: string; value: any }[]
|
||||
): Promise<void> {
|
||||
if (unconfirmedSecrets && unconfirmedSecrets.length > 0) {
|
||||
for (const secret of unconfirmedSecrets) {
|
||||
await this.addObject({
|
||||
storeName: "unconfirmed_secrets",
|
||||
object: secret,
|
||||
key: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (sharedSecrets && sharedSecrets.length > 0) {
|
||||
for (const { key, value } of sharedSecrets) {
|
||||
await this.addObject({
|
||||
storeName: "shared_secrets",
|
||||
object: value,
|
||||
key: key,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async getAllSecrets(): Promise<{
|
||||
shared_secrets: Record<string, any>;
|
||||
unconfirmed_secrets: any[];
|
||||
}> {
|
||||
const sharedSecrets = await this.dumpStore("shared_secrets");
|
||||
const unconfirmedSecrets = await this.dumpStore("unconfirmed_secrets");
|
||||
|
||||
return {
|
||||
shared_secrets: sharedSecrets,
|
||||
unconfirmed_secrets: Object.values(unconfirmedSecrets),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default Database;
|
||||
|
||||
58
src/services/domain/crypto.service.ts
Normal file
58
src/services/domain/crypto.service.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { MerkleProofResult, ProcessState } from '../../../pkg/sdk_client';
|
||||
import { SdkService } from '../core/sdk.service';
|
||||
|
||||
export class CryptoService {
|
||||
constructor(private sdk: SdkService) {}
|
||||
|
||||
public hexToBlob(hexString: string): Blob {
|
||||
const uint8Array = this.hexToUInt8Array(hexString);
|
||||
return new Blob([uint8Array as any], { type: 'application/octet-stream' });
|
||||
}
|
||||
|
||||
public hexToUInt8Array(hexString: string): Uint8Array {
|
||||
if (hexString.length % 2 !== 0) throw new Error('Invalid hex string');
|
||||
const uint8Array = new Uint8Array(hexString.length / 2);
|
||||
for (let i = 0; i < hexString.length; i += 2) {
|
||||
uint8Array[i / 2] = parseInt(hexString.substr(i, 2), 16);
|
||||
}
|
||||
return uint8Array;
|
||||
}
|
||||
|
||||
public async blobToHex(blob: Blob): Promise<string> {
|
||||
const buffer = await blob.arrayBuffer();
|
||||
const bytes = new Uint8Array(buffer);
|
||||
return Array.from(bytes)
|
||||
.map((byte) => byte.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
public getHashForFile(commitedIn: string, label: string, fileBlob: { type: string; data: Uint8Array }): string {
|
||||
return this.sdk.getClient().hash_value(fileBlob, commitedIn, label);
|
||||
}
|
||||
|
||||
public getMerkleProofForFile(processState: ProcessState, attributeName: string): MerkleProofResult {
|
||||
return this.sdk.getClient().get_merkle_proof(processState, attributeName);
|
||||
}
|
||||
|
||||
public validateMerkleProof(proof: MerkleProofResult, hash: string): boolean {
|
||||
return this.sdk.getClient().validate_merkle_proof(proof, hash);
|
||||
}
|
||||
|
||||
public splitData(obj: Record<string, any>) {
|
||||
const jsonCompatibleData: Record<string, any> = {};
|
||||
const binaryData: Record<string, { type: string; data: Uint8Array }> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (this.isFileBlob(value)) {
|
||||
binaryData[key] = value;
|
||||
} else {
|
||||
jsonCompatibleData[key] = value;
|
||||
}
|
||||
}
|
||||
return { jsonCompatibleData, binaryData };
|
||||
}
|
||||
|
||||
private isFileBlob(value: any): value is { type: string; data: Uint8Array } {
|
||||
return typeof value === 'object' && value !== null && typeof value.type === 'string' && value.data instanceof Uint8Array;
|
||||
}
|
||||
}
|
||||
95
src/services/domain/process.service.ts
Normal file
95
src/services/domain/process.service.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { Process, ProcessState, RoleDefinition } from '../../../pkg/sdk_client';
|
||||
import { SdkService } from '../core/sdk.service';
|
||||
import Database from '../database.service';
|
||||
|
||||
const EMPTY32BYTES = String('').padStart(64, '0');
|
||||
|
||||
export class ProcessService {
|
||||
private processesCache: Record<string, Process> = {};
|
||||
private myProcesses: Set<string> = new Set();
|
||||
|
||||
constructor(private sdk: SdkService, private db: Database) {}
|
||||
|
||||
public async getProcess(processId: string): Promise<Process | null> {
|
||||
if (this.processesCache[processId]) return this.processesCache[processId];
|
||||
|
||||
const process = await this.db.getProcess(processId);
|
||||
if (process) this.processesCache[processId] = process;
|
||||
return process;
|
||||
}
|
||||
|
||||
public async getProcesses(): Promise<Record<string, Process>> {
|
||||
if (Object.keys(this.processesCache).length > 0) return this.processesCache;
|
||||
|
||||
this.processesCache = await this.db.getAllProcesses();
|
||||
return this.processesCache;
|
||||
}
|
||||
|
||||
public async saveProcessToDb(processId: string, process: Process) {
|
||||
await this.db.saveProcess(processId, process);
|
||||
this.processesCache[processId] = process;
|
||||
}
|
||||
|
||||
public async batchSaveProcesses(processes: Record<string, Process>) {
|
||||
if (Object.keys(processes).length === 0) return;
|
||||
await this.db.saveProcessesBatch(processes);
|
||||
this.processesCache = { ...this.processesCache, ...processes };
|
||||
}
|
||||
|
||||
public getLastCommitedState(process: Process): ProcessState | null {
|
||||
if (process.states.length === 0) return null;
|
||||
const processTip = process.states[process.states.length - 1].commited_in;
|
||||
return process.states.findLast((state) => state.commited_in !== processTip) || null;
|
||||
}
|
||||
|
||||
public getUncommitedStates(process: Process): ProcessState[] {
|
||||
if (process.states.length === 0) return [];
|
||||
const processTip = process.states[process.states.length - 1].commited_in;
|
||||
return process.states.filter((state) => state.commited_in === processTip).filter((state) => state.state_id !== EMPTY32BYTES);
|
||||
}
|
||||
|
||||
public getStateFromId(process: Process, stateId: string): ProcessState | null {
|
||||
return process.states.find((state) => state.state_id === stateId) || null;
|
||||
}
|
||||
|
||||
public getRoles(process: Process): Record<string, RoleDefinition> | null {
|
||||
const last = this.getLastCommitedState(process);
|
||||
if (last?.roles && Object.keys(last.roles).length > 0) return last.roles;
|
||||
|
||||
const first = process.states[0];
|
||||
if (first?.roles && Object.keys(first.roles).length > 0) return first.roles;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public rolesContainsMember(roles: Record<string, RoleDefinition>, memberId: string): boolean {
|
||||
return Object.values(roles).some((role) => role.members.includes(memberId));
|
||||
}
|
||||
|
||||
public async getMyProcesses(pairingProcessId: string): Promise<string[]> {
|
||||
const processes = await this.getProcesses();
|
||||
const newMyProcesses = new Set<string>(this.myProcesses);
|
||||
if (pairingProcessId) newMyProcesses.add(pairingProcessId);
|
||||
|
||||
for (const [processId, process] of Object.entries(processes)) {
|
||||
if (newMyProcesses.has(processId)) continue;
|
||||
const roles = this.getRoles(process);
|
||||
if (roles && this.rolesContainsMember(roles, pairingProcessId)) {
|
||||
newMyProcesses.add(processId);
|
||||
}
|
||||
}
|
||||
this.myProcesses = newMyProcesses;
|
||||
return Array.from(this.myProcesses);
|
||||
}
|
||||
|
||||
public getLastCommitedStateIndex(process: Process): number | null {
|
||||
if (process.states.length === 0) return null;
|
||||
const processTip = process.states[process.states.length - 1].commited_in;
|
||||
for (let i = process.states.length - 1; i >= 0; i--) {
|
||||
if (process.states[i].commited_in !== processTip) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
93
src/services/domain/wallet.service.ts
Normal file
93
src/services/domain/wallet.service.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { Device } from '../../../pkg/sdk_client';
|
||||
import { SdkService } from '../core/sdk.service';
|
||||
import Database from '../database.service';
|
||||
|
||||
export class WalletService {
|
||||
constructor(private sdk: SdkService, private db: Database) {}
|
||||
|
||||
public isPaired(): boolean {
|
||||
try {
|
||||
return this.sdk.getClient().is_paired();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public getAmount(): BigInt {
|
||||
return this.sdk.getClient().get_available_amount();
|
||||
}
|
||||
|
||||
public getDeviceAddress(): string {
|
||||
return this.sdk.getClient().get_address();
|
||||
}
|
||||
|
||||
public getPairingProcessId(): string {
|
||||
return this.sdk.getClient().get_pairing_process_id();
|
||||
}
|
||||
|
||||
public async createNewDevice(chainTip: number): Promise<string> {
|
||||
const spAddress = await this.sdk.getClient().create_new_device(0, 'signet');
|
||||
const device = this.dumpDeviceFromMemory();
|
||||
if (device.sp_wallet.birthday === 0) {
|
||||
device.sp_wallet.birthday = chainTip;
|
||||
device.sp_wallet.last_scan = chainTip;
|
||||
this.sdk.getClient().restore_device(device);
|
||||
}
|
||||
await this.saveDeviceInDatabase(device);
|
||||
return spAddress;
|
||||
}
|
||||
|
||||
public dumpDeviceFromMemory(): Device {
|
||||
return this.sdk.getClient().dump_device();
|
||||
}
|
||||
|
||||
public dumpNeuteredDevice(): Device | null {
|
||||
try {
|
||||
return this.sdk.getClient().dump_neutered_device();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async dumpWallet(): Promise<any> {
|
||||
return await this.sdk.getClient().dump_wallet();
|
||||
}
|
||||
|
||||
public async getMemberFromDevice(): Promise<string[] | null> {
|
||||
try {
|
||||
const device = await this.getDeviceFromDatabase();
|
||||
if (device) {
|
||||
const pairedMember = device['paired_member'];
|
||||
return pairedMember.sp_addresses;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(`[WalletService] Échec: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async saveDeviceInDatabase(device: Device): Promise<void> {
|
||||
await this.db.saveDevice(device);
|
||||
}
|
||||
|
||||
public async getDeviceFromDatabase(): Promise<Device | null> {
|
||||
const db = await Database.getInstance();
|
||||
const res = await db.getObject('wallet', '1');
|
||||
return res ? res['device'] : null;
|
||||
}
|
||||
|
||||
public restoreDevice(device: Device) {
|
||||
this.sdk.getClient().restore_device(device);
|
||||
}
|
||||
|
||||
public pairDevice(processId: string, spAddressList: string[]): void {
|
||||
this.sdk.getClient().pair_device(processId, spAddressList);
|
||||
}
|
||||
|
||||
public async unpairDevice(): Promise<void> {
|
||||
this.sdk.getClient().unpair_device();
|
||||
const newDevice = this.dumpDeviceFromMemory();
|
||||
await this.saveDeviceInDatabase(newDevice);
|
||||
}
|
||||
}
|
||||
651
src/services/iframe-controller.service.ts
Normal file
651
src/services/iframe-controller.service.ts
Normal file
@ -0,0 +1,651 @@
|
||||
import { MessageType } from "../types/index";
|
||||
import Services from "./service";
|
||||
import TokenService from "./token.service";
|
||||
import { cleanSubscriptions } from "../utils/subscription.utils";
|
||||
import { splitPrivateData, isValid32ByteHex } from "../utils/service.utils";
|
||||
import { MerkleProofResult } from "../../pkg/sdk_client";
|
||||
|
||||
export class IframeController {
|
||||
private static isInitialized = false;
|
||||
|
||||
static async init() {
|
||||
if (this.isInitialized) return;
|
||||
|
||||
if (window.self !== window.top) {
|
||||
console.log(
|
||||
"[IframeController] 📡 Mode Iframe détecté. Démarrage des listeners API..."
|
||||
);
|
||||
await IframeController.registerAllListeners();
|
||||
} else {
|
||||
console.log(
|
||||
"[IframeController] ℹ️ Mode Standalone (pas d'iframe). Listeners API inactifs."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static async registerAllListeners() {
|
||||
console.log(
|
||||
"[Router:API] 🎧 Enregistrement des gestionnaires de messages (postMessage)..."
|
||||
);
|
||||
const services = await Services.getInstance();
|
||||
const tokenService = await TokenService.getInstance();
|
||||
|
||||
const errorResponse = (
|
||||
errorMsg: string,
|
||||
origin: string,
|
||||
messageId?: string
|
||||
) => {
|
||||
console.error(
|
||||
`[Router:API] 📤 Envoi Erreur: ${errorMsg} (Origine: ${origin}, MsgID: ${messageId})`
|
||||
);
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
error: errorMsg,
|
||||
messageId,
|
||||
},
|
||||
origin
|
||||
);
|
||||
};
|
||||
|
||||
const withToken = async (
|
||||
event: MessageEvent,
|
||||
action: () => Promise<void>
|
||||
) => {
|
||||
const { accessToken } = event.data;
|
||||
if (
|
||||
!accessToken ||
|
||||
!(await tokenService.validateToken(accessToken, event.origin))
|
||||
) {
|
||||
throw new Error("Invalid or expired session token");
|
||||
}
|
||||
await action();
|
||||
};
|
||||
|
||||
// --- HANDLERS ---
|
||||
|
||||
const handleRequestLink = async (event: MessageEvent) => {
|
||||
console.log(
|
||||
`[Router:API] 📨 Message ${MessageType.REQUEST_LINK} reçu de ${event.origin}`
|
||||
);
|
||||
|
||||
const device = await services.getDeviceFromDatabase();
|
||||
|
||||
if (device && device.pairing_process_commitment) {
|
||||
console.log(
|
||||
"[Router:API] Appareil déjà appairé. Pas besoin d'attendre home.ts."
|
||||
);
|
||||
} else {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const cleanup = () => {
|
||||
document.removeEventListener(
|
||||
"app:pairing-ready",
|
||||
handler as EventListener
|
||||
);
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
|
||||
const handler = (e: CustomEvent) => {
|
||||
cleanup();
|
||||
if (e.detail && e.detail.success) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(e.detail?.error || "Auto-pairing failed"));
|
||||
}
|
||||
};
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
cleanup();
|
||||
reject(new Error("Auto-pairing timed out (Event not received)"));
|
||||
}, 5000);
|
||||
|
||||
document.addEventListener(
|
||||
"app:pairing-ready",
|
||||
handler as EventListener
|
||||
);
|
||||
});
|
||||
|
||||
console.log(`[Router:API] Feu vert de home.ts reçu !`);
|
||||
}
|
||||
|
||||
console.log(`[Router:API] Traitement de la liaison...`);
|
||||
|
||||
const tokens = await tokenService.generateSessionToken(event.origin);
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.LINK_ACCEPTED,
|
||||
accessToken: tokens.accessToken,
|
||||
refreshToken: tokens.refreshToken,
|
||||
messageId: event.data.messageId,
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
console.log(
|
||||
`[Router:API] ✅ ${MessageType.REQUEST_LINK} accepté et jetons envoyés.`
|
||||
);
|
||||
};
|
||||
|
||||
const handleCreatePairing = async (event: MessageEvent) => {
|
||||
console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PAIRING} reçu`);
|
||||
|
||||
// 🔥 CORRECTION TS2801 : Ajout de await
|
||||
if (await services.isPaired()) {
|
||||
throw new Error(
|
||||
"Device already paired — ignoring CREATE_PAIRING request"
|
||||
);
|
||||
}
|
||||
|
||||
await withToken(event, async () => {
|
||||
console.log("[Router:API] 🚀 Démarrage du processus d'appairage...");
|
||||
|
||||
// 🔥 CORRECTION TS2322 : Ajout de await pour récupérer la string
|
||||
const myAddress = await services.getDeviceAddress();
|
||||
|
||||
console.log("[Router:API] 1/7: Création du processus de pairing...");
|
||||
const createPairingProcessReturn = await services.createPairingProcess(
|
||||
"",
|
||||
[myAddress]
|
||||
);
|
||||
|
||||
const pairingId =
|
||||
createPairingProcessReturn.updated_process?.process_id;
|
||||
const stateId = createPairingProcessReturn.updated_process
|
||||
?.current_process?.states[0]?.state_id as string;
|
||||
if (!pairingId || !stateId) {
|
||||
throw new Error(
|
||||
"Pairing process creation failed to return valid IDs"
|
||||
);
|
||||
}
|
||||
console.log(`[Router:API] 2/7: Processus ${pairingId} créé.`);
|
||||
|
||||
console.log("[Router:API] 3/7: Enregistrement local de l'appareil...");
|
||||
// 🔥 CORRECTION TS2322 : myAddress est maintenant une string, plus une Promise
|
||||
await services.pairDevice(pairingId, [myAddress]);
|
||||
|
||||
console.log(
|
||||
"[Router:API] 4/7: Traitement du retour (handleApiReturn)..."
|
||||
);
|
||||
await services.handleApiReturn(createPairingProcessReturn);
|
||||
|
||||
console.log("[Router:API] 5/7: Création de la mise à jour PRD...");
|
||||
const createPrdUpdateReturn = await services.createPrdUpdate(
|
||||
pairingId,
|
||||
stateId
|
||||
);
|
||||
await services.handleApiReturn(createPrdUpdateReturn);
|
||||
|
||||
console.log("[Router:API] 6/7: Approbation du changement...");
|
||||
const approveChangeReturn = await services.approveChange(
|
||||
pairingId,
|
||||
stateId
|
||||
);
|
||||
await services.handleApiReturn(approveChangeReturn);
|
||||
|
||||
console.log("[Router:API] 7/7: Confirmation finale du pairing...");
|
||||
|
||||
console.log("[Router:API] 🎉 Appairage terminé avec succès !");
|
||||
|
||||
const successMsg = {
|
||||
type: MessageType.PAIRING_CREATED,
|
||||
pairingId,
|
||||
messageId: event.data.messageId,
|
||||
};
|
||||
window.parent.postMessage(successMsg, event.origin);
|
||||
});
|
||||
};
|
||||
|
||||
const handleGetMyProcesses = async (event: MessageEvent) => {
|
||||
console.log(
|
||||
`[Router:API] 📨 Message ${MessageType.GET_MY_PROCESSES} reçu`
|
||||
);
|
||||
if (!(await services.isPaired())) throw new Error("Device not paired");
|
||||
|
||||
await withToken(event, async () => {
|
||||
const myProcesses = await services.getMyProcesses();
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.GET_MY_PROCESSES,
|
||||
myProcesses,
|
||||
messageId: event.data.messageId,
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleGetProcesses = async (event: MessageEvent) => {
|
||||
console.log(`[Router:API] 📨 Message ${MessageType.GET_PROCESSES} reçu`);
|
||||
if (!(await services.isPaired())) throw new Error("Device not paired");
|
||||
|
||||
await withToken(event, async () => {
|
||||
const processes = await services.getProcesses();
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.PROCESSES_RETRIEVED,
|
||||
processes,
|
||||
messageId: event.data.messageId,
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDecryptState = async (event: MessageEvent) => {
|
||||
console.log(`[Router:API] 📨 Message ${MessageType.RETRIEVE_DATA} reçu`);
|
||||
if (!(await services.isPaired())) throw new Error("Device not paired");
|
||||
|
||||
const { processId, stateId } = event.data;
|
||||
|
||||
await withToken(event, async () => {
|
||||
const process = await services.getProcess(processId);
|
||||
if (!process) throw new Error("Can't find process");
|
||||
|
||||
// 🔥 CORRECTION TS2339 & TS2345 : Ajout de await car getStateFromId est async
|
||||
const state = await services.getStateFromId(process, stateId);
|
||||
|
||||
if (!state)
|
||||
throw new Error(`Unknown state ${stateId} for process ${processId}`);
|
||||
|
||||
console.log(
|
||||
`[Router:API] 🔐 Démarrage du déchiffrement pour ${processId}`
|
||||
);
|
||||
await services.ensureConnections(process, stateId);
|
||||
|
||||
const res: Record<string, any> = {};
|
||||
for (const attribute of Object.keys(state.pcd_commitment)) {
|
||||
if (
|
||||
attribute === "roles" ||
|
||||
(state.public_data && state.public_data[attribute])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const decryptedAttribute = await services.decryptAttribute(
|
||||
processId,
|
||||
state,
|
||||
attribute
|
||||
);
|
||||
if (decryptedAttribute) {
|
||||
res[attribute] = decryptedAttribute;
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
`[Router:API] ✅ Déchiffrement terminé pour ${processId}. ${Object.keys(res).length
|
||||
} attribut(s) déchiffré(s).`
|
||||
);
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.DATA_RETRIEVED,
|
||||
data: res,
|
||||
messageId: event.data.messageId,
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleValidateToken = async (event: MessageEvent) => {
|
||||
// ... (Code identique, pas d'erreurs ici normalement)
|
||||
console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_TOKEN} reçu`);
|
||||
const accessToken = event.data.accessToken;
|
||||
const refreshToken = event.data.refreshToken;
|
||||
if (!accessToken || !refreshToken) {
|
||||
throw new Error("Missing access, refresh token or both");
|
||||
}
|
||||
|
||||
const isValid = await tokenService.validateToken(
|
||||
accessToken,
|
||||
event.origin
|
||||
);
|
||||
console.log(`[Router:API] 🔑 Validation Jeton: ${isValid}`);
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.VALIDATE_TOKEN,
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken,
|
||||
isValid: isValid,
|
||||
messageId: event.data.messageId,
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
};
|
||||
|
||||
const handleRenewToken = async (event: MessageEvent) => {
|
||||
// ... (Code identique)
|
||||
console.log(`[Router:API] 📨 Message ${MessageType.RENEW_TOKEN} reçu`);
|
||||
const refreshToken = event.data.refreshToken;
|
||||
if (!refreshToken) throw new Error("No refresh token provided");
|
||||
|
||||
const newAccessToken = await tokenService.refreshAccessToken(
|
||||
refreshToken,
|
||||
event.origin
|
||||
);
|
||||
if (!newAccessToken)
|
||||
throw new Error("Failed to refresh token (invalid refresh token)");
|
||||
|
||||
console.log(`[Router:API] 🔑 Jeton d'accès renouvelé.`);
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.RENEW_TOKEN,
|
||||
accessToken: newAccessToken,
|
||||
refreshToken: refreshToken,
|
||||
messageId: event.data.messageId,
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
};
|
||||
|
||||
const handleGetPairingId = async (event: MessageEvent) => {
|
||||
console.log(`[Router:API] 📨 Message ${MessageType.GET_PAIRING_ID} reçu`);
|
||||
const maxRetries = 10;
|
||||
const retryDelay = 300;
|
||||
let pairingId: string | null = null;
|
||||
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
const device = await services.getDeviceFromDatabase();
|
||||
if (device && device.pairing_process_commitment) {
|
||||
pairingId = device.pairing_process_commitment;
|
||||
console.log(
|
||||
`[Router:API] GET_PAIRING_ID: ID trouvé en BDD (tentative ${i + 1
|
||||
}/${maxRetries})`
|
||||
);
|
||||
break;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
||||
}
|
||||
|
||||
if (!pairingId) {
|
||||
throw new Error("Device not paired");
|
||||
}
|
||||
|
||||
await withToken(event, async () => {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.GET_PAIRING_ID,
|
||||
userPairingId: pairingId,
|
||||
messageId: event.data.messageId,
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateProcess = async (event: MessageEvent) => {
|
||||
console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PROCESS} reçu`);
|
||||
if (!(await services.isPaired())) throw new Error("Device not paired");
|
||||
|
||||
const { processData, privateFields, roles } = event.data;
|
||||
|
||||
await withToken(event, async () => {
|
||||
const { privateData, publicData } = splitPrivateData(
|
||||
processData,
|
||||
privateFields
|
||||
);
|
||||
|
||||
const createProcessReturn = await services.createProcess(
|
||||
privateData,
|
||||
publicData,
|
||||
roles
|
||||
);
|
||||
if (!createProcessReturn.updated_process) {
|
||||
throw new Error("Empty updated_process in createProcessReturn");
|
||||
}
|
||||
|
||||
const processId = createProcessReturn.updated_process.process_id;
|
||||
const process = createProcessReturn.updated_process.current_process;
|
||||
|
||||
await services.handleApiReturn(createProcessReturn);
|
||||
console.log(`[Router:API] 🎉 Processus ${processId} créé.`);
|
||||
|
||||
const res = {
|
||||
processId,
|
||||
process,
|
||||
processData,
|
||||
};
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.PROCESS_CREATED,
|
||||
processCreated: res,
|
||||
messageId: event.data.messageId,
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleNotifyUpdate = async (event: MessageEvent) => {
|
||||
console.log(`[Router:API] 📨 Message ${MessageType.NOTIFY_UPDATE} reçu`);
|
||||
if (!(await services.isPaired())) throw new Error("Device not paired");
|
||||
|
||||
const { processId, stateId } = event.data;
|
||||
|
||||
await withToken(event, async () => {
|
||||
if (!isValid32ByteHex(stateId)) throw new Error("Invalid state id");
|
||||
|
||||
const res = await services.createPrdUpdate(processId, stateId);
|
||||
await services.handleApiReturn(res);
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.UPDATE_NOTIFIED,
|
||||
messageId: event.data.messageId,
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleValidateState = async (event: MessageEvent) => {
|
||||
console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_STATE} reçu`);
|
||||
if (!(await services.isPaired())) throw new Error("Device not paired");
|
||||
|
||||
const { processId, stateId } = event.data;
|
||||
|
||||
await withToken(event, async () => {
|
||||
const res = await services.approveChange(processId, stateId);
|
||||
await services.handleApiReturn(res);
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.STATE_VALIDATED,
|
||||
validatedProcess: res.updated_process,
|
||||
messageId: event.data.messageId,
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdateProcess = async (event: MessageEvent) => {
|
||||
console.log(`[Router:API] 📨 Message ${MessageType.UPDATE_PROCESS} reçu`);
|
||||
if (!(await services.isPaired())) throw new Error("Device not paired");
|
||||
|
||||
const { processId, newData, privateFields, roles } = event.data;
|
||||
|
||||
await withToken(event, async () => {
|
||||
const res = await services.updateProcess(
|
||||
processId,
|
||||
newData,
|
||||
privateFields,
|
||||
roles
|
||||
);
|
||||
await services.handleApiReturn(res);
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.PROCESS_UPDATED,
|
||||
updatedProcess: res.updated_process,
|
||||
messageId: event.data.messageId,
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDecodePublicData = async (event: MessageEvent) => {
|
||||
console.log(
|
||||
`[Router:API] 📨 Message ${MessageType.DECODE_PUBLIC_DATA} reçu`
|
||||
);
|
||||
if (!(await services.isPaired())) throw new Error("Device not paired");
|
||||
|
||||
const { encodedData } = event.data;
|
||||
|
||||
await withToken(event, async () => {
|
||||
const decodedData = await services.decodeValue(encodedData);
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.PUBLIC_DATA_DECODED,
|
||||
decodedData,
|
||||
messageId: event.data.messageId,
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleHashValue = async (event: MessageEvent) => {
|
||||
console.log(`[Router:API] 📨 Message ${MessageType.HASH_VALUE} reçu`);
|
||||
const { commitedIn, label, fileBlob } = event.data;
|
||||
|
||||
await withToken(event, async () => {
|
||||
const hash = await services.getHashForFile(commitedIn, label, fileBlob);
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.VALUE_HASHED,
|
||||
hash,
|
||||
messageId: event.data.messageId,
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleGetMerkleProof = async (event: MessageEvent) => {
|
||||
console.log(
|
||||
`[Router:API] 📨 Message ${MessageType.GET_MERKLE_PROOF} reçu`
|
||||
);
|
||||
const { processState, attributeName } = event.data;
|
||||
|
||||
await withToken(event, async () => {
|
||||
const proof = await services.getMerkleProofForFile(
|
||||
processState,
|
||||
attributeName
|
||||
);
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.MERKLE_PROOF_RETRIEVED,
|
||||
proof,
|
||||
messageId: event.data.messageId,
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleValidateMerkleProof = async (event: MessageEvent) => {
|
||||
console.log(
|
||||
`[Router:API] 📨 Message ${MessageType.VALIDATE_MERKLE_PROOF} reçu`
|
||||
);
|
||||
const { merkleProof, documentHash } = event.data;
|
||||
|
||||
await withToken(event, async () => {
|
||||
let parsedMerkleProof: MerkleProofResult;
|
||||
try {
|
||||
parsedMerkleProof = JSON.parse(merkleProof);
|
||||
} catch (e) {
|
||||
throw new Error("Provided merkleProof is not a valid json object");
|
||||
}
|
||||
|
||||
const res = await services.validateMerkleProof(
|
||||
parsedMerkleProof,
|
||||
documentHash
|
||||
);
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.MERKLE_PROOF_VALIDATED,
|
||||
isValid: res,
|
||||
messageId: event.data.messageId,
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
window.removeEventListener("message", handleMessage);
|
||||
window.addEventListener("message", handleMessage);
|
||||
|
||||
async function handleMessage(event: MessageEvent) {
|
||||
try {
|
||||
// Switch/case inchangé ...
|
||||
switch (event.data.type) {
|
||||
case MessageType.REQUEST_LINK:
|
||||
await handleRequestLink(event);
|
||||
break;
|
||||
case MessageType.CREATE_PAIRING:
|
||||
await handleCreatePairing(event);
|
||||
break;
|
||||
case MessageType.GET_MY_PROCESSES:
|
||||
await handleGetMyProcesses(event);
|
||||
break;
|
||||
case MessageType.GET_PROCESSES:
|
||||
await handleGetProcesses(event);
|
||||
break;
|
||||
case MessageType.RETRIEVE_DATA:
|
||||
await handleDecryptState(event);
|
||||
break;
|
||||
case MessageType.VALIDATE_TOKEN:
|
||||
await handleValidateToken(event);
|
||||
break;
|
||||
case MessageType.RENEW_TOKEN:
|
||||
await handleRenewToken(event);
|
||||
break;
|
||||
case MessageType.GET_PAIRING_ID:
|
||||
await handleGetPairingId(event);
|
||||
break;
|
||||
case MessageType.CREATE_PROCESS:
|
||||
await handleCreateProcess(event);
|
||||
break;
|
||||
case MessageType.NOTIFY_UPDATE:
|
||||
await handleNotifyUpdate(event);
|
||||
break;
|
||||
case MessageType.VALIDATE_STATE:
|
||||
await handleValidateState(event);
|
||||
break;
|
||||
case MessageType.UPDATE_PROCESS:
|
||||
await handleUpdateProcess(event);
|
||||
break;
|
||||
case MessageType.DECODE_PUBLIC_DATA:
|
||||
await handleDecodePublicData(event);
|
||||
break;
|
||||
case MessageType.HASH_VALUE:
|
||||
await handleHashValue(event);
|
||||
break;
|
||||
case MessageType.GET_MERKLE_PROOF:
|
||||
await handleGetMerkleProof(event);
|
||||
break;
|
||||
case MessageType.VALIDATE_MERKLE_PROOF:
|
||||
await handleValidateMerkleProof(event);
|
||||
break;
|
||||
default:
|
||||
// console.warn("[Router:API] ⚠️ Message non géré reçu:", event.data);
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMsg = `[Router:API] 💥 Erreur de haut niveau: ${error}`;
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.LISTENING,
|
||||
},
|
||||
"*"
|
||||
);
|
||||
console.log(
|
||||
"[Router:API] ✅ Tous les listeners sont actifs. Envoi du message LISTENING au parent."
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,260 +0,0 @@
|
||||
import modalHtml from '../components/login-modal/login-modal.html?raw';
|
||||
import modalScript from '../components/login-modal/login-modal.js?raw';
|
||||
import validationModalStyle from '../components/validation-modal/validation-modal.css?raw';
|
||||
import Services from './service';
|
||||
import { init, navigate } from '../router';
|
||||
import { addressToEmoji } from '../utils/sp-address.utils';
|
||||
import { RoleDefinition } from 'pkg/sdk_client';
|
||||
import { initValidationModal } from '~/components/validation-modal/validation-modal';
|
||||
import { interpolate } from '~/utils/html.utils';
|
||||
|
||||
export default class ModalService {
|
||||
private static instance: ModalService;
|
||||
private stateId: string | null = null;
|
||||
private processId: string | null = null;
|
||||
private constructor() {}
|
||||
private paired_addresses: string[] = [];
|
||||
private modal: HTMLElement | null = null;
|
||||
|
||||
// Method to access the singleton instance of Services
|
||||
public static async getInstance(): Promise<ModalService> {
|
||||
if (!ModalService.instance) {
|
||||
ModalService.instance = new ModalService();
|
||||
}
|
||||
return ModalService.instance;
|
||||
}
|
||||
|
||||
public openLoginModal(myAddress: string, receiverAddress: string) {
|
||||
const container = document.querySelector('.page-container');
|
||||
let html = modalHtml;
|
||||
html = html.replace('{{device1}}', myAddress);
|
||||
html = html.replace('{{device2}}', receiverAddress);
|
||||
if (container) container.innerHTML += html;
|
||||
const modal = document.getElementById('login-modal');
|
||||
if (modal) modal.style.display = 'flex';
|
||||
const newScript = document.createElement('script');
|
||||
|
||||
newScript.setAttribute('type', 'module');
|
||||
newScript.textContent = modalScript;
|
||||
document.head.appendChild(newScript).parentNode?.removeChild(newScript);
|
||||
}
|
||||
|
||||
async injectModal(members: any[]) {
|
||||
const container = document.querySelector('#containerId');
|
||||
if (container) {
|
||||
let html = await fetch('/src/components/modal/confirmation-modal.html').then((res) => res.text());
|
||||
html = html.replace('{{device1}}', await addressToEmoji(members[0]['sp_addresses'][0]));
|
||||
html = html.replace('{{device2}}', await addressToEmoji(members[0]['sp_addresses'][1]));
|
||||
container.innerHTML += html;
|
||||
|
||||
// Dynamically load the header JS
|
||||
const script = document.createElement('script');
|
||||
script.src = '/src/components/modal/confirmation-modal.ts';
|
||||
script.type = 'module';
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
}
|
||||
|
||||
async injectCreationModal(members: any[]) {
|
||||
const container = document.querySelector('#containerId');
|
||||
if (container) {
|
||||
let html = await fetch('/src/components/modal/creation-modal.html').then((res) => res.text());
|
||||
html = html.replace('{{device1}}', await addressToEmoji(members[0]['sp_addresses'][0]));
|
||||
container.innerHTML += html;
|
||||
|
||||
// Dynamically load the header JS
|
||||
const script = document.createElement('script');
|
||||
script.src = '/src/components/modal/confirmation-modal.ts';
|
||||
script.type = 'module';
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
}
|
||||
|
||||
// Device 1 wait Device 2
|
||||
async injectWaitingModal() {
|
||||
const container = document.querySelector('#containerId');
|
||||
if (container) {
|
||||
let html = await fetch('/src/components/modal/waiting-modal.html').then((res) => res.text());
|
||||
container.innerHTML += html;
|
||||
}
|
||||
}
|
||||
|
||||
async injectValidationModal(processDiff: any) {
|
||||
const container = document.querySelector('#containerId');
|
||||
if (container) {
|
||||
let html = await fetch('/src/components/validation-modal/validation-modal.html').then((res) => res.text());
|
||||
html = interpolate(html, {processId: processDiff.processId})
|
||||
container.innerHTML += html;
|
||||
|
||||
// Dynamically load the header JS
|
||||
const script = document.createElement('script');
|
||||
script.id = 'validation-modal-script';
|
||||
script.src = '/src/components/validation-modal/validation-modal.ts';
|
||||
script.type = 'module';
|
||||
document.head.appendChild(script);
|
||||
const css = document.createElement('style');
|
||||
css.id = 'validation-modal-css';
|
||||
css.innerText = validationModalStyle;
|
||||
document.head.appendChild(css);
|
||||
initValidationModal(processDiff)
|
||||
}
|
||||
}
|
||||
|
||||
async closeValidationModal() {
|
||||
const script = document.querySelector('#validation-modal-script');
|
||||
const css = document.querySelector('#validation-modal-css');
|
||||
const component = document.querySelector('#validation-modal');
|
||||
script?.remove();
|
||||
css?.remove();
|
||||
component?.remove();
|
||||
}
|
||||
|
||||
public async openPairingConfirmationModal(roleDefinition: Record<string, RoleDefinition>, processId: string, stateId: string) {
|
||||
let members;
|
||||
if (roleDefinition['pairing']) {
|
||||
const owner = roleDefinition['pairing'];
|
||||
members = owner.members;
|
||||
} else {
|
||||
throw new Error('No "pairing" role');
|
||||
}
|
||||
|
||||
if (members.length != 1) {
|
||||
throw new Error('Must have exactly 1 member');
|
||||
}
|
||||
|
||||
console.log("MEMBERS:", members);
|
||||
// We take all the addresses except our own
|
||||
const service = await Services.getInstance();
|
||||
const localAddress = await service.getDeviceAddress();
|
||||
for (const member of members) {
|
||||
if (member.sp_addresses) {
|
||||
for (const address of member.sp_addresses) {
|
||||
if (address !== localAddress) {
|
||||
this.paired_addresses.push(address);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.processId = processId;
|
||||
this.stateId = stateId;
|
||||
|
||||
if (members[0].sp_addresses.length === 1) {
|
||||
await this.injectCreationModal(members);
|
||||
this.modal = document.getElementById('creation-modal');
|
||||
console.log("LENGTH:", members[0].sp_addresses.length);
|
||||
} else {
|
||||
await this.injectModal(members);
|
||||
this.modal = document.getElementById('modal');
|
||||
console.log("LENGTH:", members[0].sp_addresses.length);
|
||||
}
|
||||
|
||||
if (this.modal) this.modal.style.display = 'flex';
|
||||
|
||||
// Close modal when clicking outside of it
|
||||
window.onclick = (event) => {
|
||||
if (event.target === this.modal) {
|
||||
this.closeConfirmationModal();
|
||||
}
|
||||
};
|
||||
}
|
||||
confirmLogin() {
|
||||
console.log('=============> Confirm Login');
|
||||
}
|
||||
async closeLoginModal() {
|
||||
if (this.modal) this.modal.style.display = 'none';
|
||||
}
|
||||
|
||||
async confirmPairing() {
|
||||
const service = await Services.getInstance();
|
||||
if (this.modal) this.modal.style.display = 'none';
|
||||
|
||||
if (service.device1) {
|
||||
console.log("Device 1 detected");
|
||||
// We send the prd update
|
||||
if (this.stateId && this.processId) {
|
||||
try {
|
||||
// Device B shouldn't do this again
|
||||
const createPrdUpdateReturn = service.createPrdUpdate(this.processId, this.stateId);
|
||||
await service.handleApiReturn(createPrdUpdateReturn);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
throw new Error('No currentPcdCommitment');
|
||||
}
|
||||
|
||||
// We send confirmation that we validate the change
|
||||
try {
|
||||
const approveChangeReturn = await service.approveChange(this.processId!, this.stateId!);
|
||||
await service.handleApiReturn(approveChangeReturn);
|
||||
|
||||
await this.injectWaitingModal();
|
||||
const waitingModal = document.getElementById('waiting-modal');
|
||||
if (waitingModal) waitingModal.style.display = 'flex';
|
||||
|
||||
if (!service.device2Ready) {
|
||||
while (!service.device2Ready) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
console.log("Device 2 is ready - Device 1 can now proceed");
|
||||
}
|
||||
service.pairDevice(this.paired_addresses, this.processId);
|
||||
this.paired_addresses = [];
|
||||
this.processId = null;
|
||||
this.stateId = null;
|
||||
const newDevice = service.dumpDeviceFromMemory();
|
||||
console.log(newDevice);
|
||||
await service.saveDeviceInDatabase(newDevice);
|
||||
navigate('chat');
|
||||
service.resetState();
|
||||
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
// try {
|
||||
// service.pairDevice(this.paired_addresses);
|
||||
// } catch (e) {
|
||||
// throw e;
|
||||
// }
|
||||
} else {
|
||||
console.log("Device 2 detected");
|
||||
|
||||
// if (this.stateId && this.processId) {
|
||||
// try {
|
||||
// // Device B shouldn't do this again
|
||||
// const createPrdUpdateReturn = service.createPrdUpdate(this.processId, this.stateId);
|
||||
// await service.handleApiReturn(createPrdUpdateReturn);
|
||||
// } catch (e) {
|
||||
// throw e;
|
||||
// }
|
||||
// } else {
|
||||
// throw new Error('No currentPcdCommitment');
|
||||
// }
|
||||
|
||||
// We send confirmation that we validate the change
|
||||
try {
|
||||
const approveChangeReturn = await service.approveChange(this.processId!, this.stateId!);
|
||||
await service.handleApiReturn(approveChangeReturn);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
service.pairDevice(this.paired_addresses, this.processId!);
|
||||
|
||||
this.paired_addresses = [];
|
||||
this.processId = null;
|
||||
this.stateId = null;
|
||||
const newDevice = service.dumpDeviceFromMemory();
|
||||
console.log(newDevice);
|
||||
await service.saveDeviceInDatabase(newDevice);
|
||||
navigate('chat');
|
||||
}
|
||||
}
|
||||
|
||||
async closeConfirmationModal() {
|
||||
const service = await Services.getInstance();
|
||||
await service.unpairDevice();
|
||||
if (this.modal) this.modal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,81 +1,112 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
|
||||
export async function storeData(servers: string[], key: string, value: Blob, ttl: number | null): Promise<AxiosResponse | null> {
|
||||
export async function storeData(
|
||||
servers: string[],
|
||||
key: string,
|
||||
value: Blob,
|
||||
ttl: number | null
|
||||
): Promise<Response | null> {
|
||||
for (const server of servers) {
|
||||
try {
|
||||
// Append key and ttl as query parameters
|
||||
const url = new URL(`${server}/store`);
|
||||
url.searchParams.append('key', key);
|
||||
if (ttl !== null) {
|
||||
url.searchParams.append('ttl', ttl.toString());
|
||||
// 1. Vérification d'existence (GET)
|
||||
const dataExists = await testData(server, key);
|
||||
|
||||
if (dataExists) {
|
||||
console.log("Data already stored:", key);
|
||||
continue;
|
||||
} else {
|
||||
console.log(
|
||||
"Data not stored for server, proceeding to POST:",
|
||||
key,
|
||||
server
|
||||
);
|
||||
}
|
||||
|
||||
// Send the encrypted ArrayBuffer as the raw request body.
|
||||
const response = await axios.post(url.toString(), value, {
|
||||
// 2. Construction URL
|
||||
let url: string;
|
||||
if (server.startsWith("/")) {
|
||||
url = `${server}/store/${encodeURIComponent(key)}`;
|
||||
if (ttl !== null) url += `?ttl=${ttl}`;
|
||||
} else {
|
||||
const urlObj = new URL(`${server}/store/${encodeURIComponent(key)}`);
|
||||
if (ttl !== null) urlObj.searchParams.append("ttl", ttl.toString());
|
||||
url = urlObj.toString();
|
||||
}
|
||||
|
||||
// 3. Envoi (POST) avec Fetch
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream'
|
||||
"Content-Type": "application/octet-stream",
|
||||
},
|
||||
body: value,
|
||||
});
|
||||
console.log('Data stored successfully:', key);
|
||||
if (response.status !== 200) {
|
||||
console.error('Received response status', response.status);
|
||||
|
||||
if (response.ok) {
|
||||
// Status 200-299
|
||||
console.log("Data stored successfully:", key);
|
||||
return response;
|
||||
} else if (response.status === 409) {
|
||||
// Conflit (déjà existant), on retourne null comme avant
|
||||
return null;
|
||||
} else {
|
||||
console.error("Received response status", response.status);
|
||||
continue;
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error?.response?.status === 409) {
|
||||
return null;
|
||||
}
|
||||
console.error('Error storing data:', error);
|
||||
console.error("Error storing data:", error);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function retrieveData(servers: string[], key: string): Promise<ArrayBuffer | null> {
|
||||
export async function retrieveData(
|
||||
servers: string[],
|
||||
key: string
|
||||
): Promise<ArrayBuffer | null> {
|
||||
for (const server of servers) {
|
||||
try {
|
||||
// When fetching the data from the server:
|
||||
const response = await axios.get(`${server}/retrieve/${key}`, {
|
||||
responseType: 'arraybuffer'
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
console.error('Received response status', response.status);
|
||||
continue;
|
||||
const url = server.startsWith("/")
|
||||
? `${server}/retrieve/${key}`
|
||||
: new URL(`${server}/retrieve/${key}`).toString();
|
||||
|
||||
console.log("Retrieving data", key, " from:", url);
|
||||
|
||||
const response = await fetch(url, { method: "GET" });
|
||||
|
||||
if (response.ok) {
|
||||
// Transformation en ArrayBuffer
|
||||
return await response.arrayBuffer();
|
||||
} else {
|
||||
if (response.status === 404) {
|
||||
console.log(`Data not found on server ${server} for key ${key}`);
|
||||
} else {
|
||||
console.error(
|
||||
`Server ${server} returned status ${response.status}: ${response.statusText}`
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// console.log('Retrieved data:', response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error retrieving data:', error);
|
||||
console.error(`Unexpected error retrieving data from ${server}:`, error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
interface TestResponse {
|
||||
key: string;
|
||||
value: boolean;
|
||||
}
|
||||
|
||||
export async function testData(servers: string[], key: string): Promise<Record<string, boolean | null> | null> {
|
||||
const res = {};
|
||||
for (const server of servers) {
|
||||
res[server] = null;
|
||||
try {
|
||||
const response = await axios.get(`${server}/test/${key}`);
|
||||
if (response.status !== 200) {
|
||||
console.error(`${server}: Test response status: ${response.status}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const data: TestResponse = response.data;
|
||||
|
||||
res[server] = data.value;
|
||||
} catch (error) {
|
||||
console.error('Error retrieving data:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
export async function testData(server: string, key: string): Promise<boolean> {
|
||||
try {
|
||||
const testUrl = server.startsWith("/")
|
||||
? `${server}/test/${encodeURIComponent(key)}`
|
||||
: new URL(`${server}/test/${encodeURIComponent(key)}`).toString();
|
||||
|
||||
// On utilise fetch ici aussi
|
||||
const response = await fetch(testUrl, { method: "GET" });
|
||||
|
||||
// 200 OK = existe
|
||||
return response.status === 200;
|
||||
} catch (error) {
|
||||
// Erreur réseau
|
||||
console.error("Error testing data:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
138
src/services/token.service.ts
Normal file
138
src/services/token.service.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import * as jose from "jose";
|
||||
|
||||
interface TokenPair {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export default class TokenService {
|
||||
private static instance: TokenService;
|
||||
|
||||
// Constantes
|
||||
private readonly STORAGE_KEY = "4NK_SECURE_SESSION_KEY";
|
||||
private readonly ACCESS_TOKEN_EXPIRATION = "30s";
|
||||
private readonly REFRESH_TOKEN_EXPIRATION = "7d";
|
||||
|
||||
// Cache mémoire pour éviter de lire le localStorage à chaque appel
|
||||
private secretKeyCache: Uint8Array | null = null;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static async getInstance(): Promise<TokenService> {
|
||||
if (!TokenService.instance) {
|
||||
TokenService.instance = new TokenService();
|
||||
}
|
||||
return TokenService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la clé secrète existante ou en génère une nouvelle
|
||||
* et la sauvegarde dans le localStorage pour survivre aux refresh.
|
||||
*/
|
||||
private getSecretKey(): Uint8Array {
|
||||
if (this.secretKeyCache) return this.secretKeyCache;
|
||||
|
||||
const storedKey = localStorage.getItem(this.STORAGE_KEY);
|
||||
|
||||
if (storedKey) {
|
||||
// Restauration de la clé existante (Hex -> Uint8Array)
|
||||
this.secretKeyCache = this.hexToBuffer(storedKey);
|
||||
} else {
|
||||
// Génération d'une nouvelle clé aléatoire de 32 octets (256 bits)
|
||||
const newKey = new Uint8Array(32);
|
||||
crypto.getRandomValues(newKey);
|
||||
|
||||
// Sauvegarde (Uint8Array -> Hex)
|
||||
localStorage.setItem(this.STORAGE_KEY, this.bufferToHex(newKey));
|
||||
this.secretKeyCache = newKey;
|
||||
console.log(
|
||||
"[TokenService] 🔐 Nouvelle clé de session générée et stockée."
|
||||
);
|
||||
}
|
||||
|
||||
return this.secretKeyCache;
|
||||
}
|
||||
|
||||
// --- Méthodes Publiques ---
|
||||
|
||||
async generateSessionToken(origin: string): Promise<TokenPair> {
|
||||
const secret = this.getSecretKey();
|
||||
|
||||
const accessToken = await new jose.SignJWT({ origin, type: "access" })
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime(this.ACCESS_TOKEN_EXPIRATION)
|
||||
.sign(secret);
|
||||
|
||||
const refreshToken = await new jose.SignJWT({ origin, type: "refresh" })
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime(this.REFRESH_TOKEN_EXPIRATION)
|
||||
.sign(secret);
|
||||
|
||||
return { accessToken, refreshToken };
|
||||
}
|
||||
|
||||
async validateToken(token: string, origin: string): Promise<boolean> {
|
||||
try {
|
||||
const secret = this.getSecretKey();
|
||||
const { payload } = await jose.jwtVerify(token, secret);
|
||||
|
||||
return payload.origin === origin;
|
||||
} catch (error: any) {
|
||||
// On ignore les erreurs d'expiration classiques pour ne pas polluer la console
|
||||
if (error?.code === "ERR_JWT_EXPIRED") {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.warn(
|
||||
"[TokenService] Validation échouée:",
|
||||
error.code || error.message
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async refreshAccessToken(
|
||||
refreshToken: string,
|
||||
origin: string
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
// Validation du token (vérifie signature + expiration)
|
||||
const isValid = await this.validateToken(refreshToken, origin);
|
||||
if (!isValid) return null;
|
||||
|
||||
const secret = this.getSecretKey();
|
||||
const { payload } = await jose.jwtVerify(refreshToken, secret);
|
||||
|
||||
if (payload.type !== "refresh") return null;
|
||||
|
||||
// Génération du nouveau token
|
||||
return await new jose.SignJWT({ origin, type: "access" })
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime(this.ACCESS_TOKEN_EXPIRATION)
|
||||
.sign(secret);
|
||||
} catch (error) {
|
||||
console.error("[TokenService] Erreur refresh:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Utilitaires de conversion ---
|
||||
|
||||
private bufferToHex(buffer: Uint8Array): string {
|
||||
return Array.from(buffer)
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
private hexToBuffer(hex: string): Uint8Array {
|
||||
if (hex.length % 2 !== 0) throw new Error("Invalid hex string");
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
50
src/types/index.ts
Normal file
50
src/types/index.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { Device, Process, SecretsStore } from 'pkg/sdk_client';
|
||||
|
||||
export interface BackUp {
|
||||
device: Device;
|
||||
secrets: SecretsStore;
|
||||
processes: Record<string, Process>;
|
||||
}
|
||||
|
||||
export enum MessageType {
|
||||
// Establish connection and keep alive
|
||||
LISTENING = 'LISTENING',
|
||||
REQUEST_LINK = 'REQUEST_LINK',
|
||||
LINK_ACCEPTED = 'LINK_ACCEPTED',
|
||||
CREATE_PAIRING = 'CREATE_PAIRING',
|
||||
PAIRING_CREATED = 'PAIRING_CREATED',
|
||||
ERROR = 'ERROR',
|
||||
VALIDATE_TOKEN = 'VALIDATE_TOKEN',
|
||||
RENEW_TOKEN = 'RENEW_TOKEN',
|
||||
// Get various information
|
||||
GET_PAIRING_ID = 'GET_PAIRING_ID',
|
||||
GET_PROCESSES = 'GET_PROCESSES',
|
||||
GET_MY_PROCESSES = 'GET_MY_PROCESSES',
|
||||
PROCESSES_RETRIEVED = 'PROCESSES_RETRIEVED',
|
||||
RETRIEVE_DATA = 'RETRIEVE_DATA',
|
||||
DATA_RETRIEVED = 'DATA_RETRIEVED',
|
||||
DECODE_PUBLIC_DATA = 'DECODE_PUBLIC_DATA',
|
||||
PUBLIC_DATA_DECODED = 'PUBLIC_DATA_DECODED',
|
||||
GET_MEMBER_ADDRESSES = 'GET_MEMBER_ADDRESSES',
|
||||
MEMBER_ADDRESSES_RETRIEVED = 'MEMBER_ADDRESSES_RETRIEVED',
|
||||
// Processes
|
||||
CREATE_PROCESS = 'CREATE_PROCESS',
|
||||
PROCESS_CREATED = 'PROCESS_CREATED',
|
||||
UPDATE_PROCESS = 'UPDATE_PROCESS',
|
||||
PROCESS_UPDATED = 'PROCESS_UPDATED',
|
||||
NOTIFY_UPDATE = 'NOTIFY_UPDATE',
|
||||
UPDATE_NOTIFIED = 'UPDATE_NOTIFIED',
|
||||
VALIDATE_STATE = 'VALIDATE_STATE',
|
||||
STATE_VALIDATED = 'STATE_VALIDATED',
|
||||
// Hash and merkle proof
|
||||
HASH_VALUE = 'HASH_VALUE',
|
||||
VALUE_HASHED = 'VALUE_HASHED',
|
||||
GET_MERKLE_PROOF = 'GET_MERKLE_PROOF',
|
||||
MERKLE_PROOF_RETRIEVED = 'MERKLE_PROOF_RETRIEVED',
|
||||
VALIDATE_MERKLE_PROOF = 'VALIDATE_MERKLE_PROOF',
|
||||
MERKLE_PROOF_VALIDATED = 'MERKLE_PROOF_VALIDATED',
|
||||
// Account management
|
||||
ADD_DEVICE = 'ADD_DEVICE',
|
||||
DEVICE_ADDED = 'DEVICE_ADDED',
|
||||
}
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
export function getCorrectDOM(componentTag: string): Node {
|
||||
const dom = document?.querySelector(componentTag)?.shadowRoot || (document as Node);
|
||||
return dom;
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
import { messagesMock as initialMessagesMock } from '../mocks/mock-signature/messagesMock.js';
|
||||
|
||||
// Store singleton for messages
|
||||
class MessageStore {
|
||||
private readonly STORAGE_KEY = 'chat_messages';
|
||||
private messages: any[] = [];
|
||||
|
||||
constructor() {
|
||||
this.messages = this.loadFromLocalStorage() || [];
|
||||
}
|
||||
|
||||
private loadFromLocalStorage() {
|
||||
try {
|
||||
const stored = localStorage.getItem(this.STORAGE_KEY);
|
||||
return stored ? JSON.parse(stored) : null;
|
||||
} catch (error) {
|
||||
console.error('Error loading messages:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
getMessages() {
|
||||
return this.messages;
|
||||
}
|
||||
|
||||
setMessages(messages: any[]) {
|
||||
this.messages = messages;
|
||||
this.saveToLocalStorage();
|
||||
}
|
||||
|
||||
private saveToLocalStorage() {
|
||||
try {
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.messages));
|
||||
} catch (error) {
|
||||
console.error('Error saving messages:', error);
|
||||
}
|
||||
}
|
||||
|
||||
addMessage(memberId: string | number, message: any) {
|
||||
const memberMessages = this.messages.find((m) => String(m.memberId) === String(memberId));
|
||||
if (memberMessages) {
|
||||
memberMessages.messages.push(message);
|
||||
} else {
|
||||
this.messages.push({
|
||||
memberId: String(memberId),
|
||||
messages: [message],
|
||||
});
|
||||
}
|
||||
this.saveToLocalStorage();
|
||||
}
|
||||
}
|
||||
|
||||
export const messageStore = new MessageStore();
|
||||
@ -1,96 +0,0 @@
|
||||
interface INotification {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
time?: string;
|
||||
memberId?: string;
|
||||
}
|
||||
|
||||
class NotificationStore {
|
||||
private static instance: NotificationStore;
|
||||
private notifications: INotification[] = [];
|
||||
|
||||
private constructor() {
|
||||
this.loadFromLocalStorage();
|
||||
}
|
||||
|
||||
static getInstance(): NotificationStore {
|
||||
if (!NotificationStore.instance) {
|
||||
NotificationStore.instance = new NotificationStore();
|
||||
}
|
||||
return NotificationStore.instance;
|
||||
}
|
||||
|
||||
addNotification(notification: INotification) {
|
||||
this.notifications.push(notification);
|
||||
this.saveToLocalStorage();
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
removeNotification(index: number) {
|
||||
this.notifications.splice(index, 1);
|
||||
this.saveToLocalStorage();
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
getNotifications(): INotification[] {
|
||||
return this.notifications;
|
||||
}
|
||||
|
||||
private saveToLocalStorage() {
|
||||
localStorage.setItem('notifications', JSON.stringify(this.notifications));
|
||||
}
|
||||
|
||||
private loadFromLocalStorage() {
|
||||
const stored = localStorage.getItem('notifications');
|
||||
if (stored) {
|
||||
this.notifications = JSON.parse(stored);
|
||||
}
|
||||
}
|
||||
|
||||
private updateUI() {
|
||||
const badge = document.querySelector('.notification-badge') as HTMLElement;
|
||||
const board = document.querySelector('.notification-board') as HTMLElement;
|
||||
|
||||
if (badge) {
|
||||
badge.textContent = this.notifications.length.toString();
|
||||
badge.style.display = this.notifications.length > 0 ? 'block' : 'none';
|
||||
}
|
||||
|
||||
if (board) {
|
||||
this.renderNotificationBoard(board);
|
||||
}
|
||||
}
|
||||
|
||||
private renderNotificationBoard(board: HTMLElement) {
|
||||
board.innerHTML = '';
|
||||
|
||||
if (this.notifications.length === 0) {
|
||||
board.innerHTML = '<div class="no-notification">No notifications available</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.notifications.forEach((notif, index) => {
|
||||
const notifElement = document.createElement('div');
|
||||
notifElement.className = 'notification-item';
|
||||
notifElement.innerHTML = `
|
||||
<div>${notif.title}</div>
|
||||
<div>${notif.description}</div>
|
||||
${notif.time ? `<div>${notif.time}</div>` : ''}
|
||||
`;
|
||||
notifElement.onclick = () => {
|
||||
if (notif.memberId) {
|
||||
window.loadMemberChat(notif.memberId);
|
||||
}
|
||||
this.removeNotification(index);
|
||||
};
|
||||
board.appendChild(notifElement);
|
||||
});
|
||||
}
|
||||
|
||||
public refreshNotifications() {
|
||||
this.updateUI();
|
||||
}
|
||||
}
|
||||
|
||||
export const notificationStore = NotificationStore.getInstance();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user