init project with only basic page logic

This commit is contained in:
AnisHADJARAB 2024-07-10 14:34:50 +00:00
parent 111977ded3
commit fd209aeb67
22 changed files with 8362 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
target/
pkg/
Cargo.lock
node_modules/
dist/
.vscode

13
Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM node:20
ENV TZ=Europe/Paris
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# use this user because he have uid et gid 1000 like theradia
USER node
WORKDIR /app
CMD ["npm", "start"]
# "--disable-host-check", "--host", "0.0.0.0", "--ssl", "--ssl-cert", "/ssl/certs/site.crt", "--ssl-key", "/ssl/private/site.dec.key"]

6544
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "sdk_client",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build_wasm": "wasm-pack build --out-dir ../../dist/pkg ./crates/sp_client --target bundler --dev",
"start": "vite --host 0.0.0.0",
"build": "webpack",
"deploy": "sudo cp -r dist/* /var/www/html/"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"copy-webpack-plugin": "^12.0.2",
"html-webpack-plugin": "^5.6.0",
"ts-loader": "^9.5.1",
"typescript": "^5.3.3",
"vite-plugin-static-copy": "^1.0.6",
"webpack": "^5.90.3",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.2"
},
"dependencies": {
"@vitejs/plugin-react": "^4.3.1",
"@vitejs/plugin-vue": "^5.0.5",
"vite": "^5.3.3",
"vite-plugin-copy": "^0.1.6",
"vite-plugin-html": "^3.2.2",
"vite-plugin-wasm": "^3.3.0"
}
}

BIN
public/assets/4nk_image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
public/assets/bgd.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

BIN
public/assets/camera.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

34
public/assets/home.js Normal file
View 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 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

598
public/style/4nk.css Normal file
View File

@ -0,0 +1,598 @@
: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;
}
/** 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;
}
.modal-content {
width: 40%;
height: 40%;
background-color: white;
border-radius: 8px;
padding: 20px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.modal-title {
margin: 0;
padding-bottom: 20px;
width: 100%;
font-size: 1.5em;
border-bottom: 1px solid #ccc;
}
.confirmation-box {
margin-top: 20px;
align-content: center;
width: 40%;
height: 20%;
padding: 20px;
background-color: var(--secondary-color);
border-radius: 8px;
font-size: 1.2em;
color: #333333;
top: 20%;
position: relative;
}
.nav-wrapper {
position: fixed;
background: radial-gradient(circle, white, var(--primary-color));
/* background-color: #CFD8DC; */
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);
.nav-right-icons {
display: flex;
.notification-container {
position: relative;
display: inline-block;
}
.notification-bell, .burger-menu {
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;
}
}
}
.brand-logo {
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;
}
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 / 5;
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: space-evenly;
align-items: center;
height: 80%;
}
.modal-content {
width: 80%;
height: 20%;
}
}
.qr-code {
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);
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;
}
.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: .3rem 0;
}
}
.card-description {
padding: 20px;
font-size: 1em;
color: #333;
width: 90%;
}
.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;
}
/* 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;
}

178
src/database.ts Normal file
View File

@ -0,0 +1,178 @@
class Database {
private static instance: Database;
private db: IDBDatabase | null = null;
private dbName: string = '4nk';
private dbVersion: number = 1;
private storeDefinitions = {
AnkUser: {
name: "user",
options: {'keyPath': 'pre_id'},
indices: []
},
AnkSession: {
name: "session",
options: {},
indices: []
},
AnkProcess: {
name: "process",
options: {'keyPath': 'id'},
indices: [{
name: 'by_name',
keyPath: 'name',
options: {
'unique': true
}
}]
},
AnkMessages: {
name: "messages",
options: {'keyPath': 'id'},
indices: []
}
}
// Private constructor to prevent direct instantiation from outside
private constructor() {}
// Method to access the singleton instance of Database
public static async getInstance(): Promise<Database> {
if (!Database.instance) {
Database.instance = new Database();
await Database.instance.init();
}
return Database.instance;
}
// Initialize the database
private async init(): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.dbVersion);
request.onupgradeneeded = () => {
const db = request.result;
Object.values(this.storeDefinitions).forEach(({name, options, indices}) => {
if (!db.objectStoreNames.contains(name)) {
let store = db.createObjectStore(name, options);
indices.forEach(({name, keyPath, options}) => {
store.createIndex(name, keyPath, options);
})
}
});
};
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onerror = () => {
console.error("Database error:", request.error);
reject(request.error);
};
});
}
public async getDb(): Promise<IDBDatabase> {
if (!this.db) {
await this.init();
}
return this.db!;
}
public getStoreList(): {[key: string]: string} {
const objectList: {[key: string]: string} = {};
Object.keys(this.storeDefinitions).forEach(key => {
objectList[key] = this.storeDefinitions[key as keyof typeof this.storeDefinitions].name;
});
return objectList;
}
public writeObject(db: IDBDatabase, storeName: string, obj: any, key: IDBValidKey | null): Promise<IDBRequest> {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
let request: IDBRequest<any>;
if (key) {
request = store.add(obj, key);
} else {
request = store.add(obj);
}
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
public getObject<T>(db: IDBDatabase, storeName: string, key: IDBValidKey): Promise<T> {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const request = store.get(key);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
public rmObject(db: IDBDatabase, storeName: string, key: IDBValidKey): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.delete(key);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
public getFirstMatchWithIndex<T>(db: IDBDatabase, storeName: string, indexName: string, lookup: string): Promise<T | null> {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const index = store.index(indexName);
const request = index.openCursor(IDBKeyRange.only(lookup));
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const cursor = request.result;
if (cursor) {
resolve(cursor.value);
} else {
resolve(null)
}
}
});
}
public setObject(db: IDBDatabase, storeName: string, obj: any, key: string | null): Promise<IDBRequest> {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
let request: IDBRequest<any>;
if (key) {
request = store.put(obj, key);
} else {
request = store.put(obj);
}
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
public getAll<T>(db: IDBDatabase, storeName: string): Promise<T[]> {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const request = store.getAll();
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
}
export default Database;

70
src/html/home.html Normal file
View File

@ -0,0 +1,70 @@
<div class="nav-wrapper">
<div></div>
<div class="brand-logo">4NK</div>
<div class="nav-right-icons">
<div class="notification-container">
<div class="bell-icon">
<svg class="notification-bell" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path d="M224 0c-17.7 0-32 14.3-32 32V51.2C119 66 64 130.6 64 208v25.4c0 45.4-15.5 89.5-43.8 124.9L5.3 377c-5.8 7.2-6.9 17.1-2.9 25.4S14.8 416 24 416H424c9.2 0 17.6-5.3 21.6-13.6s2.9-18.2-2.9-25.4l-14.9-18.6C399.5 322.9 384 278.8 384 233.4V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32zm0 96c61.9 0 112 50.1 112 112v25.4c0 47.9 13.9 94.6 39.7 134.6H72.3C98.1 328 112 281.3 112 233.4V208c0-61.9 50.1-112 112-112zm64 352H224 160c0 17 6.7 33.3 18.7 45.3s28.3 18.7 45.3 18.7s33.3-6.7 45.3-18.7s18.7-28.3 18.7-45.3z"/></svg>
</div>
<div class="notification-badge">1</div>
</div>
<div class="burger-menu">
<svg class="burger-menu" onclick="toggleMenu()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"/>
</svg>
<div class="menu-content" id="menu">
<a href="#">Import</a>
<button onclick="openModal()">Open Modal</button>
</div>
</div>
</div>
</div>
<div class="title-container">
<h1>Create Account / New Session</h1>
</div>
<div class="tab-container">
<div class="tabs">
<div class="tab active" data-tab="tab1">Scan QR Code</div>
<div class="tab" data-tab="tab2">Scan other device</div>
</div>
</div>
<div class="page-container">
<div id="tab1" class="card tab-content active">
<div class="card-description">
Scan with your other device :
</div>
<div class="card-image qr-code">
<img src="assets/qr_code.png" alt="QR Code" width="150" height="150">
</div>
<div class="card-action">
<a id="scan-this-device" class="btn">OK</a>
</div>
</div>
<div class="separator"></div>
<div id="tab2" class="card tab-content">
<div class="card-description">
Scan your other device :
</div>
<div class="card-image qr-code">
<img src="assets/camera.jpg" alt="QR Code" width="150" height="150">
</div>
<div class="card-action">
<a id="scan-device" class="btn">OK</a>
</div>
</div>
<div id="modal" class="modal">
<div class="modal-content">
<div class="modal-title">Login</div>
<div class="confirmation-box">Waiting for confirmation...</div>
</div>
</div>
</div>

34
src/html/home.js Normal file
View 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();
}
}

49
src/html/process.html Normal file
View File

@ -0,0 +1,49 @@
<div class="nav-wrapper">
<div></div>
<div class="brand-logo">4NK</div>
<div class="nav-right-icons">
<div class="notification-container">
<div class="bell-icon">
<svg class="notification-bell" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path d="M224 0c-17.7 0-32 14.3-32 32V51.2C119 66 64 130.6 64 208v25.4c0 45.4-15.5 89.5-43.8 124.9L5.3 377c-5.8 7.2-6.9 17.1-2.9 25.4S14.8 416 24 416H424c9.2 0 17.6-5.3 21.6-13.6s2.9-18.2-2.9-25.4l-14.9-18.6C399.5 322.9 384 278.8 384 233.4V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32zm0 96c61.9 0 112 50.1 112 112v25.4c0 47.9 13.9 94.6 39.7 134.6H72.3C98.1 328 112 281.3 112 233.4V208c0-61.9 50.1-112 112-112zm64 352H224 160c0 17 6.7 33.3 18.7 45.3s28.3 18.7 45.3 18.7s33.3-6.7 45.3-18.7s18.7-28.3 18.7-45.3z"/></svg>
</div>
<div class="notification-badge">1</div>
</div>
<div class="burger-menu">
<svg class="burger-menu" onclick="toggleMenu()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"/>
</svg>
<div class="menu-content" id="menu">
<a href="#">Revoke</a>
<a href="#">Export</a>
<a href="#">Import</a>
<a href="#">Disconnect</a>
</div>
</div>
</div>
</div>
<div class="title-container">
<h1>Process Selection</h1>
</div>
<div class="process-container">
<div class="card">
<div class="card-description">
<div class="input-container">
<select multiple data-multi-select-plugin id="autocoplete" placeholder="Filter processes..." class="select-field">
</select>
<label for="autocomplete" class="input-label">Filter processes :</label>
<div class="selected-processes"></div>
</div>
<div class="card-content">
</div>
</div>
<div class="card-action">
<a class="btn">OK</a>
</div>
</div>

409
src/html/process.js Normal file
View File

@ -0,0 +1,409 @@
function toggleMenu() {
const menu = document.getElementById("menu");
if (menu.style.display === "block") {
menu.style.display = "none";
} else {
menu.style.display = "block";
}
}
// Input for filtering script
// var processeList = [
// {
// id: 1,
// name: "Messaging",
// description: "Encrypted messages",
// zoneList: [
// {
// id: 1,
// name: "General",
// },
// ],
// },
// {
// id: 2,
// name: "Storage",
// description: "Distributed storage",
// zoneList: [
// {
// id: 1,
// name: "Paris",
// },
// {
// id: 2,
// name: "Normandy",
// },
// {
// id: 3,
// name: "New York",
// },
// {
// id: 4,
// name: "Moscow",
// },
// ],
// },
// ];
// Initialize function, create initial tokens with itens that are already selected by the user
function init(element) {
// Create div that wroaps all the elements inside (select, elements selected, search div) to put select inside
const wrapper = document.createElement("div");
wrapper.addEventListener("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");
input.addEventListener("keyup", inputChange);
input.addEventListener("keydown", deletePressed);
input.addEventListener("click", openOptions);
const dropdown_icon = document.createElement("a");
dropdown_icon.setAttribute("href", "#");
dropdown_icon.classList.add("dropdown-icon");
dropdown_icon.addEventListener("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);
// const select = document.querySelector(".select-field");
// for (const process of processeList) {
// const option = document.createElement("option");
// option.setAttribute("value", process.name);
// option.innerText = process.name;
// select.appendChild(option);
// }
}
function removePlaceholder(wrapper) {
const input_search = wrapper.querySelector(".selected-input");
input_search.removeAttribute("placeholder");
}
function addPlaceholder(wrapper) {
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) {
const wrapper = e.target.parentNode.parentNode;
const select = wrapper.querySelector("select");
const dropdown = wrapper.querySelector(".dropdown-icon");
const input_val = e.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) {
const wrapper = e.target;
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.focus();
removePlaceholder(wrapper);
}
}
function openOptions(e) {
const input_search = e.target;
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, value) {
const search = wrapper.querySelector(".search-container");
const inputInderline = document.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.setAttribute("href", "#");
close.innerText = "x";
close.addEventListener("click", removeToken);
token.appendChild(token_span);
token.appendChild(close);
inputInderline.appendChild(token);
}
// Listen for clicks in the dropdown option
function clickDropdown(e) {
const dropdown = e.target;
const wrapper = dropdown.parentNode.parentNode;
const input_search = wrapper.querySelector(".selected-input");
const select = wrapper.querySelector("select");
dropdown.classList.toggle("active");
if (dropdown.classList.contains("active")) {
removePlaceholder(wrapper);
input_search.focus();
if (!input_search.value) {
populateAutocompleteList(select, "", true);
} else {
populateAutocompleteList(select, input_search.value);
}
} else {
clearAutocompleteList(select);
addPlaceholder(wrapper);
}
}
// Clears the results of the autocomplete list
function clearAutocompleteList(select) {
const wrapper = select.parentNode;
const autocomplete_list = wrapper.querySelector(".autocomplete-list");
autocomplete_list.innerHTML = "";
}
// Populate the autocomplete list following a given query from the user
function populateAutocompleteList(select, query, dropdown = false) {
const { autocomplete_options } = getOptions(select);
let options_to_show;
if (dropdown) options_to_show = autocomplete_options;
else options_to_show = autocomplete(query, autocomplete_options);
const wrapper = select.parentNode;
const input_search = wrapper.querySelector(".search-container");
const autocomplete_list = wrapper.querySelector(".autocomplete-list");
autocomplete_list.innerHTML = "";
const result_size = options_to_show.length;
if (result_size == 1) {
const li = document.createElement("li");
li.innerText = options_to_show[0];
li.setAttribute("data-value", options_to_show[0]);
li.addEventListener("click", selectOption);
autocomplete_list.appendChild(li);
if (query.length == options_to_show[0].length) {
const event = new Event("click");
li.dispatchEvent(event);
}
} else if (result_size > 1) {
for (let i = 0; i < result_size; i++) {
const li = document.createElement("li");
li.innerText = options_to_show[i];
li.setAttribute("data-value", options_to_show[i]);
li.addEventListener("click", selectOption);
autocomplete_list.appendChild(li);
}
} else {
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) {
const wrapper = e.target.parentNode.parentNode.parentNode;
const input_search = wrapper.querySelector(".selected-input");
const option = wrapper.querySelector(
`select option[value="${e.target.dataset.value}"]`
);
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, options) {
// 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) {
// 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) => el.value);
// Create an autocomplete options array with the options that are not selected by the user
const autocomplete_options = [];
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) {
// Get the value to remove
const value_to_remove = e.target.dataset.option;
const wrapper = e.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
e.target.parentNode.remove();
dropdown.classList.remove("active");
const process = document.querySelector("#" + e.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) {
const wrapper = e.target.parentNode.parentNode;
const input_search = e.target;
const key = e.keyCode || e.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;
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 {
last_token_x.dataset.hits = 2;
}
}
} else {
last_token_x.dataset.hits = 0;
}
}
return true;
}
function addOption(target, val, text) {
const select = document.querySelector(target);
let opt = document.createElement("option");
opt.value = val;
opt.innerHTML = text;
select.appendChild(opt);
}
// get select that has the options available
const select = document.querySelectorAll("[data-multi-select-plugin]");
select.forEach((select) => {
console.log(select);
init(select);
});
// Dismiss on outside click
document.addEventListener("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
);
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");
autocomplete_list.innerHTML = "";
addPlaceholder(wrapper);
}
}
}
});

18
src/index.html Normal file
View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="author" content="4NK">
<meta name="description" content="4NK Web5 Platform">
<meta name="keywords" content="4NK web5 bitcoin blockchain decentralize dapps relay contract">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/style/4nk.css">
<title>4NK Application</title>
</head>
<body>
<div id="containerId" class="container">
<!-- 4NK Web5 Solution -->
</div>
<script type="module" src="/src/index.ts"></script>
</body>
</html>

14
src/index.ts Normal file
View File

@ -0,0 +1,14 @@
import Services from './services';
import { WebSocketClient } from './websockets';
const wsurl = `wss://${window.location.hostname}/ws/`;
document.addEventListener('DOMContentLoaded', async () => {
try {
const services = await Services.getInstance();
await services.addWebsocketConnection(wsurl);
await services.recoverInjectHtml()
} catch (error) {
console.error(error);
}
});

183
src/services.ts Normal file
View File

@ -0,0 +1,183 @@
import { WebSocketClient } from './websockets';
import homePage from './html/home.html?raw';
import homeScript from './html/home.js?raw';
import processPage from './html/process.html?raw';
import processScript from './html/process.js?raw';
export default class Services {
private static instance: Services;
private current_process: string | null = null;
private websocketConnection: WebSocketClient[] = [];
private sp_address: string | null = null;
private processes = [
{
id: 1,
name: "Messaging",
description: "Encrypted messages",
zoneList: [
{
id: 1,
name: "General",
},
],
},
{
id: 2,
name: "Storage",
description: "Distributed storage",
zoneList: [
{
id: 1,
name: "Paris",
},
{
id: 2,
name: "Normandy",
},
{
id: 3,
name: "New York",
},
{
id: 4,
name: "Moscow",
},
],
},
];
private subscriptions: {element: Element; event: string; eventHandler: string;}[] = [] ;
// Private constructor to prevent direct instantiation from outside
private constructor() {}
// Method to access the singleton instance of Services
public static async getInstance(): Promise<Services> {
if (!Services.instance) {
Services.instance = new Services();
// await Services.instance.init();
}
return Services.instance;
}
public async addWebsocketConnection(url: string): Promise<void> {
const services = await Services.getInstance();
const newClient = new WebSocketClient(url, services);
if (!services.websocketConnection.includes(newClient)) {
services.websocketConnection.push(newClient);
}
}
public async recoverInjectHtml(): Promise<void> {
const container = document.getElementById('containerId');
if (!container) {
console.error("No html container");
return;
}
const services = await Services.getInstance();
container.innerHTML = homePage;
const newScript = document.createElement('script')
newScript.textContent = homeScript;
document.head.appendChild(newScript).parentNode?.removeChild(newScript);
const btn = container.querySelector('#scan-this-device')
if(btn) {
this.addSubscription(btn, 'click', 'injectProcessListPage')
}
}
private addSubscription(element: Element, event: string, eventHandler: string): void {
this.subscriptions.push({ element, event, eventHandler });
element.addEventListener(event, (this as any)[eventHandler].bind(this));
}
private cleanSubsciptions(): void {
for (const sub of this.subscriptions) {
const el = sub.element;
const eventHandler = sub.eventHandler;
el.removeEventListener(sub.event, (this as any)[eventHandler].bind(this));
}
this.subscriptions = [];
}
async injectProcessListPage(): Promise<void> {
const container = document.getElementById('containerId');
if (!container) {
console.error("No html container");
return;
}
this.cleanSubsciptions()
const services = await Services.getInstance();
container.innerHTML = processPage;
const newScript = document.createElement('script');
newScript.textContent = processScript;
document.head.appendChild(newScript).parentNode?.removeChild(newScript);
if(this.processes) {
services.setProcessesInSelectElement(this.processes)
}
}
public async setProcessesInSelectElement(processList: any[]) {
const select = document.querySelector(".select-field");
if(select) {
for (const process of processList) {
const option = document.createElement("option");
option.setAttribute("value", process.name);
option.innerText = process.name;
select.appendChild(option);
}
}
const optionList = document.querySelector('.autocomplete-list');
if(optionList) {
const observer = new MutationObserver((mutations, observer) => {
console.log(mutations, observer);
const options = optionList.querySelectorAll('li')
if(options) {
for(const option of options) {
this.addSubscription(option, 'click', 'showSelectedProcess')
}
}
});
observer.observe(document, {
subtree: true,
attributes: true,
});
}
}
public async listenToOptionListPopulating(event: Event) {
const target = event.target as HTMLUListElement;
const options = target?.querySelectorAll('li')
console.log(options)
}
public async showSelectedProcess(event: MouseEvent) {
const elem = event.target;
if(elem) {
const cardContent = document.querySelector(".card-content");
const processes = this.processes;
const process = processes.find((process: any) => process.name === (elem as any).dataset.value);
if (process) {
const processDiv = document.createElement("div");
processDiv.className = "process";
processDiv.id = process.name;
const titleDiv = document.createElement("div");
titleDiv.className = "process-title";
titleDiv.innerHTML = `${process.name} : ${process.description}`;
processDiv.appendChild(titleDiv);
for (const zone of process.zoneList) {
const zoneElement = document.createElement("div");
zoneElement.className = "process-element";
zoneElement.innerHTML = `Zone ${zone.id} : ${zone.name}`;
processDiv.appendChild(zoneElement);
}
if(cardContent) cardContent.appendChild(processDiv);
}
}
}
}

117
src/websockets.ts Normal file
View File

@ -0,0 +1,117 @@
import Services from "./services";
import { AnkFlag, AnkNetworkMsg, CachedMessage } from "../dist/pkg/sdk_client";
class WebSocketClient {
private ws: WebSocket;
private messageQueue: string[] = [];
constructor(url: string, private services: Services) {
this.ws = new WebSocket(url);
this.ws.addEventListener('open', (event) => {
console.log('WebSocket connection established');
// Once the connection is open, send all messages in the queue
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift();
if (message) {
this.ws.send(message);
}
}
});
// Listen for messages
this.ws.addEventListener('message', (event) => {
const msgData = event.data;
(async () => {
if (typeof(msgData) === 'string') {
console.log("Received text message: "+msgData);
try {
const feeRate = 1;
// By parsing the message, we can link it with existing cached message and return the updated version of the message
let res: CachedMessage = await services.parseNetworkMessage(msgData, feeRate);
console.debug(res);
if (res.status === 'FaucetComplete') {
// we received a faucet tx, there's nothing else to do
window.alert(`New faucet output\n${res.commited_in}`);
await services.updateMessages(res);
await services.updateOwnedOutputsForUser();
} else if (res.status === 'TxWaitingCipher') {
// we received a tx but we don't have the cipher
console.debug(`received notification in output ${res.commited_in}, waiting for cipher message`);
await services.updateMessages(res);
await services.updateOwnedOutputsForUser();
} else if (res.status === 'CipherWaitingTx') {
// we received a cipher but we don't have the key
console.debug(`received a cipher`);
await services.updateMessages(res);
} else if (res.status === 'SentWaitingConfirmation') {
// We are sender and we're waiting for the challenge that will confirm recipient got the transaction and the message
await services.updateMessages(res);
await services.updateOwnedOutputsForUser();
} else if (res.status === 'MustSpendConfirmation') {
// we received a challenge for a notification we made
// that means we can stop rebroadcasting the tx and we must spend the challenge to confirm
window.alert(`Spending ${res.confirmed_by} to prove our identity`);
console.debug(`sending confirm message to ${res.recipient}`);
await services.updateMessages(res);
await services.answer_confirmation_message(res);
} else if (res.status === 'ReceivedMustConfirm') {
// we found a notification and decrypted the cipher
window.alert(`Received message from ${res.sender}\n${res.plaintext}`);
// we must spend the commited_in output to sender
await services.updateMessages(res);
await services.confirm_sender_address(res);
} else if (res.status === 'Complete') {
window.alert(`Received confirmation that ${res.sender} is the author of message ${res.plaintext}`)
await services.updateMessages(res);
await services.updateOwnedOutputsForUser();
} else {
console.debug('Received an unimplemented valid message');
}
} catch (error) {
console.error('Received an invalid message:', error);
}
} else {
console.error('Received a non-string message');
}
})();
});
// Listen for possible errors
this.ws.addEventListener('error', (event) => {
console.error('WebSocket error:', event);
});
// Listen for when the connection is closed
this.ws.addEventListener('close', (event) => {
console.log('WebSocket is closed now.');
});
}
// Method to send messages
public sendMessage(flag: AnkFlag, message: string): void {
if (this.ws.readyState === WebSocket.OPEN) {
const networkMessage: AnkNetworkMsg = {
'flag': flag,
'content': message
}
// console.debug("Sending message:", JSON.stringify(networkMessage));
this.ws.send(JSON.stringify(networkMessage));
} else {
console.warn('WebSocket is not open. ReadyState:', this.ws.readyState);
this.messageQueue.push(message);
}
}
public getUrl(): string {
return this.ws.url;
}
// Method to close the WebSocket connection
public close(): void {
this.ws.close();
}
}
export { WebSocketClient };

27
tsconfig.json Normal file
View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"declaration": true,
"outDir": "./dist",
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"types": ["vite/client", "node"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "./",
"paths": {
"~/*": ["src/*"]
}
},
"include": ["src", "src/**/*", "./vite.config.ts", "src/index.d.ts"],
"exclude": ["node_modules"]
}

34
vite.config.ts Normal file
View File

@ -0,0 +1,34 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue'; // or react from '@vitejs/plugin-react' if using React
import wasm from 'vite-plugin-wasm';
import {createHtmlPlugin} from 'vite-plugin-html';
export default defineConfig({
plugins: [
vue(), // or react() if using React
wasm(),
createHtmlPlugin({
minify: true,
template: 'src/index.html',
}),
],
build: {
outDir: 'dist',
rollupOptions: {
input: './src/index.ts',
output: {
entryFileNames: 'index.js',
},
},
},
resolve: {
alias: {
'@': '/src',
},
extensions: ['.ts', '.tsx', '.js'],
},
server: {
open: false,
port: 3001,
},
});