Compare commits
No commits in common. "service-database" and "create-account" have entirely different histories.
service-da
...
create-acc
3
.env
Normal file
3
.env
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# .env
|
||||||
|
VITE_API_URL=https://api.example.com
|
||||||
|
VITE_API_KEY=your_api_key
|
||||||
@ -1,7 +0,0 @@
|
|||||||
VITE_API_URL=https://api.example.com
|
|
||||||
VITE_API_KEY=your_api_key
|
|
||||||
VITE_JWT_SECRET_KEY=your_secret_key
|
|
||||||
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
44
.github/workflows/dev.yml
vendored
@ -1,44 +0,0 @@
|
|||||||
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 }}
|
|
||||||
100
.gitignore
vendored
100
.gitignore
vendored
@ -1,103 +1,7 @@
|
|||||||
# ----------------------------
|
|
||||||
# 🦀 Rust
|
|
||||||
# ----------------------------
|
|
||||||
target/
|
target/
|
||||||
pkg/
|
pkg/
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
*.rs.bk
|
|
||||||
**/*.rlib
|
|
||||||
|
|
||||||
# ----------------------------
|
|
||||||
# 🧰 Node / Frontend
|
|
||||||
# ----------------------------
|
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
build/
|
.vscode
|
||||||
.cache/
|
public/ssl/
|
||||||
.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/
|
|
||||||
74
Dockerfile
74
Dockerfile
@ -1,61 +1,13 @@
|
|||||||
# syntax=docker/dockerfile:1.4
|
FROM node:20
|
||||||
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 \
|
# use this user because he have uid et gid 1000 like theradia
|
||||||
git \
|
USER node
|
||||||
openssh-client \
|
|
||||||
curl \
|
WORKDIR /app
|
||||||
nodejs \
|
|
||||||
npm \
|
CMD ["npm", "start"]
|
||||||
build-base \
|
# "--disable-host-check", "--host", "0.0.0.0", "--ssl", "--ssl-cert", "/ssl/certs/site.crt", "--ssl-key", "/ssl/private/site.dec.key"]
|
||||||
pkgconfig \
|
|
||||||
clang \
|
|
||||||
llvm \
|
|
||||||
musl-dev \
|
|
||||||
nginx
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# 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"]
|
|
||||||
|
|
||||||
|
|||||||
4
doc/BDD_ihm.drawio.svg
Normal file
4
doc/BDD_ihm.drawio.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 1.9 MiB |
38
index.html
38
index.html
@ -1,20 +1,26 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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>
|
||||||
<title>4NK Application</title>
|
<title>4NK Application</title>
|
||||||
<link rel="stylesheet" href="/src/assets/styles/style.css" />
|
</head>
|
||||||
</head>
|
<body>
|
||||||
<body>
|
<div id="header-container"></div>
|
||||||
<app-layout>
|
<div id="containerId" class="container">
|
||||||
|
<!-- 4NK Web5 Solution -->
|
||||||
<div id="header-slot" slot="header"></div>
|
</div>
|
||||||
|
<!-- <script type="module" src="/src/index.ts"></script> -->
|
||||||
<div id="app-container" slot="content" class="container"></div>
|
<script type="module">
|
||||||
|
import { init } from '/src/router.ts';
|
||||||
</app-layout>
|
(async () => {
|
||||||
|
await init();
|
||||||
<script type="module" src="/src/main.ts"></script>
|
})();
|
||||||
</body>
|
</script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -1,48 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,99 +0,0 @@
|
|||||||
# --- 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
Executable file
7185
package-lock.json
generated
Executable file
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@ -1,17 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "sdk_client",
|
"name": "sdk_client",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"build_wasm": "wasm-pack build --out-dir ../ihm_client_dev2/pkg ../sdk_client --target bundler --dev",
|
"build_wasm": "wasm-pack build --out-dir ../ihm_client_dev1/pkg ../sdk_client --target bundler --dev",
|
||||||
"start": "vite --host 0.0.0.0",
|
"start": "vite --host 0.0.0.0",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"deploy": "sudo cp -r dist/* /var/www/html/",
|
"deploy": "sudo cp -r dist/* /var/www/html/",
|
||||||
"prettify": "prettier --config ./.prettierrc --write \"src/**/*{.ts,.html,.css,.js}\"",
|
"prettify": "prettier --config ./.prettierrc --write \"src/**/*{.ts,.html,.css,.js}\""
|
||||||
"build:dist": "tsc -p tsconfig.build.json"
|
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@ -32,15 +30,11 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/elements": "^19.0.1",
|
"@angular/elements": "^19.0.1",
|
||||||
"@types/jsonwebtoken": "^9.0.9",
|
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
"axios": "^1.7.8",
|
"axios": "^1.7.8",
|
||||||
"html5-qrcode": "^2.3.8",
|
"html5-qrcode": "^2.3.8",
|
||||||
"jose": "^6.0.11",
|
|
||||||
"jsonwebtoken": "^9.0.2",
|
|
||||||
"pdf-lib": "^1.17.1",
|
|
||||||
"qr-scanner": "^1.4.2",
|
"qr-scanner": "^1.4.2",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"sweetalert2": "^11.14.5",
|
"sweetalert2": "^11.14.5",
|
||||||
|
|||||||
BIN
public/assets/4nk_image.png
Executable file
BIN
public/assets/4nk_image.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
BIN
public/assets/4nk_revoke.jpg
Executable file
BIN
public/assets/4nk_revoke.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
BIN
public/assets/bgd.webp
Executable file
BIN
public/assets/bgd.webp
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 509 KiB |
BIN
public/assets/camera.jpg
Executable file
BIN
public/assets/camera.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
34
public/assets/home.js
Executable file
34
public/assets/home.js
Executable file
@ -0,0 +1,34 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/assets/qr_code.png
Executable file
BIN
public/assets/qr_code.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 8.9 KiB |
@ -1,152 +0,0 @@
|
|||||||
const EMPTY32BYTES = String('').padStart(64, '0');
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// SERVICE WORKER LIFECYCLE
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
self.addEventListener('install', (event) => {
|
|
||||||
event.waitUntil(self.skipWaiting());
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener('activate', (event) => {
|
|
||||||
event.waitUntil(self.clients.claim());
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// MESSAGE HANDLER
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
self.addEventListener('message', async (event) => {
|
|
||||||
const data = event.data;
|
|
||||||
console.log('[Service Worker] Message received:', data.type);
|
|
||||||
|
|
||||||
if (data.type === 'SCAN') {
|
|
||||||
try {
|
|
||||||
const myProcessesId = data.payload;
|
|
||||||
if (myProcessesId && myProcessesId.length != 0) {
|
|
||||||
const scanResult = await scanMissingData(myProcessesId, event.source);
|
|
||||||
|
|
||||||
if (scanResult.toDownload.length != 0) {
|
|
||||||
console.log('[Service Worker] Sending TO_DOWNLOAD message');
|
|
||||||
event.source.postMessage({ type: 'TO_DOWNLOAD', data: scanResult.toDownload });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scanResult.diffsToCreate.length > 0) {
|
|
||||||
console.log('[Service Worker] Sending DIFFS_TO_CREATE message');
|
|
||||||
event.source.postMessage({ type: 'DIFFS_TO_CREATE', data: scanResult.diffsToCreate });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
event.source.postMessage({ status: 'error', message: 'Empty lists' });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Service Worker] Scan error:', error);
|
|
||||||
event.source.postMessage({ status: 'error', message: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// DATABASE COMMUNICATION
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
async function requestFromMainThread(client, action, payload) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const messageId = `sw_${Date.now()}_${Math.random()}`;
|
|
||||||
|
|
||||||
const messageHandler = (event) => {
|
|
||||||
if (event.data.id === messageId) {
|
|
||||||
self.removeEventListener('message', messageHandler);
|
|
||||||
if (event.data.type === 'DB_RESPONSE') {
|
|
||||||
resolve(event.data.result);
|
|
||||||
} else if (event.data.type === 'DB_ERROR') {
|
|
||||||
reject(new Error(event.data.error));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self.addEventListener('message', messageHandler);
|
|
||||||
|
|
||||||
client.postMessage({
|
|
||||||
type: 'DB_REQUEST',
|
|
||||||
id: messageId,
|
|
||||||
action,
|
|
||||||
payload
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
self.removeEventListener('message', messageHandler);
|
|
||||||
reject(new Error('Database request timeout'));
|
|
||||||
}, 10000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// SCAN LOGIC
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
async function scanMissingData(processesToScan, client) {
|
|
||||||
console.log('[Service Worker] Scanning for missing data...');
|
|
||||||
|
|
||||||
const myProcesses = await requestFromMainThread(client, 'GET_MULTIPLE_OBJECTS', {
|
|
||||||
storeName: 'processes',
|
|
||||||
keys: processesToScan
|
|
||||||
});
|
|
||||||
|
|
||||||
let toDownload = new Set();
|
|
||||||
let diffsToCreate = [];
|
|
||||||
|
|
||||||
if (myProcesses && myProcesses.length != 0) {
|
|
||||||
for (const process of myProcesses) {
|
|
||||||
const firstState = process.states[0];
|
|
||||||
const processId = firstState.commited_in;
|
|
||||||
for (const state of process.states) {
|
|
||||||
if (state.state_id === EMPTY32BYTES) continue;
|
|
||||||
|
|
||||||
for (const [field, hash] of Object.entries(state.pcd_commitment)) {
|
|
||||||
if (state.public_data[field] !== undefined || field === 'roles') continue;
|
|
||||||
|
|
||||||
const existingData = await requestFromMainThread(client, 'GET_OBJECT', {
|
|
||||||
storeName: 'data',
|
|
||||||
key: hash
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!existingData) {
|
|
||||||
toDownload.add(hash);
|
|
||||||
|
|
||||||
const existingDiff = await requestFromMainThread(client, 'GET_OBJECT', {
|
|
||||||
storeName: 'diffs',
|
|
||||||
key: hash
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!existingDiff) {
|
|
||||||
diffsToCreate.push({
|
|
||||||
process_id: processId,
|
|
||||||
state_id: state.state_id,
|
|
||||||
value_commitment: hash,
|
|
||||||
roles: state.roles,
|
|
||||||
field: field,
|
|
||||||
description: null,
|
|
||||||
previous_value: null,
|
|
||||||
new_value: null,
|
|
||||||
notify_user: false,
|
|
||||||
need_validation: false,
|
|
||||||
validation_status: 'None'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (toDownload.delete(hash)) {
|
|
||||||
console.log(`[Service Worker] Removing ${hash} from the set`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[Service Worker] Scan complete:', { toDownload: toDownload.size, diffsToCreate: diffsToCreate.length });
|
|
||||||
return {
|
|
||||||
toDownload: Array.from(toDownload),
|
|
||||||
diffsToCreate: diffsToCreate
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
784
public/style/4nk.css
Executable file
784
public/style/4nk.css
Executable file
@ -0,0 +1,784 @@
|
|||||||
|
: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%;
|
||||||
|
}
|
||||||
1425
public/style/account.css
Executable file
1425
public/style/account.css
Executable file
File diff suppressed because it is too large
Load Diff
597
public/style/chat.css
Executable file
597
public/style/chat.css
Executable file
@ -0,0 +1,597 @@
|
|||||||
|
/* 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);
|
||||||
|
}
|
||||||
1664
public/style/signature.css
Executable file
1664
public/style/signature.css
Executable file
File diff suppressed because it is too large
Load Diff
818
src/4nk.css
Normal file
818
src/4nk.css
Normal file
@ -0,0 +1,818 @@
|
|||||||
|
: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
69
src/App.ts
@ -1,69 +0,0 @@
|
|||||||
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);
|
|
||||||
@ -1,133 +0,0 @@
|
|||||||
: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;
|
|
||||||
}
|
|
||||||
@ -1,242 +0,0 @@
|
|||||||
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,27 +1,36 @@
|
|||||||
<nav class="navbar glass-panel">
|
<div class="nav-wrapper">
|
||||||
<div class="nav-left">
|
<div id="profile-header-container"></div>
|
||||||
<div class="brand">4NK<span class="dot">.</span></div>
|
<div class="brand-logo">4NK</div>
|
||||||
</div>
|
<div class="nav-right-icons">
|
||||||
|
<div class="notification-container">
|
||||||
<div class="nav-right">
|
<div class="bell-icon">
|
||||||
<div class="user-profile" id="profile-header-container">
|
<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>
|
||||||
|
<div class="notification-badge"></div>
|
||||||
<button class="icon-btn burger-menu" aria-label="Menu">
|
<div id="notification-board" class="notification-board">
|
||||||
<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">
|
<div class="no-notification">No notifications available</div>
|
||||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
</div>
|
||||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
</div>
|
||||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="menu-dropdown glass-panel" id="menu">
|
<div class="burger-menu">
|
||||||
<a id="btn-import">Import Data</a>
|
<svg class="burger-menu" onclick="toggleMenu()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
||||||
<a id="btn-export">Export Backup</a>
|
<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="divider"></div>
|
|
||||||
|
<div class="menu-content" id="menu">
|
||||||
<a id="btn-disconnect" class="danger">Disconnect</a>
|
<!-- <a onclick="unpair()">Revoke</a> -->
|
||||||
|
<a onclick="importJSON()">Import</a>
|
||||||
|
<a onclick="createBackUp()">Export</a>
|
||||||
|
<a onclick="navigate('account')">Account</a>
|
||||||
|
<a onclick="navigate('chat')">Chat</a>
|
||||||
|
<a onclick="navigate('signature')">Signatures</a>
|
||||||
|
<a onclick="navigate('process')">Process</a>
|
||||||
|
<a onclick="disconnect()">Disconnect</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</div>
|
||||||
|
|||||||
220
src/components/header/header.ts
Executable file
220
src/components/header/header.ts
Executable file
@ -0,0 +1,220 @@
|
|||||||
|
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;
|
||||||
14
src/components/login-modal/login-modal.html
Executable file
14
src/components/login-modal/login-modal.html
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
<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>
|
||||||
13
src/components/login-modal/login-modal.js
Executable file
13
src/components/login-modal/login-modal.js
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
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;
|
||||||
16
src/components/modal/confirmation-modal.html
Executable file
16
src/components/modal/confirmation-modal.html
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
<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>
|
||||||
13
src/components/modal/confirmation-modal.ts
Executable file
13
src/components/modal/confirmation-modal.ts
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
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;
|
||||||
14
src/components/modal/creation-modal.html
Normal file
14
src/components/modal/creation-modal.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<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>
|
||||||
8
src/components/modal/waiting-modal.html
Normal file
8
src/components/modal/waiting-modal.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<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>
|
||||||
73
src/components/qrcode-scanner/qrcode-scanner-component.ts
Normal file
73
src/components/qrcode-scanner/qrcode-scanner-component.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
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);
|
||||||
70
src/components/validation-modal/validation-modal.css
Normal file
70
src/components/validation-modal/validation-modal.css
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
11
src/components/validation-modal/validation-modal.html
Executable file
11
src/components/validation-modal/validation-modal.html
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
<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>
|
||||||
56
src/components/validation-modal/validation-modal.ts
Executable file
56
src/components/validation-modal/validation-modal.ts
Executable file
@ -0,0 +1,56 @@
|
|||||||
|
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;
|
||||||
10
src/decs.d.ts
vendored
Normal file
10
src/decs.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
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
Executable file
39
src/index.ts
Executable file
@ -0,0 +1,39 @@
|
|||||||
|
// 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);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
22
src/interface/groupInterface.ts
Normal file
22
src/interface/groupInterface.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
7
src/interface/memberInterface.ts
Normal file
7
src/interface/memberInterface.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface Member {
|
||||||
|
id: string | number;
|
||||||
|
name: string;
|
||||||
|
email?: string;
|
||||||
|
avatar?: string;
|
||||||
|
processRoles?: Array<{ processId: number | string; role: string }>;
|
||||||
|
}
|
||||||
0
src/interface/signatureResponseInterface.ts
Normal file
0
src/interface/signatureResponseInterface.ts
Normal file
95
src/main.ts
95
src/main.ts
@ -1,65 +1,30 @@
|
|||||||
import Database from './services/database.service';
|
import { SignatureComponent } from './pages/signature/signature-component';
|
||||||
import Services from './services/service';
|
import { SignatureElement } from './pages/signature/signature';
|
||||||
import { Router } from './router/index';
|
import { ChatComponent } from './pages/chat/chat-component';
|
||||||
import './components/header/Header';
|
import { ChatElement } from './pages/chat/chat';
|
||||||
import './App';
|
import { AccountComponent } from './pages/account/account-component';
|
||||||
import { IframeController } from './services/iframe-controller.service';
|
import { AccountElement } from './pages/account/account';
|
||||||
|
|
||||||
async function bootstrap() {
|
export { SignatureComponent, SignatureElement, ChatComponent, ChatElement, AccountComponent, AccountElement };
|
||||||
console.log("🚀 Démarrage de l'application 4NK...");
|
|
||||||
|
declare global {
|
||||||
try {
|
interface HTMLElementTagNameMap {
|
||||||
// 1. Initialisation des Services (WASM, Sockets, Database...)
|
'signature-component': SignatureComponent;
|
||||||
const services = await Services.getInstance();
|
'signature-element': SignatureElement;
|
||||||
|
'chat-component': ChatComponent;
|
||||||
// 2. Initialisation de la base de données (Web Worker + Service Worker)
|
'chat-element': ChatElement;
|
||||||
await Database.getInstance();
|
'account-component': AccountComponent;
|
||||||
|
'account-element': AccountElement;
|
||||||
// Injection du Header dans le slot prévu dans index.html
|
}
|
||||||
const headerSlot = document.getElementById('header-slot');
|
}
|
||||||
if (headerSlot) {
|
|
||||||
headerSlot.innerHTML = '<app-header></app-header>';
|
// Configuration pour le mode indépendant
|
||||||
}
|
if ((import.meta as any).env.VITE_IS_INDEPENDANT_LIB) {
|
||||||
|
// Initialiser les composants si nécessaire
|
||||||
// Vérification basique de l'appareil (logique reprise de ton ancien router.ts)
|
customElements.define('signature-component', SignatureComponent);
|
||||||
const device = await services.getDeviceFromDatabase();
|
customElements.define('signature-element', SignatureElement);
|
||||||
if (!device) {
|
customElements.define('chat-component', ChatComponent);
|
||||||
console.log('✨ Nouvel appareil détecté, création en cours...');
|
customElements.define('chat-element', ChatElement);
|
||||||
await services.createNewDevice();
|
customElements.define('account-component', AccountComponent);
|
||||||
} else {
|
customElements.define('account-element', AccountElement);
|
||||||
console.log("Restauration de l'appareil...");
|
}
|
||||||
services.restoreDevice(device);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialisation du contrôleur d'Iframe (API listeners)
|
|
||||||
await IframeController.init();
|
|
||||||
|
|
||||||
// 3. Restauration des données
|
|
||||||
await services.restoreProcessesFromDB();
|
|
||||||
await services.restoreSecretsFromDB();
|
|
||||||
|
|
||||||
// 4. Connexion réseau
|
|
||||||
await services.connectAllRelays();
|
|
||||||
|
|
||||||
// 5. Démarrage du Routeur (Affichage de la page)
|
|
||||||
const isIframe = window.self !== window.top;
|
|
||||||
|
|
||||||
// On redirige vers 'process' SEULEMENT si on est appairé ET qu'on n'est PAS dans une iframe
|
|
||||||
if (services.isPaired() && !isIframe) {
|
|
||||||
console.log('✅ Mode Standalone & Appairé : Redirection vers Process.');
|
|
||||||
window.history.replaceState({}, '', 'process');
|
|
||||||
Router.handleLocation();
|
|
||||||
} else {
|
|
||||||
// Cas 1 : Pas appairé
|
|
||||||
// Cas 2 : Mode Iframe (même si appairé, on reste sur Home pour attendre le parent)
|
|
||||||
console.log(isIframe ? '📡 Mode Iframe détecté : Démarrage sur Home pour attente API.' : '🆕 Non appairé : Démarrage sur Home.');
|
|
||||||
Router.init();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('💥 Erreur critique au démarrage :', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lancement
|
|
||||||
bootstrap();
|
|
||||||
|
|
||||||
|
|||||||
272
src/mocks/mock-account/constAccountMock.ts
Normal file
272
src/mocks/mock-account/constAccountMock.ts
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
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.',
|
||||||
|
},
|
||||||
|
};
|
||||||
45
src/mocks/mock-account/interfacesAccountMock.ts
Normal file
45
src/mocks/mock-account/interfacesAccountMock.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
52
src/mocks/mock-chat/groupsMock.js
Executable file
52
src/mocks/mock-chat/groupsMock.js
Executable file
@ -0,0 +1,52 @@
|
|||||||
|
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' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
64
src/mocks/mock-chat/messagesMock.js
Executable file
64
src/mocks/mock-chat/messagesMock.js
Executable file
@ -0,0 +1,64 @@
|
|||||||
|
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' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
471
src/mocks/mock-signature/groupsMock.js
Executable file
471
src/mocks/mock-signature/groupsMock.js
Executable file
@ -0,0 +1,471 @@
|
|||||||
|
// 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: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
105
src/mocks/mock-signature/membersMocks.js
Executable file
105
src/mocks/mock-signature/membersMocks.js
Executable file
@ -0,0 +1,105 @@
|
|||||||
|
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' }],
|
||||||
|
},
|
||||||
|
];
|
||||||
64
src/mocks/mock-signature/messagesMock.ts
Executable file
64
src/mocks/mock-signature/messagesMock.ts
Executable file
@ -0,0 +1,64 @@
|
|||||||
|
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' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
7
src/models/backup.model.ts
Normal file
7
src/models/backup.model.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { Device, Process, SecretsStore } from "pkg/sdk_client";
|
||||||
|
|
||||||
|
export interface BackUp {
|
||||||
|
device: Device,
|
||||||
|
secrets: SecretsStore,
|
||||||
|
processes: Record<string, Process>,
|
||||||
|
}
|
||||||
30
src/models/notification.model.ts
Executable file
30
src/models/notification.model.ts
Executable file
@ -0,0 +1,30 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
23
src/models/process.model.ts
Executable file
23
src/models/process.model.ts
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
59
src/models/signature.models.ts
Executable file
59
src/models/signature.models.ts
Executable file
@ -0,0 +1,59 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
62
src/pages/account/account-component.ts
Normal file
62
src/pages/account/account-component.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
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);
|
||||||
10
src/pages/account/account.html
Executable file
10
src/pages/account/account.html
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Account</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<account-component></account-component>
|
||||||
|
<script type="module" src="./account.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1487
src/pages/account/account.ts
Executable file
1487
src/pages/account/account.ts
Executable file
File diff suppressed because it is too large
Load Diff
49
src/pages/chat/chat-component.ts
Normal file
49
src/pages/chat/chat-component.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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);
|
||||||
13
src/pages/chat/chat.html
Executable file
13
src/pages/chat/chat.html
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Chat</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<chat-component></chat-component>
|
||||||
|
<script type="module" src="./chat.ts"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
1748
src/pages/chat/chat.ts
Executable file
1748
src/pages/chat/chat.ts
Executable file
File diff suppressed because it is too large
Load Diff
@ -1,243 +0,0 @@
|
|||||||
// 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;
|
|
||||||
|
|
||||||
(window as any).__PAIRING_READY = false;
|
|
||||||
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 ---
|
|
||||||
(window as any).__PAIRING_READY = true;
|
|
||||||
console.log('[Home] Auto-pairing terminé avec succès.');
|
|
||||||
|
|
||||||
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.');
|
|
||||||
(window as any).__PAIRING_READY = true;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error('[Home] Erreur:', e);
|
|
||||||
this.addLoaderStep(`Erreur: ${e.message}`);
|
|
||||||
(window as any).__PAIRING_READY = 'error';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
49
src/pages/home/home-component.ts
Normal file
49
src/pages/home/home-component.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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,51 +1,42 @@
|
|||||||
<div class="home-layout">
|
<div class="title-container">
|
||||||
|
<h1>Create Account / New Session</h1>
|
||||||
<div id="iframe-loader" class="loader-overlay">
|
</div>
|
||||||
<div class="loader-content glass-panel">
|
|
||||||
<div class="spinner"></div>
|
<div class="tab-container">
|
||||||
<div id="loader-steps-container">
|
<div class="tabs">
|
||||||
<p class="loader-step active">Démarrage du système...</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<p>Or</p>
|
||||||
|
<!-- <input type="text" id="addressInput" placeholder="Paste address" />
|
||||||
<div id="main-content" class="auth-container" style="display: none;">
|
<div id="emoji-display-2"></div> -->
|
||||||
|
<div class="card-description">Chose a member :</div>
|
||||||
|
<select name="memberSelect" id="memberSelect" size="5" class="custom-select">
|
||||||
|
<!-- Options -->
|
||||||
|
</select>
|
||||||
|
|
||||||
<div class="auth-card glass-panel">
|
<button id="okButton" style="display: none">OK</button>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
94
src/pages/home/home.ts
Executable file
94
src/pages/home/home.ts
Executable file
@ -0,0 +1,94 @@
|
|||||||
|
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;
|
||||||
51
src/pages/process-element/process-component.ts
Normal file
51
src/pages/process-element/process-component.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
5
src/pages/process-element/process-element.html
Executable file
5
src/pages/process-element/process-element.html
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
<div class="title-container">
|
||||||
|
<h1>Process {{processTitle}}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="process-container"></div>
|
||||||
50
src/pages/process-element/process-element.ts
Executable file
50
src/pages/process-element/process-element.ts
Executable file
@ -0,0 +1,50 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@ -1,404 +0,0 @@
|
|||||||
import processHtml from './process.html?raw';
|
|
||||||
import globalCss from '../../assets/styles/style.css?inline';
|
|
||||||
import Services from '../../services/service';
|
|
||||||
import { Router } from '../../router';
|
|
||||||
|
|
||||||
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();
|
|
||||||
// Petit délai pour s'assurer que le DOM est prêt
|
|
||||||
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; /* Pour scroller dedans si besoin */
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-header { text-align: center; }
|
|
||||||
.subtitle { color: var(--text-muted); margin-top: -0.5rem; }
|
|
||||||
|
|
||||||
/* Search Styles */
|
|
||||||
.search-input-container {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input-container input {
|
|
||||||
padding-right: 40px; /* Place pour l'icone */
|
|
||||||
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 List */
|
|
||||||
.autocomplete-dropdown {
|
|
||||||
list-style: none;
|
|
||||||
margin-top: 5px;
|
|
||||||
padding: 0;
|
|
||||||
background: #1e293b; /* Fond opaque pour lisibilité */
|
|
||||||
border: 1px solid var(--glass-border);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
display: none; /* Caché par défaut */
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 10;
|
|
||||||
box-shadow: 0 10px 25px rgba(0,0,0,0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Position relative pour que le dropdown s'aligne */
|
|
||||||
.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 */
|
|
||||||
.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; }
|
|
||||||
|
|
||||||
/* Process Details */
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar custom */
|
|
||||||
::-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;
|
|
||||||
|
|
||||||
// Récupération des éléments
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Listeners
|
|
||||||
this.inputInput.addEventListener('keyup', () => this.handleInput());
|
|
||||||
this.inputInput.addEventListener('click', () => this.openDropdown());
|
|
||||||
|
|
||||||
// Fermeture du dropdown au clic extérieur
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
const path = e.composedPath();
|
|
||||||
if (!path.includes(this.wrapper)) {
|
|
||||||
this.closeDropdown();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.okButton.addEventListener('click', () => this.goToProcess());
|
|
||||||
|
|
||||||
// Écoute des mises à jour du service
|
|
||||||
document.addEventListener('processes-updated', async () => {
|
|
||||||
await this.populateList(this.inputInput.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Chargement initial
|
|
||||||
await this.populateList('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Logique Autocomplete ---
|
|
||||||
|
|
||||||
async populateList(query: string) {
|
|
||||||
this.autocompleteList.innerHTML = '';
|
|
||||||
|
|
||||||
const mineArray = (await this.services.getMyProcesses()) ?? [];
|
|
||||||
const allProcesses = await this.services.getProcesses();
|
|
||||||
|
|
||||||
// On combine tout, en mettant les miens d'abord
|
|
||||||
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 = this.services.getProcessName(process) || pid;
|
|
||||||
|
|
||||||
// Filtre
|
|
||||||
if (query && !name.toLowerCase().includes(query.toLowerCase()) && !pid.includes(query)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
count++;
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.textContent = name;
|
|
||||||
if (mineArray.includes(pid)) {
|
|
||||||
li.classList.add('my-process');
|
|
||||||
li.innerHTML += ' <small style="opacity:0.6">(Mien)</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 ---
|
|
||||||
|
|
||||||
addTag(pid: string, name: string) {
|
|
||||||
// On nettoie les anciens tags (mode single select pour l'instant, ou multi si tu veux)
|
|
||||||
this.tagsContainer.innerHTML = '';
|
|
||||||
|
|
||||||
const tag = document.createElement('div');
|
|
||||||
tag.className = 'tag';
|
|
||||||
tag.innerHTML = `
|
|
||||||
<span>${name}</span>
|
|
||||||
<span class="tag-close">×</span>
|
|
||||||
`;
|
|
||||||
|
|
||||||
tag.querySelector('.tag-close')?.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
this.removeTag();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.tagsContainer.appendChild(tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeTag() {
|
|
||||||
this.tagsContainer.innerHTML = '';
|
|
||||||
this.detailsContainer.innerHTML = '<div class="empty-state"><p>Aucun processus sélectionné.</p></div>';
|
|
||||||
this.okButton.disabled = true;
|
|
||||||
this.okButton.classList.add('disabled');
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Détails du processus ---
|
|
||||||
|
|
||||||
async showProcessDetails(pid: string) {
|
|
||||||
this.detailsContainer.innerHTML = '<p style="text-align:center">Chargement...</p>';
|
|
||||||
|
|
||||||
const process = await this.services.getProcess(pid);
|
|
||||||
if (!process) return;
|
|
||||||
|
|
||||||
this.detailsContainer.innerHTML = ''; // Clear
|
|
||||||
|
|
||||||
const name = this.services.getProcessName(process) || 'Sans nom';
|
|
||||||
|
|
||||||
// Description
|
|
||||||
let description = 'Pas de description';
|
|
||||||
const lastState = 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';
|
|
||||||
containerDiv.innerHTML = `
|
|
||||||
<div class="process-title-display">${name}</div>
|
|
||||||
<div style="font-size:0.9rem; margin-bottom:10px">${description}</div>
|
|
||||||
<div style="font-size:0.8rem; opacity:0.7; margin-bottom:10px">ID: ${pid}</div>
|
|
||||||
<div style="font-weight:bold; margin-top:15px">États en attente :</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const uncommitted = 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', () => {
|
|
||||||
// Gestion de la sélection
|
|
||||||
this.shadowRoot?.querySelectorAll('.state-element').forEach((x) => x.classList.remove('selected'));
|
|
||||||
el.classList.add('selected');
|
|
||||||
|
|
||||||
// Activation du bouton
|
|
||||||
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 la page de détail du processus (à implémenter): ' + target);
|
|
||||||
// Router.navigate(`process-detail/${target}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define('process-list-page', ProcessListPage);
|
|
||||||
49
src/pages/process/process-list-component.ts
Normal file
49
src/pages/process/process-list-component.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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,45 +1,19 @@
|
|||||||
<div class="process-layout">
|
<div class="title-container">
|
||||||
<div class="dashboard-container glass-panel">
|
<h1>Process Selection</h1>
|
||||||
|
</div>
|
||||||
<div class="dashboard-header">
|
|
||||||
<h1>Mes Processus</h1>
|
|
||||||
<p class="subtitle">Sélectionnez et gérez vos flux de travail</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="search-section">
|
<div class="process-container">
|
||||||
<div class="input-group">
|
<div class="process-card">
|
||||||
<label>Rechercher un processus</label>
|
<div class="process-card-description">
|
||||||
<div id="autocomplete-wrapper" class="custom-select-wrapper">
|
<div class="input-container">
|
||||||
<select multiple id="process-select" style="display:none"></select>
|
<select multiple data-multi-select-plugin id="autocomplete" placeholder="Filter processes..." class="select-field"></select>
|
||||||
<div class="search-input-container">
|
<label for="autocomplete" class="input-label">Filter processes :</label>
|
||||||
<input type="text" id="process-input" placeholder="Filtrer par nom ou ID..." autocomplete="off">
|
<div class="selected-processes"></div>
|
||||||
<span class="search-icon">🔍</span>
|
|
||||||
</div>
|
|
||||||
<ul id="autocomplete-list" class="autocomplete-dropdown"></ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="process-card-content"></div>
|
||||||
<div id="selected-tags-container" class="tags-container">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="process-card-action">
|
||||||
<div class="divider"></div>
|
<a class="btn" onclick="goToProcessPage()">OK</a>
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
520
src/pages/process/process.ts
Executable file
520
src/pages/process/process.ts
Executable file
@ -0,0 +1,520 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
58
src/pages/signature/signature-component.ts
Normal file
58
src/pages/signature/signature-component.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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);
|
||||||
12
src/pages/signature/signature.html
Executable file
12
src/pages/signature/signature.html
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Signatures</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<signature-component></signature-component>
|
||||||
|
<script type="module" src="./signature.ts"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
1758
src/pages/signature/signature.ts
Executable file
1758
src/pages/signature/signature.ts
Executable file
File diff suppressed because it is too large
Load Diff
209
src/router.ts
Executable file
209
src/router.ts
Executable file
@ -0,0 +1,209 @@
|
|||||||
|
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 || '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
@ -1,64 +0,0 @@
|
|||||||
// 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);
|
|
||||||
13
src/scanner.js
Executable file
13
src/scanner.js
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
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);
|
||||||
8
src/service-workers/cache.worker.js
Normal file
8
src/service-workers/cache.worker.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
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']));
|
||||||
|
});
|
||||||
266
src/service-workers/database.worker.js
Executable file
266
src/service-workers/database.worker.js
Executable file
@ -0,0 +1,266 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
38
src/services/chat.service.ts
Normal file
38
src/services/chat.service.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,463 +1,417 @@
|
|||||||
import Services from './service';
|
import Services from './service';
|
||||||
|
|
||||||
/**
|
export class Database {
|
||||||
* Database service managing IndexedDB operations via Web Worker and Service Worker
|
private static instance: Database;
|
||||||
*/
|
private db: IDBDatabase | null = null;
|
||||||
export class Database {
|
private dbName: string = '4nk';
|
||||||
// ============================================
|
private dbVersion: number = 1;
|
||||||
// PRIVATE PROPERTIES
|
private serviceWorkerRegistration: ServiceWorkerRegistration | null = null;
|
||||||
// ============================================
|
private messageChannel: MessageChannel | null = null;
|
||||||
|
private messageChannelForGet: MessageChannel | null = null;
|
||||||
private static instance: Database;
|
private serviceWorkerCheckIntervalId: number | null = null;
|
||||||
private serviceWorkerRegistration: ServiceWorkerRegistration | null = null;
|
private storeDefinitions = {
|
||||||
private serviceWorkerCheckIntervalId: number | null = null;
|
AnkLabels: {
|
||||||
private indexedDBWorker: Worker | null = null;
|
name: 'labels',
|
||||||
private messageIdCounter: number = 0;
|
options: { keyPath: 'emoji' },
|
||||||
private pendingMessages: Map<number, { resolve: (value: any) => void; reject: (error: any) => void }> = new Map();
|
indices: [],
|
||||||
|
},
|
||||||
// ============================================
|
AnkWallet: {
|
||||||
// INITIALIZATION & SINGLETON
|
name: 'wallet',
|
||||||
// ============================================
|
options: { keyPath: 'pre_id' },
|
||||||
|
indices: [],
|
||||||
private constructor() {
|
},
|
||||||
this.initIndexedDBWorker();
|
AnkProcess: {
|
||||||
this.initServiceWorker();
|
name: 'processes',
|
||||||
}
|
options: {},
|
||||||
|
indices: [],
|
||||||
public static async getInstance(): Promise<Database> {
|
},
|
||||||
if (!Database.instance) {
|
AnkSharedSecrets: {
|
||||||
Database.instance = new Database();
|
name: 'shared_secrets',
|
||||||
await Database.instance.waitForWorkerReady();
|
options: {},
|
||||||
}
|
indices: [],
|
||||||
return Database.instance;
|
},
|
||||||
}
|
AnkUnconfirmedSecrets: {
|
||||||
|
name: 'unconfirmed_secrets',
|
||||||
// ============================================
|
options: { autoIncrement: true },
|
||||||
// INDEXEDDB WEB WORKER
|
indices: [],
|
||||||
// ============================================
|
},
|
||||||
|
AnkPendingDiffs: {
|
||||||
private initIndexedDBWorker(): void {
|
name: 'diffs',
|
||||||
this.indexedDBWorker = new Worker(new URL('../workers/database.worker.js', import.meta.url), { type: 'module' });
|
options: { keyPath: 'value_commitment' },
|
||||||
|
indices: [
|
||||||
this.indexedDBWorker.onmessage = (event) => {
|
{ name: 'byStateId', keyPath: 'state_id', options: { unique: false } },
|
||||||
const { id, type, result, error } = event.data;
|
{ name: 'byNeedValidation', keyPath: 'need_validation', options: { unique: false } },
|
||||||
const pending = this.pendingMessages.get(id);
|
{ name: 'byStatus', keyPath: 'validation_status', options: { unique: false } },
|
||||||
|
],
|
||||||
if (pending) {
|
},
|
||||||
this.pendingMessages.delete(id);
|
AnkData: {
|
||||||
|
name: 'data',
|
||||||
if (type === 'SUCCESS') {
|
options: {},
|
||||||
pending.resolve(result);
|
indices: [],
|
||||||
} else if (type === 'ERROR') {
|
},
|
||||||
pending.reject(new Error(error));
|
};
|
||||||
}
|
|
||||||
}
|
// Private constructor to prevent direct instantiation from outside
|
||||||
};
|
private constructor() {}
|
||||||
|
|
||||||
this.indexedDBWorker.onerror = (error) => {
|
// Method to access the singleton instance of Database
|
||||||
console.error('[Database] IndexedDB Worker error:', error);
|
public static async getInstance(): Promise<Database> {
|
||||||
};
|
if (!Database.instance) {
|
||||||
}
|
Database.instance = new Database();
|
||||||
|
await Database.instance.init();
|
||||||
private async waitForWorkerReady(): Promise<void> {
|
}
|
||||||
return this.sendMessageToWorker('INIT', {});
|
return Database.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendMessageToWorker<T = any>(type: string, payload: any): Promise<T> {
|
// Initialize the database
|
||||||
return new Promise((resolve, reject) => {
|
private async init(): Promise<void> {
|
||||||
if (!this.indexedDBWorker) {
|
return new Promise((resolve, reject) => {
|
||||||
reject(new Error('IndexedDB Worker not initialized'));
|
const request = indexedDB.open(this.dbName, this.dbVersion);
|
||||||
return;
|
|
||||||
}
|
request.onupgradeneeded = () => {
|
||||||
|
const db = request.result;
|
||||||
const id = this.messageIdCounter++;
|
|
||||||
this.pendingMessages.set(id, { resolve, reject });
|
Object.values(this.storeDefinitions).forEach(({ name, options, indices }) => {
|
||||||
|
if (!db.objectStoreNames.contains(name)) {
|
||||||
this.indexedDBWorker.postMessage({ type, payload, id });
|
let store = db.createObjectStore(name, options as IDBObjectStoreParameters);
|
||||||
|
|
||||||
// Timeout de sécurité (30 secondes)
|
indices.forEach(({ name, keyPath, options }) => {
|
||||||
setTimeout(() => {
|
store.createIndex(name, keyPath, options);
|
||||||
if (this.pendingMessages.has(id)) {
|
});
|
||||||
this.pendingMessages.delete(id);
|
}
|
||||||
reject(new Error(`Worker message timeout for type: ${type}`));
|
});
|
||||||
}
|
};
|
||||||
}, 30000);
|
|
||||||
});
|
request.onsuccess = async () => {
|
||||||
}
|
this.db = request.result;
|
||||||
|
await this.initServiceWorker();
|
||||||
// ============================================
|
resolve();
|
||||||
// SERVICE WORKER
|
};
|
||||||
// ============================================
|
|
||||||
|
request.onerror = () => {
|
||||||
private initServiceWorker(): void {
|
console.error('Database error:', request.error);
|
||||||
this.registerServiceWorker('/data.worker.js');
|
reject(request.error);
|
||||||
}
|
};
|
||||||
|
});
|
||||||
private async registerServiceWorker(path: string): Promise<void> {
|
}
|
||||||
if (!('serviceWorker' in navigator)) return;
|
|
||||||
console.log('[Database] Initializing Service Worker:', path);
|
public async getDb(): Promise<IDBDatabase> {
|
||||||
|
if (!this.db) {
|
||||||
try {
|
await this.init();
|
||||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
}
|
||||||
|
return this.db!;
|
||||||
for (const registration of registrations) {
|
}
|
||||||
const scriptURL = registration.active?.scriptURL || registration.installing?.scriptURL || registration.waiting?.scriptURL;
|
|
||||||
const scope = registration.scope;
|
public getStoreList(): { [key: string]: string } {
|
||||||
|
const objectList: { [key: string]: string } = {};
|
||||||
if (scope.includes('/src/service-workers/') || (scriptURL && scriptURL.includes('/src/service-workers/'))) {
|
Object.keys(this.storeDefinitions).forEach((key) => {
|
||||||
console.warn(`[Database] Removing old Service Worker (${scope})`);
|
objectList[key] = this.storeDefinitions[key as keyof typeof this.storeDefinitions].name;
|
||||||
await registration.unregister();
|
});
|
||||||
}
|
return objectList;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingValidWorker = registrations.find((r) => {
|
private async initServiceWorker() {
|
||||||
const url = r.active?.scriptURL || r.installing?.scriptURL || r.waiting?.scriptURL;
|
if (!('serviceWorker' in navigator)) return; // Ensure service workers are supported
|
||||||
return url && url.endsWith(path.replace(/^\//,''));
|
|
||||||
});
|
try {
|
||||||
|
// Get existing service worker registrations
|
||||||
if (!existingValidWorker) {
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||||
console.log('[Database] Registering new Service Worker');
|
if (registrations.length === 0) {
|
||||||
this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module', scope: '/' });
|
// No existing workers: register a new one.
|
||||||
} else {
|
this.serviceWorkerRegistration = await navigator.serviceWorker.register('/src/service-workers/database.worker.js', { type: 'module' });
|
||||||
console.log('[Database] Service Worker already active');
|
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
|
||||||
this.serviceWorkerRegistration = existingValidWorker;
|
} else if (registrations.length === 1) {
|
||||||
await this.serviceWorkerRegistration.update();
|
// One existing worker: update it (restart it) without unregistering.
|
||||||
}
|
this.serviceWorkerRegistration = registrations[0];
|
||||||
|
await this.serviceWorkerRegistration.update();
|
||||||
navigator.serviceWorker.addEventListener('message', async (event) => {
|
console.log('Service Worker updated');
|
||||||
if (event.data.type === 'DB_REQUEST') {
|
} else {
|
||||||
await this.handleDatabaseRequest(event.data);
|
// More than one existing worker: unregister them all and register a new one.
|
||||||
return;
|
console.log('Multiple Service Worker(s) detected. Unregistering all...');
|
||||||
}
|
await Promise.all(registrations.map(reg => reg.unregister()));
|
||||||
await this.handleServiceWorkerMessage(event.data);
|
console.log('All previous Service Workers unregistered.');
|
||||||
});
|
this.serviceWorkerRegistration = await navigator.serviceWorker.register('/src/service-workers/database.worker.js', { type: 'module' });
|
||||||
|
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
|
||||||
if (this.serviceWorkerCheckIntervalId) clearInterval(this.serviceWorkerCheckIntervalId);
|
}
|
||||||
this.serviceWorkerCheckIntervalId = window.setInterval(async () => {
|
|
||||||
const activeWorker = this.serviceWorkerRegistration?.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration!));
|
await this.checkForUpdates();
|
||||||
const service = await Services.getInstance();
|
|
||||||
const payload = await service.getMyProcesses();
|
// Set up a global message listener for responses from the service worker.
|
||||||
if (payload && payload.length != 0) {
|
navigator.serviceWorker.addEventListener('message', async (event) => {
|
||||||
activeWorker?.postMessage({ type: 'SCAN', payload });
|
console.log('Received message from service worker:', event.data);
|
||||||
}
|
await this.handleServiceWorkerMessage(event.data);
|
||||||
}, 5000);
|
});
|
||||||
} catch (error) {
|
|
||||||
console.error('[Database] Service Worker error:', error);
|
// 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();
|
||||||
private async waitForServiceWorkerActivation(registration: ServiceWorkerRegistration): Promise<ServiceWorker | null> {
|
const payload = await service.getMyProcesses();
|
||||||
return new Promise((resolve) => {
|
if (payload.length != 0) {
|
||||||
if (registration.active) {
|
activeWorker?.postMessage({ type: 'SCAN', payload });
|
||||||
resolve(registration.active);
|
}
|
||||||
} else {
|
}, 5000);
|
||||||
const listener = () => {
|
} catch (error) {
|
||||||
if (registration.active) {
|
console.error('Service Worker registration failed:', error);
|
||||||
navigator.serviceWorker.removeEventListener('controllerchange', listener);
|
}
|
||||||
resolve(registration.active);
|
}
|
||||||
}
|
|
||||||
};
|
// Helper function to wait for service worker activation
|
||||||
navigator.serviceWorker.addEventListener('controllerchange', listener);
|
private async waitForServiceWorkerActivation(registration: ServiceWorkerRegistration): Promise<ServiceWorker | null> {
|
||||||
}
|
return new Promise((resolve) => {
|
||||||
});
|
if (registration.active) {
|
||||||
}
|
resolve(registration.active);
|
||||||
|
} else {
|
||||||
private async checkForUpdates(): Promise<void> {
|
const listener = () => {
|
||||||
if (this.serviceWorkerRegistration) {
|
if (registration.active) {
|
||||||
try {
|
navigator.serviceWorker.removeEventListener('controllerchange', listener);
|
||||||
await this.serviceWorkerRegistration.update();
|
resolve(registration.active);
|
||||||
|
}
|
||||||
if (this.serviceWorkerRegistration.waiting) {
|
};
|
||||||
this.serviceWorkerRegistration.waiting.postMessage({ type: 'SKIP_WAITING' });
|
navigator.serviceWorker.addEventListener('controllerchange', listener);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
});
|
||||||
console.error('Error checking for service worker updates:', error);
|
}
|
||||||
}
|
|
||||||
}
|
private async checkForUpdates() {
|
||||||
}
|
if (this.serviceWorkerRegistration) {
|
||||||
|
// Check for updates to the service worker
|
||||||
// ============================================
|
try {
|
||||||
// SERVICE WORKER MESSAGE HANDLERS
|
await this.serviceWorkerRegistration.update();
|
||||||
// ============================================
|
|
||||||
private async handleDatabaseRequest(request: any): Promise<void> {
|
// If there's a new worker waiting, activate it immediately
|
||||||
const { id, action, payload } = request;
|
if (this.serviceWorkerRegistration.waiting) {
|
||||||
|
this.serviceWorkerRegistration.waiting.postMessage({ type: 'SKIP_WAITING' });
|
||||||
try {
|
}
|
||||||
let result;
|
} catch (error) {
|
||||||
|
console.error('Error checking for service worker updates:', error);
|
||||||
switch (action) {
|
}
|
||||||
case 'GET_OBJECT':
|
}
|
||||||
result = await this.getObject(payload.storeName, payload.key);
|
}
|
||||||
break;
|
|
||||||
|
private async handleServiceWorkerMessage(message: any) {
|
||||||
case 'GET_MULTIPLE_OBJECTS':
|
switch (message.type) {
|
||||||
result = await this.sendMessageToWorker('GET_MULTIPLE_OBJECTS', payload);
|
case 'TO_DOWNLOAD':
|
||||||
break;
|
await this.handleDownloadList(message.data);
|
||||||
|
break;
|
||||||
case 'GET_ALL_OBJECTS':
|
default:
|
||||||
result = await this.sendMessageToWorker('GET_ALL_OBJECTS', payload);
|
console.warn('Unknown message type received from service worker:', message);
|
||||||
break;
|
}
|
||||||
|
}
|
||||||
case 'GET_ALL_OBJECTS_WITH_FILTER':
|
|
||||||
result = await this.sendMessageToWorker('GET_ALL_OBJECTS_WITH_FILTER', payload);
|
private async handleDownloadList(downloadList: string[]): void {
|
||||||
break;
|
// Download the missing data
|
||||||
|
let requestedStateId = [];
|
||||||
default:
|
const service = await Services.getInstance();
|
||||||
throw new Error(`Unknown database action: ${action}`);
|
for (const hash of downloadList) {
|
||||||
}
|
const diff = await service.getDiffByValue(hash);
|
||||||
|
if (!diff) {
|
||||||
if (this.serviceWorkerRegistration?.active) {
|
// This should never happen
|
||||||
this.serviceWorkerRegistration.active.postMessage({
|
console.warn(`Missing a diff for hash ${hash}`);
|
||||||
type: 'DB_RESPONSE',
|
continue;
|
||||||
id,
|
}
|
||||||
result
|
const processId = diff.process_id;
|
||||||
});
|
const stateId = diff.state_id;
|
||||||
}
|
const roles = diff.roles;
|
||||||
} catch (error: any) {
|
try {
|
||||||
console.error('[Database] Error handling database request:', error);
|
const valueBytes = await service.fetchValueFromStorage(hash);
|
||||||
|
if (valueBytes) {
|
||||||
if (this.serviceWorkerRegistration?.active) {
|
// Save data to db
|
||||||
this.serviceWorkerRegistration.active.postMessage({
|
const blob = new Blob([valueBytes], {type: "application/octet-stream"});
|
||||||
type: 'DB_ERROR',
|
await service.saveBlobToDb(hash, blob);
|
||||||
id,
|
document.dispatchEvent(new CustomEvent('newDataReceived', {
|
||||||
error: error.message || String(error)
|
detail: {
|
||||||
});
|
processId,
|
||||||
}
|
stateId,
|
||||||
}
|
hash,
|
||||||
}
|
}
|
||||||
|
}));
|
||||||
private async handleServiceWorkerMessage(message: any) {
|
} else {
|
||||||
switch (message.type) {
|
// We first request the data from managers
|
||||||
case 'TO_DOWNLOAD':
|
console.log('Request data from managers of the process');
|
||||||
await this.handleDownloadList(message.data);
|
// get the diff from db
|
||||||
break;
|
if (!requestedStateId.includes(stateId)) {
|
||||||
case 'DIFFS_TO_CREATE':
|
await service.requestDataFromPeers(processId, [stateId], [roles]);
|
||||||
await this.handleDiffsToCreate(message.data);
|
requestedStateId.push(stateId);
|
||||||
break;
|
}
|
||||||
default:
|
}
|
||||||
console.warn('Unknown message type received from service worker:', message);
|
} catch (e) {
|
||||||
}
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private async handleDiffsToCreate(diffs: any[]): Promise<void> {
|
}
|
||||||
console.log(`[Database] Creating ${diffs.length} diffs from Service Worker scan`);
|
|
||||||
try {
|
private handleAddObjectResponse = async (event: MessageEvent) => {
|
||||||
await this.saveDiffs(diffs);
|
const data = event.data;
|
||||||
console.log('[Database] Diffs created successfully');
|
console.log('Received response from service worker (ADD_OBJECT):', data);
|
||||||
} catch (error) {
|
const service = await Services.getInstance();
|
||||||
console.error('[Database] Error creating diffs:', error);
|
if (data.type === 'NOTIFICATIONS') {
|
||||||
}
|
service.setNotifications(data.data);
|
||||||
}
|
} else if (data.type === 'TO_DOWNLOAD') {
|
||||||
|
console.log(`Received missing data ${data}`);
|
||||||
private async handleDownloadList(downloadList: string[]): Promise<void> {
|
// Download the missing data
|
||||||
let requestedStateId: string[] = [];
|
let requestedStateId = [];
|
||||||
const service = await Services.getInstance();
|
for (const hash of data.data) {
|
||||||
for (const hash of downloadList) {
|
try {
|
||||||
const diff = await service.getDiffByValue(hash);
|
const valueBytes = await service.fetchValueFromStorage(hash);
|
||||||
if (!diff) {
|
if (valueBytes) {
|
||||||
console.warn(`Missing a diff for hash ${hash}`);
|
// Save data to db
|
||||||
continue;
|
const blob = new Blob([valueBytes], {type: "application/octet-stream"});
|
||||||
}
|
await service.saveBlobToDb(hash, blob);
|
||||||
const processId = diff.process_id;
|
} else {
|
||||||
const stateId = diff.state_id;
|
// We first request the data from managers
|
||||||
const roles = diff.roles;
|
console.log('Request data from managers of the process');
|
||||||
try {
|
// get the diff from db
|
||||||
const valueBytes = await service.fetchValueFromStorage(hash);
|
const diff = await service.getDiffByValue(hash);
|
||||||
if (valueBytes) {
|
const processId = diff.process_id;
|
||||||
const blob = new Blob([valueBytes], { type: 'application/octet-stream' });
|
const stateId = diff.state_id;
|
||||||
await service.saveBlobToDb(hash, blob);
|
const roles = diff.roles;
|
||||||
document.dispatchEvent(
|
if (!requestedStateId.includes(stateId)) {
|
||||||
new CustomEvent('newDataReceived', {
|
await service.requestDataFromPeers(processId, [stateId], [roles]);
|
||||||
detail: { processId, stateId, hash },
|
requestedStateId.push(stateId);
|
||||||
}),
|
}
|
||||||
);
|
}
|
||||||
} else {
|
} catch (e) {
|
||||||
console.log('Request data from managers of the process');
|
console.error(e);
|
||||||
if (!requestedStateId.includes(stateId)) {
|
}
|
||||||
await service.requestDataFromPeers(processId, [stateId], [roles]);
|
}
|
||||||
requestedStateId.push(stateId);
|
}
|
||||||
}
|
};
|
||||||
}
|
|
||||||
} catch (e) {
|
private handleGetObjectResponse = (event: MessageEvent) => {
|
||||||
console.error(e);
|
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
|
||||||
// GENERIC INDEXEDDB OPERATIONS
|
if (!this.serviceWorkerRegistration) {
|
||||||
// ============================================
|
// console.warn('Service worker registration is not ready. Waiting...');
|
||||||
|
this.serviceWorkerRegistration = await navigator.serviceWorker.ready;
|
||||||
public async getStoreList(): Promise<{ [key: string]: string }> {
|
}
|
||||||
return this.sendMessageToWorker('GET_STORE_LIST', {});
|
|
||||||
}
|
const activeWorker = await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration);
|
||||||
|
|
||||||
public async addObject(payload: { storeName: string; object: any; key: any }): Promise<void> {
|
// Create a message channel for communication
|
||||||
await this.sendMessageToWorker('ADD_OBJECT', payload);
|
const messageChannel = new MessageChannel();
|
||||||
}
|
|
||||||
|
// Handle the response from the service worker
|
||||||
public async batchWriting(payload: { storeName: string; objects: { key: any; object: any }[] }): Promise<void> {
|
messageChannel.port1.onmessage = (event) => {
|
||||||
await this.sendMessageToWorker('BATCH_WRITING', payload);
|
if (event.data.status === 'success') {
|
||||||
}
|
resolve();
|
||||||
|
} else {
|
||||||
public async getObject(storeName: string, key: string): Promise<any | null> {
|
const error = event.data.message;
|
||||||
return this.sendMessageToWorker('GET_OBJECT', { storeName, key });
|
reject(new Error(error || 'Unknown error occurred while adding object'));
|
||||||
}
|
}
|
||||||
|
};
|
||||||
public async dumpStore(storeName: string): Promise<Record<string, any>> {
|
|
||||||
return this.sendMessageToWorker('DUMP_STORE', { storeName });
|
// Send the add object request to the service worker
|
||||||
}
|
try {
|
||||||
|
activeWorker?.postMessage(
|
||||||
public async deleteObject(storeName: string, key: string): Promise<void> {
|
{
|
||||||
await this.sendMessageToWorker('DELETE_OBJECT', { storeName, key });
|
type: 'ADD_OBJECT',
|
||||||
}
|
payload,
|
||||||
|
},
|
||||||
public async clearStore(storeName: string): Promise<void> {
|
[messageChannel.port2],
|
||||||
await this.sendMessageToWorker('CLEAR_STORE', { storeName });
|
);
|
||||||
}
|
} catch (error) {
|
||||||
|
reject(new Error(`Failed to send message to service worker: ${error}`));
|
||||||
public async requestStoreByIndex(storeName: string, indexName: string, request: string): Promise<any[]> {
|
}
|
||||||
return this.sendMessageToWorker('REQUEST_STORE_BY_INDEX', { storeName, indexName, request });
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async clearMultipleStores(storeNames: string[]): Promise<void> {
|
public async getObject(storeName: string, key: string): Promise<any | null> {
|
||||||
for (const storeName of storeNames) {
|
const db = await this.getDb();
|
||||||
await this.clearStore(storeName);
|
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);
|
||||||
// BUSINESS METHODS - DEVICE
|
getRequest.onerror = () => reject(getRequest.error);
|
||||||
// ============================================
|
});
|
||||||
|
return result;
|
||||||
public async saveDevice(device: any): Promise<void> {
|
}
|
||||||
try {
|
|
||||||
const existing = await this.getObject('wallet', '1');
|
public async dumpStore(storeName: string): Promise<Record<string, any>> {
|
||||||
if (existing) {
|
const db = await this.getDb();
|
||||||
await this.deleteObject('wallet', '1');
|
const tx = db.transaction(storeName, 'readonly');
|
||||||
}
|
const store = tx.objectStore(storeName);
|
||||||
} catch (e) {}
|
|
||||||
|
try {
|
||||||
await this.addObject({
|
// Wait for both getAllKeys() and getAll() to resolve
|
||||||
storeName: 'wallet',
|
const [keys, values] = await Promise.all([
|
||||||
object: { pre_id: '1', device },
|
new Promise<any[]>((resolve, reject) => {
|
||||||
key: null,
|
const request = store.getAllKeys();
|
||||||
});
|
request.onsuccess = () => resolve(request.result);
|
||||||
}
|
request.onerror = () => reject(request.error);
|
||||||
|
}),
|
||||||
public async getDevice(): Promise<any | null> {
|
new Promise<any[]>((resolve, reject) => {
|
||||||
const result = await this.getObject('wallet', '1');
|
const request = store.getAll();
|
||||||
return result ? result['device'] : null;
|
request.onsuccess = () => resolve(request.result);
|
||||||
}
|
request.onerror = () => reject(request.error);
|
||||||
|
}),
|
||||||
// ============================================
|
]);
|
||||||
// BUSINESS METHODS - PROCESS
|
|
||||||
// ============================================
|
// Combine keys and values into an object
|
||||||
|
const result: Record<string, any> = Object.fromEntries(keys.map((key, index) => [key, values[index]]));
|
||||||
public async saveProcess(processId: string, process: any): Promise<void> {
|
return result;
|
||||||
await this.addObject({
|
} catch (error) {
|
||||||
storeName: 'processes',
|
console.error('Error fetching data from IndexedDB:', error);
|
||||||
object: process,
|
throw error;
|
||||||
key: processId,
|
}
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
public async deleteObject(storeName: string, key: string): Promise<void> {
|
||||||
public async saveProcessesBatch(processes: Record<string, any>): Promise<void> {
|
const db = await this.getDb();
|
||||||
if (Object.keys(processes).length === 0) return;
|
const tx = db.transaction(storeName, 'readwrite');
|
||||||
|
const store = tx.objectStore(storeName);
|
||||||
await this.batchWriting({
|
try {
|
||||||
storeName: 'processes',
|
await new Promise((resolve, reject) => {
|
||||||
objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })),
|
const getRequest = store.delete(key);
|
||||||
});
|
getRequest.onsuccess = () => resolve(getRequest.result);
|
||||||
}
|
getRequest.onerror = () => reject(getRequest.error);
|
||||||
|
});
|
||||||
public async getProcess(processId: string): Promise<any | null> {
|
} catch (e) {
|
||||||
return this.getObject('processes', processId);
|
throw e;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public async getAllProcesses(): Promise<Record<string, any>> {
|
|
||||||
return this.dumpStore('processes');
|
public async clearStore(storeName: string): Promise<void> {
|
||||||
}
|
const db = await this.getDb();
|
||||||
|
const tx = db.transaction(storeName, 'readwrite');
|
||||||
// ============================================
|
const store = tx.objectStore(storeName);
|
||||||
// BUSINESS METHODS - BLOBS
|
try {
|
||||||
// ============================================
|
await new Promise((resolve, reject) => {
|
||||||
|
const clearRequest = store.clear();
|
||||||
public async saveBlob(hash: string, data: Blob): Promise<void> {
|
clearRequest.onsuccess = () => resolve(clearRequest.result);
|
||||||
await this.addObject({
|
clearRequest.onerror = () => reject(clearRequest.error);
|
||||||
storeName: 'data',
|
});
|
||||||
object: data,
|
} catch (e) {
|
||||||
key: hash,
|
throw e;
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getBlob(hash: string): Promise<Blob | null> {
|
// Request a store by index
|
||||||
return this.getObject('data', hash);
|
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);
|
||||||
// BUSINESS METHODS - DIFFS
|
const index = store.index(indexName);
|
||||||
// ============================================
|
|
||||||
|
try {
|
||||||
public async saveDiffs(diffs: any[]): Promise<void> {
|
return new Promise((resolve, reject) => {
|
||||||
if (diffs.length === 0) return;
|
const getAllRequest = index.getAll(request);
|
||||||
|
getAllRequest.onsuccess = () => {
|
||||||
for (const diff of diffs) {
|
const allItems = getAllRequest.result;
|
||||||
await this.addObject({
|
const filtered = allItems.filter(item => item.state_id === request);
|
||||||
storeName: 'diffs',
|
resolve(filtered);
|
||||||
object: diff,
|
};
|
||||||
key: null,
|
getAllRequest.onerror = () => reject(getAllRequest.error);
|
||||||
});
|
});
|
||||||
}
|
} catch (e) {
|
||||||
}
|
throw e;
|
||||||
|
}
|
||||||
public async getDiff(hash: string): Promise<any | null> {
|
}
|
||||||
return this.getObject('diffs', hash);
|
}
|
||||||
}
|
|
||||||
|
export default Database;
|
||||||
public async getAllDiffs(): Promise<Record<string, any>> {
|
|
||||||
return this.dumpStore('diffs');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// BUSINESS METHODS - SECRETS
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
public async getSharedSecret(address: string): Promise<string | null> {
|
|
||||||
return this.getObject('shared_secrets', address);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async saveSecretsBatch(unconfirmedSecrets: any[], sharedSecrets: { key: string; value: any }[]): Promise<void> {
|
|
||||||
if (unconfirmedSecrets && unconfirmedSecrets.length > 0) {
|
|
||||||
for (const secret of unconfirmedSecrets) {
|
|
||||||
await this.addObject({
|
|
||||||
storeName: 'unconfirmed_secrets',
|
|
||||||
object: secret,
|
|
||||||
key: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sharedSecrets && sharedSecrets.length > 0) {
|
|
||||||
for (const { key, value } of sharedSecrets) {
|
|
||||||
await this.addObject({
|
|
||||||
storeName: 'shared_secrets',
|
|
||||||
object: value,
|
|
||||||
key: key,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getAllSecrets(): Promise<{ shared_secrets: Record<string, any>; unconfirmed_secrets: any[] }> {
|
|
||||||
const sharedSecrets = await this.dumpStore('shared_secrets');
|
|
||||||
const unconfirmedSecrets = await this.dumpStore('unconfirmed_secrets');
|
|
||||||
|
|
||||||
return {
|
|
||||||
shared_secrets: sharedSecrets,
|
|
||||||
unconfirmed_secrets: Object.values(unconfirmedSecrets),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Database;
|
|
||||||
|
|||||||
@ -1,577 +0,0 @@
|
|||||||
import { MessageType } from '../types/index';
|
|
||||||
import Services from './service';
|
|
||||||
import TokenService from './token';
|
|
||||||
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; // <--- VERROU
|
|
||||||
|
|
||||||
static async init() {
|
|
||||||
if (this.isInitialized) return; // On sort si déjà lancé
|
|
||||||
|
|
||||||
// On ne lance l'écoute que si on est dans une iframe
|
|
||||||
if (window.self !== window.top) {
|
|
||||||
console.log('[IframeController] 📡 Mode Iframe détecté. Démarrage des listeners API...');
|
|
||||||
await IframeController.registerAllListeners();
|
|
||||||
} else {
|
|
||||||
console.log("[IframeController] ℹ️ Mode Standalone (pas d'iframe). Listeners API inactifs.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async registerAllListeners() {
|
|
||||||
console.log('[Router:API] 🎧 Enregistrement des gestionnaires de messages (postMessage)...');
|
|
||||||
const services = await Services.getInstance();
|
|
||||||
const tokenService = await TokenService.getInstance();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fonction centralisée pour envoyer des réponses d'erreur à la fenêtre parente (l'application A).
|
|
||||||
*/
|
|
||||||
const errorResponse = (errorMsg: string, origin: string, messageId?: string) => {
|
|
||||||
console.error(`[Router:API] 📤 Envoi Erreur: ${errorMsg} (Origine: ${origin}, MsgID: ${messageId})`);
|
|
||||||
window.parent.postMessage(
|
|
||||||
{
|
|
||||||
type: MessageType.ERROR,
|
|
||||||
error: errorMsg,
|
|
||||||
messageId,
|
|
||||||
},
|
|
||||||
origin,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper pour vérifier le token avant chaque action sensible
|
|
||||||
const withToken = async (event: MessageEvent, action: () => Promise<void>) => {
|
|
||||||
const { accessToken } = event.data;
|
|
||||||
// On vérifie si le token est présent ET valide pour l'origine de l'iframe
|
|
||||||
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
|
||||||
throw new Error('Invalid or expired session token');
|
|
||||||
}
|
|
||||||
// Si tout est bon, on exécute l'action
|
|
||||||
await action();
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Définitions des gestionnaires (Handlers) ---
|
|
||||||
|
|
||||||
const handleRequestLink = async (event: MessageEvent) => {
|
|
||||||
console.log(`[Router:API] 📨 Message ${MessageType.REQUEST_LINK} reçu de ${event.origin}`);
|
|
||||||
|
|
||||||
// 1. Vérifier si l'appareil est DÉJÀ appairé (cas de la 2ème connexion)
|
|
||||||
const device = await services.getDeviceFromDatabase();
|
|
||||||
|
|
||||||
if (device && device.pairing_process_commitment) {
|
|
||||||
console.log("[Router:API] Appareil déjà appairé. Pas besoin d'attendre home.ts.");
|
|
||||||
// On saute l'attente et on passe directement à la suite.
|
|
||||||
} else {
|
|
||||||
// 2. Cas de la 1ère connexion (appareil non appairé)
|
|
||||||
// On doit attendre que home.ts (auto-pairing) ait fini son travail.
|
|
||||||
console.log('[Router:API] Appareil non appairé. En attente du feu vert de home.ts...');
|
|
||||||
const maxWait = 5000; // 5 sec
|
|
||||||
let waited = 0;
|
|
||||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
|
|
||||||
// On attend le drapeau global
|
|
||||||
while (!(window as any).__PAIRING_READY && waited < maxWait) {
|
|
||||||
await delay(100);
|
|
||||||
waited += 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Vérifier le résultat de l'attente
|
|
||||||
if ((window as any).__PAIRING_READY === 'error') {
|
|
||||||
throw new Error('Auto-pairing failed');
|
|
||||||
}
|
|
||||||
if (!(window as any).__PAIRING_READY) {
|
|
||||||
throw new Error('Auto-pairing timed out');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Router:API] Feu vert de home.ts reçu !`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Router:API] Traitement de la liaison...`);
|
|
||||||
const result = true; // Auto-confirmation
|
|
||||||
|
|
||||||
const tokens = await tokenService.generateSessionToken(event.origin);
|
|
||||||
window.parent.postMessage(
|
|
||||||
{
|
|
||||||
type: MessageType.LINK_ACCEPTED,
|
|
||||||
accessToken: tokens.accessToken,
|
|
||||||
refreshToken: tokens.refreshToken,
|
|
||||||
messageId: event.data.messageId,
|
|
||||||
},
|
|
||||||
event.origin,
|
|
||||||
);
|
|
||||||
console.log(`[Router:API] ✅ ${MessageType.REQUEST_LINK} accepté et jetons envoyés.`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreatePairing = async (event: MessageEvent) => {
|
|
||||||
console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PAIRING} reçu`);
|
|
||||||
|
|
||||||
if (services.isPaired()) {
|
|
||||||
throw new Error('Device already paired — ignoring CREATE_PAIRING request');
|
|
||||||
}
|
|
||||||
|
|
||||||
await withToken(event, async () => {
|
|
||||||
console.log("[Router:API] 🚀 Démarrage du processus d'appairage...");
|
|
||||||
|
|
||||||
const myAddress = services.getDeviceAddress();
|
|
||||||
console.log('[Router:API] 1/7: Création du processus de pairing...');
|
|
||||||
const createPairingProcessReturn = await services.createPairingProcess('', [myAddress]);
|
|
||||||
|
|
||||||
const pairingId = createPairingProcessReturn.updated_process?.process_id;
|
|
||||||
const stateId = createPairingProcessReturn.updated_process?.current_process?.states[0]?.state_id as string;
|
|
||||||
if (!pairingId || !stateId) {
|
|
||||||
throw new Error('Pairing process creation failed to return valid IDs');
|
|
||||||
}
|
|
||||||
console.log(`[Router:API] 2/7: Processus ${pairingId} créé.`);
|
|
||||||
|
|
||||||
console.log("[Router:API] 3/7: Enregistrement local de l'appareil...");
|
|
||||||
services.pairDevice(pairingId, [myAddress]);
|
|
||||||
|
|
||||||
console.log('[Router:API] 4/7: Traitement du retour (handleApiReturn)...');
|
|
||||||
await services.handleApiReturn(createPairingProcessReturn);
|
|
||||||
|
|
||||||
console.log('[Router:API] 5/7: Création de la mise à jour PRD...');
|
|
||||||
const createPrdUpdateReturn = await services.createPrdUpdate(pairingId, stateId);
|
|
||||||
await services.handleApiReturn(createPrdUpdateReturn);
|
|
||||||
|
|
||||||
console.log('[Router:API] 6/7: Approbation du changement...');
|
|
||||||
const approveChangeReturn = await services.approveChange(pairingId, stateId);
|
|
||||||
await services.handleApiReturn(approveChangeReturn);
|
|
||||||
|
|
||||||
console.log('[Router:API] 7/7: Confirmation finale du pairing...');
|
|
||||||
await services.confirmPairing();
|
|
||||||
|
|
||||||
console.log('[Router:API] 🎉 Appairage terminé avec succès !');
|
|
||||||
|
|
||||||
const successMsg = {
|
|
||||||
type: MessageType.PAIRING_CREATED,
|
|
||||||
pairingId,
|
|
||||||
messageId: event.data.messageId,
|
|
||||||
};
|
|
||||||
window.parent.postMessage(successMsg, event.origin);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGetMyProcesses = async (event: MessageEvent) => {
|
|
||||||
console.log(`[Router:API] 📨 Message ${MessageType.GET_MY_PROCESSES} reçu`);
|
|
||||||
if (!services.isPaired()) throw new Error('Device not paired');
|
|
||||||
|
|
||||||
await withToken(event, async () => {
|
|
||||||
const myProcesses = await services.getMyProcesses();
|
|
||||||
|
|
||||||
window.parent.postMessage(
|
|
||||||
{
|
|
||||||
type: MessageType.GET_MY_PROCESSES,
|
|
||||||
myProcesses,
|
|
||||||
messageId: event.data.messageId,
|
|
||||||
},
|
|
||||||
event.origin,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGetProcesses = async (event: MessageEvent) => {
|
|
||||||
console.log(`[Router:API] 📨 Message ${MessageType.GET_PROCESSES} reçu`);
|
|
||||||
if (!services.isPaired()) throw new Error('Device not paired');
|
|
||||||
|
|
||||||
await withToken(event, async () => {
|
|
||||||
const processes = await services.getProcesses();
|
|
||||||
|
|
||||||
window.parent.postMessage(
|
|
||||||
{
|
|
||||||
type: MessageType.PROCESSES_RETRIEVED,
|
|
||||||
processes,
|
|
||||||
messageId: event.data.messageId,
|
|
||||||
},
|
|
||||||
event.origin,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDecryptState = async (event: MessageEvent) => {
|
|
||||||
console.log(`[Router:API] 📨 Message ${MessageType.RETRIEVE_DATA} reçu`);
|
|
||||||
if (!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 = services.getStateFromId(process, stateId);
|
|
||||||
if (!state) throw new Error(`Unknown state ${stateId} for process ${processId}`);
|
|
||||||
|
|
||||||
console.log(`[Router:API] 🔐 Démarrage du déchiffrement pour ${processId}`);
|
|
||||||
await services.ensureConnections(process, stateId);
|
|
||||||
|
|
||||||
const res: Record<string, any> = {};
|
|
||||||
for (const attribute of Object.keys(state.pcd_commitment)) {
|
|
||||||
if (attribute === 'roles' || (state.public_data && state.public_data[attribute])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const decryptedAttribute = await services.decryptAttribute(processId, state, attribute);
|
|
||||||
if (decryptedAttribute) {
|
|
||||||
res[attribute] = decryptedAttribute;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(`[Router:API] ✅ Déchiffrement terminé pour ${processId}. ${Object.keys(res).length} attribut(s) déchiffré(s).`);
|
|
||||||
|
|
||||||
window.parent.postMessage(
|
|
||||||
{
|
|
||||||
type: MessageType.DATA_RETRIEVED,
|
|
||||||
data: res,
|
|
||||||
messageId: event.data.messageId,
|
|
||||||
},
|
|
||||||
event.origin,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleValidateToken = async (event: MessageEvent) => {
|
|
||||||
console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_TOKEN} reçu`);
|
|
||||||
const accessToken = event.data.accessToken;
|
|
||||||
const refreshToken = event.data.refreshToken;
|
|
||||||
if (!accessToken || !refreshToken) {
|
|
||||||
throw new Error('Missing access, refresh token or both');
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = await tokenService.validateToken(accessToken, event.origin);
|
|
||||||
console.log(`[Router:API] 🔑 Validation Jeton: ${isValid}`);
|
|
||||||
window.parent.postMessage(
|
|
||||||
{
|
|
||||||
type: MessageType.VALIDATE_TOKEN,
|
|
||||||
accessToken: accessToken,
|
|
||||||
refreshToken: refreshToken,
|
|
||||||
isValid: isValid,
|
|
||||||
messageId: event.data.messageId,
|
|
||||||
},
|
|
||||||
event.origin,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRenewToken = async (event: MessageEvent) => {
|
|
||||||
console.log(`[Router:API] 📨 Message ${MessageType.RENEW_TOKEN} reçu`);
|
|
||||||
const refreshToken = event.data.refreshToken;
|
|
||||||
if (!refreshToken) throw new Error('No refresh token provided');
|
|
||||||
|
|
||||||
const newAccessToken = await tokenService.refreshAccessToken(refreshToken, event.origin);
|
|
||||||
if (!newAccessToken) throw new Error('Failed to refresh token (invalid refresh token)');
|
|
||||||
|
|
||||||
console.log(`[Router:API] 🔑 Jeton d'accès renouvelé.`);
|
|
||||||
window.parent.postMessage(
|
|
||||||
{
|
|
||||||
type: MessageType.RENEW_TOKEN,
|
|
||||||
accessToken: newAccessToken,
|
|
||||||
refreshToken: refreshToken,
|
|
||||||
messageId: event.data.messageId,
|
|
||||||
},
|
|
||||||
event.origin,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGetPairingId = async (event: MessageEvent) => {
|
|
||||||
console.log(`[Router:API] 📨 Message ${MessageType.GET_PAIRING_ID} reçu`);
|
|
||||||
|
|
||||||
const maxRetries = 10;
|
|
||||||
const retryDelay = 300;
|
|
||||||
let pairingId: string | null = null;
|
|
||||||
|
|
||||||
// Boucle de polling
|
|
||||||
for (let i = 0; i < maxRetries; i++) {
|
|
||||||
// On lit DIRECTEMENT la BDD (la "source de vérité")
|
|
||||||
const device = await services.getDeviceFromDatabase();
|
|
||||||
|
|
||||||
// On vérifie si l'ID est maintenant présent dans la BDD
|
|
||||||
if (device && device.pairing_process_commitment) {
|
|
||||||
// SUCCÈS ! L'ID est dans la BDD
|
|
||||||
pairingId = device.pairing_process_commitment;
|
|
||||||
console.log(`[Router:API] GET_PAIRING_ID: ID trouvé en BDD (tentative ${i + 1}/${maxRetries})`);
|
|
||||||
break; // On sort de la boucle
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si non trouvé, on patiente
|
|
||||||
console.warn(`[Router:API] GET_PAIRING_ID: Non trouvé en BDD, nouvelle tentative... (${i + 1}/${maxRetries})`);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si la boucle se termine sans succès
|
|
||||||
if (!pairingId) {
|
|
||||||
console.error(`[Router:API] GET_PAIRING_ID: Échec final, non trouvé en BDD après ${maxRetries} tentatives.`);
|
|
||||||
throw new Error('Device not paired');
|
|
||||||
}
|
|
||||||
|
|
||||||
await withToken(event, async () => {
|
|
||||||
window.parent.postMessage(
|
|
||||||
{
|
|
||||||
type: MessageType.GET_PAIRING_ID,
|
|
||||||
userPairingId: pairingId,
|
|
||||||
messageId: event.data.messageId,
|
|
||||||
},
|
|
||||||
event.origin,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateProcess = async (event: MessageEvent) => {
|
|
||||||
console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PROCESS} reçu`);
|
|
||||||
if (!services.isPaired()) throw new Error('Device not paired');
|
|
||||||
|
|
||||||
const { processData, privateFields, roles } = event.data;
|
|
||||||
|
|
||||||
await withToken(event, async () => {
|
|
||||||
console.log('[Router:API] 🚀 Démarrage de la création de processus standard...');
|
|
||||||
const { privateData, publicData } = splitPrivateData(processData, privateFields);
|
|
||||||
|
|
||||||
console.log('[Router:API] 1/2: Création du processus...');
|
|
||||||
const createProcessReturn = await services.createProcess(privateData, publicData, roles);
|
|
||||||
if (!createProcessReturn.updated_process) {
|
|
||||||
throw new Error('Empty updated_process in createProcessReturn');
|
|
||||||
}
|
|
||||||
|
|
||||||
const processId = createProcessReturn.updated_process.process_id;
|
|
||||||
const process = createProcessReturn.updated_process.current_process;
|
|
||||||
const stateId = process.states[0].state_id;
|
|
||||||
console.log(`[Router:API] 2/2: Processus ${processId} créé. Traitement...`);
|
|
||||||
await services.handleApiReturn(createProcessReturn);
|
|
||||||
|
|
||||||
console.log(`[Router:API] 🎉 Processus ${processId} créé.`);
|
|
||||||
|
|
||||||
const res = {
|
|
||||||
processId,
|
|
||||||
process,
|
|
||||||
processData,
|
|
||||||
};
|
|
||||||
|
|
||||||
window.parent.postMessage(
|
|
||||||
{
|
|
||||||
type: MessageType.PROCESS_CREATED,
|
|
||||||
processCreated: res,
|
|
||||||
messageId: event.data.messageId,
|
|
||||||
},
|
|
||||||
event.origin,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNotifyUpdate = async (event: MessageEvent) => {
|
|
||||||
console.log(`[Router:API] 📨 Message ${MessageType.NOTIFY_UPDATE} reçu`);
|
|
||||||
if (!services.isPaired()) throw new Error('Device not paired');
|
|
||||||
|
|
||||||
const { processId, stateId } = event.data;
|
|
||||||
|
|
||||||
await withToken(event, async () => {
|
|
||||||
if (!isValid32ByteHex(stateId)) throw new Error('Invalid state id');
|
|
||||||
|
|
||||||
const res = await services.createPrdUpdate(processId, stateId);
|
|
||||||
await services.handleApiReturn(res);
|
|
||||||
|
|
||||||
window.parent.postMessage(
|
|
||||||
{
|
|
||||||
type: MessageType.UPDATE_NOTIFIED,
|
|
||||||
messageId: event.data.messageId,
|
|
||||||
},
|
|
||||||
event.origin,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleValidateState = async (event: MessageEvent) => {
|
|
||||||
console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_STATE} reçu`);
|
|
||||||
if (!services.isPaired()) throw new Error('Device not paired');
|
|
||||||
|
|
||||||
const { processId, stateId } = event.data;
|
|
||||||
|
|
||||||
await withToken(event, async () => {
|
|
||||||
const res = await services.approveChange(processId, stateId);
|
|
||||||
await services.handleApiReturn(res);
|
|
||||||
|
|
||||||
window.parent.postMessage(
|
|
||||||
{
|
|
||||||
type: MessageType.STATE_VALIDATED,
|
|
||||||
validatedProcess: res.updated_process,
|
|
||||||
messageId: event.data.messageId,
|
|
||||||
},
|
|
||||||
event.origin,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdateProcess = async (event: MessageEvent) => {
|
|
||||||
console.log(`[Router:API] 📨 Message ${MessageType.UPDATE_PROCESS} reçu`);
|
|
||||||
if (!services.isPaired()) throw new Error('Device not paired');
|
|
||||||
|
|
||||||
const { processId, newData, privateFields, roles } = event.data;
|
|
||||||
|
|
||||||
await withToken(event, async () => {
|
|
||||||
console.log(`[Router:API] 🔄 Transfert de la mise à jour de ${processId} au service...`);
|
|
||||||
|
|
||||||
// Le service gère maintenant tout : récupération, réparation d'état, et mise à jour.
|
|
||||||
const res = await services.updateProcess(processId, newData, privateFields, roles);
|
|
||||||
|
|
||||||
// Nous appelons handleApiReturn ici, comme avant.
|
|
||||||
await services.handleApiReturn(res);
|
|
||||||
// --- FIN DE LA MODIFICATION ---
|
|
||||||
|
|
||||||
window.parent.postMessage(
|
|
||||||
{
|
|
||||||
type: MessageType.PROCESS_UPDATED,
|
|
||||||
updatedProcess: res.updated_process, // res vient directement de l'appel service
|
|
||||||
messageId: event.data.messageId,
|
|
||||||
},
|
|
||||||
event.origin,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDecodePublicData = async (event: MessageEvent) => {
|
|
||||||
console.log(`[Router:API] 📨 Message ${MessageType.DECODE_PUBLIC_DATA} reçu`);
|
|
||||||
if (!services.isPaired()) throw new Error('Device not paired');
|
|
||||||
|
|
||||||
const { encodedData } = event.data;
|
|
||||||
|
|
||||||
await withToken(event, async () => {
|
|
||||||
const decodedData = services.decodeValue(encodedData);
|
|
||||||
window.parent.postMessage(
|
|
||||||
{
|
|
||||||
type: MessageType.PUBLIC_DATA_DECODED,
|
|
||||||
decodedData,
|
|
||||||
messageId: event.data.messageId,
|
|
||||||
},
|
|
||||||
event.origin,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHashValue = async (event: MessageEvent) => {
|
|
||||||
console.log(`[Router:API] 📨 Message ${MessageType.HASH_VALUE} reçu`);
|
|
||||||
const { commitedIn, label, fileBlob } = event.data;
|
|
||||||
|
|
||||||
await withToken(event, async () => {
|
|
||||||
const hash = services.getHashForFile(commitedIn, label, fileBlob);
|
|
||||||
window.parent.postMessage(
|
|
||||||
{
|
|
||||||
type: MessageType.VALUE_HASHED,
|
|
||||||
hash,
|
|
||||||
messageId: event.data.messageId,
|
|
||||||
},
|
|
||||||
event.origin,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGetMerkleProof = async (event: MessageEvent) => {
|
|
||||||
console.log(`[Router:API] 📨 Message ${MessageType.GET_MERKLE_PROOF} reçu`);
|
|
||||||
const { processState, attributeName } = event.data;
|
|
||||||
|
|
||||||
await withToken(event, async () => {
|
|
||||||
const proof = services.getMerkleProofForFile(processState, attributeName);
|
|
||||||
window.parent.postMessage(
|
|
||||||
{
|
|
||||||
type: MessageType.MERKLE_PROOF_RETRIEVED,
|
|
||||||
proof,
|
|
||||||
messageId: event.data.messageId,
|
|
||||||
},
|
|
||||||
event.origin,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleValidateMerkleProof = async (event: MessageEvent) => {
|
|
||||||
console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_MERKLE_PROOF} reçu`);
|
|
||||||
const { merkleProof, documentHash } = event.data;
|
|
||||||
|
|
||||||
await withToken(event, async () => {
|
|
||||||
let parsedMerkleProof: MerkleProofResult;
|
|
||||||
try {
|
|
||||||
parsedMerkleProof = JSON.parse(merkleProof);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error('Provided merkleProof is not a valid json object');
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = services.validateMerkleProof(parsedMerkleProof, documentHash);
|
|
||||||
window.parent.postMessage(
|
|
||||||
{
|
|
||||||
type: MessageType.MERKLE_PROOF_VALIDATED,
|
|
||||||
isValid: res,
|
|
||||||
messageId: event.data.messageId,
|
|
||||||
},
|
|
||||||
event.origin,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Le "Switchyard" : il reçoit tous les messages et les dispatche ---
|
|
||||||
|
|
||||||
window.removeEventListener('message', handleMessage);
|
|
||||||
window.addEventListener('message', handleMessage);
|
|
||||||
|
|
||||||
async function handleMessage(event: MessageEvent) {
|
|
||||||
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.VALIDATE_TOKEN:
|
|
||||||
await handleValidateToken(event);
|
|
||||||
break;
|
|
||||||
case MessageType.RENEW_TOKEN:
|
|
||||||
await handleRenewToken(event);
|
|
||||||
break;
|
|
||||||
case MessageType.GET_PAIRING_ID:
|
|
||||||
await handleGetPairingId(event);
|
|
||||||
break;
|
|
||||||
case MessageType.CREATE_PROCESS:
|
|
||||||
await handleCreateProcess(event);
|
|
||||||
break;
|
|
||||||
case MessageType.NOTIFY_UPDATE:
|
|
||||||
await handleNotifyUpdate(event);
|
|
||||||
break;
|
|
||||||
case MessageType.VALIDATE_STATE:
|
|
||||||
await handleValidateState(event);
|
|
||||||
break;
|
|
||||||
case MessageType.UPDATE_PROCESS:
|
|
||||||
await handleUpdateProcess(event);
|
|
||||||
break;
|
|
||||||
case MessageType.DECODE_PUBLIC_DATA:
|
|
||||||
await handleDecodePublicData(event);
|
|
||||||
break;
|
|
||||||
case MessageType.HASH_VALUE:
|
|
||||||
await handleHashValue(event);
|
|
||||||
break;
|
|
||||||
case MessageType.GET_MERKLE_PROOF:
|
|
||||||
await handleGetMerkleProof(event);
|
|
||||||
break;
|
|
||||||
case MessageType.VALIDATE_MERKLE_PROOF:
|
|
||||||
await handleValidateMerkleProof(event);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.warn('[Router:API] ⚠️ Message non géré reçu:', event.data);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
const errorMsg = `[Router:API] 💥 Erreur de haut niveau: ${error}`;
|
|
||||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.parent.postMessage(
|
|
||||||
{
|
|
||||||
type: MessageType.LISTENING,
|
|
||||||
},
|
|
||||||
'*',
|
|
||||||
);
|
|
||||||
console.log('[Router:API] ✅ Tous les listeners sont actifs. Envoi du message LISTENING au parent.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
260
src/services/modal.service.ts
Executable file
260
src/services/modal.service.ts
Executable file
@ -0,0 +1,260 @@
|
|||||||
|
import modalHtml from '../components/login-modal/login-modal.html?raw';
|
||||||
|
import modalScript from '../components/login-modal/login-modal.js?raw';
|
||||||
|
import validationModalStyle from '../components/validation-modal/validation-modal.css?raw';
|
||||||
|
import Services from './service';
|
||||||
|
import { init, navigate } from '../router';
|
||||||
|
import { addressToEmoji } from '../utils/sp-address.utils';
|
||||||
|
import { RoleDefinition } from 'pkg/sdk_client';
|
||||||
|
import { initValidationModal } from '~/components/validation-modal/validation-modal';
|
||||||
|
import { interpolate } from '~/utils/html.utils';
|
||||||
|
|
||||||
|
export default class ModalService {
|
||||||
|
private static instance: ModalService;
|
||||||
|
private stateId: string | null = null;
|
||||||
|
private processId: string | null = null;
|
||||||
|
private constructor() {}
|
||||||
|
private paired_addresses: string[] = [];
|
||||||
|
private modal: HTMLElement | null = null;
|
||||||
|
|
||||||
|
// Method to access the singleton instance of Services
|
||||||
|
public static async getInstance(): Promise<ModalService> {
|
||||||
|
if (!ModalService.instance) {
|
||||||
|
ModalService.instance = new ModalService();
|
||||||
|
}
|
||||||
|
return ModalService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public openLoginModal(myAddress: string, receiverAddress: string) {
|
||||||
|
const container = document.querySelector('.page-container');
|
||||||
|
let html = modalHtml;
|
||||||
|
html = html.replace('{{device1}}', myAddress);
|
||||||
|
html = html.replace('{{device2}}', receiverAddress);
|
||||||
|
if (container) container.innerHTML += html;
|
||||||
|
const modal = document.getElementById('login-modal');
|
||||||
|
if (modal) modal.style.display = 'flex';
|
||||||
|
const newScript = document.createElement('script');
|
||||||
|
|
||||||
|
newScript.setAttribute('type', 'module');
|
||||||
|
newScript.textContent = modalScript;
|
||||||
|
document.head.appendChild(newScript).parentNode?.removeChild(newScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
async injectModal(members: any[]) {
|
||||||
|
const container = document.querySelector('#containerId');
|
||||||
|
if (container) {
|
||||||
|
let html = await fetch('/src/components/modal/confirmation-modal.html').then((res) => res.text());
|
||||||
|
html = html.replace('{{device1}}', await addressToEmoji(members[0]['sp_addresses'][0]));
|
||||||
|
html = html.replace('{{device2}}', await addressToEmoji(members[0]['sp_addresses'][1]));
|
||||||
|
container.innerHTML += html;
|
||||||
|
|
||||||
|
// Dynamically load the header JS
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = '/src/components/modal/confirmation-modal.ts';
|
||||||
|
script.type = 'module';
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async injectCreationModal(members: any[]) {
|
||||||
|
const container = document.querySelector('#containerId');
|
||||||
|
if (container) {
|
||||||
|
let html = await fetch('/src/components/modal/creation-modal.html').then((res) => res.text());
|
||||||
|
html = html.replace('{{device1}}', await addressToEmoji(members[0]['sp_addresses'][0]));
|
||||||
|
container.innerHTML += html;
|
||||||
|
|
||||||
|
// Dynamically load the header JS
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = '/src/components/modal/confirmation-modal.ts';
|
||||||
|
script.type = 'module';
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Device 1 wait Device 2
|
||||||
|
async injectWaitingModal() {
|
||||||
|
const container = document.querySelector('#containerId');
|
||||||
|
if (container) {
|
||||||
|
let html = await fetch('/src/components/modal/waiting-modal.html').then((res) => res.text());
|
||||||
|
container.innerHTML += html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async injectValidationModal(processDiff: any) {
|
||||||
|
const container = document.querySelector('#containerId');
|
||||||
|
if (container) {
|
||||||
|
let html = await fetch('/src/components/validation-modal/validation-modal.html').then((res) => res.text());
|
||||||
|
html = interpolate(html, {processId: processDiff.processId})
|
||||||
|
container.innerHTML += html;
|
||||||
|
|
||||||
|
// Dynamically load the header JS
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.id = 'validation-modal-script';
|
||||||
|
script.src = '/src/components/validation-modal/validation-modal.ts';
|
||||||
|
script.type = 'module';
|
||||||
|
document.head.appendChild(script);
|
||||||
|
const css = document.createElement('style');
|
||||||
|
css.id = 'validation-modal-css';
|
||||||
|
css.innerText = validationModalStyle;
|
||||||
|
document.head.appendChild(css);
|
||||||
|
initValidationModal(processDiff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async closeValidationModal() {
|
||||||
|
const script = document.querySelector('#validation-modal-script');
|
||||||
|
const css = document.querySelector('#validation-modal-css');
|
||||||
|
const component = document.querySelector('#validation-modal');
|
||||||
|
script?.remove();
|
||||||
|
css?.remove();
|
||||||
|
component?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async openPairingConfirmationModal(roleDefinition: Record<string, RoleDefinition>, processId: string, stateId: string) {
|
||||||
|
let members;
|
||||||
|
if (roleDefinition['pairing']) {
|
||||||
|
const owner = roleDefinition['pairing'];
|
||||||
|
members = owner.members;
|
||||||
|
} else {
|
||||||
|
throw new Error('No "pairing" role');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (members.length != 1) {
|
||||||
|
throw new Error('Must have exactly 1 member');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("MEMBERS:", members);
|
||||||
|
// We take all the addresses except our own
|
||||||
|
const service = await Services.getInstance();
|
||||||
|
const localAddress = await service.getDeviceAddress();
|
||||||
|
for (const member of members) {
|
||||||
|
if (member.sp_addresses) {
|
||||||
|
for (const address of member.sp_addresses) {
|
||||||
|
if (address !== localAddress) {
|
||||||
|
this.paired_addresses.push(address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.processId = processId;
|
||||||
|
this.stateId = stateId;
|
||||||
|
|
||||||
|
if (members[0].sp_addresses.length === 1) {
|
||||||
|
await this.injectCreationModal(members);
|
||||||
|
this.modal = document.getElementById('creation-modal');
|
||||||
|
console.log("LENGTH:", members[0].sp_addresses.length);
|
||||||
|
} else {
|
||||||
|
await this.injectModal(members);
|
||||||
|
this.modal = document.getElementById('modal');
|
||||||
|
console.log("LENGTH:", members[0].sp_addresses.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.modal) this.modal.style.display = 'flex';
|
||||||
|
|
||||||
|
// Close modal when clicking outside of it
|
||||||
|
window.onclick = (event) => {
|
||||||
|
if (event.target === this.modal) {
|
||||||
|
this.closeConfirmationModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
confirmLogin() {
|
||||||
|
console.log('=============> Confirm Login');
|
||||||
|
}
|
||||||
|
async closeLoginModal() {
|
||||||
|
if (this.modal) this.modal.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirmPairing() {
|
||||||
|
const service = await Services.getInstance();
|
||||||
|
if (this.modal) this.modal.style.display = 'none';
|
||||||
|
|
||||||
|
if (service.device1) {
|
||||||
|
console.log("Device 1 detected");
|
||||||
|
// We send the prd update
|
||||||
|
if (this.stateId && this.processId) {
|
||||||
|
try {
|
||||||
|
// Device B shouldn't do this again
|
||||||
|
const createPrdUpdateReturn = service.createPrdUpdate(this.processId, this.stateId);
|
||||||
|
await service.handleApiReturn(createPrdUpdateReturn);
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('No currentPcdCommitment');
|
||||||
|
}
|
||||||
|
|
||||||
|
// We send confirmation that we validate the change
|
||||||
|
try {
|
||||||
|
const approveChangeReturn = await service.approveChange(this.processId!, this.stateId!);
|
||||||
|
await service.handleApiReturn(approveChangeReturn);
|
||||||
|
|
||||||
|
await this.injectWaitingModal();
|
||||||
|
const waitingModal = document.getElementById('waiting-modal');
|
||||||
|
if (waitingModal) waitingModal.style.display = 'flex';
|
||||||
|
|
||||||
|
if (!service.device2Ready) {
|
||||||
|
while (!service.device2Ready) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
console.log("Device 2 is ready - Device 1 can now proceed");
|
||||||
|
}
|
||||||
|
service.pairDevice(this.paired_addresses, this.processId);
|
||||||
|
this.paired_addresses = [];
|
||||||
|
this.processId = null;
|
||||||
|
this.stateId = null;
|
||||||
|
const newDevice = service.dumpDeviceFromMemory();
|
||||||
|
console.log(newDevice);
|
||||||
|
await service.saveDeviceInDatabase(newDevice);
|
||||||
|
navigate('chat');
|
||||||
|
service.resetState();
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// service.pairDevice(this.paired_addresses);
|
||||||
|
// } catch (e) {
|
||||||
|
// throw e;
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
console.log("Device 2 detected");
|
||||||
|
|
||||||
|
// if (this.stateId && this.processId) {
|
||||||
|
// try {
|
||||||
|
// // Device B shouldn't do this again
|
||||||
|
// const createPrdUpdateReturn = service.createPrdUpdate(this.processId, this.stateId);
|
||||||
|
// await service.handleApiReturn(createPrdUpdateReturn);
|
||||||
|
// } catch (e) {
|
||||||
|
// throw e;
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// throw new Error('No currentPcdCommitment');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// We send confirmation that we validate the change
|
||||||
|
try {
|
||||||
|
const approveChangeReturn = await service.approveChange(this.processId!, this.stateId!);
|
||||||
|
await service.handleApiReturn(approveChangeReturn);
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
service.pairDevice(this.paired_addresses, this.processId!);
|
||||||
|
|
||||||
|
this.paired_addresses = [];
|
||||||
|
this.processId = null;
|
||||||
|
this.stateId = null;
|
||||||
|
const newDevice = service.dumpDeviceFromMemory();
|
||||||
|
console.log(newDevice);
|
||||||
|
await service.saveDeviceInDatabase(newDevice);
|
||||||
|
navigate('chat');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async closeConfirmationModal() {
|
||||||
|
const service = await Services.getInstance();
|
||||||
|
await service.unpairDevice();
|
||||||
|
if (this.modal) this.modal.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -3,43 +3,15 @@ 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<AxiosResponse | null> {
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
try {
|
try {
|
||||||
// --- DÉBUT DE LA CORRECTION ---
|
// Append key and ttl as query parameters
|
||||||
// 1. On vérifie d'abord si la donnée existe en appelant le bon service
|
const url = new URL(`${server}/store`);
|
||||||
// On passe 'server' au lieu de 'url' pour que testData construise la bonne URL
|
url.searchParams.append('key', key);
|
||||||
const dataExists = await testData(server, key);
|
if (ttl !== null) {
|
||||||
|
url.searchParams.append('ttl', ttl.toString());
|
||||||
if (dataExists) {
|
|
||||||
console.log('Data already stored:', key);
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
console.log('Data not stored for server, proceeding to POST:', key, server);
|
|
||||||
}
|
|
||||||
// --- FIN DE LA CORRECTION ---
|
|
||||||
|
|
||||||
|
|
||||||
// Construction de l'URL pour le POST (stockage)
|
|
||||||
// Cette partie était correcte
|
|
||||||
let url: string;
|
|
||||||
if (server.startsWith('/')) {
|
|
||||||
// Relative path
|
|
||||||
url = `${server}/store/${encodeURIComponent(key)}`;
|
|
||||||
if (ttl !== null) {
|
|
||||||
url += `?ttl=${ttl}`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Absolute URL
|
|
||||||
const urlObj = new URL(`${server}/store/${encodeURIComponent(key)}`);
|
|
||||||
if (ttl !== null) {
|
|
||||||
urlObj.searchParams.append('ttl', ttl.toString());
|
|
||||||
}
|
|
||||||
url = urlObj.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// La ligne ci-dessous a été supprimée car le test est fait au-dessus
|
// Send the encrypted ArrayBuffer as the raw request body.
|
||||||
// const testResponse = await testData(url, key); // <-- LIGNE BOGUÉE SUPPRIMÉE
|
const response = await axios.post(url.toString(), value, {
|
||||||
|
|
||||||
// Send the encrypted data as the raw request body.
|
|
||||||
const response = await axios.post(url, value, { // Note: c'est bien un POST sur 'url'
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/octet-stream'
|
'Content-Type': 'application/octet-stream'
|
||||||
},
|
},
|
||||||
@ -51,8 +23,8 @@ export async function storeData(servers: string[], key: string, value: Blob, ttl
|
|||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (axios.isAxiosError(error) && error.response?.status === 409) {
|
if (error?.response?.status === 409) {
|
||||||
return null; // 409 Conflict (Key already exists)
|
return null;
|
||||||
}
|
}
|
||||||
console.error('Error storing data:', error);
|
console.error('Error storing data:', error);
|
||||||
}
|
}
|
||||||
@ -60,51 +32,24 @@ export async function storeData(servers: string[], key: string, value: Blob, ttl
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fonction retrieveData (inchangée, elle était correcte)
|
|
||||||
export async function retrieveData(servers: string[], key: string): Promise<ArrayBuffer | null> {
|
export async function retrieveData(servers: string[], key: string): Promise<ArrayBuffer | null> {
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
try {
|
try {
|
||||||
const url = server.startsWith('/')
|
// When fetching the data from the server:
|
||||||
? `${server}/retrieve/${key}`
|
const response = await axios.get(`${server}/retrieve/${key}`, {
|
||||||
: new URL(`${server}/retrieve/${key}`).toString();
|
|
||||||
|
|
||||||
console.log('Retrieving data', key,' from:', url);
|
|
||||||
|
|
||||||
const response = await axios.get(url, {
|
|
||||||
responseType: 'arraybuffer'
|
responseType: 'arraybuffer'
|
||||||
});
|
});
|
||||||
|
if (response.status !== 200) {
|
||||||
if (response.status === 200) {
|
console.error('Received response status', response.status);
|
||||||
if (response.data instanceof ArrayBuffer) {
|
|
||||||
return response.data;
|
|
||||||
} else {
|
|
||||||
console.error('Server returned non-ArrayBuffer data:', typeof response.data);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error(`Server ${server} returned status ${response.status}`);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
// console.log('Retrieved data:', response.data);
|
||||||
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (axios.isAxiosError(error)) {
|
console.error('Error retrieving data:', error);
|
||||||
if (error.response?.status === 404) {
|
|
||||||
// C'est normal si la donnée n'existe pas
|
|
||||||
console.log(`Data not found on server ${server} for key ${key}`);
|
|
||||||
continue;
|
|
||||||
} else if (error.response?.status) {
|
|
||||||
console.error(`Server ${server} error ${error.response.status}:`, error.response.statusText);
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
console.error(`Network error connecting to ${server}:`, error.message);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error(`Unexpected error retrieving data from ${server}:`, error);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TestResponse {
|
interface TestResponse {
|
||||||
@ -112,27 +57,25 @@ interface TestResponse {
|
|||||||
value: boolean;
|
value: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- FONCTION testData CORRIGÉE ---
|
export async function testData(servers: string[], key: string): Promise<Record<string, boolean | null> | null> {
|
||||||
// Elle prend 'server' au lieu de 'url' et construit sa propre URL '/test/...'
|
const res = {};
|
||||||
export async function testData(server: string, key: string): Promise<boolean> {
|
for (const server of servers) {
|
||||||
try {
|
res[server] = null;
|
||||||
// Construit l'URL /test/...
|
try {
|
||||||
const testUrl = server.startsWith('/')
|
const response = await axios.get(`${server}/test/${key}`);
|
||||||
? `${server}/test/${encodeURIComponent(key)}`
|
if (response.status !== 200) {
|
||||||
: new URL(`${server}/test/${encodeURIComponent(key)}`).toString();
|
console.error(`${server}: Test response status: ${response.status}`);
|
||||||
|
continue;
|
||||||
const response = await axios.get(testUrl); // Fait un GET sur /test/...
|
}
|
||||||
|
|
||||||
// 200 OK = la donnée existe
|
const data: TestResponse = response.data;
|
||||||
return response.status === 200;
|
|
||||||
|
res[server] = data.value;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
console.error('Error retrieving data:', error);
|
||||||
// 404 Not Found = la donnée n'existe pas. C'est une réponse valide.
|
return null;
|
||||||
return false;
|
}
|
||||||
}
|
}
|
||||||
// Toute autre erreur (serveur offline, 500, etc.)
|
|
||||||
console.error('Error testing data:', error);
|
return res;
|
||||||
return false; // On considère que le test a échoué
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,87 +0,0 @@
|
|||||||
import * as jose from 'jose';
|
|
||||||
|
|
||||||
interface TokenPair {
|
|
||||||
accessToken: string;
|
|
||||||
refreshToken: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class TokenService {
|
|
||||||
private static instance: TokenService;
|
|
||||||
private readonly SECRET_KEY = import.meta.env.VITE_JWT_SECRET_KEY;
|
|
||||||
private readonly ACCESS_TOKEN_EXPIRATION = '30s';
|
|
||||||
private readonly REFRESH_TOKEN_EXPIRATION = '7d';
|
|
||||||
private readonly encoder = new TextEncoder();
|
|
||||||
|
|
||||||
private constructor() {}
|
|
||||||
|
|
||||||
static async getInstance(): Promise<TokenService> {
|
|
||||||
if (!TokenService.instance) {
|
|
||||||
TokenService.instance = new TokenService();
|
|
||||||
}
|
|
||||||
return TokenService.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
async generateSessionToken(origin: string): Promise<TokenPair> {
|
|
||||||
const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY));
|
|
||||||
|
|
||||||
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 = new Uint8Array(this.encoder.encode(this.SECRET_KEY));
|
|
||||||
const { payload } = await jose.jwtVerify(token, secret);
|
|
||||||
|
|
||||||
return payload.origin === origin;
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error?.code === 'ERR_JWT_EXPIRED') {
|
|
||||||
console.log('Token expiré');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('Erreur de validation du token:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshAccessToken(refreshToken: string, origin: string): Promise<string | null> {
|
|
||||||
try {
|
|
||||||
// Vérifier si le refresh token est valide
|
|
||||||
const isValid = await this.validateToken(refreshToken, origin);
|
|
||||||
if (!isValid) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier le type du token
|
|
||||||
const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY));
|
|
||||||
const { payload } = await jose.jwtVerify(refreshToken, secret);
|
|
||||||
if (payload.type !== 'refresh') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Générer un nouveau access token
|
|
||||||
const newAccessToken = await new jose.SignJWT({ origin, type: 'access' })
|
|
||||||
.setProtectedHeader({ alg: 'HS256' })
|
|
||||||
.setIssuedAt()
|
|
||||||
.setExpirationTime(this.ACCESS_TOKEN_EXPIRATION)
|
|
||||||
.sign(secret);
|
|
||||||
|
|
||||||
return newAccessToken;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur lors du refresh du token:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,135 +0,0 @@
|
|||||||
import { AnkFlag } from '../../pkg/sdk_client'; // Vérifie le chemin vers pkg
|
|
||||||
import Services from './service';
|
|
||||||
|
|
||||||
let ws: WebSocket | null = null;
|
|
||||||
let messageQueue: string[] = [];
|
|
||||||
let reconnectInterval = 1000; // Délai initial de 1s avant reconnexion
|
|
||||||
const MAX_RECONNECT_INTERVAL = 30000; // Max 30s
|
|
||||||
let isConnecting = false;
|
|
||||||
let urlReference: string = '';
|
|
||||||
let pingIntervalId: any = null;
|
|
||||||
|
|
||||||
export async function initWebsocket(url: string) {
|
|
||||||
urlReference = url;
|
|
||||||
connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
function connect() {
|
|
||||||
if (isConnecting || (ws && ws.readyState === WebSocket.OPEN)) return;
|
|
||||||
isConnecting = true;
|
|
||||||
|
|
||||||
console.log(`[WS] 🔌 Tentative de connexion à ${urlReference}...`);
|
|
||||||
ws = new WebSocket(urlReference);
|
|
||||||
|
|
||||||
ws.onopen = async () => {
|
|
||||||
console.log('[WS] ✅ Connexion établie !');
|
|
||||||
isConnecting = false;
|
|
||||||
reconnectInterval = 1000; // Reset du délai
|
|
||||||
|
|
||||||
// Démarrer le Heartbeat (Ping pour garder la connexion vivante)
|
|
||||||
startHeartbeat();
|
|
||||||
|
|
||||||
// Vider la file d'attente (messages envoyés pendant la coupure)
|
|
||||||
while (messageQueue.length > 0) {
|
|
||||||
const message = messageQueue.shift();
|
|
||||||
if (message) ws?.send(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
|
||||||
const msgData = event.data;
|
|
||||||
if (typeof msgData === 'string') {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const parsedMessage = JSON.parse(msgData);
|
|
||||||
const services = await Services.getInstance();
|
|
||||||
|
|
||||||
// Gestion des messages
|
|
||||||
switch (parsedMessage.flag) {
|
|
||||||
case 'Handshake':
|
|
||||||
await services.handleHandshakeMsg(urlReference, parsedMessage.content);
|
|
||||||
break;
|
|
||||||
case 'NewTx':
|
|
||||||
await services.parseNewTx(parsedMessage.content);
|
|
||||||
break;
|
|
||||||
case 'Cipher':
|
|
||||||
await services.parseCipher(parsedMessage.content);
|
|
||||||
break;
|
|
||||||
case 'Commit':
|
|
||||||
await services.handleCommitError(parsedMessage.content);
|
|
||||||
break;
|
|
||||||
// Ajoute d'autres cas si nécessaire
|
|
||||||
default:
|
|
||||||
// console.log('[WS] Message reçu:', parsedMessage.flag);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[WS] Erreur traitement message:', error);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = (event) => {
|
|
||||||
console.error('[WS] 💥 Erreur:', event);
|
|
||||||
// Pas besoin de reconnecter ici, onclose sera appelé juste après
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = (event) => {
|
|
||||||
isConnecting = false;
|
|
||||||
stopHeartbeat();
|
|
||||||
console.warn(`[WS] ⚠️ Déconnecté (Code: ${event.code}). Reconnexion dans ${reconnectInterval / 1000}s...`);
|
|
||||||
|
|
||||||
// Reconnexion exponentielle (1s, 1.5s, 2.25s...)
|
|
||||||
setTimeout(() => {
|
|
||||||
connect();
|
|
||||||
reconnectInterval = Math.min(reconnectInterval * 1.5, MAX_RECONNECT_INTERVAL);
|
|
||||||
}, reconnectInterval);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function startHeartbeat() {
|
|
||||||
stopHeartbeat();
|
|
||||||
// Envoie un ping toutes les 30 secondes pour éviter que le serveur ou le navigateur ne coupe la connexion
|
|
||||||
pingIntervalId = setInterval(() => {
|
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
||||||
// Adapter selon ce que ton serveur attend comme Ping, ou envoyer un message vide
|
|
||||||
// ws.send(JSON.stringify({ flag: 'Ping', content: '' }));
|
|
||||||
}
|
|
||||||
}, 30000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopHeartbeat() {
|
|
||||||
if (pingIntervalId) clearInterval(pingIntervalId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sendMessage(flag: AnkFlag, message: string): void {
|
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
||||||
const networkMessage = {
|
|
||||||
flag: flag,
|
|
||||||
content: message,
|
|
||||||
};
|
|
||||||
ws.send(JSON.stringify(networkMessage));
|
|
||||||
} else {
|
|
||||||
console.warn(`[WS] Pas connecté. Message '${flag}' mis en file d'attente.`);
|
|
||||||
const networkMessage = {
|
|
||||||
flag: flag,
|
|
||||||
content: message,
|
|
||||||
};
|
|
||||||
messageQueue.push(JSON.stringify(networkMessage));
|
|
||||||
|
|
||||||
// Si on n'est pas déjà en train de se connecter, on force une tentative
|
|
||||||
if (!isConnecting) connect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUrl(): string {
|
|
||||||
return urlReference;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function close(): void {
|
|
||||||
if (ws) {
|
|
||||||
ws.onclose = null; // On évite la reconnexion auto si fermeture volontaire
|
|
||||||
stopHeartbeat();
|
|
||||||
ws.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
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',
|
|
||||||
}
|
|
||||||
|
|
||||||
4
src/utils/document.utils.ts
Normal file
4
src/utils/document.utils.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export function getCorrectDOM(componentTag: string): Node {
|
||||||
|
const dom = document?.querySelector(componentTag)?.shadowRoot || (document as Node);
|
||||||
|
return dom;
|
||||||
|
}
|
||||||
53
src/utils/messageMock.ts
Executable file
53
src/utils/messageMock.ts
Executable file
@ -0,0 +1,53 @@
|
|||||||
|
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();
|
||||||
96
src/utils/notification.store.ts
Executable file
96
src/utils/notification.store.ts
Executable file
@ -0,0 +1,96 @@
|
|||||||
|
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();
|
||||||
@ -1,24 +0,0 @@
|
|||||||
export function splitPrivateData(data: Record<string, any>, privateFields: string[]): { privateData: Record<string, any>, publicData: Record<string, any> } {
|
|
||||||
const privateData: Record<string, any> = {};
|
|
||||||
const publicData: Record<string, any> = {};
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(data)) {
|
|
||||||
if (privateFields.includes(key)) {
|
|
||||||
privateData[key] = value;
|
|
||||||
} else {
|
|
||||||
publicData[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { privateData, publicData };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isValid32ByteHex(value: string): boolean {
|
|
||||||
// Check if string is exactly 64 characters (32 bytes in hex)
|
|
||||||
if (value.length !== 64) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if string only contains valid hex characters
|
|
||||||
return /^[0-9a-fA-F]{64}$/.test(value);
|
|
||||||
}
|
|
||||||
@ -1,247 +1,241 @@
|
|||||||
import Services from '../services/service';
|
import Services from '../services/service';
|
||||||
import { getCorrectDOM } from './html.utils';
|
import { getCorrectDOM } from './html.utils';
|
||||||
import { addSubscription } from './subscription.utils';
|
import { addSubscription } from './subscription.utils';
|
||||||
import QRCode from 'qrcode';
|
import QRCode from 'qrcode';
|
||||||
|
|
||||||
//Copy Address
|
//Copy Address
|
||||||
export async function copyToClipboard(fullAddress: string) {
|
export async function copyToClipboard(fullAddress: string) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(fullAddress);
|
await navigator.clipboard.writeText(fullAddress);
|
||||||
alert('Adresse copiée dans le presse-papiers !');
|
alert('Adresse copiée dans le presse-papiers !');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to copy the address: ', err);
|
console.error('Failed to copy the address: ', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Generate emojis list
|
//Generate emojis list
|
||||||
export function generateEmojiList(): string[] {
|
export function generateEmojiList(): string[] {
|
||||||
const emojiRanges = [
|
const emojiRanges = [
|
||||||
[0x1f600, 0x1f64f],
|
[0x1f600, 0x1f64f],
|
||||||
[0x1f300, 0x1f5ff],
|
[0x1f300, 0x1f5ff],
|
||||||
[0x1f680, 0x1f6ff],
|
[0x1f680, 0x1f6ff],
|
||||||
[0x1f700, 0x1f77f],
|
[0x1f700, 0x1f77f],
|
||||||
];
|
];
|
||||||
|
|
||||||
const emojiList: string[] = [];
|
const emojiList: string[] = [];
|
||||||
for (const range of emojiRanges) {
|
for (const range of emojiRanges) {
|
||||||
const [start, end] = range;
|
const [start, end] = range;
|
||||||
for (let i = start; i <= end && emojiList.length < 256; i++) {
|
for (let i = start; i <= end && emojiList.length < 256; i++) {
|
||||||
emojiList.push(String.fromCodePoint(i));
|
emojiList.push(String.fromCodePoint(i));
|
||||||
}
|
}
|
||||||
if (emojiList.length >= 256) {
|
if (emojiList.length >= 256) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return emojiList.slice(0, 256);
|
return emojiList.slice(0, 256);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Adress to emojis
|
//Adress to emojis
|
||||||
export async function addressToEmoji(text: string): Promise<string> {
|
export async function addressToEmoji(text: string): Promise<string> {
|
||||||
//Adress to Hash
|
//Adress to Hash
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const data = encoder.encode(text);
|
const data = encoder.encode(text);
|
||||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||||
|
|
||||||
const hash = new Uint8Array(hashBuffer);
|
const hash = new Uint8Array(hashBuffer);
|
||||||
const bytes = hash.slice(-4);
|
const bytes = hash.slice(-4);
|
||||||
|
|
||||||
//Hash slice to emojis
|
//Hash slice to emojis
|
||||||
const emojiList = generateEmojiList();
|
const emojiList = generateEmojiList();
|
||||||
const emojis = Array.from(bytes)
|
const emojis = Array.from(bytes)
|
||||||
.map((byte) => emojiList[byte])
|
.map((byte) => emojiList[byte])
|
||||||
.join('');
|
.join('');
|
||||||
return emojis;
|
return emojis;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Get emojis from other device
|
//Get emojis from other device
|
||||||
async function emojisPairingRequest() {
|
async function emojisPairingRequest() {
|
||||||
try {
|
try {
|
||||||
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
|
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
|
||||||
|
|
||||||
const urlParams: URLSearchParams = new URLSearchParams(window.location.search);
|
const urlParams: URLSearchParams = new URLSearchParams(window.location.search);
|
||||||
const sp_adress: string | null = urlParams.get('sp_address');
|
const sp_adress: string | null = urlParams.get('sp_address');
|
||||||
|
|
||||||
if (!sp_adress) {
|
if (!sp_adress) {
|
||||||
// console.error("No 'sp_adress' parameter found in the URL.");
|
// console.error("No 'sp_adress' parameter found in the URL.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emojis = await addressToEmoji(sp_adress);
|
const emojis = await addressToEmoji(sp_adress);
|
||||||
const emojiDisplay = container?.querySelector('.pairing-request');
|
const emojiDisplay = container?.querySelector('.pairing-request');
|
||||||
|
|
||||||
if (emojiDisplay) {
|
if (emojiDisplay) {
|
||||||
emojiDisplay.textContent = '(Request from: ' + emojis + ')';
|
emojiDisplay.textContent = '(Request from: ' + emojis + ')';
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display address emojis and other device emojis
|
// Display address emojis and other device emojis
|
||||||
export async function displayEmojis(text: string) {
|
export async function displayEmojis(text: string) {
|
||||||
console.log('🚀 ~ Services ~ adressToEmoji');
|
console.log('🚀 ~ Services ~ adressToEmoji');
|
||||||
try {
|
try {
|
||||||
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
|
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
|
||||||
const emojis = await addressToEmoji(text);
|
const emojis = await addressToEmoji(text);
|
||||||
const emojiDisplay = container?.querySelector('.emoji-display');
|
const emojiDisplay = container?.querySelector('.emoji-display');
|
||||||
|
|
||||||
if (emojiDisplay) {
|
if (emojiDisplay) {
|
||||||
emojiDisplay.textContent = emojis;
|
emojiDisplay.textContent = emojis;
|
||||||
}
|
}
|
||||||
|
|
||||||
emojisPairingRequest();
|
emojisPairingRequest();
|
||||||
|
|
||||||
initAddressInput();
|
initAddressInput();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify Other address
|
// Verify Other address
|
||||||
export function initAddressInput() {
|
export function initAddressInput() {
|
||||||
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
|
const container = getCorrectDOM('login-4nk-component') as HTMLElement
|
||||||
const addressInput = container.querySelector('#addressInput') as HTMLInputElement;
|
const addressInput = container.querySelector('#addressInput') as HTMLInputElement;
|
||||||
const emojiDisplay = container.querySelector('#emoji-display-2');
|
const emojiDisplay = container.querySelector('#emoji-display-2');
|
||||||
const okButton = container.querySelector('#okButton') as HTMLButtonElement;
|
const okButton = container.querySelector('#okButton') as HTMLButtonElement;
|
||||||
const createButton = container.querySelector('#createButton') as HTMLButtonElement;
|
const createButton = container.querySelector('#createButton') as HTMLButtonElement;
|
||||||
const actionButton = container.querySelector('#actionButton') as HTMLButtonElement;
|
addSubscription(addressInput, 'input', async () => {
|
||||||
addSubscription(addressInput, 'input', async () => {
|
let address = addressInput.value;
|
||||||
let address = addressInput.value;
|
|
||||||
|
// Vérifie si l'adresse est une URL
|
||||||
// Vérifie si l'adresse est une URL
|
try {
|
||||||
try {
|
const url = new URL(address);
|
||||||
const url = new URL(address);
|
// Si c'est une URL valide, extraire le paramètre sp_address
|
||||||
// Si c'est une URL valide, extraire le paramètre sp_address
|
const urlParams = new URLSearchParams(url.search);
|
||||||
const urlParams = new URLSearchParams(url.search);
|
const extractedAddress = urlParams.get('sp_address') || ''; // Prend sp_address ou une chaîne vide
|
||||||
const extractedAddress = urlParams.get('sp_address') || ''; // Prend sp_address ou une chaîne vide
|
|
||||||
|
if (extractedAddress) {
|
||||||
if (extractedAddress) {
|
address = extractedAddress;
|
||||||
address = extractedAddress;
|
addressInput.value = address; // Met à jour l'input pour afficher uniquement l'adresse extraite
|
||||||
addressInput.value = address; // Met à jour l'input pour afficher uniquement l'adresse extraite
|
}
|
||||||
}
|
} catch (e) {
|
||||||
} catch (e) {
|
// Si ce n'est pas une URL valide, on garde l'adresse originale
|
||||||
// Si ce n'est pas une URL valide, on garde l'adresse originale
|
console.log("Ce n'est pas une URL valide, on garde l'adresse originale.");
|
||||||
console.log("Ce n'est pas une URL valide, on garde l'adresse originale.");
|
}
|
||||||
}
|
if (address) {
|
||||||
if (address) {
|
const emojis = await addressToEmoji(address);
|
||||||
const emojis = await addressToEmoji(address);
|
if (emojiDisplay) {
|
||||||
if (emojiDisplay) {
|
emojiDisplay.innerHTML = emojis;
|
||||||
emojiDisplay.innerHTML = emojis;
|
}
|
||||||
}
|
if (okButton) {
|
||||||
if (okButton) {
|
okButton.style.display = 'inline-block';
|
||||||
okButton.style.display = 'inline-block';
|
}
|
||||||
}
|
} else {
|
||||||
} else {
|
if (emojiDisplay) {
|
||||||
if (emojiDisplay) {
|
emojiDisplay.innerHTML = '';
|
||||||
emojiDisplay.innerHTML = '';
|
}
|
||||||
}
|
if (okButton) {
|
||||||
if (okButton) {
|
okButton.style.display = 'none';
|
||||||
okButton.style.display = 'none';
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
if (okButton) {
|
||||||
if (createButton) {
|
addSubscription(okButton, 'click', () => {
|
||||||
addSubscription(createButton, 'click', () => {
|
onOkButtonClick();
|
||||||
onCreateButtonClick();
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
}
|
if (createButton) {
|
||||||
|
addSubscription(createButton, 'click', () => {
|
||||||
async function onCreateButtonClick() {
|
onCreateButtonClick();
|
||||||
try {
|
});
|
||||||
await prepareAndSendPairingTx();
|
}
|
||||||
// Don't call confirmPairing immediately - it will be called when the pairing process is complete
|
}
|
||||||
console.log('Pairing process initiated. Waiting for completion...');
|
|
||||||
} catch (e) {
|
async function onOkButtonClick() {
|
||||||
console.error(`onCreateButtonClick error: ${e}`);
|
const container = getCorrectDOM('login-4nk-component') as HTMLElement
|
||||||
}
|
const secondDeviceAddress = (container.querySelector('#addressInput') as HTMLInputElement).value;
|
||||||
}
|
try {
|
||||||
|
// Connect to target, if necessary
|
||||||
// Une constante est plus claire qu'une 'magic string' ("")
|
await prepareAndSendPairingTx(secondDeviceAddress);
|
||||||
const DEFAULT_PAIRING_PAYLOAD = '';
|
} catch (e) {
|
||||||
|
console.error(`onOkButtonClick error: ${e}`);
|
||||||
export async function prepareAndSendPairingTx(): Promise<void> {
|
}
|
||||||
const service = await Services.getInstance();
|
}
|
||||||
|
|
||||||
try {
|
async function onCreateButtonClick() {
|
||||||
// 1. Création du processus d'appairage
|
try {
|
||||||
// const relayAddress = service.getAllRelays(); // <-- Cette variable n'était pas utilisée
|
await prepareAndSendPairingTx();
|
||||||
const createPairingProcessReturn = await service.createPairingProcess(DEFAULT_PAIRING_PAYLOAD, []);
|
} catch (e) {
|
||||||
|
console.error(`onCreateButtonClick error: ${e}`);
|
||||||
if (!createPairingProcessReturn.updated_process) {
|
}
|
||||||
throw new Error('createPairingProcess returned an empty new process');
|
}
|
||||||
}
|
|
||||||
|
export async function prepareAndSendPairingTx(promptName: boolean = false) {
|
||||||
// Utilisation du "destructuring" pour assigner les variables
|
const service = await Services.getInstance();
|
||||||
const { process_id: pairingId, current_process: process } = createPairingProcessReturn.updated_process;
|
|
||||||
|
// Device 1 wait Device 2
|
||||||
// Ajout d'une vérification pour éviter les erreurs si states est vide
|
// service.device1 = true;
|
||||||
if (!process.states || process.states.length === 0) {
|
|
||||||
throw new Error('Le processus reçu ne contient aucun état (state)');
|
try {
|
||||||
}
|
await service.checkConnections([]);
|
||||||
const stateId = process.states[0].state_id;
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
// 2. Assurer les connexions
|
}
|
||||||
await service.ensureConnections(process, stateId);
|
|
||||||
|
// Prompt the user for a username.
|
||||||
// 3. Mettre à jour l'état du service
|
let userName;
|
||||||
service.setProcessId(pairingId);
|
if (promptName) {
|
||||||
service.setStateId(stateId);
|
userName = prompt("Please enter your user name:");
|
||||||
|
} else {
|
||||||
// 4. Appairer le 'device'
|
userName = "";
|
||||||
service.pairDevice(pairingId, [service.getDeviceAddress()]);
|
}
|
||||||
|
|
||||||
// 5. Mettre à jour la BDD
|
// Create the process after a delay.
|
||||||
try {
|
setTimeout(async () => {
|
||||||
const currentDevice = await service.getDeviceFromDatabase();
|
const relayAddress = service.getAllRelays();
|
||||||
if (currentDevice) {
|
|
||||||
currentDevice.pairing_process_commitment = pairingId;
|
// Pass the userName as an additional parameter.
|
||||||
await service.saveDeviceInDatabase(currentDevice);
|
const createPairingProcessReturn = await service.createPairingProcess(
|
||||||
}
|
userName,
|
||||||
} catch (err) {
|
[],
|
||||||
console.error('Échec non-critique de la mise à jour BDD (pairing_process_commitment):', err);
|
relayAddress[0].spAddress,
|
||||||
}
|
1,
|
||||||
|
userName
|
||||||
// 6. Gérer les étapes suivantes du processus
|
);
|
||||||
await service.handleApiReturn(createPairingProcessReturn);
|
|
||||||
|
if (!createPairingProcessReturn.updated_process) {
|
||||||
const createPrdUpdateReturn = await service.createPrdUpdate(pairingId, stateId);
|
throw new Error('createPairingProcess returned an empty new process'); // This should never happen
|
||||||
await service.handleApiReturn(createPrdUpdateReturn);
|
}
|
||||||
|
|
||||||
const approveChangeReturn = await service.approveChange(pairingId, stateId);
|
await service.handleApiReturn(createPairingProcessReturn);
|
||||||
await service.handleApiReturn(approveChangeReturn);
|
}, 1000);
|
||||||
|
}
|
||||||
// await service.confirmPairing();
|
|
||||||
} catch (err) {
|
export async function generateQRCode(spAddress: string) {
|
||||||
console.error("Le processus d'appairage a échoué :", err);
|
try {
|
||||||
throw err;
|
const container = getCorrectDOM('login-4nk-component') as HTMLElement
|
||||||
}
|
const currentUrl = 'https://' + window.location.host;
|
||||||
}
|
const url = await QRCode.toDataURL(currentUrl + '?sp_address=' + spAddress);
|
||||||
|
const qrCode = container?.querySelector('.qr-code img');
|
||||||
export async function generateQRCode(spAddress: string) {
|
qrCode?.setAttribute('src', url);
|
||||||
try {
|
} catch (err) {
|
||||||
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
|
console.error(err);
|
||||||
const currentUrl = 'https://' + window.location.host;
|
}
|
||||||
const url = await QRCode.toDataURL(currentUrl + '?sp_address=' + spAddress);
|
}
|
||||||
const qrCode = container?.querySelector('.qr-code img');
|
|
||||||
qrCode?.setAttribute('src', url);
|
export async function generateCreateBtn() {
|
||||||
} catch (err) {
|
try{
|
||||||
console.error(err);
|
//Generate CreateBtn
|
||||||
}
|
const container = getCorrectDOM('login-4nk-component') as HTMLElement
|
||||||
}
|
const createBtn = container?.querySelector('.create-btn');
|
||||||
|
if (createBtn) {
|
||||||
export async function generateCreateBtn() {
|
createBtn.textContent = 'CREATE';
|
||||||
try {
|
}
|
||||||
// Generate CreateBtn
|
} catch (err) {
|
||||||
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
|
console.error(err);
|
||||||
const createBtn = container?.querySelector('.create-btn');
|
}
|
||||||
if (createBtn) {
|
|
||||||
createBtn.textContent = 'CREATE';
|
}
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
14
src/vite-env.d.ts
vendored
14
src/vite-env.d.ts
vendored
@ -1,14 +0,0 @@
|
|||||||
/// <reference types="vite/client" />
|
|
||||||
/// <reference types="vite-plugin-wasm/client" />
|
|
||||||
|
|
||||||
// Permet d'importer des fichiers HTML comme des chaînes de caractères
|
|
||||||
declare module '*.html?raw' {
|
|
||||||
const content: string;
|
|
||||||
export default content;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Permet d'importer des fichiers CSS comme des chaînes de caractères (inline)
|
|
||||||
declare module '*.css?inline' {
|
|
||||||
const content: string;
|
|
||||||
export default content;
|
|
||||||
}
|
|
||||||
89
src/websockets.ts
Executable file
89
src/websockets.ts
Executable file
@ -0,0 +1,89 @@
|
|||||||
|
import { AnkFlag } from 'pkg/sdk_client';
|
||||||
|
import Services from './services/service';
|
||||||
|
|
||||||
|
let ws: WebSocket;
|
||||||
|
let messageQueue: string[] = [];
|
||||||
|
export async function initWebsocket(url: string) {
|
||||||
|
ws = new WebSocket(url);
|
||||||
|
|
||||||
|
if (ws !== null) {
|
||||||
|
ws.onopen = async (event) => {
|
||||||
|
console.log('WebSocket connection established');
|
||||||
|
|
||||||
|
while (messageQueue.length > 0) {
|
||||||
|
const message = messageQueue.shift();
|
||||||
|
if (message) {
|
||||||
|
ws.send(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for messages
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const msgData = event.data;
|
||||||
|
|
||||||
|
// console.log("Received text message: ", msgData);
|
||||||
|
(async () => {
|
||||||
|
if (typeof msgData === 'string') {
|
||||||
|
try {
|
||||||
|
const parsedMessage = JSON.parse(msgData);
|
||||||
|
const services = await Services.getInstance();
|
||||||
|
switch (parsedMessage.flag) {
|
||||||
|
case 'Handshake':
|
||||||
|
await services.handleHandshakeMsg(url, parsedMessage.content);
|
||||||
|
break;
|
||||||
|
case 'NewTx':
|
||||||
|
await services.parseNewTx(parsedMessage.content);
|
||||||
|
break;
|
||||||
|
case 'Cipher':
|
||||||
|
await services.parseCipher(parsedMessage.content);
|
||||||
|
break;
|
||||||
|
case 'Commit':
|
||||||
|
// Basically if we see this it means we have an error
|
||||||
|
await services.handleCommitError(parsedMessage.content);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Received an invalid message:', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Received a non-string message');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for possible errors
|
||||||
|
ws.onerror = (event) => {
|
||||||
|
console.error('WebSocket error:', event);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for when the connection is closed
|
||||||
|
ws.onclose = (event) => {
|
||||||
|
console.log('WebSocket is closed now.');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to send messages
|
||||||
|
export function sendMessage(flag: AnkFlag, message: string): void {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
const networkMessage = {
|
||||||
|
flag: flag,
|
||||||
|
content: message,
|
||||||
|
};
|
||||||
|
console.log('Sending message of type:', flag);
|
||||||
|
ws.send(JSON.stringify(networkMessage));
|
||||||
|
} else {
|
||||||
|
console.error('WebSocket is not open. ReadyState:', ws.readyState);
|
||||||
|
messageQueue.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUrl(): string {
|
||||||
|
return ws.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to close the WebSocket connection
|
||||||
|
export function close(): void {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
@ -1,381 +0,0 @@
|
|||||||
/**
|
|
||||||
* Database Web Worker - Handles all IndexedDB operations in background
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type {
|
|
||||||
StoreDefinition,
|
|
||||||
WorkerMessagePayload,
|
|
||||||
WorkerMessageResponse,
|
|
||||||
BatchWriteItem
|
|
||||||
} from './worker.types';
|
|
||||||
|
|
||||||
const DB_NAME = '4nk';
|
|
||||||
const DB_VERSION = 1;
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// STORE DEFINITIONS
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
const STORE_DEFINITIONS: Record<string, StoreDefinition> = {
|
|
||||||
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: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let db: IDBDatabase | null = null;
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// DATABASE INITIALIZATION
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
async function openDatabase(): Promise<IDBDatabase> {
|
|
||||||
if (db) {
|
|
||||||
return db;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
||||||
|
|
||||||
request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
|
|
||||||
const database = (event.target as IDBOpenDBRequest).result;
|
|
||||||
|
|
||||||
Object.values(STORE_DEFINITIONS).forEach(({ name, options, indices }) => {
|
|
||||||
if (!database.objectStoreNames.contains(name)) {
|
|
||||||
const store = database.createObjectStore(name, options);
|
|
||||||
|
|
||||||
indices.forEach(({ name: indexName, keyPath, options: indexOptions }) => {
|
|
||||||
store.createIndex(indexName, keyPath, indexOptions);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
request.onsuccess = () => {
|
|
||||||
db = request.result;
|
|
||||||
resolve(db);
|
|
||||||
};
|
|
||||||
|
|
||||||
request.onerror = () => {
|
|
||||||
reject(request.error);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// WRITE OPERATIONS
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
async function addObject(storeName: string, object: any, key?: IDBValidKey): Promise<{ success: boolean }> {
|
|
||||||
const database = await openDatabase();
|
|
||||||
const tx = database.transaction(storeName, 'readwrite');
|
|
||||||
const store = tx.objectStore(storeName);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let request: IDBRequest;
|
|
||||||
if (key !== null && key !== undefined) {
|
|
||||||
request = store.put(object, key);
|
|
||||||
} else {
|
|
||||||
request = store.put(object);
|
|
||||||
}
|
|
||||||
|
|
||||||
request.onsuccess = () => resolve({ success: true });
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function batchWriting(storeName: string, objects: BatchWriteItem[]): Promise<{ success: boolean }> {
|
|
||||||
const database = await openDatabase();
|
|
||||||
const tx = database.transaction(storeName, 'readwrite');
|
|
||||||
const store = tx.objectStore(storeName);
|
|
||||||
|
|
||||||
for (const { key, object } of objects) {
|
|
||||||
if (key !== null && key !== undefined) {
|
|
||||||
store.put(object, key);
|
|
||||||
} else {
|
|
||||||
store.put(object);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
tx.oncomplete = () => resolve({ success: true });
|
|
||||||
tx.onerror = () => reject(tx.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// READ OPERATIONS
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
async function getObject(storeName: string, key: IDBValidKey): Promise<any> {
|
|
||||||
const database = await openDatabase();
|
|
||||||
const tx = database.transaction(storeName, 'readonly');
|
|
||||||
const store = tx.objectStore(storeName);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = store.get(key);
|
|
||||||
request.onsuccess = () => resolve(request.result ?? null);
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dumpStore(storeName: string): Promise<Record<string, any>> {
|
|
||||||
const database = await openDatabase();
|
|
||||||
const tx = database.transaction(storeName, 'readonly');
|
|
||||||
const store = tx.objectStore(storeName);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const result: Record<string, any> = {};
|
|
||||||
const request = store.openCursor();
|
|
||||||
|
|
||||||
request.onsuccess = (event) => {
|
|
||||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue | null>).result;
|
|
||||||
if (cursor) {
|
|
||||||
result[cursor.key as string] = cursor.value;
|
|
||||||
cursor.continue();
|
|
||||||
} else {
|
|
||||||
resolve(result);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAllObjects(storeName: string): Promise<any[]> {
|
|
||||||
const database = await openDatabase();
|
|
||||||
const tx = database.transaction(storeName, 'readonly');
|
|
||||||
const store = tx.objectStore(storeName);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = store.getAll();
|
|
||||||
request.onsuccess = () => resolve(request.result || []);
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMultipleObjects(storeName: string, keys: IDBValidKey[]): Promise<any[]> {
|
|
||||||
const database = await openDatabase();
|
|
||||||
const tx = database.transaction(storeName, 'readonly');
|
|
||||||
const store = tx.objectStore(storeName);
|
|
||||||
|
|
||||||
const requests = keys.map((key) => {
|
|
||||||
return new Promise<any>((resolve) => {
|
|
||||||
const request = store.get(key);
|
|
||||||
request.onsuccess = () => resolve(request.result || null);
|
|
||||||
request.onerror = () => {
|
|
||||||
console.error(`Error fetching key ${key}:`, request.error);
|
|
||||||
resolve(null);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const results = await Promise.all(requests);
|
|
||||||
return results.filter(result => result !== null);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAllObjectsWithFilter(storeName: string, filterFn?: string): Promise<any[]> {
|
|
||||||
const database = await openDatabase();
|
|
||||||
const tx = database.transaction(storeName, 'readonly');
|
|
||||||
const store = tx.objectStore(storeName);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = store.getAll();
|
|
||||||
request.onsuccess = () => {
|
|
||||||
const allItems = request.result || [];
|
|
||||||
if (filterFn) {
|
|
||||||
const filter = new Function('item', `return ${filterFn}`) as (item: any) => boolean;
|
|
||||||
resolve(allItems.filter(filter));
|
|
||||||
} else {
|
|
||||||
resolve(allItems);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// DELETE OPERATIONS
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
async function deleteObject(storeName: string, key: IDBValidKey): Promise<{ success: boolean }> {
|
|
||||||
const database = await openDatabase();
|
|
||||||
const tx = database.transaction(storeName, 'readwrite');
|
|
||||||
const store = tx.objectStore(storeName);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = store.delete(key);
|
|
||||||
request.onsuccess = () => resolve({ success: true });
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function clearStore(storeName: string): Promise<{ success: boolean }> {
|
|
||||||
const database = await openDatabase();
|
|
||||||
const tx = database.transaction(storeName, 'readwrite');
|
|
||||||
const store = tx.objectStore(storeName);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = store.clear();
|
|
||||||
request.onsuccess = () => resolve({ success: true });
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// INDEX OPERATIONS
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
async function requestStoreByIndex(storeName: string, indexName: string, requestValue: IDBValidKey): Promise<any[]> {
|
|
||||||
const database = await openDatabase();
|
|
||||||
const tx = database.transaction(storeName, 'readonly');
|
|
||||||
const store = tx.objectStore(storeName);
|
|
||||||
const index = store.index(indexName);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = index.getAll(requestValue);
|
|
||||||
request.onsuccess = () => {
|
|
||||||
const allItems = request.result;
|
|
||||||
const filtered = allItems.filter((item: any) => item.state_id === requestValue);
|
|
||||||
resolve(filtered);
|
|
||||||
};
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// UTILITY FUNCTIONS
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
function getStoreList(): Record<string, string> {
|
|
||||||
const storeList: Record<string, string> = {};
|
|
||||||
Object.keys(STORE_DEFINITIONS).forEach((key) => {
|
|
||||||
storeList[key] = STORE_DEFINITIONS[key].name;
|
|
||||||
});
|
|
||||||
return storeList;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// MESSAGE HANDLER
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
self.addEventListener('message', async (event: MessageEvent<WorkerMessagePayload>) => {
|
|
||||||
const { type, payload, id } = event.data;
|
|
||||||
|
|
||||||
try {
|
|
||||||
let result: any;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'INIT':
|
|
||||||
await openDatabase();
|
|
||||||
result = { success: true };
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'ADD_OBJECT':
|
|
||||||
result = await addObject(payload.storeName, payload.object, payload.key);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'BATCH_WRITING':
|
|
||||||
result = await batchWriting(payload.storeName, payload.objects);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'GET_OBJECT':
|
|
||||||
result = await getObject(payload.storeName, payload.key);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'DUMP_STORE':
|
|
||||||
result = await dumpStore(payload.storeName);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'DELETE_OBJECT':
|
|
||||||
result = await deleteObject(payload.storeName, payload.key);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'CLEAR_STORE':
|
|
||||||
result = await clearStore(payload.storeName);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'REQUEST_STORE_BY_INDEX':
|
|
||||||
result = await requestStoreByIndex(
|
|
||||||
payload.storeName,
|
|
||||||
payload.indexName,
|
|
||||||
payload.request
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'GET_ALL_OBJECTS':
|
|
||||||
result = await getAllObjects(payload.storeName);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'GET_MULTIPLE_OBJECTS':
|
|
||||||
result = await getMultipleObjects(payload.storeName, payload.keys);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'GET_ALL_OBJECTS_WITH_FILTER':
|
|
||||||
result = await getAllObjectsWithFilter(payload.storeName, payload.filterFn);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'GET_STORE_LIST':
|
|
||||||
result = getStoreList();
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown message type: ${type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.postMessage({
|
|
||||||
id,
|
|
||||||
type: 'SUCCESS',
|
|
||||||
result,
|
|
||||||
} as WorkerMessageResponse);
|
|
||||||
} catch (error) {
|
|
||||||
self.postMessage({
|
|
||||||
id,
|
|
||||||
type: 'ERROR',
|
|
||||||
error: (error as Error).message || String(error),
|
|
||||||
} as WorkerMessageResponse);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// INITIALIZATION
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
openDatabase().catch((error) => {
|
|
||||||
console.error('[Database Worker] Failed to initialize database:', error);
|
|
||||||
});
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
/**
|
|
||||||
* Shared types for Web Workers
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface StoreDefinition {
|
|
||||||
name: string;
|
|
||||||
options: IDBObjectStoreParameters;
|
|
||||||
indices: IndexDefinition[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IndexDefinition {
|
|
||||||
name: string;
|
|
||||||
keyPath: string | string[];
|
|
||||||
options: IDBIndexParameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkerMessagePayload {
|
|
||||||
type: string;
|
|
||||||
payload?: any;
|
|
||||||
id: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkerMessageResponse {
|
|
||||||
id: number;
|
|
||||||
type: 'SUCCESS' | 'ERROR';
|
|
||||||
result?: any;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BatchWriteItem {
|
|
||||||
key?: IDBValidKey;
|
|
||||||
object: any;
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"noEmit": false, // Ici on peut vouloir émettre des fichiers si nécessaire, ou garder true pour juste check
|
|
||||||
"outDir": "./dist"
|
|
||||||
},
|
|
||||||
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
|
|
||||||
}
|
|
||||||
@ -1,42 +1,29 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "./dist",
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"useDefineForClassFields": true,
|
"lib": ["DOM", "DOM.Iterable", "ESNext", "webworker"],
|
||||||
"module": "ESNext",
|
"types": ["vite/client", "node"],
|
||||||
"lib": ["ESNext", "DOM", "DOM.Iterable", "WebWorker"],
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": false,
|
||||||
/* Mode Bundler (Vite) */
|
"allowSyntheticDefaultImports": true,
|
||||||
"moduleResolution": "bundler",
|
"strict": true,
|
||||||
"allowImportingTsExtensions": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true, /* Vite s'occupe de générer les fichiers, tsc fait juste la vérif */
|
"experimentalDecorators": true,
|
||||||
|
"useDefineForClassFields": true,
|
||||||
/* Qualité du code */
|
"noEmit": true,
|
||||||
"strict": true, /* Active toutes les vérifications strictes */
|
"jsx": "react-jsx",
|
||||||
"noUnusedLocals": false,
|
"baseUrl": "./",
|
||||||
"noUnusedParameters": false,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"allowJs": true, /* Permet d'importer du JS si besoin (ex: legacy) */
|
|
||||||
|
|
||||||
/* Chemins (Alias) */
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"],
|
|
||||||
"~/*": ["src/*"]
|
"~/*": ["src/*"]
|
||||||
},
|
}
|
||||||
|
|
||||||
/* Support des types Vite (client, workers, etc.) */
|
|
||||||
"types": ["vite/client"]
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src", "src/*/", "./vite.config.ts", "src/*.d.ts", "src/main.ts"],
|
||||||
"src/**/*.ts",
|
"exclude": ["node_modules"]
|
||||||
"src/**/*.d.ts",
|
}
|
||||||
"src/**/*.tsx",
|
|
||||||
"src/**/*.vue",
|
|
||||||
"src/**/*.html", /* Important pour les imports ?raw */
|
|
||||||
"vite.config.ts"
|
|
||||||
],
|
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
|
||||||
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