Compare commits
283 Commits
create-acc
...
webworkers
| Author | SHA1 | Date | |
|---|---|---|---|
| 833ea5227a | |||
| a15abb8a33 | |||
| e3447195e3 | |||
| 77dcb8b9ae | |||
| 1cebd0ba7b | |||
| 87168dffd8 | |||
| 633fd57793 | |||
| 0ae251a1bd | |||
| 8a6e30d226 | |||
| 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_dev3/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 |
@ -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>
|
||||
<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" />
|
||||
</svg>
|
||||
<div class="nav-right">
|
||||
<div class="user-profile" id="profile-header-container">
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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-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>
|
||||
</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 }>;
|
||||
}
|
||||
91
src/main.ts
91
src/main.ts
@ -1,30 +1,71 @@
|
||||
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';
|
||||
import Services from "./services/service";
|
||||
import { Router } from "./router/index";
|
||||
import "./components/header/Header";
|
||||
import "./App";
|
||||
import { IframeController } from "./services/iframe-controller.service";
|
||||
|
||||
export { SignatureComponent, SignatureElement, ChatComponent, ChatElement, AccountComponent, AccountElement };
|
||||
async function bootstrap() {
|
||||
// Optionnel : Désactiver les logs verbeux en prod si nécessaire
|
||||
console.log(
|
||||
"🚀 Démarrage de l'application 4NK (Multi-Worker Architecture)..."
|
||||
);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'signature-component': SignatureComponent;
|
||||
'signature-element': SignatureElement;
|
||||
'chat-component': ChatComponent;
|
||||
'chat-element': ChatElement;
|
||||
'account-component': AccountComponent;
|
||||
'account-element': AccountElement;
|
||||
try {
|
||||
// 1. Initialisation des Services & Workers
|
||||
const services = await Services.getInstance();
|
||||
|
||||
// Injection du Header (Si le DOM est prêt)
|
||||
const headerSlot = document.getElementById("header-slot");
|
||||
if (headerSlot) {
|
||||
headerSlot.innerHTML = "<app-header></app-header>";
|
||||
}
|
||||
|
||||
// 2. Gestion Appareil (Device)
|
||||
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. Iframe Controller
|
||||
await IframeController.init();
|
||||
|
||||
// 4. Restauration des données et secrets
|
||||
await services.restoreProcessesFromDB();
|
||||
await services.restoreSecretsFromDB();
|
||||
|
||||
// 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.");
|
||||
// Nettoyage de l'URL pour éviter de recharger sur une route intermédiaire
|
||||
window.history.replaceState({}, "", "process");
|
||||
Router.handleLocation();
|
||||
} else {
|
||||
console.log(
|
||||
isIframe
|
||||
? "📡 Mode Iframe détecté : Prêt."
|
||||
: "🆕 Non appairé : Démarrage sur Home."
|
||||
);
|
||||
// En mode Iframe, on laisse souvent le parent piloter ou on charge la route par défaut
|
||||
Router.init();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("💥 Erreur critique au démarrage :", error);
|
||||
if (window.self !== window.top) {
|
||||
window.parent.postMessage(
|
||||
{ type: "4NK_ERROR", error: String(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>
|
||||
</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>
|
||||
|
||||
<button id="okButton" style="display: none">OK</button>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<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="process-card-content"></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>
|
||||
<div class="process-card-action">
|
||||
<a class="btn" onclick="goToProcessPage()">OK</a>
|
||||
<ul id="autocomplete-list" class="autocomplete-dropdown"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="selected-tags-container" class="tags-container">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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;
|
||||
}
|
||||
243
src/service-workers/network.sw.ts
Normal file
243
src/service-workers/network.sw.ts
Normal file
@ -0,0 +1,243 @@
|
||||
// Service Worker for Network Management
|
||||
// Handles WebSocket connections to backend relays
|
||||
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
type AnkFlag = 'Handshake' | 'NewTx' | 'Cipher' | 'Commit' | 'Faucet' | 'Ping';
|
||||
|
||||
interface ServiceWorkerMessage {
|
||||
type: string;
|
||||
payload?: any;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
// State management
|
||||
const sockets: Map<string, WebSocket> = new Map();
|
||||
const relayAddresses: Map<string, string> = new Map(); // wsUrl -> spAddress
|
||||
const messageQueue: string[] = [];
|
||||
const reconnectTimers: Map<string, any> = new Map();
|
||||
let heartbeatInterval: any = null;
|
||||
|
||||
// ==========================================
|
||||
// SERVICE WORKER LIFECYCLE
|
||||
// ==========================================
|
||||
|
||||
(self as unknown as ServiceWorkerGlobalScope).addEventListener('install', (event: ExtendableEvent) => {
|
||||
console.log('[NetworkSW] Installing...');
|
||||
event.waitUntil((self as unknown as ServiceWorkerGlobalScope).skipWaiting());
|
||||
});
|
||||
|
||||
(self as unknown as ServiceWorkerGlobalScope).addEventListener('activate', (event: ExtendableEvent) => {
|
||||
console.log('[NetworkSW] Activating...');
|
||||
event.waitUntil((self as unknown as ServiceWorkerGlobalScope).clients.claim());
|
||||
startHeartbeat();
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// MESSAGE HANDLING
|
||||
// ==========================================
|
||||
|
||||
(self as unknown as ServiceWorkerGlobalScope).addEventListener('message', async (event: ExtendableMessageEvent) => {
|
||||
const { type, payload, id } = event.data;
|
||||
console.log(`[NetworkSW] Received message: ${type} (id: ${id})`);
|
||||
|
||||
// Get the client to respond to
|
||||
const client = event.source as Client | null;
|
||||
if (!client) {
|
||||
console.error('[NetworkSW] No client source available');
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'CONNECT':
|
||||
connect(payload.url).then(() => {
|
||||
respondToClient(client, { type: 'CONNECTED', id, payload: { url: payload.url } });
|
||||
}).catch((error) => {
|
||||
respondToClient(client, { type: 'ERROR', id, payload: { error: error.message } });
|
||||
});
|
||||
break;
|
||||
|
||||
case 'SEND_MESSAGE':
|
||||
sendMessage(payload.flag, payload.content);
|
||||
respondToClient(client, { type: 'MESSAGE_SENT', id, payload: {} });
|
||||
break;
|
||||
|
||||
case 'GET_AVAILABLE_RELAY':
|
||||
const relay = getAvailableRelay();
|
||||
respondToClient(client, { type: 'AVAILABLE_RELAY', id, payload: { relay } });
|
||||
break;
|
||||
|
||||
case 'GET_ALL_RELAYS':
|
||||
const relays = getAllRelays();
|
||||
respondToClient(client, { type: 'ALL_RELAYS', id, payload: { relays } });
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn('[NetworkSW] Unknown message type:', type);
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// WEBSOCKET MANAGEMENT
|
||||
// ==========================================
|
||||
|
||||
async function connect(url: string): Promise<void> {
|
||||
if (sockets.has(url) && sockets.get(url)?.readyState === WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[NetworkSW] 🔌 Connexion à ${url}...`);
|
||||
const ws = new WebSocket(url);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log(`[NetworkSW] ✅ Connecté à ${url}`);
|
||||
sockets.set(url, ws);
|
||||
|
||||
// Reset reconnect timer if exists
|
||||
if (reconnectTimers.has(url)) {
|
||||
clearTimeout(reconnectTimers.get(url));
|
||||
reconnectTimers.delete(url);
|
||||
}
|
||||
|
||||
// Flush message queue
|
||||
flushQueue();
|
||||
|
||||
// Notify all clients
|
||||
broadcastToClients({ type: 'STATUS_CHANGE', payload: { url, status: 'OPEN' } });
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
|
||||
// If it's a Handshake, update the local map
|
||||
if (msg.flag === 'Handshake' && msg.content) {
|
||||
const handshake = JSON.parse(msg.content);
|
||||
if (handshake.sp_address) {
|
||||
relayAddresses.set(url, handshake.sp_address);
|
||||
broadcastToClients({
|
||||
type: 'STATUS_CHANGE',
|
||||
payload: { url, status: 'OPEN', spAddress: handshake.sp_address }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Forward all messages to clients
|
||||
broadcastToClients({
|
||||
type: 'MESSAGE_RECEIVED',
|
||||
payload: { flag: msg.flag, content: msg.content, url }
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[NetworkSW] Erreur parsing message:', e);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (e) => {
|
||||
// Silently handle errors (reconnection will be handled by onclose)
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.warn(`[NetworkSW] ❌ Déconnecté de ${url}.`);
|
||||
sockets.delete(url);
|
||||
relayAddresses.set(url, ''); // Reset spAddress
|
||||
|
||||
broadcastToClients({ type: 'STATUS_CHANGE', payload: { url, status: 'CLOSED' } });
|
||||
scheduleReconnect(url);
|
||||
};
|
||||
}
|
||||
|
||||
function sendMessage(flag: AnkFlag, content: string) {
|
||||
const msgStr = JSON.stringify({ flag, content });
|
||||
|
||||
// Broadcast to all connected relays
|
||||
let sent = false;
|
||||
for (const [url, ws] of sockets) {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(msgStr);
|
||||
sent = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sent) {
|
||||
messageQueue.push(msgStr);
|
||||
}
|
||||
}
|
||||
|
||||
function getAvailableRelay(): string | null {
|
||||
for (const sp of relayAddresses.values()) {
|
||||
if (sp && sp !== '') return sp;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getAllRelays(): Record<string, string> {
|
||||
return Object.fromEntries(relayAddresses);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// INTERNAL HELPERS
|
||||
// ==========================================
|
||||
|
||||
function flushQueue() {
|
||||
while (messageQueue.length > 0) {
|
||||
const msg = messageQueue.shift();
|
||||
if (!msg) break;
|
||||
for (const ws of sockets.values()) {
|
||||
if (ws.readyState === WebSocket.OPEN) ws.send(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleReconnect(url: string) {
|
||||
if (reconnectTimers.has(url)) return;
|
||||
|
||||
console.log(`[NetworkSW] ⏳ Reconnexion à ${url} dans 3s...`);
|
||||
const timer = setTimeout(() => {
|
||||
reconnectTimers.delete(url);
|
||||
connect(url);
|
||||
}, 3000);
|
||||
|
||||
reconnectTimers.set(url, timer);
|
||||
}
|
||||
|
||||
function startHeartbeat() {
|
||||
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
||||
heartbeatInterval = setInterval(() => {
|
||||
// Heartbeat logic can be added here if needed
|
||||
// sendMessage('Ping', '');
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// CLIENT COMMUNICATION
|
||||
// ==========================================
|
||||
|
||||
function respondToClient(client: Client | null, message: any) {
|
||||
if (!client) {
|
||||
console.error('[NetworkSW] Cannot respond: client is null');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[NetworkSW] Sending response: ${message.type} (id: ${message.id})`);
|
||||
client.postMessage(message);
|
||||
} catch (error) {
|
||||
console.error('[NetworkSW] Error sending message to client:', error);
|
||||
// Fallback: try to get the client by ID or use broadcast
|
||||
if (client.id) {
|
||||
(self as unknown as ServiceWorkerGlobalScope).clients.get(client.id).then((c: Client | undefined) => {
|
||||
if (c) c.postMessage(message);
|
||||
}).catch((err: any) => {
|
||||
console.error('[NetworkSW] Failed to get client by ID:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function broadcastToClients(message: any) {
|
||||
const clients = await (self as unknown as ServiceWorkerGlobalScope).clients.matchAll({ includeUncontrolled: true });
|
||||
clients.forEach((client: Client) => {
|
||||
client.postMessage(message);
|
||||
});
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
64
src/services/crypto.service.ts
Normal file
64
src/services/crypto.service.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { MerkleProofResult, ProcessState } from '../../pkg/sdk_client';
|
||||
|
||||
// Type for WasmService proxy (passed from Core Worker)
|
||||
type WasmServiceProxy = {
|
||||
hashValue(fileBlob: { type: string; data: Uint8Array }, commitedIn: string, label: string): Promise<string>;
|
||||
getMerkleProof(processState: any, attributeName: string): Promise<any>;
|
||||
validateMerkleProof(proof: any, hash: string): Promise<boolean>;
|
||||
};
|
||||
|
||||
export class CryptoService {
|
||||
constructor(private wasm: WasmServiceProxy) {}
|
||||
|
||||
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 async getHashForFile(commitedIn: string, label: string, fileBlob: { type: string; data: Uint8Array }): Promise<string> {
|
||||
return await this.wasm.hashValue(fileBlob, commitedIn, label);
|
||||
}
|
||||
|
||||
public async getMerkleProofForFile(processState: ProcessState, attributeName: string): Promise<MerkleProofResult> {
|
||||
return await this.wasm.getMerkleProof(processState, attributeName);
|
||||
}
|
||||
|
||||
public async validateMerkleProof(proof: MerkleProofResult, hash: string): Promise<boolean> {
|
||||
return await this.wasm.validateMerkleProof(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;
|
||||
}
|
||||
}
|
||||
@ -1,417 +1,291 @@
|
||||
import Services from './service';
|
||||
/**
|
||||
* Database service managing IndexedDB operations via Web Worker
|
||||
* Pure Data Store Layer
|
||||
*/
|
||||
export class DatabaseService {
|
||||
// ============================================
|
||||
// PRIVATE PROPERTIES
|
||||
// ============================================
|
||||
|
||||
export class Database {
|
||||
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 static instance: DatabaseService;
|
||||
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
|
||||
// ============================================
|
||||
|
||||
// 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();
|
||||
}
|
||||
return Database.instance;
|
||||
private constructor() {
|
||||
this.initIndexedDBWorker();
|
||||
}
|
||||
|
||||
// Initialize the database
|
||||
private async init(): Promise<void> {
|
||||
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);
|
||||
});
|
||||
public static async getInstance(): Promise<DatabaseService> {
|
||||
if (!DatabaseService.instance) {
|
||||
DatabaseService.instance = new DatabaseService();
|
||||
await DatabaseService.instance.waitForWorkerReady();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
request.onsuccess = async () => {
|
||||
this.db = request.result;
|
||||
await this.initServiceWorker();
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
console.error('Database error:', request.error);
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
return DatabaseService.instance;
|
||||
}
|
||||
|
||||
public async getDb(): Promise<IDBDatabase> {
|
||||
if (!this.db) {
|
||||
await this.init();
|
||||
}
|
||||
return this.db!;
|
||||
}
|
||||
// ============================================
|
||||
// INDEXEDDB WEB WORKER
|
||||
// ============================================
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
await this.checkForUpdates();
|
||||
|
||||
// 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);
|
||||
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));
|
||||
const service = await Services.getInstance();
|
||||
const payload = await service.getMyProcesses();
|
||||
if (payload.length != 0) {
|
||||
activeWorker?.postMessage({ type: 'SCAN', payload });
|
||||
}
|
||||
}, 5000);
|
||||
} catch (error) {
|
||||
console.error('Service Worker registration failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to wait for service worker activation
|
||||
private async waitForServiceWorkerActivation(registration: ServiceWorkerRegistration): Promise<ServiceWorker | null> {
|
||||
return new Promise((resolve) => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async checkForUpdates() {
|
||||
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' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking for service worker updates:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleServiceWorkerMessage(message: any) {
|
||||
switch (message.type) {
|
||||
case 'TO_DOWNLOAD':
|
||||
await this.handleDownloadList(message.data);
|
||||
break;
|
||||
default:
|
||||
console.warn('Unknown message type received from service worker:', message);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleDownloadList(downloadList: string[]): void {
|
||||
// Download the missing data
|
||||
let requestedStateId = [];
|
||||
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;
|
||||
}
|
||||
const processId = diff.process_id;
|
||||
const stateId = diff.state_id;
|
||||
const roles = diff.roles;
|
||||
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);
|
||||
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
|
||||
if (!requestedStateId.includes(stateId)) {
|
||||
await service.requestDataFromPeers(processId, [stateId], [roles]);
|
||||
requestedStateId.push(stateId);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private handleGetObjectResponse = (event: MessageEvent) => {
|
||||
console.log('Received response from service worker (GET_OBJECT):', event.data);
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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],
|
||||
private initIndexedDBWorker(): void {
|
||||
this.indexedDBWorker = new Worker(
|
||||
new URL("../workers/database.worker.ts", import.meta.url),
|
||||
{ type: "module" }
|
||||
);
|
||||
} catch (error) {
|
||||
reject(new Error(`Failed to send message to service worker: ${error}`));
|
||||
|
||||
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("[DatabaseService] 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) => {
|
||||
if (!this.indexedDBWorker) {
|
||||
reject(new Error("IndexedDB Worker not initialized"));
|
||||
return;
|
||||
}
|
||||
|
||||
const id = this.messageIdCounter++;
|
||||
this.pendingMessages.set(id, { resolve, reject });
|
||||
|
||||
this.indexedDBWorker.postMessage({ type, payload, id });
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.pendingMessages.has(id)) {
|
||||
this.pendingMessages.delete(id);
|
||||
reject(new Error(`Worker message timeout for type: ${type}`));
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// GENERIC INDEXEDDB OPERATIONS
|
||||
// ============================================
|
||||
|
||||
public async getStoreList(): Promise<{ [key: string]: string }> {
|
||||
return this.sendMessageToWorker("GET_STORE_LIST", {});
|
||||
}
|
||||
|
||||
public async addObject(payload: {
|
||||
storeName: string;
|
||||
object: any;
|
||||
key: any;
|
||||
}): Promise<void> {
|
||||
await this.sendMessageToWorker("ADD_OBJECT", payload);
|
||||
}
|
||||
|
||||
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);
|
||||
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,
|
||||
});
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
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);
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
getAllRequest.onerror = () => reject(getAllRequest.error);
|
||||
});
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Database;
|
||||
export default DatabaseService;
|
||||
|
||||
410
src/services/iframe-controller.service.ts
Normal file
410
src/services/iframe-controller.service.ts
Normal file
@ -0,0 +1,410 @@
|
||||
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;
|
||||
this.isInitialized = true; // Marquage immédiat pour éviter les doubles appels
|
||||
|
||||
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. Listeners API inactifs."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static async registerAllListeners() {
|
||||
const services = await Services.getInstance();
|
||||
const tokenService = await TokenService.getInstance();
|
||||
|
||||
// --- UTILS ---
|
||||
|
||||
const errorResponse = (
|
||||
errorMsg: string,
|
||||
origin: string,
|
||||
messageId?: string
|
||||
) => {
|
||||
console.error(
|
||||
`[Router:API] 📤 Envoi Erreur: ${errorMsg} (Origine: ${origin})`
|
||||
);
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
error: errorMsg,
|
||||
messageId,
|
||||
},
|
||||
origin // On renvoie à l'origine exacte reçue, même en mode *
|
||||
);
|
||||
};
|
||||
|
||||
const withToken = async (
|
||||
event: MessageEvent,
|
||||
action: () => Promise<void>
|
||||
) => {
|
||||
const { accessToken } = event.data;
|
||||
// Note: Pour la démo, on accepte souvent tout, mais la validation du token reste une bonne pratique
|
||||
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();
|
||||
|
||||
// Si déjà appairé, on accepte immédiatement sans attendre l'événement UI
|
||||
if (device && device.pairing_process_commitment) {
|
||||
console.log("[Router:API] Appareil déjà appairé. Liaison immédiate.");
|
||||
} else {
|
||||
// Sinon, on attend que l'utilisateur clique sur "Lier" dans l'interface (Home.ts)
|
||||
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 (User action missing)"));
|
||||
}, 60000); // Augmenté à 60s pour laisser le temps à l'utilisateur de cliquer
|
||||
|
||||
document.addEventListener(
|
||||
"app:pairing-ready",
|
||||
handler as EventListener
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[Router:API] Génération des tokens de session...`);
|
||||
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
|
||||
);
|
||||
};
|
||||
|
||||
const handleCreatePairing = async (event: MessageEvent) => {
|
||||
console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PAIRING} reçu`);
|
||||
|
||||
if (await services.isPaired()) {
|
||||
throw new Error(
|
||||
"Device already paired — ignoring CREATE_PAIRING request"
|
||||
);
|
||||
}
|
||||
|
||||
await withToken(event, async () => {
|
||||
const myAddress = await services.getDeviceAddress(); // string
|
||||
|
||||
console.log("[Router:API] 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 (invalid IDs)");
|
||||
}
|
||||
|
||||
await services.pairDevice(pairingId, [myAddress]);
|
||||
await services.handleApiReturn(createPairingProcessReturn);
|
||||
|
||||
// Auto-approbation du premier état (PRD)
|
||||
const createPrdUpdateReturn = await services.createPrdUpdate(
|
||||
pairingId,
|
||||
stateId
|
||||
);
|
||||
await services.handleApiReturn(createPrdUpdateReturn);
|
||||
|
||||
const approveChangeReturn = await services.approveChange(
|
||||
pairingId,
|
||||
stateId
|
||||
);
|
||||
await services.handleApiReturn(approveChangeReturn);
|
||||
|
||||
console.log("[Router:API] 🎉 Appairage terminé !");
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.PAIRING_CREATED,
|
||||
pairingId,
|
||||
messageId: event.data.messageId,
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleGetMyProcesses = async (event: MessageEvent) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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");
|
||||
|
||||
const state = await services.getStateFromId(process, stateId);
|
||||
if (!state) throw new Error(`Unknown state ${stateId}`);
|
||||
|
||||
await services.ensureConnections(process, stateId);
|
||||
|
||||
const res: Record<string, any> = {};
|
||||
|
||||
// Déchiffrement des attributs privés
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.DATA_RETRIEVED,
|
||||
data: res,
|
||||
messageId: event.data.messageId,
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleGetPairingId = async (event: MessageEvent) => {
|
||||
// Logique de retry si la DB est lente au démarrage
|
||||
const maxRetries = 10;
|
||||
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;
|
||||
break;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
}
|
||||
|
||||
if (!pairingId) throw new Error("Device not paired (Timeout)");
|
||||
|
||||
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) => {
|
||||
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("Process creation failed");
|
||||
|
||||
const processId = createProcessReturn.updated_process.process_id;
|
||||
const process = createProcessReturn.updated_process.current_process;
|
||||
|
||||
await services.handleApiReturn(createProcessReturn);
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.PROCESS_CREATED,
|
||||
processCreated: { processId, process, processData },
|
||||
messageId: event.data.messageId,
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// --- MAIN MESSAGE DISPATCHER ---
|
||||
|
||||
async function handleMessage(event: MessageEvent) {
|
||||
// 🛡️ SÉCURITÉ : Ignorer les messages qui ne sont pas des objets (ex: extensions Chrome)
|
||||
if (!event.data || typeof event.data !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
// 🛡️ FILTRAGE : On ne traite que les messages avec un 'type' connu
|
||||
if (!event.data.type) return;
|
||||
|
||||
try {
|
||||
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.GET_PAIRING_ID:
|
||||
await handleGetPairingId(event);
|
||||
break;
|
||||
case MessageType.CREATE_PROCESS:
|
||||
await handleCreateProcess(event);
|
||||
break;
|
||||
|
||||
// Cas simples (tokens, notifications...)
|
||||
case MessageType.VALIDATE_TOKEN:
|
||||
const { accessToken, refreshToken } = event.data;
|
||||
if (!accessToken || !refreshToken)
|
||||
throw new Error("Tokens missing");
|
||||
const isValid = await tokenService.validateToken(
|
||||
accessToken,
|
||||
event.origin
|
||||
);
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.VALIDATE_TOKEN,
|
||||
isValid,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
messageId: event.data.messageId,
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
break;
|
||||
|
||||
case MessageType.RENEW_TOKEN:
|
||||
if (!event.data.refreshToken) throw new Error("No refresh token");
|
||||
const newAccess = await tokenService.refreshAccessToken(
|
||||
event.data.refreshToken,
|
||||
event.origin
|
||||
);
|
||||
if (!newAccess) throw new Error("Refresh failed");
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.RENEW_TOKEN,
|
||||
accessToken: newAccess,
|
||||
refreshToken: event.data.refreshToken,
|
||||
messageId: event.data.messageId,
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// Silence sur les types inconnus (peut être un autre protocole)
|
||||
break;
|
||||
}
|
||||
} catch (error: any) {
|
||||
// En cas d'erreur métier, on prévient le parent
|
||||
errorResponse(
|
||||
String(error.message || error),
|
||||
event.origin,
|
||||
event.data?.messageId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Ajout du listener unique
|
||||
window.addEventListener("message", handleMessage);
|
||||
|
||||
// Handshake final : "Je suis prêt"
|
||||
window.parent.postMessage({ type: MessageType.LISTENING }, "*");
|
||||
console.log("[Router:API] ✅ Listeners actifs. Handshake envoyé.");
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
}
|
||||
}
|
||||
316
src/services/network.service.ts
Normal file
316
src/services/network.service.ts
Normal file
@ -0,0 +1,316 @@
|
||||
import Services from "./service";
|
||||
|
||||
interface ServiceWorkerMessage {
|
||||
type: string;
|
||||
payload?: any;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export class NetworkService {
|
||||
private serviceWorkerRegistration: ServiceWorkerRegistration | null = null;
|
||||
private messageIdCounter: number = 0;
|
||||
private pendingMessages: Map<string, { resolve: (value: any) => void; reject: (error: any) => void }> = new Map();
|
||||
|
||||
// Relay ready promise mechanism (waits for first relay to become available)
|
||||
private relayReadyResolver: ((addr: string) => void) | null = null;
|
||||
private relayReadyPromise: Promise<string> | null = null;
|
||||
|
||||
constructor(private bootstrapUrls: string[]) {
|
||||
this.setupMessageListener();
|
||||
}
|
||||
|
||||
public async initRelays() {
|
||||
try {
|
||||
// Register Service Worker
|
||||
console.log("[NetworkService] Registering Service Worker...");
|
||||
await this.registerServiceWorker();
|
||||
|
||||
// Wait for Service Worker to be ready
|
||||
console.log("[NetworkService] Waiting for Service Worker to be ready...");
|
||||
await this.waitForServiceWorkerReady();
|
||||
console.log("[NetworkService] Service Worker is ready");
|
||||
|
||||
// Connect to bootstrap URLs
|
||||
console.log("[NetworkService] Connecting to bootstrap URLs...");
|
||||
for (const url of this.bootstrapUrls) {
|
||||
await this.addWebsocketConnection(url);
|
||||
}
|
||||
console.log("[NetworkService] Initialization complete");
|
||||
} catch (error) {
|
||||
console.error("[NetworkService] Initialization failed:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async addWebsocketConnection(url: string) {
|
||||
await this.sendToServiceWorker({ type: 'CONNECT', payload: { url } });
|
||||
}
|
||||
|
||||
public async connectAllRelays() {
|
||||
for (const url of this.bootstrapUrls) {
|
||||
this.addWebsocketConnection(url);
|
||||
}
|
||||
}
|
||||
|
||||
public async sendMessage(flag: string, content: string) {
|
||||
await this.sendToServiceWorker({
|
||||
type: 'SEND_MESSAGE',
|
||||
payload: { flag, content }
|
||||
});
|
||||
}
|
||||
|
||||
// Called by onStatusChange when a relay becomes available
|
||||
// Triggers the relay ready promise if someone is waiting
|
||||
public updateRelay(url: string, spAddress: string) {
|
||||
// Trigger relay ready promise if someone is waiting
|
||||
if (spAddress && spAddress !== "" && this.relayReadyResolver) {
|
||||
this.relayReadyResolver(spAddress);
|
||||
this.relayReadyResolver = null;
|
||||
this.relayReadyPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
public async getAllRelays(): Promise<Record<string, string>> {
|
||||
const response = await this.sendToServiceWorker({ type: 'GET_ALL_RELAYS' });
|
||||
return response?.relays || {};
|
||||
}
|
||||
|
||||
public async getAvailableRelayAddress(): Promise<string> {
|
||||
// 1. Query Service Worker first (fast path if relay already available)
|
||||
const response = await this.sendToServiceWorker({ type: 'GET_AVAILABLE_RELAY' });
|
||||
if (response?.relay) return response.relay;
|
||||
|
||||
// 2. If no relay yet, wait for one to become available
|
||||
if (!this.relayReadyPromise) {
|
||||
console.log("[NetworkService] ⏳ Waiting for relay Handshake...");
|
||||
this.relayReadyPromise = new Promise<string>((resolve, reject) => {
|
||||
this.relayReadyResolver = resolve;
|
||||
|
||||
// Timeout after 10s to avoid blocking indefinitely
|
||||
setTimeout(() => {
|
||||
if (this.relayReadyResolver) {
|
||||
reject(new Error("Timeout: No relay received after 10s"));
|
||||
this.relayReadyResolver = null;
|
||||
this.relayReadyPromise = null;
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
return this.relayReadyPromise;
|
||||
}
|
||||
|
||||
// --- INTERNES ---
|
||||
|
||||
private async registerServiceWorker(): Promise<void> {
|
||||
if (!("serviceWorker" in navigator)) {
|
||||
throw new Error("Service Workers are not supported");
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if already registered
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
const existing = registrations.find((r) => {
|
||||
const url = r.active?.scriptURL || r.installing?.scriptURL || r.waiting?.scriptURL;
|
||||
return url && url.includes("network.sw.js");
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
console.log("[NetworkService] Found existing Service Worker registration");
|
||||
this.serviceWorkerRegistration = existing;
|
||||
|
||||
// Listen for controller change in case it activates
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
console.log("[NetworkService] Service Worker controller changed");
|
||||
});
|
||||
|
||||
// Try to update
|
||||
try {
|
||||
await existing.update();
|
||||
} catch (updateError) {
|
||||
console.warn("[NetworkService] Service Worker update failed:", updateError);
|
||||
}
|
||||
} else {
|
||||
// Register new Service Worker
|
||||
console.log("[NetworkService] Registering new Service Worker at /network.sw.js");
|
||||
this.serviceWorkerRegistration = await navigator.serviceWorker.register(
|
||||
"/network.sw.js",
|
||||
{ type: "module", scope: "/" }
|
||||
);
|
||||
console.log("[NetworkService] Service Worker registered:", this.serviceWorkerRegistration);
|
||||
}
|
||||
|
||||
// Listen for registration errors
|
||||
this.serviceWorkerRegistration.addEventListener('error', (event) => {
|
||||
console.error("[NetworkService] Service Worker error:", event);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[NetworkService] Failed to register Service Worker:", error);
|
||||
// Check if it's a 404 error
|
||||
if (error instanceof Error && error.message.includes('404')) {
|
||||
throw new Error("Service Worker file not found at /network.sw.js. Check Vite configuration.");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async waitForServiceWorkerReady(): Promise<void> {
|
||||
if (!this.serviceWorkerRegistration) {
|
||||
throw new Error("Service Worker registration is null");
|
||||
}
|
||||
|
||||
// Wait for the Service Worker to be ready (installed and activated)
|
||||
if (this.serviceWorkerRegistration.installing) {
|
||||
const installing = this.serviceWorkerRegistration.installing;
|
||||
await new Promise<void>((resolve) => {
|
||||
installing.addEventListener('statechange', () => {
|
||||
if (installing.state === 'installed') {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Also ensure it's active
|
||||
if (this.serviceWorkerRegistration.active) {
|
||||
// Wait a bit for it to become the controller
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error("Service Worker activation timeout after 10 seconds"));
|
||||
}, 10000);
|
||||
|
||||
const checkState = () => {
|
||||
if (this.serviceWorkerRegistration?.active) {
|
||||
clearTimeout(timeout);
|
||||
// Wait a bit for it to become the controller
|
||||
setTimeout(resolve, 100);
|
||||
} else if (this.serviceWorkerRegistration?.installing) {
|
||||
// Service Worker is installing, wait for it
|
||||
this.serviceWorkerRegistration.installing.addEventListener('statechange', () => {
|
||||
if (this.serviceWorkerRegistration?.active) {
|
||||
clearTimeout(timeout);
|
||||
setTimeout(resolve, 100);
|
||||
}
|
||||
});
|
||||
} else if (this.serviceWorkerRegistration?.waiting) {
|
||||
// Service Worker is waiting, skip waiting
|
||||
this.serviceWorkerRegistration.waiting.postMessage({ type: 'SKIP_WAITING' });
|
||||
setTimeout(checkState, 100);
|
||||
} else {
|
||||
setTimeout(checkState, 100);
|
||||
}
|
||||
};
|
||||
checkState();
|
||||
});
|
||||
}
|
||||
|
||||
private setupMessageListener(): void {
|
||||
if (!navigator.serviceWorker) {
|
||||
console.warn("[NetworkService] Service Workers not supported, message listener not set up");
|
||||
return;
|
||||
}
|
||||
|
||||
const messageHandler = (event: MessageEvent<ServiceWorkerMessage>) => {
|
||||
const { type, payload, id } = event.data;
|
||||
console.log(`[NetworkService] Received message from SW: ${type} (id: ${id})`);
|
||||
|
||||
// Handle response messages
|
||||
if (id && this.pendingMessages.has(id)) {
|
||||
const { resolve, reject } = this.pendingMessages.get(id)!;
|
||||
this.pendingMessages.delete(id);
|
||||
|
||||
if (type === "ERROR") {
|
||||
reject(new Error(payload?.error || "Unknown error"));
|
||||
} else {
|
||||
resolve(payload);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle event messages (not responses to requests)
|
||||
switch (type) {
|
||||
case "MESSAGE_RECEIVED":
|
||||
this.onMessageReceived(payload.flag, payload.content, payload.url);
|
||||
break;
|
||||
case "STATUS_CHANGE":
|
||||
this.onStatusChange(payload.url, payload.status, payload.spAddress);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Listen on both the serviceWorker and controller
|
||||
navigator.serviceWorker.addEventListener("message", messageHandler as EventListener);
|
||||
|
||||
// Also listen on the controller if it exists
|
||||
if (navigator.serviceWorker.controller) {
|
||||
navigator.serviceWorker.controller.addEventListener("message", messageHandler as EventListener);
|
||||
}
|
||||
|
||||
// Listen for controller changes
|
||||
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
||||
console.log("[NetworkService] Service Worker controller changed");
|
||||
if (navigator.serviceWorker.controller) {
|
||||
navigator.serviceWorker.controller.addEventListener("message", messageHandler as EventListener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async sendToServiceWorker(message: ServiceWorkerMessage): Promise<any> {
|
||||
if (!this.serviceWorkerRegistration) {
|
||||
throw new Error("Service Worker is not registered");
|
||||
}
|
||||
|
||||
// Use the controller if available, otherwise use active
|
||||
const target = navigator.serviceWorker.controller || this.serviceWorkerRegistration.active;
|
||||
|
||||
if (!target) {
|
||||
throw new Error(`Service Worker is not active. State: ${this.serviceWorkerRegistration.installing ? 'installing' : this.serviceWorkerRegistration.waiting ? 'waiting' : 'unknown'}`);
|
||||
}
|
||||
|
||||
const id = `msg_${++this.messageIdCounter}`;
|
||||
message.id = id;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pendingMessages.set(id, { resolve, reject });
|
||||
|
||||
// Timeout after 10 seconds (reduced from 30)
|
||||
const timeout = setTimeout(() => {
|
||||
if (this.pendingMessages.has(id)) {
|
||||
this.pendingMessages.delete(id);
|
||||
reject(new Error(`Service Worker message timeout after 10s. Message type: ${message.type}`));
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
try {
|
||||
target.postMessage(message);
|
||||
console.log(`[NetworkService] Sent message to SW via ${target === navigator.serviceWorker.controller ? 'controller' : 'active'}: ${message.type} (id: ${id})`);
|
||||
} catch (error) {
|
||||
clearTimeout(timeout);
|
||||
this.pendingMessages.delete(id);
|
||||
reject(new Error(`Failed to send message to Service Worker: ${error instanceof Error ? error.message : String(error)}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
// Trigger relay ready promise if someone is waiting
|
||||
this.updateRelay(url, spAddress);
|
||||
}
|
||||
// Note: Service worker is the source of truth for relay state
|
||||
// We don't need to track CLOSED here since we query the SW directly
|
||||
}
|
||||
}
|
||||
101
src/services/process.service.ts
Normal file
101
src/services/process.service.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { Process, ProcessState, RoleDefinition } from '../../pkg/sdk_client';
|
||||
|
||||
// Type for Database proxy (passed from Core Worker)
|
||||
type DatabaseServiceProxy = {
|
||||
getProcess(processId: string): Promise<Process | null>;
|
||||
getAllProcesses(): Promise<Record<string, Process>>;
|
||||
saveProcess(processId: string, process: Process): Promise<void>;
|
||||
saveProcessesBatch(processes: Record<string, Process>): Promise<void>;
|
||||
};
|
||||
|
||||
const EMPTY32BYTES = String('').padStart(64, '0');
|
||||
|
||||
export class ProcessService {
|
||||
private processesCache: Record<string, Process> = {};
|
||||
private myProcesses: Set<string> = new Set();
|
||||
|
||||
constructor(private db: any) {}
|
||||
|
||||
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: ProcessState) => 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: ProcessState) => state.commited_in === processTip).filter((state: ProcessState) => state.state_id !== EMPTY32BYTES);
|
||||
}
|
||||
|
||||
public getStateFromId(process: Process, stateId: string): ProcessState | null {
|
||||
return process.states.find((state: ProcessState) => 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;
|
||||
}
|
||||
}
|
||||
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);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
// Status 200-299
|
||||
console.log("Data stored successfully:", key);
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error?.response?.status === 409) {
|
||||
} else if (response.status === 409) {
|
||||
// Conflit (déjà existant), on retourne null comme avant
|
||||
return null;
|
||||
}
|
||||
console.error('Error storing data:', error);
|
||||
}
|
||||
}
|
||||
return 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);
|
||||
} else {
|
||||
console.error("Received response status", response.status);
|
||||
continue;
|
||||
}
|
||||
// console.log('Retrieved data:', response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error retrieving data:', error);
|
||||
console.error("Error storing data:", error);
|
||||
}
|
||||
}
|
||||
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 = {};
|
||||
export async function retrieveData(
|
||||
servers: string[],
|
||||
key: string
|
||||
): Promise<ArrayBuffer | null> {
|
||||
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}`);
|
||||
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;
|
||||
}
|
||||
|
||||
const data: TestResponse = response.data;
|
||||
|
||||
res[server] = data.value;
|
||||
} catch (error) {
|
||||
console.error('Error retrieving data:', error);
|
||||
console.error(`Unexpected error retrieving data from ${server}:`, error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
134
src/services/wallet.service.ts
Normal file
134
src/services/wallet.service.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import { Device } from '../../pkg/sdk_client';
|
||||
import Database from './database.service';
|
||||
|
||||
// Type for WasmService proxy (passed from Core Worker)
|
||||
type WasmServiceProxy = {
|
||||
isPaired(): Promise<boolean>;
|
||||
getAvailableAmount(): Promise<BigInt>;
|
||||
getAddress(): Promise<string>;
|
||||
getPairingProcessId(): Promise<string>;
|
||||
createNewDevice(birthday: number, network: string): Promise<string>;
|
||||
dumpDevice(): Promise<any>;
|
||||
dumpNeuteredDevice(): Promise<any>;
|
||||
dumpWallet(): Promise<any>;
|
||||
restoreDevice(device: any): Promise<void>;
|
||||
pairDevice(processId: string, spAddresses: string[]): Promise<void>;
|
||||
unpairDevice(): Promise<void>;
|
||||
};
|
||||
|
||||
type DatabaseServiceProxy = {
|
||||
getStoreList(): Promise<{ [key: string]: string }>;
|
||||
addObject(payload: { storeName: string; object: any; key: any }): Promise<void>;
|
||||
batchWriting(payload: { storeName: string; objects: { key: any; object: any }[] }): Promise<void>;
|
||||
getObject(storeName: string, key: string): Promise<any | null>;
|
||||
dumpStore(storeName: string): Promise<Record<string, any>>;
|
||||
deleteObject(storeName: string, key: string): Promise<void>;
|
||||
clearStore(storeName: string): Promise<void>;
|
||||
requestStoreByIndex(storeName: string, indexName: string, request: string): Promise<any[]>;
|
||||
clearMultipleStores(storeNames: string[]): Promise<void>;
|
||||
saveDevice(device: any): Promise<void>;
|
||||
getDevice(): Promise<any | null>;
|
||||
saveProcess(processId: string, process: any): Promise<void>;
|
||||
saveProcessesBatch(processes: Record<string, any>): Promise<void>;
|
||||
getProcess(processId: string): Promise<any | null>;
|
||||
getAllProcesses(): Promise<Record<string, any>>;
|
||||
saveBlob(hash: string, data: Blob): Promise<void>;
|
||||
getBlob(hash: string): Promise<Blob | null>;
|
||||
saveDiffs(diffs: any[]): Promise<void>;
|
||||
getDiff(hash: string): Promise<any | null>;
|
||||
getAllDiffs(): Promise<Record<string, any>>;
|
||||
getSharedSecret(address: string): Promise<string | null>;
|
||||
saveSecretsBatch(unconfirmedSecrets: any[], sharedSecrets: { key: string; value: any }[]): Promise<void>;
|
||||
getAllSecrets(): Promise<{ shared_secrets: Record<string, any>; unconfirmed_secrets: any[] }>;
|
||||
};
|
||||
|
||||
|
||||
export class WalletService {
|
||||
constructor(private wasm: WasmServiceProxy, private db: DatabaseServiceProxy) {}
|
||||
|
||||
public async isPaired(): Promise<boolean> {
|
||||
try {
|
||||
return await this.wasm.isPaired();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async getAmount(): Promise<BigInt> {
|
||||
return await this.wasm.getAvailableAmount();
|
||||
}
|
||||
|
||||
public async getDeviceAddress(): Promise<string> {
|
||||
return await this.wasm.getAddress();
|
||||
}
|
||||
|
||||
public async getPairingProcessId(): Promise<string> {
|
||||
return await this.wasm.getPairingProcessId();
|
||||
}
|
||||
|
||||
public async createNewDevice(chainTip: number): Promise<string> {
|
||||
const spAddress = await this.wasm.createNewDevice(0, 'signet');
|
||||
const device = await this.dumpDeviceFromMemory();
|
||||
if (device.sp_wallet.birthday === 0) {
|
||||
device.sp_wallet.birthday = chainTip;
|
||||
device.sp_wallet.last_scan = chainTip;
|
||||
await this.wasm.restoreDevice(device);
|
||||
}
|
||||
await this.saveDeviceInDatabase(device);
|
||||
return spAddress;
|
||||
}
|
||||
|
||||
public async dumpDeviceFromMemory(): Promise<Device> {
|
||||
return await this.wasm.dumpDevice();
|
||||
}
|
||||
|
||||
public async dumpNeuteredDevice(): Promise<Device | null> {
|
||||
try {
|
||||
return await this.wasm.dumpNeuteredDevice();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async dumpWallet(): Promise<any> {
|
||||
return await this.wasm.dumpWallet();
|
||||
}
|
||||
|
||||
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 async restoreDevice(device: Device): Promise<void> {
|
||||
await this.wasm.restoreDevice(device);
|
||||
}
|
||||
|
||||
public async pairDevice(processId: string, spAddressList: string[]): Promise<void> {
|
||||
await this.wasm.pairDevice(processId, spAddressList);
|
||||
}
|
||||
|
||||
public async unpairDevice(): Promise<void> {
|
||||
await this.wasm.unpairDevice();
|
||||
const newDevice = await this.dumpDeviceFromMemory();
|
||||
await this.saveDeviceInDatabase(newDevice);
|
||||
}
|
||||
}
|
||||
428
src/services/wasm.service.ts
Normal file
428
src/services/wasm.service.ts
Normal file
@ -0,0 +1,428 @@
|
||||
/**
|
||||
* WASM Service - Manages communication with WASM Worker
|
||||
* Similar to Database Service, but for WASM operations
|
||||
*/
|
||||
export class WasmService {
|
||||
private static instance: WasmService;
|
||||
private wasmWorker: Worker | null = null;
|
||||
private messageIdCounter: number = 0;
|
||||
private pendingMessages: Map<
|
||||
string,
|
||||
{ resolve: (value: any) => void; reject: (error: any) => void }
|
||||
> = new Map();
|
||||
|
||||
// ============================================
|
||||
// INITIALIZATION & SINGLETON
|
||||
// ============================================
|
||||
|
||||
private constructor() {
|
||||
this.initWasmWorker();
|
||||
}
|
||||
|
||||
public static async getInstance(): Promise<WasmService> {
|
||||
if (!WasmService.instance) {
|
||||
WasmService.instance = new WasmService();
|
||||
await WasmService.instance.waitForWorkerReady();
|
||||
}
|
||||
return WasmService.instance;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// WASM WORKER MANAGEMENT
|
||||
// ============================================
|
||||
|
||||
private initWasmWorker(): void {
|
||||
this.wasmWorker = new Worker(
|
||||
new URL("../workers/wasm.worker.ts", import.meta.url),
|
||||
{ type: "module" }
|
||||
);
|
||||
|
||||
this.wasmWorker.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.wasmWorker.onerror = (error) => {
|
||||
console.error("[WasmService] WASM 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) => {
|
||||
if (!this.wasmWorker) {
|
||||
reject(new Error("WASM Worker not initialized"));
|
||||
return;
|
||||
}
|
||||
|
||||
const id = `wasm_${++this.messageIdCounter}`;
|
||||
this.pendingMessages.set(id, { resolve, reject });
|
||||
|
||||
this.wasmWorker.postMessage({
|
||||
id,
|
||||
type,
|
||||
...payload,
|
||||
});
|
||||
|
||||
// Timeout after 30 seconds
|
||||
setTimeout(() => {
|
||||
if (this.pendingMessages.has(id)) {
|
||||
this.pendingMessages.delete(id);
|
||||
reject(new Error(`WASM Worker message timeout for type: ${type}`));
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// WASM METHOD CALLS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Generic method to call any WASM function
|
||||
*/
|
||||
public async callMethod(method: string, ...args: any[]): Promise<any> {
|
||||
return this.sendMessageToWorker("WASM_METHOD", {
|
||||
method,
|
||||
args,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// WS MESSAGE PROCESSING METHODS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Parse cipher message from WebSocket
|
||||
*/
|
||||
public async parseCipher(
|
||||
msg: string,
|
||||
membersList: any,
|
||||
processes: any
|
||||
): Promise<any> {
|
||||
return this.callMethod("parse_cipher", msg, membersList, processes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse new transaction message from WebSocket
|
||||
*/
|
||||
public async parseNewTx(
|
||||
msg: string,
|
||||
blockHeight: number,
|
||||
membersList: any
|
||||
): Promise<any> {
|
||||
return this.callMethod("parse_new_tx", msg, blockHeight, membersList);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TRANSACTION METHODS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Create a transaction
|
||||
*/
|
||||
public async createTransaction(
|
||||
addresses: string[],
|
||||
feeRate: number
|
||||
): Promise<any> {
|
||||
return this.callMethod("create_transaction", addresses, feeRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign a partial transaction
|
||||
*/
|
||||
public async signTransaction(partialTx: any): Promise<any> {
|
||||
return this.callMethod("sign_transaction", partialTx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transaction ID
|
||||
*/
|
||||
public async getTxid(transaction: string): Promise<string> {
|
||||
return this.callMethod("get_txid", transaction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OP_RETURN from transaction
|
||||
*/
|
||||
public async getOpReturn(transaction: string): Promise<string> {
|
||||
return this.callMethod("get_opreturn", transaction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get prevouts from transaction
|
||||
*/
|
||||
public async getPrevouts(transaction: string): Promise<string[]> {
|
||||
return this.callMethod("get_prevouts", transaction);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PROCESS MANIPULATION METHODS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Create a new process
|
||||
*/
|
||||
public async createProcess(
|
||||
privateData: any,
|
||||
publicData: any,
|
||||
roles: any,
|
||||
membersList: any
|
||||
): Promise<any> {
|
||||
return this.callMethod(
|
||||
"create_process",
|
||||
privateData,
|
||||
publicData,
|
||||
roles,
|
||||
membersList
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a process
|
||||
*/
|
||||
public async updateProcess(
|
||||
process: any,
|
||||
privateData: any,
|
||||
publicData: any,
|
||||
roles: any,
|
||||
membersList: any
|
||||
): Promise<any> {
|
||||
return this.callMethod(
|
||||
"update_process",
|
||||
process,
|
||||
privateData,
|
||||
publicData,
|
||||
roles,
|
||||
membersList
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create update message
|
||||
*/
|
||||
public async createUpdateMessage(
|
||||
process: any,
|
||||
stateId: string,
|
||||
membersList: any
|
||||
): Promise<any> {
|
||||
return this.callMethod("create_update_message", process, stateId, membersList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create PRD response
|
||||
*/
|
||||
public async createPrdResponse(
|
||||
process: any,
|
||||
stateId: string,
|
||||
membersList: any
|
||||
): Promise<any> {
|
||||
return this.callMethod("create_response_prd", process, stateId, membersList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate state
|
||||
*/
|
||||
public async validateState(
|
||||
process: any,
|
||||
stateId: string,
|
||||
membersList: any
|
||||
): Promise<any> {
|
||||
return this.callMethod("validate_state", process, stateId, membersList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refuse state
|
||||
*/
|
||||
public async refuseState(process: any, stateId: string): Promise<any> {
|
||||
return this.callMethod("refuse_state", process, stateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request data from peers
|
||||
*/
|
||||
public async requestData(
|
||||
processId: string,
|
||||
stateIds: string[],
|
||||
roles: any,
|
||||
membersList: any
|
||||
): Promise<any> {
|
||||
return this.callMethod(
|
||||
"request_data",
|
||||
processId,
|
||||
stateIds,
|
||||
roles,
|
||||
membersList
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process commit new state
|
||||
*/
|
||||
public async processCommitNewState(
|
||||
process: any,
|
||||
newStateId: string,
|
||||
newTip: string
|
||||
): Promise<void> {
|
||||
return this.callMethod("process_commit_new_state", process, newStateId, newTip);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ENCODING/DECODING METHODS
|
||||
// ============================================
|
||||
|
||||
public async encodeJson(data: any): Promise<any> {
|
||||
return this.callMethod("encode_json", data);
|
||||
}
|
||||
|
||||
public async encodeBinary(data: any): Promise<any> {
|
||||
return this.callMethod("encode_binary", data);
|
||||
}
|
||||
|
||||
public async decodeValue(value: number[]): Promise<any> {
|
||||
return this.callMethod("decode_value", value);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// WALLET METHODS
|
||||
// ============================================
|
||||
|
||||
public async isPaired(): Promise<boolean> {
|
||||
return this.callMethod("is_paired");
|
||||
}
|
||||
|
||||
public async getAvailableAmount(): Promise<BigInt> {
|
||||
return this.callMethod("get_available_amount");
|
||||
}
|
||||
|
||||
public async getAddress(): Promise<string> {
|
||||
return this.callMethod("get_address");
|
||||
}
|
||||
|
||||
public async getPairingProcessId(): Promise<string> {
|
||||
return this.callMethod("get_pairing_process_id");
|
||||
}
|
||||
|
||||
public async createNewDevice(birthday: number, network: string): Promise<string> {
|
||||
return this.callMethod("create_new_device", birthday, network);
|
||||
}
|
||||
|
||||
public async dumpDevice(): Promise<any> {
|
||||
return this.callMethod("dump_device");
|
||||
}
|
||||
|
||||
public async dumpNeuteredDevice(): Promise<any> {
|
||||
return this.callMethod("dump_neutered_device");
|
||||
}
|
||||
|
||||
public async dumpWallet(): Promise<any> {
|
||||
return this.callMethod("dump_wallet");
|
||||
}
|
||||
|
||||
public async restoreDevice(device: any): Promise<void> {
|
||||
return this.callMethod("restore_device", device);
|
||||
}
|
||||
|
||||
public async pairDevice(processId: string, spAddresses: string[]): Promise<void> {
|
||||
return this.callMethod("pair_device", processId, spAddresses);
|
||||
}
|
||||
|
||||
public async unpairDevice(): Promise<void> {
|
||||
return this.callMethod("unpair_device");
|
||||
}
|
||||
|
||||
public async resetDevice(): Promise<void> {
|
||||
return this.callMethod("reset_device");
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CRYPTO METHODS
|
||||
// ============================================
|
||||
|
||||
public async hashValue(
|
||||
fileBlob: { type: string; data: Uint8Array },
|
||||
commitedIn: string,
|
||||
label: string
|
||||
): Promise<string> {
|
||||
return this.callMethod("hash_value", fileBlob, commitedIn, label);
|
||||
}
|
||||
|
||||
public async getMerkleProof(processState: any, attributeName: string): Promise<any> {
|
||||
return this.callMethod("get_merkle_proof", processState, attributeName);
|
||||
}
|
||||
|
||||
public async validateMerkleProof(proof: any, hash: string): Promise<boolean> {
|
||||
return this.callMethod("validate_merkle_proof", proof, hash);
|
||||
}
|
||||
|
||||
public async decryptData(key: Uint8Array, data: Uint8Array): Promise<Uint8Array> {
|
||||
return this.callMethod("decrypt_data", key, data);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PROCESS CREATION (with relay and fee)
|
||||
// ============================================
|
||||
|
||||
public async createNewProcess(
|
||||
privateData: any,
|
||||
roles: any,
|
||||
publicData: any,
|
||||
relayAddress: string,
|
||||
feeRate: number,
|
||||
membersList: any
|
||||
): Promise<any> {
|
||||
return this.callMethod(
|
||||
"create_new_process",
|
||||
privateData,
|
||||
roles,
|
||||
publicData,
|
||||
relayAddress,
|
||||
feeRate,
|
||||
membersList
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SECRETS MANAGEMENT
|
||||
// ============================================
|
||||
|
||||
public async setSharedSecrets(secretsJson: string): Promise<void> {
|
||||
return this.callMethod("set_shared_secrets", secretsJson);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// BLOCKCHAIN SCANNING
|
||||
// ============================================
|
||||
|
||||
public async scanBlocks(tipHeight: number, blindbitUrl: string): Promise<void> {
|
||||
return this.callMethod("scan_blocks", tipHeight, blindbitUrl);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// UTILITY METHODS
|
||||
// ============================================
|
||||
|
||||
public async createFaucetMessage(): Promise<string> {
|
||||
return this.callMethod("create_faucet_msg");
|
||||
}
|
||||
|
||||
public async isChildRole(parent: any, child: any): Promise<boolean> {
|
||||
return this.callMethod("is_child_role", JSON.stringify(parent), JSON.stringify(child));
|
||||
}
|
||||
}
|
||||
|
||||
export default WasmService;
|
||||
|
||||
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