init project with only basic page logic
This commit is contained in:
parent
111977ded3
commit
fd209aeb67
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
target/
|
||||
pkg/
|
||||
Cargo.lock
|
||||
node_modules/
|
||||
dist/
|
||||
.vscode
|
13
Dockerfile
Normal file
13
Dockerfile
Normal 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
6544
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal 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
BIN
public/assets/4nk_image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 61 KiB |
BIN
public/assets/4nk_revoke.jpg
Normal file
BIN
public/assets/4nk_revoke.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
BIN
public/assets/bgd.webp
Normal file
BIN
public/assets/bgd.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 509 KiB |
BIN
public/assets/camera.jpg
Normal file
BIN
public/assets/camera.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 73 KiB |
34
public/assets/home.js
Normal file
34
public/assets/home.js
Normal 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
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
598
public/style/4nk.css
Normal 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
178
src/database.ts
Normal 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
70
src/html/home.html
Normal 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
34
src/html/home.js
Normal 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
49
src/html/process.html
Normal 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
409
src/html/process.js
Normal 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
18
src/index.html
Normal 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
14
src/index.ts
Normal 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
183
src/services.ts
Normal 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
117
src/websockets.ts
Normal 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
27
tsconfig.json
Normal 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
34
vite.config.ts
Normal 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,
|
||||
},
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user